高并发系统设计
高并发系统设计的目标,不是让系统接受无限请求,而是在负载上升和局部故障时,仍以可预测的延迟完成尽可能多的有效工作。
并发、吞吐与延迟
Little 定律
在稳定系统中,平均在途工作数量、平均到达速率和平均停留时间满足:
:系统中的平均请求数,也就是平均并发量; :平均有效到达速率; :请求在系统中的平均停留时间,包括排队与处理。
例如,服务稳定处理 2,000 QPS,平均响应时间为 150 ms:
这意味着系统平均约有 300 个请求处于排队、计算或等待 I/O 状态。如果平均延迟因为下游变慢上升到 1 s,而到达速率不变,在途请求就会增长到约 2,000,并继续消耗连接、内存和线程。
公式来源:John D. C. Little,A Proof for the Queuing Formula: L = λW
线程模型与并发能力
同步阻塞服务可能用一个线程承载一个在途请求;事件循环、协程和异步 I/O 可以用较少线程维护大量等待中的连接。但最终容量仍受 CPU、内存、网络、数据库连接、锁和下游服务限制。
异步模型降低的是“等待期间占用线程”的成本,不会让 CPU 计算和数据库写入变成免费资源。
过载反馈环
系统接近饱和时,吞吐不一定继续增长;它可能因为上下文切换、垃圾回收、缓存失效和无效重试而下降。设计目标应是让系统在过载点之后“有控制地拒绝”,而不是进入吞吐坍塌。
请求链路上的容量边界
每一层都可能有独立上限。入口能承载 10 万 QPS,不代表数据库也能承载;服务横向扩容如果同时增加数据库连接,还可能更快耗尽数据库容量。
容量规划应沿调用链寻找最窄瓶颈,并明确每一层的:
- 稳态容量与突发容量;
- 最大并发、队列和连接数;
- 超时与截止时间;
- 拒绝、降级和重试策略;
- 单实例或单分区故障后的剩余容量。
提升容量的主要手段
减少单次工作的成本
先做性能剖析和测量,再优化热点:
- 减少重复计算、序列化和内存分配;
- 批处理能够合并的网络和磁盘操作;
- 避免 N+1 查询和不必要的跨服务调用;
- 使用连接复用、数据库索引和更合适的数据结构;
- 把非关键工作移出用户请求关键路径。
单次请求成本降低,通常同时改善吞吐、延迟和成本,是优先级最高的优化。
缓存
缓存用更便宜的读取换取回源压力下降,但必须明确:
- 缓存什么,以及允许多旧;
- Key 的基数、过期时间和淘汰策略;
- 穿透、击穿和雪崩时如何保护回源;
- 更新使用失效、写穿、旁路还是异步刷新;
- 缓存故障时,数据库是否能承受全部回源流量。
高命中率不一定代表安全。如果所有热门 Key 同时过期,瞬时回源仍可能击穿下游。
异步化与削峰
消息队列适合把“必须立即确认”与“可以稍后完成”的工作拆开。它可以吸收短时突发,但不会消除工作量:
队列需要监控积压量、最老消息年龄、消费速率和失败重试。若平均生产速率长期大于消费速率,队列只是在推迟故障。
水平扩展与分片
- 无状态服务可通过增加实例分摊请求;
- 有状态数据可按租户、用户或 Key 分片;
- 热点分片需要拆分、复制、隔离或单独限流;
- 扩容必须连同负载均衡、连接池和下游配额一起评估。
分片提高总容量,却引入跨分片查询、数据迁移、重平衡和热点倾斜等复杂度。单机或单库还有余量时,不要为了“高并发”过早分片。
过载保护
截止时间与超时
超时应来自请求的端到端预算,而不是每一层随意设置相同数字。下游必须知道剩余截止时间,避免继续处理客户端已经放弃的请求。
有界队列
队列只适合吸收有限突发。队列长度应结合处理速率、服务时间和延迟目标设置;超过边界时尽早拒绝通常比排队到超时更便宜。
限流与并发限制
| 机制 | 控制对象 | 更适合 |
|---|---|---|
| 固定窗口 / 滑动窗口 | 时间窗口内请求数 | API 配额、租户限制 |
| 令牌桶 | 平均速率与允许突发 | 入口流量整形 |
| 漏桶 | 平滑输出速率 | 保护处理速率稳定的下游 |
| 并发限制 | 同时在途请求数 | 延迟随并发变化明显的服务 |
| 负载丢弃 | 超载时拒绝低优先级请求 | 保护 Goodput 和核心业务 |
限流阈值不能只按正常容量设置。故障转移时总容量会下降,保护机制必须根据健康容量或保守下限工作。
背压
背压让下游明确告诉上游“当前无法继续接收”,避免无限缓存。它可以表现为:
- 同步接口返回
429或503; - 消息消费者减少拉取或暂停分区;
- 流式协议减少窗口或请求额度;
- 生产者根据队列水位降低速率。
背压必须传播到真正产生负载的一端;只在中间层积压,最终仍会耗尽中间层。
重试预算与抖动
自动重试应满足:
- 只重试临时且幂等的失败;
- 使用指数退避和随机抖动;
- 限制每次请求的尝试次数;
- 设置进程或服务级重试预算;
- 避免调用链的每一层都独立重试。
假设三层调用各重试 3 次,也就是最多尝试 4 次,最底层可能面对
尾延迟
平均延迟无法描述少量极慢请求。扇出会放大尾延迟:一次聚合请求同时访问 100 个分片,只要一个分片很慢,整个请求就可能变慢。
常见缓解方式包括:
- 缩短关键路径,减少不必要的串行和扇出;
- 为子调用设置截止时间;
- 隔离慢分区、慢租户和后台任务;
- 谨慎使用对冲请求,并计入额外负载;
- 使用部分结果、近似结果或旧缓存降级;
- 观察 P50、P95、P99 和超时率,而不只看平均值。
延伸阅读:Jeffrey Dean、Luiz André Barroso,The Tail at Scale
自动扩容的边界
自动扩容是容量调节机制,不是瞬时过载保护。完整链路包括检测窗口、控制器计算、资源调度、进程启动、依赖连接和缓存预热。
这段延迟期间仍需要安全余量、限流和降级。CPU 也未必是最佳扩容信号:I/O 服务可能在 CPU 很低时已经被连接池或下游限额卡住。可以结合队列年龄、在途请求、业务 QPS 和 SLO 指标。
容量测试方法
通过条件
至少同时约束:
- 成功率;
- P95 / P99 延迟;
- Goodput;
- CPU、内存、连接池、队列和下游利用率;
- 超时、拒绝、重试和降级比例。
覆盖不同负载形态
- 阶梯测试:逐步提高负载,找到拐点和安全容量;
- 突发测试:验证队列、限流和扩容滞后;
- 浸泡测试:发现内存泄漏、连接泄漏和长期热数据;
- 故障负载测试:在实例、节点或可用区退出时施加目标流量;
- 恢复测试:确认负载下降后系统能退出重试和崩溃循环。
输出容量结论
不要只报告“最高达到 8,000 QPS”,而应记录:
- 在哪个版本、数据量和硬件配置下;
- 满足什么延迟与错误率目标;
- 瓶颈资源是什么;
- 稳态容量、短时突发和压垮点分别是多少;
- 建议的运行水位和故障余量是多少。
设计检查
- [ ] 已区分入口流量、成功吞吐和 Goodput。
- [ ] 已测量平均并发、P99 延迟和关键资源利用率。
- [ ] 所有队列、线程池、连接池和缓存都有明确上限。
- [ ] 超时沿调用链递减,并能取消过期工作。
- [ ] 重试具有幂等条件、退避、抖动和预算。
- [ ] 超载时能优先保护核心请求并尽早拒绝。
- [ ] 自动扩容之前有足够余量覆盖检测与预热时间。
- [ ] 已在下游变慢、缓存失效和实例退出条件下做负载测试。
参考资料
- John D. C. Little:A Proof for the Queuing Formula: L = λW
- Google SRE Book:Addressing Cascading Failures
- Google SRE Book:Production Services Best Practices
- Jeffrey Dean、Luiz André Barroso:The Tail at Scale
- Amazon Builders' Library:Using load shedding to avoid overload
- Amazon Builders' Library:Timeouts, retries and backoff with jitter
- Kubernetes:Horizontal Pod Autoscaling