本页目录

消费组与协作

consumer group 是消费者对分区所有权的协商——本质是一个分布式共识变体。eager vs cooperative rebalance 决定了再平衡时整个 group 是停止还是继续消费。

概述

Kafka 架构讲了 partition 和 ISR——这是存储/复制层的分布式机制。但消费者一侧同样有分布式协调问题:多个消费者共享一个 topic 的分区,谁消费哪个分区?有人挂了怎么重新分配?分配期间消费停不停?这是消费组(consumer group)⁠解决的问题——本质是"一组消费者对分区的所有权协商",一个不折不扣的分布式共识变体。

Consumer group 是怎么工作的

Kafka 的消费模型:⁠一个 partition 在一个 consumer group 内只能被一个 consumer 消费。 这是保证"同 partition 内消息有序"的前提——如果两个 consumer 同时读同一个 partition,它们各自看到的 offset 区间不连续,顺序就乱了。

Consumer Group 分配:6 个 partition 分给 3 个 consumer 分配前(rebalance 中) 分配后 P0 ?? P1 ?? P2 ?? P3 ?? P4 ?? P5 ?? consumer 尚不持有任何 partition C1 C2 C3 等待 group coordinator 计算新分配 P0 P1 C1 P2 P3 C2 P4 P5 C3 一个 partition 在一个 group 内只能被一个 consumer 消费——6 个 partition 决定并行度上限,第 7 个 consumer 加入即闲置。

关键:partition 数决定并行度上限。6 个 partition → 最多 6 个 consumer;第 7 个 consumer 加入后会闲置⁠(没有 partition 分配给它)。⁠要扩展消费并行度就要加 partition,不是加 consumer。

再平衡(rebalance):所有权的协商

当 consumer 加入或离开 group 时,触发再平衡——重新分配 partition 所有权。这是 consumer group 机制里最关键也最容易出问题的一环。

eager rebalance(旧协议):停掉再分

1. 新 consumer C4 加入 → group coordinator 通知所有 consumer: 释放当前分配
2. 所有 consumer 停止消费、释放 partition 所有权
3. 重新分配: C1←P0,P1  C2←P2  C3←P3,P4  C4←P5
4. 所有 consumer 从上次 committed offset 恢复消费

这个过程叫 stop-the-world:步骤 2-3 期间整个 consumer group 停止消费⁠,延迟堆积。对于延迟敏感的流处理应用,每次 rebalance 都是事故。

cooperative rebalance(新协议,KIP-429):渐进式

1. C4 加入 → coordinator 计算新分配,只通知"需要释放 partition"的 consumer
2. 受影响的 consumer 释放被夺走的 partition,继续消费未受影响的 partition
3. 新 owner(C4)接手释放的 partition
4. 整个过程中,未受影响的 consumer 不停

本质差别:eager 让所有人释放再重新分配;cooperative 只动受影响的,其余不停。⁠从"全部停"变成"部分不停",大幅降低 rebalance 的业务影响。

sticky assignment:尽量保持已有分配

rebalance 后的分配不仅看"均匀",还要看"最小变动":如果一个 consumer 已经在消费某个 partition 且不需要释放,就保持不动⁠。这叫 sticky——避免无谓的分配变动,减少 consumer 状态切换。

offset 管理:消费进度的"持久化指针"

consumer 读到哪儿了,需要记下来——这就是 offset。管理方式有两种:

自动提交手动提交
时机按时间间隔(如每 5 秒)自动提交当前 offset应用程序显式调用 commitSync() / commitAsync()
风险process 完成但未到提交间隔 → crash → 重复消费(at-least-once)process 完成 → commit → crash → 不重复; process 完成后但 commit 前 crash → 重复
适用容忍少量重复需要精细控制(如 exactly-once 消费流程)

offset 存在哪儿⁠:Kafka 的 __consumer_offsets 内部 topic(多 partition、有 replication),consumer group 的 offset commit 是一条写进这个 topic 的消息——Kafka 用自己的存储机制存消费进度。

exactly-once 消费:把消费端的事务拼起来

消息语义讲了 producer 端的 exactly-once(幂等 producer + 事务)。消费端怎么做 exactly-once?核心是把消费 offset 的提交处理结果的输出放在同一个事务里:

消费者事务:
  1. 从 topic-A 读消息 (offset=N)
  2. 处理 → 写结果到 topic-B
  3. 提交 offset(N+1) 到 __consumer_offsets
  步骤 2 和 3 在同一个 Kafka 事务里:
    - 事务成功: 结果写入 topic-B, offset 前进到 N+1 → 消费"发生过"
    - 事务失败/消费者 crash: 结果未写入, offset 回滚到 N → 消息会被重新消费

关键要求:消费的 isolation.level 必须设为 read_committed——只读已提交事务的消息,忽略 abort 的事务和未完成的 dangling 事务。

这整套组合(幂等 producer + 事务 producer + read_committed consumer + offset 和输出在同一事务)构成了 Kafka 的 exactly-once 语义——不是理论上的"完美仅一次",而是工程上"效果等同于仅一次,不会出现重复或丢失"。

RabbitMQ 的对比:队列模型 vs 日志模型

同一章里的另一套思路:RabbitMQ 用队列而非日志。消息被确认后删除⁠(不是 append 到不可变 log),没有 replay/回溯,没有 partition 内的全局顺序。consumer group 协调也不同:RabbitMQ 的 consumer 不协商 partition 所有权,而是 broker 按 round-robin 或 consistent-hash exchange 分发消息——更简单,但没有 Kafka 的"同 key 到同 partition 保证顺序"。

选 Kafka 的日志模型,是因为分布式流处理需要回放(replay)⁠全局有序⁠——这两件是队列模型做不了的。Kafka 的 partition 本质是一个分片的、不可变的、可回放的 log,consumer group 是这个 log 之上的"谁读哪个分片"的协商层。

常见问题与修法

  • 再平衡风暴⁠:consumer 频繁加入/离开(如心跳超时阈值太短、处理时间过长 → 被认为挂了)。每次 rebalance 导致整个 group 停顿。→ 设 max.poll.interval.ms 足够大(大于最长处理时间),用 cooperative rebalance,监控 rebalance 频率。
  • partition 数 < consumer 数⁠:多余的 consumer 闲置 → 要么加 partition,要么接受闲置(consumer 作为热备)。
  • 消费滞后(lag):consumer 处理速度跟不上 producer 写入速度 → 监控 lag(consumer offset vs partition end offset),加 consumer 或加 partition。
  • 自动提交 + 异步处理⁠:consumer 把消息扔进线程池就提交 offset → 异步线程处理失败时消息已"消费完成"→ 丢失。→ 要么同步处理,要么手动 commit 在异步处理完成后。

参考

  • Kafka 文档⁠: Consumer Group Protocol、KIP-429 (Incremental Cooperative Rebalancing)

Keywords: consumer group, rebalance, eager, cooperative (incremental), sticky assignment, stop-the-world, group coordinator, offset management, __consumer_offsets, exactly-once consumption, isolation.level, read_committed, transactional consumer, 消费滞后, consumer lag, rebalance storm, Kafka vs RabbitMQ, log vs queue