消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?
消息堆积时,先别急着加消费者。只有把入队速度、单消息执行时长、下游 RT、数据库等待、重试回流和热点分区放到同一个时间窗里对齐,才能解释为什么“消费者看起来很多”,积压却还是越来越重。
消息开始堆积时,团队最常见的第一反应通常很一致:
- 消费者数量是不是不够
- 要不要赶紧扩副本
- 线程池是不是太小了
- 先把消费并发提上去再说
这些动作有时能短暂缓一口气,但真实线上里,消息越堆越多,最常见的起点并不是消费者太少,而是单条消息处理速度先掉下来了。
而单条消息处理速度掉下来,往往不是消息系统本身的 bug,而是更后面的链路开始变慢了:
- 下游 HTTP / RPC 变慢
- 数据库锁等待或长事务变多
- 连接池开始紧
- 外部回执迟迟不回来
- 重试、补偿和死信回流继续放大
所以这类问题最值得先问的不是:
- 我们要不要多开几个消费者?
而是:
当前积压,是因为生产速度真的超出了消费模型,还是因为每个消费者虽然都在忙,但它们其实忙在“等待下游”?
这篇主要想讲清楚一件事:很多消息积压现场里,真正该优先看的不是消费者数量,而是下游变慢以后,整条消费链怎样一步步掉速、排队、回流并自我放大。
先判断 backlog 卡在哪一段
先把 backlog 背后是消费能力问题、锁与幂等问题,还是等待链传导分开,后面才不容易一上来就继续扩消费者。
| 你现在看到的现象 | 更适合先看哪条线 | 为什么 |
|---|---|---|
| MQ backlog 持续上涨,团队第一反应是“消费者是不是不够” | 本文就处理这个误判 | 先把 backlog 是消费者不够,还是下游拖慢消费链分开 |
| 你真正头疼的是重复消费、补偿重入、ack 重投 | 先看《幂等校验明明通过了,为什么消息还是会重复消费?》 | 这时先收窄到幂等和副作用收口,更容易抓首因 |
| 你已经用了分布式锁,现在主要问题是热点 key、持锁时间、续约抖动 | 先看《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?》 | 先去锁链路里找慢点更合适 |
| 你还没决定这个场景到底该不该上分布式锁 | 先看《分布式锁什么时候该用,什么时候不该用》 | 先分清是不是互斥问题 |
| backlog 背后更像线程池、连接池、下游等待一起传导 | 先看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》 | 更像整条等待链在互相放大 |
这篇更适合处理什么场景
如果你现在最想知道的是:为什么消费者已经不少了,消息还是越堆越多,这篇就对路。
它不讲 MQ 参数大全,也不展开分布式锁、幂等边界和锁运行态这些相邻问题。
它只先帮你把一个常见误判拆开:
backlog 变大,到底是消费者真的不够,还是消费者虽然都在忙,但主要忙在等下游、等数据库、等外部回执?
先把这个判断做清楚,再决定后面该扩消费者、治下游,还是先切断回流放大。
做一轮现场快判
- 对齐入队速率、完成速率和 backlog 拐点是不是同一时间窗。
- 再看单条消息耗时有没有抬升,而不是只看消费者数量。
- 接着确认消费者线程是在做业务,还是主要在等 RPC、数据库、锁、外部回执。
- 如果 backlog 伴随重试、补偿、死信回流一起上涨,优先怀疑已经进入放大链。
- 最后再决定动作应该是扩消费者、治理下游慢点,还是切断回流放大。
一、消费者多,不等于消费能力就强
很多团队默认的容量直觉是:
- 10 个消费者比 5 个强
- 20 个线程比 10 个强
- 实例数翻倍,积压应该就会降
这只在一个前提下成立:
- 单条消息处理成本大致稳定
- 消费者大部分时间都在真正推进业务,而不是等资源
但真实系统里,消费者经常并不是“算力不足”,而是:
- 线程在等下游
- 线程在等数据库连接
- 线程在等锁
- 线程在等外部系统回执
这时多开消费者的效果往往很差,因为:
你只是让更多线程更快地排到同一个慢点前面。
于是现场就会出现一种特别迷惑的状态:
- active 线程很多
- 消费实例数也不少
- CPU 却不高
- backlog 还是持续上涨
这几乎就是“消费者在等下游”的典型画像。
二、先分型:到底是来得太快,还是做得太慢
消息积压最值钱的第一刀,不是先看线程池,而是先分清下面这件事:
积压是因为消息生产突然变多了,还是因为单条消息消费变慢了?
1. 更像生产变快的信号
通常会看到:
- 入队 QPS 突然明显上涨
- 消费单条耗时变化不大
- backlog 在活动、批量导入、补偿任务窗口里快速上升
- 某类业务请求量确实同步增长
这类场景里,消费者数量和分区数可能确实是关键因素。
2. 更像消费变慢的信号
通常会看到:
- 入队量没大变,甚至只小幅波动
- 单条消息耗时、P95、P99 明显抬高
- 消费线程一直很忙,但消费速率持续下降
- 下游 RT、数据库等待、连接池 pending 同时间窗变差
这类就不要再默认“加消费者一定有用”了。
3. 两边一起坏时最危险
真实线上更常见的是:
- 下游先慢一点
- 单条消息执行时长上升
- 消费吞吐下降
- backlog 形成
- 超时、补偿、死信重投让生产侧看起来也变多了
一旦走到这一步,现场会特别容易误判成:
- 好像就是消息量突然大了
其实真正第一推动力,常常还是消费链后半段变慢了。
三、为什么“下游慢一点”,最后会演变成“消息堆很多”
因为消息系统看的不是你有多少消费者,而是:
- 单位时间里,系统到底完成了多少条消息
而下游一旦变慢,会直接压低这个完成速度。
1. 下游变慢,单条消息执行时长被拉长
典型路径通常是:
- 消费者拿到消息
- 先查库或改状态
- 再调一个下游 HTTP / RPC
- 下游从 80ms 抬到 800ms,甚至 2s
- 当前消费者线程一直占着不释放
这意味着同样一个消费者实例,在同样一分钟里能完成的消息数突然变少了。
2. 单条消息一变慢,消费者周转速度就掉
原来一条消息 100ms 完成,10 个线程一秒能完成约 100 条;
如果后来一条消息变成 1s,10 个线程一秒可能就只能完成 10 条左右。
这时即使:
- 实例数没变
- 线程数没变
- 没有任何报错
积压也会非常诚实地涨起来。
3. backlog 一旦形成,系统很容易进入自我放大
接下来通常还会发生几件事:
- 老消息越来越多,占着分区和队列
- 消费超时开始增加
- 重试、补偿、死信回流又继续回灌
- 下游本来就慢,现在还要扛更多重复调用
于是问题从“下游有点慢”很快演变成“消息怎么越来越多”。
四、最常见的慢点,不在 MQ,而在消费链后半段
以后再遇到这种现场,我更建议先假设:
- MQ 只是把慢点暴露出来了
然后顺着消费链往后看。
1. HTTP / RPC 下游变慢
这是最高频的一类。
常见表现:
- 消费线程栈大量停在网络等待
- CPU 不高
- 消费线程 active 很高
- backlog 和下游 RT 同时抬头
这时加消费者通常只会带来一个后果:
- 更多并发一起压向已经变慢的下游
2. 数据库链路变慢
这类也非常常见。
表现通常是:
- 慢 SQL 增多
- 锁等待变多
- 长事务把连接拿得更久
- 消费线程先花很多时间在等连接或跑 SQL
这时你看到的是消息堆积,真正起点往往是数据库等待链。
3. 外部副作用等待
例如:
- 发券回执
- 风控确认
- 对账回调
- 文件或对象存储写入
- 第三方接口确认
这些链路通常 CPU 不高,却能把单消息耗时直接拉长。
4. 本地事务边界过大
很多消费者代码会这样写:
- 开事务
- 查库、改库
- 调下游
- 再写回状态
- 最后提交
这时真正慢的可能不是 SQL,而是:
- 事务里夹了慢下游
- 连接和锁都被一起拖长
- 消费者线程释放速度显著下降
五、最值钱的判断:消费者是在“处理消息”,还是在“等资源”
排查积压时,我更建议优先围绕这句话判断:
当前消费者线程大部分时间,到底在真正做业务,还是在等资源?
这能很快把问题收敛成两类。
1. 真正在做业务,且确实吞吐不够
更像:
- 分区数不足
- 消费实例数不足
- CPU 真打满了
- 本地业务计算本身很重
这类场景下,加消费者或改消费模型才更有意义。
2. 大部分时间在等资源
更像:
- 下游 HTTP / RPC 变慢
- 数据库等待
- 连接池等待
- 锁等待
- 外部确认或 IO 等待
这类场景下,盲目加消费者往往只会继续放大等待。
六、现场可以这样往下收
如果线上已经出现“消息越堆越多”,我更建议按下面顺序把证据串起来。
第 1 步:先对齐生产速率和消费速率
重点看:
- 入队 QPS 是否真的上涨
- 出队 / 完成 QPS 是什么时候开始下降的
- backlog 拐点是否和某次发布、活动、下游抖动一致
第 2 步:再看单条消息耗时
重点看:
- 平均耗时和高分位是否抬升
- 是全部消息都慢,还是某一类消息慢
- 是特定 topic、partition、租户、业务键更慢
第 3 步:看消费者线程在干什么
重点确认:
- 线程栈是在跑计算,还是在网络 / DB / lock 等待
- CPU 是否同步升高
- active 高时 queue 是否也高,还是 queue 不高但线程都卡住了
第 4 步:把下游 RT、数据库和连接池拉进同一时间窗
重点看:
- 下游 HTTP / RPC RT
- 数据库慢 SQL、锁等待、活跃事务
- 连接池 active / idle / pending
- 是否和 backlog 抬升同时间出现
第 5 步:最后再看重试、补偿和回流
重点看:
- 消费失败后的自动重试是否显著增加
- 死信重放、补偿任务是否同时在跑
- 同一业务是否因幂等不稳被反复回灌
七、一个典型案例:为什么消费者已经很多,积压还是压不下去
假设某个订单履约 topic 平时稳定消费,后来突然 backlog 从几千涨到几十万。
第一眼看现场
你会看到:
- 消费实例已经有不少
- active 线程长期很高
- CPU 却没有特别高
- 团队第一反应是继续扩副本
继续往下看
发现:
- 入队 QPS 只比平时高了 15%
- 单条消息耗时却从 120ms 涨到了 1.6s
- 线程栈大量停在下游库存服务调用上
- 库存服务最近 RT 从 90ms 抬到了 1s 以上
再往下看
由于库存服务慢:
- 消费者线程长时间不释放
- backlog 开始形成
- 超时消息触发了补偿任务
- 补偿又继续生成额外消息
最后结论
这次问题的主因不是消费者数量,而是:
下游库存服务变慢 -> 单消息处理时长上升 -> 消费吞吐下降 -> backlog 形成 -> 重试和补偿继续放大
如果这个时候只扩消费者,只会让更多请求一起打向库存服务,反而更糟。
八、关键误判
误判 1:消息积压就一定是消费者数量不够
错。
更多时候是消费者周转速度先掉了。
误判 2:active 线程很高,说明消费者已经很努力,问题一定不在消费端
也不一定。
active 高只能说明线程忙着什么,不说明它们忙得有效。
误判 3:CPU 不高,说明系统还有余量,可以继续加消费者
很多等待型瓶颈下,CPU 本来就不高。继续加消费者只是把更多线程送去排队。
误判 4:先扩消费者总不会错
如果根因在下游慢、数据库慢或锁等待,这通常是最容易把问题进一步放大的动作。
误判 5:只盯 MQ 指标,不看下游和数据库
这几乎是最常见的绕远路方式。
九、FAQ:积压现场最常见的几个疑问
1. 怎么快速判断该不该先加消费者?
先看单条消息耗时是不是明显上涨、消费者线程是不是主要在等资源。如果是,就不要把扩消费者当第一动作。
2. 为什么消费者很多,积压还是会越来越多?
因为决定积压的不是线程总数,而是单位时间真实完成的消息数。只要单条消息变慢,再多消费者也可能不够。
3. 下游慢为什么会直接变成 backlog?
因为消费者线程被占更久,单消息完成速率下降;只要生产速率没有跟着降,队列就会自然积压。
4. 什么时候扩消费者才更有效?
当你确认:
- 单条消息成本相对稳定
- 主要瓶颈不在等待型下游
- 分区和本地资源模型支持更高并发
- CPU 或本地执行能力才是主瓶颈
这时扩消费者才更可能有效。
如果你要顺着 backlog 继续往下查
如果你读到这里,已经知道 backlog 不一定是消费者数量问题,下一步更适合按手里最硬的证据继续收窄:
- 重复消费、补偿重入、死信回流已经开始放大 backlog:去《幂等校验明明通过了,为什么消息还是会重复消费?》
- 你已经看到锁、热点 key、持锁时间把消费链压慢:去《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?》
- 你其实还在犹豫这个场景是否该靠分布式锁保护:先回《分布式锁什么时候该用,什么时候不该用》
- 你发现本质不是 MQ,而是线程、连接、下游一起形成等待型协作放大链:去《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》
十、如果你还没切完范围
先留在这个专题里
如果证据已经带到别的慢点
一种更顺手的阅读顺序
- 先用这篇,把“消费者太少”和“下游变慢导致消费掉速”这两个方向分开
- 如果你处理的是更泛化的异步 backlog,再去看 异步任务越堆越多,问题常常不在异步本身
- 如果线程池看起来很忙但说不清慢在哪,再看 线程池队列不长但任务还是慢,常见瓶颈在哪里?
- 如果消费链已经伴随数据库和事务等待,再转到数据库连接池和长事务相关文章
- 如果 backlog 已经引发重试风暴或超时扩散,再把 上游重试把慢接口放大的典型链路怎么识别? 和 接口超时风暴里,先止血还是先定位?如何判断分界线? 一起看
十一、最后总结:消息堆积时,先问消费为什么掉速
这类问题最容易把人带偏的地方在于:
- 一看到 backlog 上涨
- 就本能地把它理解成“消费并发不足”
但真实线上里,更实用的判断顺序是:
先分清是生产变快还是消费变慢,再看消费者线程主要是在处理业务还是在等下游,最后把下游 RT、数据库等待、连接池和重试回流放进同一条证据链里判断。
只要这条主线立住,很多原本会被一句“多加几个消费者试试”带偏的现场,最后都会回到真正的慢点上。