Kafka 消费积压排查:从 lag 曲线到 rebalance 风暴
## 一、lag 大不等于消费慢,先看它是怎么涨的
Kafka 消费积压(consumer lag)是所有消息队列问题里最容易被误判的一类。看到 lag 报警,绝大多数人的第一反应是"消费者不够,加机器"。但实际上 lag 上涨至少有四种完全不同的成因,其中只有一种能靠加机器解决。
判断的第一步不是看 lag 的绝对值,而是看它的**曲线形状**。三个问题必须问清楚:
- lag 是**阶跃式上涨**(某个时刻突然跳上去)还是**线性增长**(持续缓慢上涨)?
- 上涨期间**消费速率**(每秒消费条数)是掉了,还是没变?
- **生产速率**是否同步变化?
阶跃式上涨 + 消费速率正常,说明是某个消费者掉线后的 rebalance,或者某个分区卡住了。线性增长 + 消费速率正常,说明是生产端提速了,消费能力本身没退化。只有"消费速率明显下降 + lag 持续增长"才是真的消费能力不足。
这三种情况的处理方式完全不同:第一种要查 rebalance 和卡住的分区,第二种要么扩容要么限流生产端,第三种才需要加消费者。用错方向,加十台机器也没用。
## 二、lag 到底怎么算的,为什么你和监控看到的数字不一样
`kafka-consumer-groups.sh --describe` 输出的 LAG 是 `LOG-END-OFFSET - CURRENT-OFFSET`,这是**分区级别**的差值。汇总成 group 级别时,有人求和、有人取最大值,两者差别很大。
更关键的是两个坑:
**坑一:lag 是 offset 差值,不是消息条数。** 如果 topic 开了 compaction,或者有事务消息、控制批次,offset 的差值和实际未消费消息数并不相等。极端情况下 lag 显示几百万,实际待消费消息只有几万(大量 offset 被压缩掉了)。
**坑二:`--describe` 拿到的是某一瞬间的快照,不是速率。** 要算速率必须连续采样两次:
```bash
# 采样两次,间隔 10 秒,算真实消费速率
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group order-consumer | awk 'NR>1 && $6 ~ /^[0-9]+$/ {sum+=$6} END {print sum}'
sleep 10
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group order-consumer | awk 'NR>1 && $6 ~ /^[0-9]+$/ {sum+=$6} END {print sum}'
```
两次结果的差值除以 10,才是真实消费速率(条/秒)。同时用 `kafka-run-class.sh kafka.tools.GetOffsetShell` 或监控平台的 produce rate 拿到生产速率,两者对比才能判断是"消费变慢"还是"生产变快"。
## 三、消费速率掉了,先分清是消费者慢还是下游慢
消费者线程本身很少是瓶颈。真正的瓶颈通常在它调用的下游:
- **数据库写入慢**:批量 insert 变成逐条 insert,或者下游 DB 出现锁等待
- **同步 HTTP 调用**:消费逻辑里调了第三方接口,对方抖动直接把消费线程拖住
- **单条消息处理过重**:一条消息触发全量计算或大文件处理
判断方法很直接:看消费者的 **`poll` 间隔**。如果 `max.poll.interval.ms` 频繁超时,说明单批处理时间过长;如果没超时但吞吐上不去,基本可以确定是下游阻塞。
```bash
# 看消费者日志里的 rebalance 与 poll 超时
grep -E "Max poll interval|Rebalance|Attempt to heartbeat" consumer.log | tail -30
```
另一个高频原因是 **`max.poll.records` 设得太大**。默认 500 条,如果单条处理 10ms,一批就是 5 秒,再加上下游抖动,很容易突破 `max.poll.interval.ms`(默认 300 秒),触发 rebalance,rebalance 期间整个 group 停摆,lag 瞬间飙升,然后形成"越积压越 rebalance、越 rebalance 越积压"的正反馈。
**这是积压最常见的自我放大机制**,也是阶跃式 lag 上涨的头号原因。
## 四、rebalance 风暴:lag 阶跃上涨的真凶
rebalance 的触发条件有四种:
1. 消费者加入或离开 group(包括被判定死亡)
2. 订阅的 topic 分区数变化
3. 消费组 coordinator 变更
4. `max.poll.interval.ms` 超时被踢出
其中第 4 种最隐蔽,因为它会在**消费越慢的时候越容易触发**,形成正反馈。第 1 种则常见于容器环境:Pod 滚动更新、OOM 重启、健康检查失败,都会让消费者反复进出 group。
判断是不是 rebalance 风暴,看 coordinator 日志里的 `Preparing to rebalance group` 频率,以及 group 状态的 `state` 字段:
```bash
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group order-consumer --state
```
如果短时间内 state 反复在 `PreparingRebalance` / `CompletingRebalance` / `Stable` 之间跳,就是风暴。处理顺序是:
1. **先调大 `max.poll.interval.ms`**(比如 300000 → 600000),给慢消费留出时间
2. **调小 `max.poll.records`**(比如 500 → 100),让单批处理时间可控
3. **检查消费者是否被频繁重启**:`kubectl get pod -w` 看 restart 次数
4. 容器环境配合 `session.timeout.ms` 和 `heartbeat.interval.ms` 调整,避免 GC 停顿被误判死亡
## 五、分区分配不均:一半消费者在空转
`--describe` 输出里如果出现某些分区 LAG 几十万、另一些分区 LAG 为 0,就是分区分配不均。
常见原因有两个:
**原因一:分区数少于消费者数。** 5 个分区、8 个消费者,永远有 3 个消费者分不到分区,纯空转。这种情况加消费者毫无意义,必须先扩分区。
**原因二:range 分配策略导致倾斜。** `RangeAssignor` 在分区数不能整除时,前面的消费者会多拿一个分区。如果多拿的那个恰好是热点分区,那台消费者就会成为瓶颈。改成 `CooperativeStickyAssignor` 或 `RoundRobinAssignor` 可以缓解。
```properties
# 推荐:协作式粘性分配,减少 rebalance 影响范围
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
```
`CooperativeStickyAssignor` 相比老的 `StickyAssignor` 最大的好处是**支持增量 rebalance**,不必在 rebalance 期间全量停止消费,积压不会因为一次扩容而雪上加霜。
## 六、热点分区:key 设计的问题,加消费者治不了
即使分区数足够、分配也均匀,仍然可能出现单个分区 lag 远超其他分区。这通常是 **key 设计导致的数据倾斜**:比如用 `userId` 做 key,但有几个大客户的订单量占了 80%。
判断方法是看每个分区的 produce rate,而不是只看 lag:
```bash
kafka-run-class.sh kafka.tools.GetOffsetShell \
--bootstrap-server broker1:9092 --topic order-events --time -1
sleep 30
kafka-run-class.sh kafka.tools.GetOffsetShell \
--bootstrap-server broker1:9092 --topic order-events --time -1
```
两次差值就是各分区的生产速率。如果某个分区的速率是其他分区的十倍以上,就是热点分区。
热点分区的解法只有三个:**换 key(加随机后缀打散)、拆 topic、在消费端做二级并行**。加消费者对热点分区完全无效,因为一个分区只能被一个消费者消费。
## 七、消费位点提交方式:自动提交掩盖了真实进度
`enable.auto.commit=true` 是默认值,它按 `auto.commit.interval.ms`(默认 5 秒)提交。问题在于**自动提交提交的是 poll 到的位置,不是处理完的位置**。如果处理逻辑是异步的,或者处理完才落库,那么 consumer 的 committed offset 会"虚高",lag 显示正常但实际有大量消息没处理完。
反过来,如果处理失败但位点已提交,消息就永久丢失了。
排查时先确认提交方式:
```bash
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group order-consumer --members --verbose
```
生产环境建议一律手动提交,并且**处理完成后再提交**,配合幂等消费(业务唯一键 + 去重表)。这样才能保证 lag 反映真实进度。
## 八、八条命令清单
```bash
# 1. 看 group 整体 lag 与分区分布
kafka-consumer-groups.sh --bootstrap-server broker1:9092 --describe --group <group>
# 2. 看 group 状态(是否在 rebalance)
kafka-consumer-groups.sh --bootstrap-server broker1:9092 --describe --group <group> --state
# 3. 看成员与分配(谁分到了哪些分区)
kafka-consumer-groups.sh --bootstrap-server broker1:9092 --describe --group <group> --members --verbose
# 4. 看 topic 各分区最新位点(算生产速率)
kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server broker1:9092 --topic <topic> --time -1
# 5. 看 topic 各分区最早位点(算保留量)
kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server broker1:9092 --topic <topic> --time -2
# 6. 看消费者日志里的 rebalance / poll 超时
grep -E "Rebalance|Max poll|heartbeat" consumer.log | tail -50
# 7. 看分区副本是否同步(ISR 不足也会拖慢消费)
kafka-topics.sh --bootstrap-server broker1:9092 --describe --topic <topic>
# 8. 看消费者线程堆栈(确认卡在哪)
jstack <pid> | grep -A 20 "consumer-"
```
## 九、积压已经很大了,怎么安全地追上
lag 上百万时,"重启消费者"往往会让情况更糟:重启触发 rebalance,所有分区重新分配,消费从零开始预热,lag 继续上涨。
正确的追平顺序:
1. **先定位根因**,确认是 rebalance、下游慢还是热点分区,别盲目扩容
2. **临时提高并行度**:如果分区数够,加消费者实例;如果分区数不够,只能扩分区(注意扩分区会改变 key 的路由,可能引入新的倾斜)
3. **降低单批处理成本**:调小 `max.poll.records`,把批量写库改成真正的批量(`insert ... values (...),(...)`)
4. **必要时跳过历史消息**:`kafka-consumer-groups.sh --reset-offsets --to-latest --execute`,前提是业务允许丢历史
5. **追平后回归配置**:把临时调大的参数改回去,否则下次故障时更难判断
第 4 条要特别谨慎。跳过 offset 是不可逆的,必须先确认这些消息是否已通过其他途径补偿(比如落库的 dead letter 表),否则就是数据丢失。
## 十、决策表
| 现象 | 判断 | 处理 |
|---|---|---|
| lag 阶跃上涨,消费速率正常 | rebalance 或分区卡住 | 查 `--state`、查消费者重启次数 |
| lag 线性上涨,消费速率正常 | 生产端提速 | 扩容或限流生产端 |
| lag 上涨 + 消费速率下降 + 下游 RT 升高 | 下游阻塞 | 优化下游,别加消费者 |
| 部分分区 lag 高、部分为 0 | 分配不均或分区数不足 | 扩分区或换分配策略 |
| 单个分区 lag 远高于其他 | 热点分区(key 倾斜) | 换 key 或拆 topic,加消费者无效 |
| 频繁 `Max poll interval` 超时 | 单批处理过久 | 调小 `max.poll.records`,调大 `max.poll.interval.ms` |
| lag 显示正常但业务有未处理消息 | 自动提交掩盖进度 | 改手动提交,处理完再提交 |
| ISR 副本不足 | broker 侧问题 | 先修 broker,与消费者无关 |
一句话总结:**lag 是结果,不是原因。先看曲线形状和消费速率,再决定是加机器还是改配置——用错方向,加十台机器也只是让 rebalance 来得更热闹。**
- 点赞
- 收藏
- 关注作者
评论(0)