低延迟与高吞吐取舍
Category
networking / IPC。节点定位:延迟、吞吐、顺序、可靠、成本五个目标的拉扯与定价。
Definition
消息系统里最常见的幻觉,是以为可以同时轻松拿到低延迟、高吞吐、强顺序、强可靠和低成本。现实通常不是做不到其中一个,而是你每强化一个目标,另外几个目标就会开始涨价。
所以讨论通信方案时,不能只看平均吞吐或峰值 QPS,而要先问清楚:这条链路更怕慢,还是更怕积压;更怕抖动,还是更怕丢;更需要立刻完成,还是更需要在高峰期稳住。
低延迟和高吞吐为什么天然拉扯
要追求极低延迟,通常意味着:
- 减少排队
- 减少批处理
- 减少中间环节
- 尽量靠近调用方和执行方
要追求极高吞吐,通常意味着:
- 接受排队和缓冲
- 接受批量处理
- 接受更深的异步链路
- 接受消费者按自己的节奏拉取
这两组策略天然存在张力。也就是说,消息系统能帮你提高总体吞吐,但它常常会把单条消息的完成时刻变得不那么确定。
Problem
哪些链路更偏低延迟优先
更偏低延迟优先的链路,通常具备这些特征:
- 直接影响玩家当前交互
- 对尾延迟非常敏感
- 一次处理的上下文很小,但时间窗口很紧
- 积压本身就等于业务失败
例如:
- 局内实时输入与反馈
- 关键状态确认
- 玩家进入或离开某个实时实例
这类链路与其说追求大吞吐,不如说更怕任何不可预测的排队。
哪些链路更偏高吞吐优先
更偏高吞吐优先的链路,通常是:
- 单条消息不要求马上完成
- 允许批量处理
- 高峰流量明显
- 业务可以接受秒级甚至分钟级延迟
例如:
- 埋点上报
- 日志采集
- 报表和统计
- 批量通知
- 历史归档
这类链路真正要解决的是高峰期别把系统打穿,而不是单条消息尽快被处理。
消息队列为什么擅长削峰,也擅长制造积压
队列的价值之一就是把瞬时高峰摊平,让生产者和消费者解耦。但同一套机制也会带来另一个现实:如果消费者长期跟不上,系统不会立即报错,而是以积压的形式把问题藏起来。
积压一旦发生,后果通常不是简单“慢一点”,而是:
- 事件价值随时间下降
- 顺序窗口被拉长
- 失败重试叠加更多压力
- 业务看到的状态越来越滞后
所以削峰不是免费能力,它必须和容量规划、背压策略、过期策略一起设计。
真正要设计的是背压,而不是只设计吞吐
一条链路流量上来以后,迟早要面对一个问题:下游处理不过来怎么办。常见选项无非几种:
- 阻塞上游
- 限流丢弃
- 持续积压
- 降级处理
- 只保留最新值或关键值
不同链路的答案完全不同。埋点、日志允许采样和丢弃;玩家关键结算不能丢,只能重试或补偿;实时状态广播可能宁可覆盖旧消息,也不愿保留所有历史。
如果背压策略没有先定,所谓高吞吐只是把风险换成了未来某个时刻的系统性积压。
平均性能没意义,尾部性能才决定体验
很多通信方案看起来吞吐很漂亮,但一看尾延迟和积压恢复时间就不行。对游戏系统来说,这尤其关键:
- 平均 5ms 但偶发 500ms,玩家感知的是 500ms
- 平均每秒处理十万条,但高峰恢复要十分钟,业务感知的是十分钟
所以性能判断不能只看均值,还要看:
- P95 / P99 延迟
- 积压增长速度
- 高峰恢复时间
- 下游故障时的退化行为
一个更接近工程现实的取舍方式
比较稳妥的做法通常是:
- 主链路优先保证延迟和确定性
- 外围链路优先保证吞吐和解耦
- 高频链路尽量减少 Broker 和持久化环节
- 大吞吐链路必须设计积压治理与过期策略
这不是保守,而是承认不同链路真正怕的不是同一种问题。
常见误区
最常见的错误,是拿批处理链路的吞吐思维去设计实时链路。第二类错误,是只看生产速度,不看消费者长期处理能力,结果队列长期积压。第三类错误,是完全没有背压和过期策略,默认所有消息都应该一直排下去,最后把问题从“现在慢”拖成“以后全线滞后”。
实战案例:一次「晚高峰队列越积越多,排行刷新拖到九秒」的背压缺失排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「消息队列为什么擅长削峰,也擅长制造积压」「真正要设计的是背压,而不是只设计吞吐」「平均性能没意义,尾部性能才决定体验」用到深水区:积压不报错,所以积压最危险——这是高峰积压的尾部排查,也是「如果背压策略没有先定,所谓高吞吐只是把风险换成了未来某个时刻的系统性积压」的完整展开。
背景与现象:某项目把埋点、日志与结算外围事件(排行刷新、成就进度)混在同一条 MQ 管道里,按吞吐指标选型,消费组没有背压与过期策略。晚高峰进场流量 8 万条/秒(日常 1.2 万条/秒的近七倍),积压峰值 1400 万条,恢复耗时 22 分钟;期间排行刷新 P99 从 180 毫秒涨到 9 秒,均值只从 180 毫秒涨到 260 毫秒;高峰期业务告警 60 起。
第一步:现象与口径——积压、尾部、告警三笔账
积压口径:高峰进场 8 万条/秒,消费组处理不及,缺口全变积压,峰值 1400 万条、恢复 22 分钟——系统不报错,只是越来越滞后;事件价值随时间下降、顺序窗口被拉长、失败重试叠加更多压力、业务状态越来越滞后。
尾部口径:排行刷新 P99 从 180 毫秒涨到 9 秒,均值只从 180 毫秒涨到 260 毫秒——玩家和业务感知的是尾部,不是均值。
告警口径:高峰期业务告警 60 起,全部集中在排行刷新与外围事件超时——是积压的下游症状,不是独立故障。
第二步:分层归因——按本页背压与尾部清单排查
晚高峰积压失控归因 占比
背压策略没先定,所有消息默认一直排队 50%
用批处理吞吐思维设计实时外围链路 30%
只看生产速度,不看消费长期处理能力 20%逐项比对:本页背压五选一(阻塞上游、限流丢弃、持续积压、降级处理、只保留最新值或关键值),系统实际默默选了「持续积压」且无人决策;埋点日志允许采样和丢弃,却与结算外围共用一条管道;尾部四项指标(P95/P99、积压增长速度、高峰恢复时间、下游故障时的退化行为)只报了吞吐。60 起告警逐项归位:30 + 18 + 12 = 60,账能对上。
第三步:根因——背压是链路设计的第一问,被跳过了
本页原话:如果背压策略没有先定,所谓高吞吐只是把风险换成了未来某个时刻的系统性积压;积压一旦发生,系统不会立即报错,而是以积压的形式把问题藏起来。团队没做「这条链路更怕慢,还是更怕积压」的定价步骤,高吞吐的验收指标成了唯一指标。
第四步:处置与回填——先定背压,再谈吞吐
- 链路分级:结算外围事件独立管道优先保延迟,埋点日志独立管道保吞吐,高频链路尽量减少 Broker 和持久化环节,大吞吐链路配积压治理与过期策略。
- 背压与过期先定:每条链路先答阻塞、丢弃、积压、降级、保最新五选一,积压超阈值按过期策略清退,高峰自动降级埋点。
- 尾部指标进大盘:P95/P99、积压增长速度、高峰恢复时间、下游故障退化四项列为链路 SLO,均值只做参考。
复核账:积压恢复 22 分钟 → 90 秒;排行刷新 P99 9 秒 → 160 毫秒;高峰告警 60 起 → 5 起。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 链路分级 | 主链路低延迟优先、埋点报表高吞吐优先,分管道治理 | 一个更接近工程现实的取舍方式 |
| 背压五选一 | 阻塞、丢弃、积压、降级、保最新,每条链路先定再上线 | 真正要设计的是背压,而不是只设计吞吐 |
| 过期削峰策略 | 积压超阈值按价值过期清退,削峰配容量规划一起设计 | 消息队列为什么擅长削峰,也擅长制造积压 |
| 尾部口径 | P95/P99、积压增速、恢复时间、故障退化进 SLO | 平均性能没意义,尾部性能才决定体验 |
案例的三个教训
- 平均值是给报表看的,玩家感知尾部——平均 5ms 但偶发 500ms,玩家感知的是 500ms。
- 积压不报错,所以积压最危险——问题被藏起来,等到业务全线滞后才暴露。
- 背压是链路设计的第一问——没定背压的高吞吐,只是把风险换成未来的系统性积压。
Used By
链路分级(主链路低延迟优先、报表高吞吐优先);队列容量与背压设计;尾部延迟 SLO 制定。
Related
上游:通信模式(02)。下游:背压与流控(server 树)、容量扩缩(server 树)。关系链:目标定价 → 背压 → 尾部监控 → 容量。
Reference
本页为工程经验归纳;尾部延迟口径见页内「平均性能没意义,尾部性能才决定体验」一节;页内实战案例为教学示例(数字取整简化)。