Skip to content

高并发系统设计

高并发系统设计的目标,不是让系统接受无限请求,而是在负载上升和局部故障时,仍以可预测的延迟完成尽可能多的有效工作。

并发、吞吐与延迟

Little 定律

在稳定系统中,平均在途工作数量、平均到达速率和平均停留时间满足:

L=λW
  • L:系统中的平均请求数,也就是平均并发量;
  • λ:平均有效到达速率;
  • W:请求在系统中的平均停留时间,包括排队与处理。

例如,服务稳定处理 2,000 QPS,平均响应时间为 150 ms:

L=2000×0.15=300

这意味着系统平均约有 300 个请求处于排队、计算或等待 I/O 状态。如果平均延迟因为下游变慢上升到 1 s,而到达速率不变,在途请求就会增长到约 2,000,并继续消耗连接、内存和线程。

公式来源:John D. C. Little,A Proof for the Queuing Formula: L = λW

线程模型与并发能力

同步阻塞服务可能用一个线程承载一个在途请求;事件循环、协程和异步 I/O 可以用较少线程维护大量等待中的连接。但最终容量仍受 CPU、内存、网络、数据库连接、锁和下游服务限制。

异步模型降低的是“等待期间占用线程”的成本,不会让 CPU 计算和数据库写入变成免费资源。

过载反馈环

图示参考:Google SRE:Addressing Cascading Failures

系统接近饱和时,吞吐不一定继续增长;它可能因为上下文切换、垃圾回收、缓存失效和无效重试而下降。设计目标应是让系统在过载点之后“有控制地拒绝”,而不是进入吞吐坍塌。

请求链路上的容量边界

每一层都可能有独立上限。入口能承载 10 万 QPS,不代表数据库也能承载;服务横向扩容如果同时增加数据库连接,还可能更快耗尽数据库容量。

容量规划应沿调用链寻找最窄瓶颈,并明确每一层的:

  • 稳态容量与突发容量;
  • 最大并发、队列和连接数;
  • 超时与截止时间;
  • 拒绝、降级和重试策略;
  • 单实例或单分区故障后的剩余容量。

提升容量的主要手段

减少单次工作的成本

先做性能剖析和测量,再优化热点:

  • 减少重复计算、序列化和内存分配;
  • 批处理能够合并的网络和磁盘操作;
  • 避免 N+1 查询和不必要的跨服务调用;
  • 使用连接复用、数据库索引和更合适的数据结构;
  • 把非关键工作移出用户请求关键路径。

单次请求成本降低,通常同时改善吞吐、延迟和成本,是优先级最高的优化。

缓存

缓存用更便宜的读取换取回源压力下降,但必须明确:

  • 缓存什么,以及允许多旧;
  • Key 的基数、过期时间和淘汰策略;
  • 穿透、击穿和雪崩时如何保护回源;
  • 更新使用失效、写穿、旁路还是异步刷新;
  • 缓存故障时,数据库是否能承受全部回源流量。

高命中率不一定代表安全。如果所有热门 Key 同时过期,瞬时回源仍可能击穿下游。

异步化与削峰

消息队列适合把“必须立即确认”与“可以稍后完成”的工作拆开。它可以吸收短时突发,但不会消除工作量:

队列需要监控积压量、最老消息年龄、消费速率和失败重试。若平均生产速率长期大于消费速率,队列只是在推迟故障。

水平扩展与分片

  • 无状态服务可通过增加实例分摊请求;
  • 有状态数据可按租户、用户或 Key 分片;
  • 热点分片需要拆分、复制、隔离或单独限流;
  • 扩容必须连同负载均衡、连接池和下游配额一起评估。

分片提高总容量,却引入跨分片查询、数据迁移、重平衡和热点倾斜等复杂度。单机或单库还有余量时,不要为了“高并发”过早分片。

过载保护

截止时间与超时

超时应来自请求的端到端预算,而不是每一层随意设置相同数字。下游必须知道剩余截止时间,避免继续处理客户端已经放弃的请求。

有界队列

队列只适合吸收有限突发。队列长度应结合处理速率、服务时间和延迟目标设置;超过边界时尽早拒绝通常比排队到超时更便宜。

限流与并发限制

机制控制对象更适合
固定窗口 / 滑动窗口时间窗口内请求数API 配额、租户限制
令牌桶平均速率与允许突发入口流量整形
漏桶平滑输出速率保护处理速率稳定的下游
并发限制同时在途请求数延迟随并发变化明显的服务
负载丢弃超载时拒绝低优先级请求保护 Goodput 和核心业务

限流阈值不能只按正常容量设置。故障转移时总容量会下降,保护机制必须根据健康容量或保守下限工作。

背压

背压让下游明确告诉上游“当前无法继续接收”,避免无限缓存。它可以表现为:

  • 同步接口返回 429503
  • 消息消费者减少拉取或暂停分区;
  • 流式协议减少窗口或请求额度;
  • 生产者根据队列水位降低速率。

背压必须传播到真正产生负载的一端;只在中间层积压,最终仍会耗尽中间层。

重试预算与抖动

自动重试应满足:

  • 只重试临时且幂等的失败;
  • 使用指数退避和随机抖动;
  • 限制每次请求的尝试次数;
  • 设置进程或服务级重试预算;
  • 避免调用链的每一层都独立重试。

假设三层调用各重试 3 次,也就是最多尝试 4 次,最底层可能面对 43=64 次调用。重试是一种额外负载,不能脱离容量预算。

尾延迟

平均延迟无法描述少量极慢请求。扇出会放大尾延迟:一次聚合请求同时访问 100 个分片,只要一个分片很慢,整个请求就可能变慢。

常见缓解方式包括:

  • 缩短关键路径,减少不必要的串行和扇出;
  • 为子调用设置截止时间;
  • 隔离慢分区、慢租户和后台任务;
  • 谨慎使用对冲请求,并计入额外负载;
  • 使用部分结果、近似结果或旧缓存降级;
  • 观察 P50、P95、P99 和超时率,而不只看平均值。

延伸阅读:Jeffrey Dean、Luiz André Barroso,The Tail at Scale

自动扩容的边界

自动扩容是容量调节机制,不是瞬时过载保护。完整链路包括检测窗口、控制器计算、资源调度、进程启动、依赖连接和缓存预热。

这段延迟期间仍需要安全余量、限流和降级。CPU 也未必是最佳扩容信号:I/O 服务可能在 CPU 很低时已经被连接池或下游限额卡住。可以结合队列年龄、在途请求、业务 QPS 和 SLO 指标。

图示参考:Kubernetes Horizontal Pod Autoscaling

容量测试方法

通过条件

至少同时约束:

  • 成功率;
  • P95 / P99 延迟;
  • Goodput;
  • CPU、内存、连接池、队列和下游利用率;
  • 超时、拒绝、重试和降级比例。

覆盖不同负载形态

  • 阶梯测试:逐步提高负载,找到拐点和安全容量;
  • 突发测试:验证队列、限流和扩容滞后;
  • 浸泡测试:发现内存泄漏、连接泄漏和长期热数据;
  • 故障负载测试:在实例、节点或可用区退出时施加目标流量;
  • 恢复测试:确认负载下降后系统能退出重试和崩溃循环。

输出容量结论

不要只报告“最高达到 8,000 QPS”,而应记录:

  • 在哪个版本、数据量和硬件配置下;
  • 满足什么延迟与错误率目标;
  • 瓶颈资源是什么;
  • 稳态容量、短时突发和压垮点分别是多少;
  • 建议的运行水位和故障余量是多少。

设计检查

  • [ ] 已区分入口流量、成功吞吐和 Goodput。
  • [ ] 已测量平均并发、P99 延迟和关键资源利用率。
  • [ ] 所有队列、线程池、连接池和缓存都有明确上限。
  • [ ] 超时沿调用链递减,并能取消过期工作。
  • [ ] 重试具有幂等条件、退避、抖动和预算。
  • [ ] 超载时能优先保护核心请求并尽早拒绝。
  • [ ] 自动扩容之前有足够余量覆盖检测与预热时间。
  • [ ] 已在下游变慢、缓存失效和实例退出条件下做负载测试。

参考资料