凌晨两点十七分,手机在床头柜上震。不是电话,是钉钉。我闭着眼摸过来瞄一眼:订单履约队列,积压 412 万条,消费速率掉了七成。
这种消息最烦人的地方在于——它不说为什么。
群里已经有人开始喊“扩容”。我劝过很多次,别急。积压这件事,加机器能解决的顶多占一半,剩下那一半,你加多少台都是在给故障添燃料。
先看消费速率 再看积压量
很多人盯着 lag 曲线,越看越慌。lag 这个东西会骗人。它只是一个快照,反映的是“还欠着多少”,不告诉你“现在还得动还不不动”。
真正要先看的两个数:生产速率和消费速率。生产还是每秒 8000 条吗?消费是不是从 7000 掉到了 900。如果消费端掉下来了,那问题在消费者,跟堆积量关系不大;如果消费没掉、生产翻了三倍,那是业务洪峰,扩容就是对的。

那天晚上属于前者。消费速率断崖式下跌,而且不是全组下跌——八个消费者实例,六个几乎不动,两个在疯狂跑。
典型的分区分配问题。
Rebalance 才是真正的隐形杀手
Kafka 的消费组模型,本质上是把分区均摊给实例。听起来很公平。但每一次实例上下线、每一次心跳超时、每一轮 max.poll.interval 被顶穿,都会触发一次 rebalance。
而 rebalance 期间,整个消费组是停顿的。不是变慢,是停。停几秒、十几秒,甚至更久。
更坑的是,如果你的消费逻辑里有耗时的同步操作——比如单条消息要调一次外部接口、跑一次几百毫秒的查询——那 max.poll.interval.ms 很容易被顶穿。顶穿一次,实例被踢出组;重新加入,又触发 rebalance;又停。于是形成循环。
那天就是。有个下游服务在抖,单次调用从 50ms 变成了 3s。消费者线程全堵在那儿。表面看是消费慢,根子上是下游超时把消费线程池占满了。

这种场景下,你加实例,只会让 rebalance 更频繁。八个变十六个,协调成本翻倍,lag 反而涨得更快。
分区数是天花板 消费者加不过它
这条规则老生常谈,但每次出事还是会有人踩:一个消费组里,同时工作的消费者实例数,不能超过订阅主题的分区总数。多出来的实例就是站着的观众席,白花钱。
我见过一个团队,队列 topic 只有 6 个分区,消费者扩到了 30 个 Pod。他们觉得扩容了,其实只有 6 个 Pod 在干活,剩下 24 个在空转抢心跳。CPU 上去了,吞吐一点没变。后来把分区从 6 扩到 60,同样的 Pod 数,吞吐直接翻了六倍。
不过话说回来,分区也不能无脑扩。分区多了,端到端延迟会变高,broker 上的文件句柄和元数据压力也上去了;如果消息带 key 做顺序消费,分区数改了以后,同一个 key 会落到不同分区,顺序保证就没了。这点得提前想清楚。
扩容的正确姿势 分三步走
梳理下来,我一般按这个顺序处理:
第一步,止血。先确认消费端是不是被某个慢依赖拖住。如果是,临时降级、加大超时容忍、把耗时逻辑挪到异步线程池,让消费线程尽快空出来。这一步不碰集群,改代码或改配置,代价最小。
第二步,调参。调 max.poll.records,一次少拉一点,别让单次处理时长超过 max.poll.interval;调 session.timeout.ms 和 heartbeat.interval.ms,给网络抖动留点余地;如果是静态成员分配,把 group.instance.id 配上,滚动发布时能少触发好几次 rebalance。
第三步,才是扩。扩之前先算一下:现在有多少分区?消费者最多能到多少?如果分区数已经卡住,那第一步是扩分区,不是扩 Pod。扩分区之后,等数据再均衡稳定了,再加实例。
还有一点,扩容之后 lag 不会立刻回落。因为消费位点是从旧的地方追的,如果生产还在继续,你会看到一个缓慢下降的坡度。判断扩容有没有效,看消费速率,别看 lag 的绝对值。

一份能直接抄的检查清单
下次告警响了,按这个走,能省很多时间:
生产速率有没有异常抬升——有,就是业务洪峰,扩容合理。
消费速率是不是集体下跌——是,先查消费者。
是不是只有少数实例在消费——是,查 rebalance,查分区数够不够。
有没有实例反复被踢出组——查 max.poll.interval 和单批处理耗时。
消费逻辑里有没有同步调外部服务——有,就是它。
分区数 vs 实例数——实例数超过分区数,多出来的都是摆设。
lag 在降但很慢——正常,看速率不看绝对值。
最后说一句反直觉的:大部分积压,不是容量问题,是阻塞问题。加机器能救急,但救不了设计。真正该花时间的地方,是让消费逻辑尽量短、尽量幂等、尽量不依赖外部。队列本身很少是瓶颈——瓶颈在你处理它的方式。