华为云DMS-Kafka消费积压与rebalance治理
华为云 DMS Kafka 消费积压排查:从 3 个消费者加到 8 个,积压反而涨得更快
积压从 10 万条涨到 800 万条,第一反应是加消费者。从 3 个加到 8 个之后,积压增长速度反而更快了。根因不是消费能力不足,而是每次都掉进同一个陷阱:消费者频繁被踢出消费组,触发 rebalance,全组停摆。加的人越多,停的次数越频繁。
一、现象:加人反而更慢
线上订单消息消费组,某天开始积压:
| 时间点 | 消费者数 | 积压量 | 观察 |
|---|---|---|---|
| 故障前 | 3 | 常年在 0 附近 | 正常 |
| 故障初期 | 3 | 10 万,缓慢上涨 | 开始告警 |
| 处置动作 | 3 → 8 | — | 以为加人能解决 |
| 加人后 30 分钟 | 8 | 80 万,加速上涨 | 反向恶化 |
| 加人后 2 小时 | 8 | 800 万 | 慌了 |
"加消费者反而更慢"这个现象本身就排除了"单机处理能力不足"这个假设——如果只是处理慢,加人至少会把曲线压平。
二、先分清三个数字
排查消费积压,必须把这三个指标分开看,混在一起会得出错误结论:
| 指标 | 含义 | 诊断价值 |
|---|---|---|
| 积压量(lag) | 已生产但未被消费的消息数 | 反映"欠了多少账" |
| 消费速率(TPS) | 每秒成功消费的消息数 | 反映"还账能力" |
| 生产速率 | 每秒进入的消息数 | 反映"欠账速度" |
判断标准很简单:
- 消费速率 ≈ 生产速率 → 积压稳定,只是历史欠账,能靠时间消化
- 消费速率 < 生产速率 → 积压持续增长,必须提升消费能力
- 消费速率反复掉到 0 → 不是能力问题,是"停摆"问题
本案属于第三种:监控曲线显示消费 TPS 周期性掉到 0,每次停 20~40 秒,然后恢复。这个"周期性归零"是 rebalance 的典型指纹。
三、为什么加消费者没用:分区数是并行度的天花板
这是 Kafka 消费模型最硬的一条约束:
一个分区在同一时刻只能被同一个消费组内的一个消费者消费。
假设 topic 有 3 个分区:
| 消费者数 | 实际有效消费者 | 说明 |
|---|---|---|
| 3 | 3 | 每个消费者分到 1 个分区,满负荷 |
| 5 | 3 | 2 个消费者分不到分区,完全空闲 |
| 8 | 3 | 5 个消费者空转 |
所以从 3 加到 8,实际消费能力一点没涨,反而引入了 5 个空闲消费者——而这 5 个"加入/离开"的动作,每一次都会触发全组 rebalance。
这就是"加人反而更慢"的第一层原因:没增加产能,只增加了扰动。
四、rebalance:最贵的隐形停顿
rebalance 是消费组重新分配分区的过程。它有一个致命特性:
rebalance 期间,全组所有消费者停止消费,包括那些本来工作正常的。
典型的一次 rebalance 耗时 20~40 秒(取决于组内消费者数量、协调器负载、是否有消费者失联超时)。按一天 200 次计算:
200 次 × 30 秒 = 6000 秒 ≈ 100 分钟
一天有一个多小时全组在停摆。 而这些停顿分散在各个时刻,监控上只表现为 TPS 曲线的小坑,很容易被忽略。
触发 rebalance 的常见原因
| 触发条件 | 说明 |
|---|---|
| 消费者加入/退出 | 扩容、重启、发布都会触发 |
心跳超时(session.timeout.ms) |
消费者进程卡住、GC 停顿 |
处理超时(max.poll.interval.ms) |
两次 poll 之间间隔太长,最常见 |
| 分区数变化 | topic 扩分区 |
| 订阅关系变化 | 正则订阅匹配到了新 topic |
本案的根因:处理超时导致的死循环
看三个关键参数的默认值:
| 参数 | 默认值 | 含义 |
|---|---|---|
session.timeout.ms |
45 s | 多久没心跳判定消费者死亡 |
heartbeat.interval.ms |
3 s | 心跳发送间隔 |
max.poll.interval.ms |
300 s(5 分钟) | 两次 poll 的最大间隔 |
max.poll.records |
500 | 一次 poll 拉取的最大消息数 |
问题出在 max.poll.interval.ms。消费者的处理逻辑是:
poll 500 条 → 逐条处理(每条要调下游接口) → 处理完再 poll 下一批
单条处理 0.5 秒,500 条就是 250 秒——已经逼近 300 秒上限。如果下游抖动一下,单条变成 0.7 秒,500 条 = 350 秒 > 300 秒,消费者就被判定为"处理超时"踢出消费组。
被踢出后:
- 触发 rebalance,全组停摆 30 秒
- 该消费者重新入组,重新分配分区
- 从上次提交的位点重新消费,又拉到一大批消息
- 处理时间再次超过 300 秒 → 再次被踢出
这是一个自我强化的死循环。 而且消费者越多,rebalance 越频繁,每次停摆损失的消费能力越多——所以加人反而更慢。
五、定位:三个证据
1. 消费 TPS 周期性归零
监控上把"消费 TPS"和"rebalance 次数"画在一起看。如果 TPS 掉到 0 的时间点与 rebalance 次数上升的时间点吻合,基本可以确认。
2. 客户端日志里的 rebalance 原因
客户端日志会明确打印 rebalance 的触发原因,重点关注:
Attempt to heartbeat failed
Member ... has left the group
Consumer poll timeout has expired ← 就是这一条
看到 poll timeout has expired,直接锁定 max.poll.interval.ms 超时。
3. 服务端与客户端时间戳对比
在消费者里记录"开始 poll 时间 → 处理完成时间",算出一批的实际处理耗时,与 max.poll.interval.ms 对比。这个数字最能说明问题。
六、修复:按顺序做
1. 消费者数对齐分区数(立刻)
# 先看 topic 有几个分区
kafka-topics.sh --describe --topic order-topic
消费者数设为等于或略小于分区数。多出来的消费者只会空转并增加 rebalance 频率。
2. 减小单批处理量(立刻)
把 max.poll.records 从 500 降到 50~100,让单批处理时间远小于 max.poll.interval.ms:
max.poll.records=100
单批处理时间从 250 秒降到 50 秒,安全边界立刻拉开。
3. 参数对齐(关键)
三个超时参数必须满足这个关系:
heartbeat.interval.ms < session.timeout.ms < max.poll.interval.ms
3 s(默认) 45 s 300 s
配置建议:
session.timeout.ms=45000
heartbeat.interval.ms=3000
max.poll.interval.ms=600000 # 放大到 10 分钟,给慢处理留余量
max.poll.records=100
enable.auto.commit=false # 手动提交,避免重复消费
enable.auto.commit=false 这条必须配:自动提交是"拉取后按时间间隔提交位点",一旦处理失败,位点已经提交了,消息就丢了。手动提交能保证"处理成功才提交"。
4. 把处理逻辑异步化(1~2 周)
单条处理 0.5 秒是根本问题。两种改法:
- 批量处理:攒 100 条一次调用下游批量接口,把 N 次 RPC 变成 1 次
- 异步化:poll 到消息后扔进本地队列,由线程池并发处理,poll 线程立刻返回
异步化能显著提升吞吐,但必须自己控制"未完成消息数",否则位点提交会乱序。
5. 治理分区倾斜(视情况)
如果消息按业务 key 分区,某个 key 特别热,会造成单个分区积压、其他分区空闲——看起来"总共积压不多但一直在涨"。
解法是给 key 加随机后缀打散,代价是牺牲这个 key 的顺序性。所以只对不要求顺序的消息做。
6. 扩分区(慎用)
扩分区能提升并行度上限,但会破坏按 key 的顺序性(同一个 key 在扩容后可能映射到新分区),并且需要在业务低峰做。顺序性要求强的场景优先考虑拆成多个 topic。
七、效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 消费者数 | 8(分区数 3) | 3(对齐分区数) |
| 消费 TPS | 周期性归零 | 平稳,无归零 |
| rebalance 次数 | 约 200 次/天 | 2~3 次/天(正常发布) |
| 积压量 | 800 万且增长 | 30 分钟内消化完 |
| 单批处理耗时 | 250~350 s | 40~50 s |
消费者从 8 个减到 3 个,吞吐反而提升了。 因为去掉了扰动源。
八、小结
四句话:
- 分区数是消费并行度的天花板,消费者数超过分区数是纯粹的浪费,还会增加 rebalance 频率。
- rebalance 期间全组停摆,这是消费积压最隐蔽的原因——监控上只表现为 TPS 的小坑。
max.poll.interval.ms超时是最常见的 rebalance 触发源,症状是poll timeout has expired,解法是减小max.poll.records并放大超时阈值。- 加消费者之前,先确认积压是"能力不足"还是"反复停摆",两者的解法完全相反。
如果只记一句:消费积压先看 TPS 是不是周期性归零,归零就是 rebalance,加人只会让它更糟。
- 点赞
- 收藏
- 关注作者
评论(0)