Kafka consumer lag 一直涨,扩 consumer 也没用

扩了一堆 consumer pod,lag 还在往上走。瓶颈基本不是 consumer 不够多,而是 partition 数、poison message、慢下游写入、或者 rebalance 抖动。

一个 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 / rebalancerebalance 抖动或 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 failedMember ... sending LeaveGroupkafka-consumer-groups.sh 看 group 状态在 StablePreparingRebalance 之间反复横跳。

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.recordsmax.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=consumerpartition.assignment.strategy 就不再可用了。分配改在服务端进行,由 broker 配置 group.consumer.assignors 控制(默认 uniform,尽量把 partition 摊得最均匀;另一个选项是 range)。心跳和 session 超时也一并挪到服务端,由 group.consumer.heartbeat.interval.msgroup.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 的同时还在掉队。用三条来确认:

  1. lag 在缩小,而不只是稳住。 隔一分钟再跑一次 kafka-consumer-groups.sh --describe --group orders-consumerLAG 总和应该在往下走。如果它在一个很高的数上持平,说明你只是让进出打平了,并没有把积压排掉。短期内多扩一点把它追回来。
  2. 没有单个 partition 当长尾。 LAG 应该在各 partition 之间大致均匀。还留着一个热 partition,说明 poison message 或倾斜的修复没完全落地。
  3. 没有新的 rebalance。 tail 几分钟 consumer 日志,应该看不到新的 leaving groupAttempt to heartbeat failedPreparingRebalance。整个观察窗口里 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=Fetchkafka.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,才是真正要管的症状。

标签: #后端 #排查 #infra #kafka #messaging #consumer-lag #streaming