一个 topic 每秒生产 5 万条消息。你的 consumer group 之前还跟得上,流量翻倍之后 lag 就开始爬,你把 consumer Deployment 从 8 个 pod 扩到 24 个。lag 还是涨,部分 pod 现在 CPU 在 0%。Kafka UI 显示 group 是 Stable,但从 produce 到 commit 的端到端延迟从 200 ms 涨到 14 分钟,还在涨。
最快的修法: 先跑 kafka-consumer-groups.sh --describe,看每个 partition 的 LAG 那一列。如果 lag 全堆在 1-2 个 partition 上,那是 poison message 或者 producer 倾斜(别加 pod)。如果 lag 均匀但 pod CPU 很低,瓶颈是下游写入(去 batch 它)。如果 pod 数比 partition 数还多,加 pod 完全没用。只有当 consumer 是 CPU bound 而且还没吃满时,加 consumer 才有用,而这是少数情况。
这篇讲清楚真正的几种瓶颈、怎么用你手上已有的指标把它们区分开、以及每一种对应的确切命令或配置。示例用的是 Kafka 自带的 kafka-consumer-groups.sh / kafka-topics.sh 命令行工具(Apache Kafka 4.3,2026 年 5 月 22 日发布,截至 2026 年 6 月是当前的稳定线)和一个 Node 风格的 consumer 循环,但诊断思路对任何客户端都适用。
你属于哪一类
两个指标一分钟内就能缩小范围:每个 partition 的 LAG 分布、以及 consumer pod 的 CPU。
| 你看到的现象 | 最可能的原因 | 跳到 |
|---|---|---|
| lag 集中在 1-2 个 partition,其他接近 0;日志里同一 offset 反复重试 | poison message | 原因 2 / 第 3 步 |
| lag 集中在 1-2 个 partition,某个 partition 的 produce 速率远高于其他 | producer 倾斜 | 原因 6 / 第 6 步 |
| lag 在各 partition 均匀,pod CPU 低于约 30% | 慢下游写入 | 原因 3 / 第 5 步 |
| lag 均匀,pod 数比 partition 多,部分 pod 闲着 | partition 用完了 | 原因 1 / 第 2 步 |
lag 一会儿涨一会儿掉;日志里有 leaving group / rebalance | rebalance 抖动或 max.poll.records 太高 | 原因 4-5 / 第 4 和第 7 步 |
| consumer 网络吞吐远低于 partition 的 produce 速率,CPU 全在解压上 | 压缩 / fetch 大小 | 原因 7 |
常见原因
按命中率从高到低排。
1. consumer 数比 partition 数还多
一个 partition 在同一个 group 内只能被一个 consumer 消费。topic 有 12 个 partition,你开 24 个 consumer,那有 12 个就是闲着。继续加完全没意义。
怎么发现:kafka-consumer-groups.sh --describe --group orders-consumer 看 partition、current-offset、consumer-id。如果好几个 consumer-id 没分到 partition,瓶颈就是 partition 数。
2. 有一条 poison message 把某个 partition 卡住
某个 partition 里有一条消息处理失败,consumer 就一直重试它(或者长 backoff 后重试),永远不 commit 越过它。后面所有消息都在排队。其他 partition 看着都正常,只有这一个堵了。
怎么发现:group 的 lag 集中在 1-2 个 partition 上,其他都是 0。日志里同一个 offset 在反复重试。
3. commit 之前要等慢下游写完
consumer 拉得很快,但每条消息处理都要同步写一个慢下游(Postgres insert、外部 API、embedding 模型调用)。端到端吞吐等于下游吞吐,跟 Kafka 没关系。consumer pod CPU 一直很低。
怎么发现:consumer CPU 不到 30%,下游服务的 p99 远高于你这一条消息的预算(大概是 messages_per_second / num_partitions)。
4. rebalance 在反复抖
每次扩容或者 pod 重启,Kafka 都会暂停整个 group、重新分配 partition、再恢复。如果 max.poll.interval.ms 太短或者消息处理特别慢,rebalance 就会不停触发。group 大部分时间在 rebalance 而不是在消费。
怎么发现:consumer 日志里反复出现 Attempt to heartbeat failed 或 Member ... sending LeaveGroup。kafka-consumer-groups.sh 看 group 状态在 Stable 和 PreparingRebalance 之间反复横跳。
5. max.poll.records 调得太高
一次 poll 5000 条,处理时间超过了 max.poll.interval.ms(默认 5 分钟)。因为这段时间内没有再调 poll(),客户端就发了一个 LeaveGroup,group rebalance,这批活又得重来。lag 上下剧烈震荡。
怎么发现:lag 一会儿掉一会儿冲,反复。日志里有那条确切的告警 Maximum poll interval (300000ms) exceeded by 1532ms (adjust max.poll.interval.ms for long-running message processing): leaving group(其中 adjust max.poll.interval.ms... 这段提示是 Kafka 4.x 给这条消息加上的),紧接着下一次 commit 时还会看到 Auto-offset-commit failed 或者 This consumer instance is no longer part of the group。
6. producer 倾斜——所有消息都落在一个 partition
producer 用的 key hash 到了少数几个 partition,或者完全没 key、配 sticky partitioner 又赶上突发流量。某一个 partition 拿到 80% 的流量。无论开多少 consumer,那个 partition 只能被其中一个吃。
怎么发现:kafka-topics.sh --describe --topic orders 加上各 partition 的 produce 监控。如果一个 partition 的 produce 速率是其他的 10 倍,就是倾斜。
7. producer 和 consumer 的压缩配置不匹配
producer 用 zstd 发,consumer 的 fetch.max.bytes 太小,一次装不下一个完整的解压 batch。结果就是 consumer 拉一小撮、解压、处理、再拉,吞吐崩盘。
怎么发现:consumer 这边的网络吞吐远低于 partition 实际生产的量,CPU 大头都在解压上。
最短修复路径
第 1 步:先量清楚 lag 到底在哪儿
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
--describe --group orders-consumer
重点看每个 partition 的 LAG 那一列。如果 99% 的 lag 都在一个 partition 上,那就是 poison message 或者 producer 倾斜。如果 lag 在所有 partition 上均匀分布,那是吞吐问题。
第 2 步:对比一下 partition 数和 consumer 数
kafka-topics.sh --bootstrap-server kafka:9092 --describe --topic orders
如果 partition < consumer,再扩 consumer 是浪费。先加 partition:
kafka-topics.sh --bootstrap-server kafka:9092 \
--alter --topic orders --partitions 48
partition 数只能加不能减。挑一个能给你 2-4 倍扩容余量的数。
第 3 步:用 dead letter 模式处理 poison message
在 consumer 里给每条消息设一个重试预算。失败 N 次之后扔到 DLQ topic,然后 commit 往前走。
try {
await processMessage(message);
} catch (err) {
const attempts = (message.headers?.attempts ?? 0) + 1;
if (attempts >= 3) {
await producer.send({
topic: 'orders.dlq',
messages: [{ ...message, headers: { ...message.headers, attempts, lastError: err.message } }]
});
} else {
throw err; // 下一次 poll 会重试
}
}
await consumer.commitOffsets([{ topic, partition, offset: message.offset + 1 }]);
绝对不要让单条坏消息永远堵住一个 partition。
第 4 步:max.poll.records 和 max.poll.interval.ms 一起调
关系是:max.poll.records * 单条平均处理时间 < max.poll.interval.ms。
max.poll.records: 500
max.poll.interval.ms: 300000 # 5 分钟
session.timeout.ms: 45000
heartbeat.interval.ms: 3000
batch 小一点 commit 更勤,handler 慢也不会被踢。
第 5 步:如果是下游瓶颈,就 batch 写
不要每条消息一次 DB insert,攒 200 条做一次 bulk insert。bulk insert 成功之后再 commit。
const batch = [];
for await (const message of consumer) {
batch.push(message);
if (batch.length >= 200) {
await db.bulkInsert(batch.map(parse));
await consumer.commitOffsets(lastOffsetFor(batch));
batch.length = 0;
}
}
这一步通常是收益最大的一步。
第 6 步:换更好的 partition key 修 producer 倾斜
如果你的 key 是 userId,而 0.1% 的用户产生了 50% 的事件,那 partition 永远会倾斜。要么换 key(event id,或者能把负载摊开的复合 key),要么把热用户显式 shard。
const key = isHotUser(userId) ? `${userId}:${randomShard()}` : userId;
producer.send({ topic, messages: [{ key, value }] });
第 7 步:用对协议来减轻 rebalance
默认的 range 和 round-robin assignor 一 rebalance 就 stop-the-world:每个成员把所有 partition 全交回去,整个 group 同步,再重新分配。一个 pod 重启就能把整个 group 停住。
有两种修法,选哪种取决于你的 Kafka 版本。
如果你在 Apache Kafka 4.0 或更新版本上(4.0 于 2025 年 3 月发布,4.3 是截至 2026 年 6 月的当前稳定线): 把 consumer 切到 KIP-848 的新 rebalance 协议——它在 4.0 已经正式 GA。它把分配工作挪到了 broker 端的 group coordinator,而且是完全增量的,所以没受影响的成员在后台 rebalance 期间仍能继续消费。Confluent 和 Instaclustr 实测大 group 的 rebalance 大致快了一个数量级(比如一个 10 consumer 的 group 吸收 900 个新 partition,从 103 秒降到约 5 秒)。
group.protocol: consumer # KIP-848 新协议;默认仍是 "classic"
注意:一旦设了 group.protocol=consumer,partition.assignment.strategy 就不再可用了。分配改在服务端进行,由 broker 配置 group.consumer.assignors 控制(默认 uniform,尽量把 partition 摊得最均匀;另一个选项是 range)。心跳和 session 超时也一并挪到服务端,由 group.consumer.heartbeat.interval.ms 和 group.consumer.session.timeout.ms 控制,所以第 4 步里那些客户端侧的 heartbeat.interval.ms / session.timeout.ms 只在 classic 协议下才生效。broker 和客户端都必须支持新协议,所以等集群升到 4.0+ 之后再灰度上线。完整配置清单见官方的 Kafka consumer rebalance protocol 文档。
如果你还卡在 classic 协议上(Kafka 3.x,或者客户端还不支持 group.protocol=consumer): 用 cooperative-sticky assignor,它在 rebalance 时会尽量原地保留大部分分配,而不是把全部交回去。
partition.assignment.strategy: org.apache.kafka.clients.consumer.CooperativeStickyAssignor
不管哪种,扩缩容都不再让整个 group 停摆。
怎么确认修好了
别只信 UI 上的 Stable:一个 group 可以是 Stable 的同时还在掉队。用三条来确认:
- lag 在缩小,而不只是稳住。 隔一分钟再跑一次
kafka-consumer-groups.sh --describe --group orders-consumer,LAG总和应该在往下走。如果它在一个很高的数上持平,说明你只是让进出打平了,并没有把积压排掉。短期内多扩一点把它追回来。 - 没有单个 partition 当长尾。
LAG应该在各 partition 之间大致均匀。还留着一个热 partition,说明 poison message 或倾斜的修复没完全落地。 - 没有新的 rebalance。 tail 几分钟 consumer 日志,应该看不到新的
leaving group、Attempt to heartbeat failed或PreparingRebalance。整个观察窗口里 group 状态应该一直保持Stable。
一条把 LAG 那一列加总的小命令。--describe 的输出列依次是 GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID,所以 LAG 是第 6 列:
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
--describe --group orders-consumer \
| awk 'NR>1 && $6 ~ /^[0-9]+$/ {sum += $6} END {print "total lag:", sum}'
加上 $6 ~ /^[0-9]+$/ 这个判断是为了跳过表头、以及那些 LAG 列是 - 的行(一个还没有提交过 offset 的 partition,LAG 列会打印成 -)。
这种情况不怪你
broker 端的限流会卡住整个 consumer group,不管你客户端怎么调。如果集群配了 per-client 的 consumer_byte_rate quota 并且你撞上了,再怎么调 consumer 都没用。看 kafka.server:type=Fetch 和 kafka.server:type=ClientQuotaManager 的 JMX 指标,或者问下管集群的人。
集群本身资源不够也是真实原因:broker 磁盘或者网络打满,fetch 就慢,跟你 consumer 数没关系。
容易被误诊成
“consumer 不够多”。前三次扩容这话是对的。再往后基本就是撞到 partition 上限了,或者瓶颈已经移到下游。扩 pod 之前先看每个 partition 的 lag 分布。
另一个常见的:怪 Kafka 自己慢。Kafka broker 在便宜硬件上每秒能扛几百万条。你这边吞吐才几万还撑不住,瓶颈几乎一定在 consumer 代码或者下游服务。
预防
- 一开始就按最大可能的 consumer 数 × 2-4 倍来规划 partition 数。后期加 partition 会破坏在途数据的 key 序。
- DLQ 从第一天就接上。poison message 一定会出现。
- 监控按 partition 分的 lag,不要只看 group 总 lag。平均值会把 bug 藏起来。
- Kafka 4.0+ 上,新建 consumer group 默认用 KIP-848 协议(
group.protocol=consumer);3.x 上默认用 cooperative-sticky assignor。classic 协议已经在弃用路线上(KIP-1274),现在就迁过去可以省掉以后被迫迁移的麻烦。 - consumer CPU 和下游写延迟当成两个独立信号看;lag 涨但 CPU 没涨,瓶颈在下游。
FAQ
- 能减少 partition 数来修倾斜吗? 不行。partition 数只能加。要修就只能新建一个 partition 更多的 topic,双写或者 replay 进去,再把 consumer group 迁过去。
- 每个 consumer 都同步 commit 吗? 同步 commit 更安全但更慢。常见做法是正常处理时异步 commit,在关停和 rebalance 时做一次同步 commit,避免重复处理。
- 加 partition 能立刻清掉现有的 lag 吗? 不能。新 partition 只接新消息。已经积在老 partition 里的那批积压,还是得由负责那些 partition 的 consumer 去排。加 partition 修的是未来的吞吐,不是当前的积压。
- KIP-848 协议自己能修好 lag 吗? 不能。它让 rebalance 变快、变成非阻塞,所以它能消掉由 rebalance 抖动(原因 4 和 5)造成的那部分 lag。对 poison message、慢下游、producer 倾斜它什么也做不了。
- 我的 group 显示
Stable但 lag 一直涨,是 Kafka 在骗我吗? 不是。Stable只表示当前没有 rebalance 在进行。一个完全 stable 的 group 照样可以掉队,因为瓶颈是某个下游写入或者卡住的 partition。永远看每个 partition 的LAG,别只看 group 状态。 - 有个 partition lag 是 0 但没分给任何 consumer,为什么? 那个 partition 没有新数据,或者你的 consumer 数比 partition 多、有些 consumer 一个都没分到。跑
kafka-consumer-groups.sh --describe;一个有 lag 的 partition 对应空的CONSUMER-ID,才是真正要管的症状。
Related
- 消息队列 dead letter 堆积
- RabbitMQ consumer 卡住
- GraphQL rate limit 级联
- Postgres connection pool 被打爆
- Postgres autovacuum 被长事务卡住
- Redis cluster failover 卡住
- Docker OOM killed
- gRPC deadline exceeded
- Cron job 静默跳过
- Webhook 不触发
标签: #后端 #排查 #infra #kafka #messaging #consumer-lag #streaming