Java

消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?

消息堆积时,先别急着加消费者。只有把入队速度、单消息执行时长、下游 RT、数据库等待、重试回流和热点分区放到同一个时间窗里对齐,才能解释为什么“消费者看起来很多”,积压却还是越来越重。

  • 消息堆积
  • MQ
  • 下游依赖
  • 异步任务
  • 稳定性治理
18 分钟阅读

消息开始堆积时,团队最常见的第一反应通常很一致:

  • 消费者数量是不是不够
  • 要不要赶紧扩副本
  • 线程池是不是太小了
  • 先把消费并发提上去再说

这些动作有时能短暂缓一口气,但真实线上里,消息越堆越多,最常见的起点并不是消费者太少,而是单条消息处理速度先掉下来了。

而单条消息处理速度掉下来,往往不是消息系统本身的 bug,而是更后面的链路开始变慢了:

  • 下游 HTTP / RPC 变慢
  • 数据库锁等待或长事务变多
  • 连接池开始紧
  • 外部回执迟迟不回来
  • 重试、补偿和死信回流继续放大

所以这类问题最值得先问的不是:

  • 我们要不要多开几个消费者?

而是:

当前积压,是因为生产速度真的超出了消费模型,还是因为每个消费者虽然都在忙,但它们其实忙在“等待下游”?

这篇主要想讲清楚一件事:很多消息积压现场里,真正该优先看的不是消费者数量,而是下游变慢以后,整条消费链怎样一步步掉速、排队、回流并自我放大。

先判断 backlog 卡在哪一段

先把 backlog 背后是消费能力问题、锁与幂等问题,还是等待链传导分开,后面才不容易一上来就继续扩消费者。

你现在看到的现象更适合先看哪条线为什么
MQ backlog 持续上涨,团队第一反应是“消费者是不是不够”本文就处理这个误判先把 backlog 是消费者不够,还是下游拖慢消费链分开
你真正头疼的是重复消费、补偿重入、ack 重投先看《幂等校验明明通过了,为什么消息还是会重复消费?这时先收窄到幂等和副作用收口,更容易抓首因
你已经用了分布式锁,现在主要问题是热点 key、持锁时间、续约抖动先看《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?先去锁链路里找慢点更合适
你还没决定这个场景到底该不该上分布式锁先看《分布式锁什么时候该用,什么时候不该用先分清是不是互斥问题
backlog 背后更像线程池、连接池、下游等待一起传导先看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?更像整条等待链在互相放大

这篇更适合处理什么场景

如果你现在最想知道的是:为什么消费者已经不少了,消息还是越堆越多,这篇就对路。

它不讲 MQ 参数大全,也不展开分布式锁、幂等边界和锁运行态这些相邻问题。

它只先帮你把一个常见误判拆开:

backlog 变大,到底是消费者真的不够,还是消费者虽然都在忙,但主要忙在等下游、等数据库、等外部回执?

先把这个判断做清楚,再决定后面该扩消费者、治下游,还是先切断回流放大。

做一轮现场快判

  1. 对齐入队速率、完成速率和 backlog 拐点是不是同一时间窗。
  2. 再看单条消息耗时有没有抬升,而不是只看消费者数量。
  3. 接着确认消费者线程是在做业务,还是主要在等 RPC、数据库、锁、外部回执。
  4. 如果 backlog 伴随重试、补偿、死信回流一起上涨,优先怀疑已经进入放大链。
  5. 最后再决定动作应该是扩消费者、治理下游慢点,还是切断回流放大。

一、消费者多,不等于消费能力就强

很多团队默认的容量直觉是:

  • 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 不一定是消费者数量问题,下一步更适合按手里最硬的证据继续收窄:

十、如果你还没切完范围

先留在这个专题里

如果证据已经带到别的慢点

一种更顺手的阅读顺序

  1. 先用这篇,把“消费者太少”和“下游变慢导致消费掉速”这两个方向分开
  2. 如果你处理的是更泛化的异步 backlog,再去看 异步任务越堆越多,问题常常不在异步本身
  3. 如果线程池看起来很忙但说不清慢在哪,再看 线程池队列不长但任务还是慢,常见瓶颈在哪里?
  4. 如果消费链已经伴随数据库和事务等待,再转到数据库连接池和长事务相关文章
  5. 如果 backlog 已经引发重试风暴或超时扩散,再把 上游重试把慢接口放大的典型链路怎么识别?接口超时风暴里,先止血还是先定位?如何判断分界线? 一起看

十一、最后总结:消息堆积时,先问消费为什么掉速

这类问题最容易把人带偏的地方在于:

  • 一看到 backlog 上涨
  • 就本能地把它理解成“消费并发不足”

但真实线上里,更实用的判断顺序是:

先分清是生产变快还是消费变慢,再看消费者线程主要是在处理业务还是在等下游,最后把下游 RT、数据库等待、连接池和重试回流放进同一条证据链里判断。

只要这条主线立住,很多原本会被一句“多加几个消费者试试”带偏的现场,最后都会回到真正的慢点上。