欢迎光临
我们一直在努力

Kafka 消息积压 100 万条:消费者组 rebalance 的 4 个触发条件与 3 步定位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • Kafka 消息积压 100 万条:消费者组 rebalance 的 4 个触发条件与 3 步定位
    • 一、先说那个最容易误判的现象
    • 二、rebalance 到底什么时候会发生
      • 触发条件 1:组成员数量变化
      • 触发条件 2:订阅的 topic 元数据变化
      • 触发条件 3:两次 poll 的间隔超过 `max.poll.interval.ms`
      • 触发条件 4:心跳真的发不出去
    • 三、3 步定位
      • 第 1 步:确认积压是"慢"还是"停"
      • 第 2 步:确认是不是真的在 rebalance
      • 第 3 步:区分是被"踢"还是"主动加入"
    • 四、怎么改,以及各自的代价
    • 五、带走物

Kafka 消息积压 100 万条:消费者组 rebalance 的 4 个触发条件与 3 步定位

一、先说那个最容易误判的现象

线上最常见的报警长这样:某个 topic 的积压量一路涨到几十万、上百万条,消费者像死掉一样一动不动。上去重启一下服务,积压开始往下掉,掉到一半又停住,然后继续涨。

第一反应通常是"消费能力不够",于是加消费者实例、加 Pod。但加完之后问题照旧——因为多出来的消费者根本没分到分区,它们只是空转。

真正的原因往往在别处:这个消费组正在反复 rebalance,或者消费者已经被 coordinator 踢出了组,只是进程还活着、连接还在,看起来"很健康"。

这类问题我在排查时会先确认一件事:积压期间,分区到底有没有被分配出去。

# 看每个分区的当前 offset、末端 offset、积压量,以及谁在消费
kafka-consumer-groups.sh –bootstrap-server 10.0.0.1:9092 \\
–describe –group order-service

输出里如果 CONSUMER-ID 那一列是空的、或者一会儿有一会儿没有,那就不是"消费慢",而是分区被反复收回再分配。消费慢和拿不到分区,是两条完全不同的排查路线。

二、rebalance 到底什么时候会发生

Kafka 的消费组里,分区要分配给成员。只要"成员集合"或者"订阅关系"发生变化,就需要重新分配,这个重新分配的过程就是 rebalance。触发它的一共有四类情况。

触发条件 1:组成员数量变化

这是最直观的一类:

  • 手动扩缩容消费者实例;
  • 发布时的滚动重启(旧 Pod 退出、新 Pod 加入,一进一出算两次变化);
  • 消费者进程被 OOM 杀掉、被 K8s 驱逐;
  • 消费者优雅退出时没有主动 LeaveGroup,coordinator 要等 session.timeout.ms 超时才发现它走了。

滚动发布是最常见也最容易被忽略的触发源。 一个 6 实例的消费者组做一次滚动发布,如果每个 Pod 都是"先杀后起",就会连续触发 6 次以上的 rebalance,每次 rebalance 期间整个组的消费都是暂停的。

触发条件 2:订阅的 topic 元数据变化

  • 订阅用的是正则(比如 order-.*),新建了一个匹配该正则的 topic;
  • topic 的分区数被扩过(kafka-topics.sh –alter –partitions);
  • 订阅关系本身被改了。

分区数变化尤其隐蔽:运维为了提升吞吐扩了分区,结果消费组立刻进入一轮 rebalance,而如果消费者数量没跟着涨,扩出来的分区反而没人消费。

触发条件 3:两次 poll 的间隔超过 max.poll.interval.ms

这一条是"重启后又能消费,过一会儿又停"的标准答案。

Kafka 用两个参数判断消费者是否健康:

参数默认值谁在维护判的是什么
session.timeout.ms 45s(旧版 10s) 后台心跳线程 消费者进程还活着吗
max.poll.interval.ms 5 分钟 业务线程 消费者还在正常调 poll() 吗

关键点在于:从 0.10.1.0 开始,心跳是后台线程发的,所以哪怕你的业务处理卡了 10 分钟,session.timeout.ms 也不会超时——心跳线程照发不误。真正会超时的是 max.poll.interval.ms:它衡量的是两次 poll() 调用之间隔了多久。

一旦单次业务处理(或者一次失败重试、一次批量落库)超过 5 分钟,coordinator 就判定这个消费者"hung 住了",直接把它踢出组、收回分区、触发 rebalance。

被踢出去的那个消费者此时进程还在、日志也不报错,它只是拿着已经不属于自己的分区在慢慢处理,处理完再提交 offset 会被拒绝。表现出来就是:消费停了,但服务看起来一切正常。

这才是"重启后好一阵、过一会儿又不行"的完整链路:重启 → 重新加入组 → 拿到分区 → 开始消费 → 某批消息处理超过 5 分钟 → 被踢 → 消费停 → 积压继续涨。

触发条件 4:心跳真的发不出去

  • JVM Full GC 停顿超过 session.timeout.ms(Go 服务对应的是长时间 STW 或者调度延迟);
  • 网络抖动、broker 侧压力过大导致心跳请求超时;
  • 消费者所在节点 CPU 被打满,心跳线程抢不到调度。

这一类在 Go 服务里相对少见(没有 JVM GC 那种秒级停顿),但容器 CPU throttling 是等效的风险源:如果 limits.cpu 设得太紧,进程会被内核限流,心跳照样发不出去。

三、3 步定位

第 1 步:确认积压是"慢"还是"停"

# 连续采几次,看 LAG 是缓慢下降还是完全不动
watch -n 5 'kafka-consumer-groups.sh –bootstrap-server 10.0.0.1:9092 \\
–describe –group order-service | awk "{print \\$1, \\$2, \\$5, \\$6}"'

  • LAG 缓慢下降 → 消费慢,去查处理耗时、批量大小、下游依赖;
  • LAG 完全不动、或者周期性归零后又涨 → 几乎可以断定是 rebalance。

第 2 步:确认是不是真的在 rebalance

三个地方能看到:

# 1) 消费者日志(最直接的证据)
grep -E "Rebalance|revoked|assigned|Attempt to heartbeat failed" consumer.log

# 2) broker 侧 coordinator 日志
grep -i "Preparing to rebalance group\\|Stabilized group" server.log

# 3) 消费者 JMX / 客户端指标
# rebalance-total、rebalance-rate-per-hour、last-rebalance-seconds-ago

用 sarama 的话,可以在 ConsumerGroupHandler 里直接打点,把每次 rebalance 都记下来:

type handler struct{}

// Setup 在每次 rebalance 之后、消费开始前调用
func (h *handler) Setup(sarama.ConsumerGroupSession) error {
log.Printf("rebalance: 拿到分区 %v, generation=%d",
s.Claims(), s.GenerationID())
rebalanceCounter.Inc()
return nil
}

// Cleanup 在分区被收回时调用
func (h *handler) Cleanup(sarama.ConsumerGroupSession) error {
log.Printf("rebalance: 分区被收回, generation=%d", s.GenerationID())
return nil
}

只要 rebalanceCounter 在积压期间持续上涨,第 2 步就确认了。

第 3 步:区分是被"踢"还是"主动加入"

看被踢之前有没有这些日志:

  • 出现 Attempt to heartbeat failed since group is rebalancing → 有成员进出;
  • 出现 max.poll.interval.ms exceeded / (Re-)joining group 但业务线程还在跑 → 触发条件 3;
  • 消费者没有任何异常日志,但 coordinator 侧记录成员离开 → 触发条件 4(心跳超时)。

判定的分水岭就一句话:业务线程有没有真的卡住超过 5 分钟。 卡住了就是 max.poll.interval.ms,没卡住就是心跳或成员变化。

四、怎么改,以及各自的代价

方案解决哪一类代价
减小 max.poll.records(默认 500) 触发条件 3 单次拉取变少,吞吐略降,但最安全
把重活挪到线程池异步做,poll 线程只做拉取和分发 触发条件 3 必须自己管 offset,否则会丢消息
调大 max.poll.interval.ms 触发条件 3 只是拖延,处理慢的根因没解决,只是把 5 分钟改成 10 分钟
滚动发布改成先起后停 / 调大 maxSurge 之外的摘除窗口 触发条件 1 发布期间会短暂超配
退出时主动 LeaveGroup(优雅关闭里调 Close()) 触发条件 1 依赖优雅停机链路是通的
放宽容器 limits.cpu,避开 throttling 触发条件 4 成本上升

我的建议顺序:先把单次处理时间压下来(max.poll.records 调小 + 重活异步化),再考虑调参数。直接把 max.poll.interval.ms 调到 10 分钟是最常见的"治标"操作——它能让报警消失,但你的消费者依然会在每个小时里停止消费若干分钟。

五、带走物

一个可以直接贴到 runbook 里的排查清单:

1. kafka-consumer-groups.sh –describe 看 LAG 与 CONSUMER-ID
└─ LAG 不动 / CONSUMER-ID 时有时无 → 进入第 2 步
2. grep Rebalance 日志 + rebalance 指标
└─ 确认在 rebalance → 进入第 3 步
3. 看业务线程是否卡住 > max.poll.interval.ms(默认 5min)
├─ 是 → 压单次处理耗时(调小 max.poll.records / 异步化)
└─ 否 → 查成员进出(发布?扩缩容?)和心跳(CPU throttling?GC?)
4. 复盘中记录:触发原因 + 证据 + 动作,避免下次从零排查

另外记一条容易被忽略的:分区数 > 消费者数时,多出来的消费者是空闲的。加实例前先看分区数,不然加了也白加。

赞(0)
未经允许不得转载:171主机测评 » Kafka 消息积压 100 万条:消费者组 rebalance 的 4 个触发条件与 3 步定位
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址