消费组工作流程
1. 消费模式概述
1.1 消费模式类型
在消息中间件中,常见的消费模式有两种:
- Poll(拉)模式:消费者主动向服务端拉取消息
- Push(推)模式:服务端主动推送消息给消费者
1.2 Kafka选择Poll模式的原因
Kafka采用Poll模式的核心原因:
- 推模式很难考虑到每个客户端不同的消费速率
- 可能导致消费者无法消费消息而宕机
- Poll模式允许消费者按自己的节奏消费消息
Poll模式的优化:
- 问题:如果服务端没有消息,消费端会一直空轮询
- 解决方案:Kafka做了改进,如果没消息服务端会暂时保持该请求,在一段时间内有消息再回应给客户端
sequenceDiagram
participant Consumer as 消费者
participant Broker as Kafka Broker
Consumer->>Broker: poll()请求消息
alt 有消息可用
Broker-->>Consumer: 返回消息批次
else 无消息可用
Note over Broker: 保持请求一段时间
Broker-->>Consumer: 超时后返回空结果
end
Consumer->>Consumer: 处理消息
Consumer->>Broker: 提交offset
2. 消费者工作流程
2.1 消费者总体工作流程
消费者的核心工作包括:
- 从Kafka集群拉取消息
- 处理消息
- 提交消费位移到
__consumer_offsets
主题
flowchart LR
subgraph "Kafka Cluster"
B1["Broker1<br/>Topic-Partition0"]
B2["Broker2<br/>Topic-Partition1"]
B3["Broker3<br/>Topic-Partition2"]
CO["__consumer_offsets<br/>存储消费位移"]
end
subgraph "Consumer Group"
C1["Consumer1"]
C2["Consumer2"]
C3["Consumer3"]
end
Producer["生产者"] --> B1
Producer --> B2
Producer --> B3
B1 --> C1
B2 --> C2
B3 --> C3
C1 --> CO
C2 --> CO
C3 --> CO
style CO fill:#f9f,stroke:#333,stroke-width:2px
2.2 消费者组原理
Consumer Group(CG):消费者组,由多个consumer组成。形成消费者组的条件是所有消费者的group.id
相同。
核心特性:
- 消费者组内每个消费者负责消费不同分区的数据
- 一个分区只能由一个组内消费者消费
- 消费者组之间互不影响
- 所有的消费者都属于某个消费者组
2.3 消费者与分区关系的各种场景
场景1:消费者数量等于分区数量(理想状态)
flowchart LR
subgraph "Topic: orders (3个分区)"
P0["Partition 0"]
P1["Partition 1"]
P2["Partition 2"]
end
subgraph "Consumer Group A (3个消费者)"
CA1["Consumer A1"]
CA2["Consumer A2"]
CA3["Consumer A3"]
end
P0 --> CA1
P1 --> CA2
P2 --> CA3
style P0 fill:#e1f5fe
style P1 fill:#e1f5fe
style P2 fill:#e1f5fe
style CA1 fill:#f3e5f5
style CA2 fill:#f3e5f5
style CA3 fill:#f3e5f5
场景2:消费者数量少于分区数量
flowchart LR
subgraph "Topic: orders (5个分区)"
P0["Partition 0"]
P1["Partition 1"]
P2["Partition 2"]
P3["Partition 3"]
P4["Partition 4"]
end
subgraph "Consumer Group B (2个消费者)"
CB1["Consumer B1<br/>负责: P0, P1, P2"]
CB2["Consumer B2<br/>负责: P3, P4"]
end
P0 --> CB1
P1 --> CB1
P2 --> CB1
P3 --> CB2
P4 --> CB2
Note1["说明:消费者数量不足时<br/>部分消费者需要处理多个分区"]
style CB1 fill:#ffcdd2
style CB2 fill:#ffcdd2
场景3:消费者数量多于分区数量
flowchart LR
subgraph "Topic: orders (3个分区)"
P0["Partition 0"]
P1["Partition 1"]
P2["Partition 2"]
end
subgraph "Consumer Group C (5个消费者)"
CC1["Consumer C1<br/>消费: P0"]
CC2["Consumer C2<br/>消费: P1"]
CC3["Consumer C3<br/>消费: P2"]
CC4["Consumer C4<br/>空闲状态"]
CC5["Consumer C5<br/>空闲状态"]
end
P0 --> CC1
P1 --> CC2
P2 --> CC3
Note2["说明:消费者数量过多时<br/>多余的消费者将处于空闲状态"]
style CC4 fill:#f5f5f5,stroke:#999,stroke-dasharray: 5 5
style CC5 fill:#f5f5f5,stroke:#999,stroke-dasharray: 5 5
场景4:多个消费组同时消费同一Topic
flowchart LR
subgraph "Kafka Cluster"
subgraph "Topic: orders (4个分区)"
P0["Partition 0<br/>offset: 0,1,2,3,4"]
P1["Partition 1<br/>offset: 0,1,2,3"]
P2["Partition 2<br/>offset: 0,1,2,3,4,5"]
P3["Partition 3<br/>offset: 0,1,2"]
end
CO["__consumer_offsets<br/>存储各组消费位移"]
end
subgraph "Consumer Group A: 订单处理服务"
CA1["Consumer A1<br/>处理: P0"]
CA2["Consumer A2<br/>处理: P1"]
CA3["Consumer A3<br/>处理: P2"]
CA4["Consumer A4<br/>处理: P3"]
end
subgraph "Consumer Group B: 数据分析服务"
CB1["Consumer B1<br/>处理: P0, P1"]
CB2["Consumer B2<br/>处理: P2, P3"]
end
subgraph "Consumer Group C: 审计日志服务"
CC1["Consumer C1<br/>处理: P0, P1, P2, P3"]
end
P0 --> CA1
P1 --> CA2
P2 --> CA3
P3 --> CA4
P0 --> CB1
P1 --> CB1
P2 --> CB2
P3 --> CB2
P0 --> CC1
P1 --> CC1
P2 --> CC1
P3 --> CC1
CA1 --> CO
CA2 --> CO
CA3 --> CO
CA4 --> CO
CB1 --> CO
CB2 --> CO
CC1 --> CO
style CO fill:#f9f,stroke:#333,stroke-width:2px
场景5:动态扩缩容场景
flowchart LR
subgraph "扩容前:2个消费者处理4个分区"
subgraph "Topic: orders"
BP0["Partition 0"]
BP1["Partition 1"]
BP2["Partition 2"]
BP3["Partition 3"]
end
subgraph "Consumer Group (2个消费者)"
BC1["Consumer 1<br/>处理: P0, P1"]
BC2["Consumer 2<br/>处理: P2, P3"]
end
BP0 --> BC1
BP1 --> BC1
BP2 --> BC2
BP3 --> BC2
end
Rebalance["触发Rebalance<br/>新增2个消费者"]
subgraph "扩容后:4个消费者处理4个分区"
subgraph "Topic: orders"
AP0["Partition 0"]
AP1["Partition 1"]
AP2["Partition 2"]
AP3["Partition 3"]
end
subgraph "Consumer Group (4个消费者)"
AC1["Consumer 1<br/>处理: P0"]
AC2["Consumer 2<br/>处理: P1"]
AC3["Consumer 3<br/>处理: P2"]
AC4["Consumer 4<br/>处理: P3"]
end
AP0 --> AC1
AP1 --> AC2
AP2 --> AC3
AP3 --> AC4
end
BC1 -.-> Rebalance
BC2 -.-> Rebalance
Rebalance -.-> AC1
Rebalance -.-> AC2
Rebalance -.-> AC3
Rebalance -.-> AC4
各场景总结:
场景 | 消费者数 | 分区数 | 特点 | 适用场景 |
---|---|---|---|---|
理想状态 | 3 | 3 | 1:1对应,负载均衡 | 稳定的生产环境 |
消费者不足 | 2 | 5 | 部分消费者处理多个分区 | 资源受限或初期部署 |
消费者过多 | 5 | 3 | 部分消费者空闲 | 预留容量或故障恢复 |
多消费组 | 变化 | 4 | 不同业务独立消费 | 微服务架构 |
动态扩缩容 | 2→4 | 4 | 根据负载动态调整 | 弹性伸缩场景 |
3. 消费者组选举Leader
3.1 选举流程概述
消费者组的初始化和管理通过以下流程实现:
- Coordinator定位:通过对GroupId进行Hash确定Coordinator所在的Broker
- Leader选举:Coordinator负责选出消费组中的Leader
- 信息协调:Coordinator协调分区分配等信息
- 位移存储:真正存储消费记录的是
__consumer_offsets
分区
flowchart TD
subgraph "步骤1: 确定Coordinator"
GID["Group ID: test-group"]
Hash["Hash(group.id) % 50"]
Coord["确定Coordinator<br/>Broker2"]
GID --> Hash
Hash --> Coord
end
subgraph "步骤2: 消费者加入组"
C1["Consumer1<br/>发送JoinGroup"]
C2["Consumer2<br/>发送JoinGroup"]
C3["Consumer3<br/>发送JoinGroup"]
C1 --> Coord
C2 --> Coord
C3 --> Coord
end
subgraph "步骤3: Leader选举与分区分配"
Leader["选举Leader<br/>(第一个加入的消费者)"]
Assignment["Leader制定分区分配方案"]
Sync["SyncGroup同步分配结果"]
Coord --> Leader
Leader --> Assignment
Assignment --> Sync
end
subgraph "步骤4: 开始消费"
Start["各消费者开始消费分配的分区"]
Heartbeat["定期发送心跳维持组成员关系"]
Sync --> Start
Start --> Heartbeat
end
3.2 详细选举过程
sequenceDiagram
participant C1 as Consumer1
participant C2 as Consumer2
participant C3 as Consumer3
participant Coord as Coordinator
Note over C1,Coord: 阶段1: 发现Coordinator
C1->>Coord: FindCoordinator请求
Coord-->>C1: 返回Coordinator地址
Note over C1,Coord: 阶段2: 加入消费组
C1->>Coord: JoinGroup请求
C2->>Coord: JoinGroup请求
C3->>Coord: JoinGroup请求
Note over Coord: 选举C1为Leader(第一个加入)
Coord-->>C1: JoinGroup响应(Leader)
Coord-->>C2: JoinGroup响应(Follower)
Coord-->>C3: JoinGroup响应(Follower)
Note over C1: Leader制定分区分配策略
Note over C1,Coord: 阶段3: 同步分配结果
C1->>Coord: SyncGroup(分配方案)
C2->>Coord: SyncGroup(空)
C3->>Coord: SyncGroup(空)
Coord-->>C1: SyncGroup响应(分配结果)
Coord-->>C2: SyncGroup响应(分配结果)
Coord-->>C3: SyncGroup响应(分配结果)
Note over C1,Coord: 阶段4: 维持心跳
loop 定期心跳
C1->>Coord: Heartbeat
C2->>Coord: Heartbeat
C3->>Coord: Heartbeat
Coord-->>C1: Heartbeat响应
Coord-->>C2: Heartbeat响应
Coord-->>C3: Heartbeat响应
end
3.3 关键参数配置
参数名称 | 默认值 | 说明 |
---|---|---|
heartbeat.interval.ms | 3000ms | 消费者和coordinator之间的心跳时间 |
session.timeout.ms | 45000ms | 连接超时时间,超过该值消费者被移除 |
max.poll.interval.ms | 300000ms | 消费者处理消息的最大时长 |
partition.assignment.strategy | Range + CooperativeSticky | 分区分配策略 |
4. 分区分配策略及再平衡
4.1 分区分配策略概述
核心问题:一个Consumer Group中有多个Consumer,一个Topic也有多个Partition,如何确定哪个Partition由哪个Consumer来消费?
Kafka提供的分配策略:
- RangeAssignor:范围分配策略
- RoundRobinAssignor:轮询分配策略
- StickyAssignor:粘性分配策略
- CooperativeSticky:协作粘性分配策略
4.2 Range分配策略
特点:针对单个Topic进行分区分配
算法:
- 将分区按数字顺序排列
- 将消费者按字典序排列
- 用分区数除以消费者数,得到每个消费者应分配的分区数
- 余数分区分配给前面的消费者
flowchart TD
subgraph "Range分配策略示例"
subgraph "Topic: orders (7个分区)"
P0["Partition 0"]
P1["Partition 1"]
P2["Partition 2"]
P3["Partition 3"]
P4["Partition 4"]
P5["Partition 5"]
P6["Partition 6"]
end
subgraph "3个消费者"
C1["Consumer1<br/>分配: P0,P1,P2"]
C2["Consumer2<br/>分配: P3,P4"]
C3["Consumer3<br/>分配: P5,P6"]
end
P0 --> C1
P1 --> C1
P2 --> C1
P3 --> C2
P4 --> C2
P5 --> C3
P6 --> C3
Note1["计算: 7÷3=2余1<br/>前1个消费者多分配1个分区"]
end
Range再平衡过程:
flowchart TD
subgraph "初始状态"
IC1["Consumer1: P0,P1,P2"]
IC2["Consumer2: P3,P4"]
IC3["Consumer3: P5,P6"]
end
subgraph "Consumer1故障"
FC1["Consumer1: 离线"]
FC2["Consumer2: P3,P4"]
FC3["Consumer3: P5,P6"]
Note2["45s内: P0,P1,P2暂时无人消费"]
end
subgraph "重新分配(45s后)"
RC1["Consumer1: 已移除"]
RC2["Consumer2: P0,P1,P2,P3"]
RC3["Consumer3: P4,P5,P6"]
Note3["重新按Range策略分配<br/>7÷2=3余1"]
end
IC1 --> FC1
IC2 --> FC2
IC3 --> FC3
FC1 --> RC1
FC2 --> RC2
FC3 --> RC3
4.3 RoundRobin分配策略
特点:针对所有Topic进行轮询分配
算法:
- 将所有Topic的所有分区按Topic名称和分区号排序
- 将消费者按字典序排列
- 轮询分配分区给消费者
flowchart TD
subgraph "RoundRobin分配策略示例"
subgraph "Topic: orders (7个分区)"
P0["Partition 0"]
P1["Partition 1"]
P2["Partition 2"]
P3["Partition 3"]
P4["Partition 4"]
P5["Partition 5"]
P6["Partition 6"]
end
subgraph "3个消费者"
C1["Consumer1<br/>分配: P0,P3,P6"]
C2["Consumer2<br/>分配: P1,P4"]
C3["Consumer3<br/>分配: P2,P5"]
end
P0 --> C1
P1 --> C2
P2 --> C3
P3 --> C1
P4 --> C2
P5 --> C3
P6 --> C1
Note4["轮询分配: 0→C1, 1→C2, 2→C3<br/>3→C1, 4→C2, 5→C3, 6→C1"]
end
RoundRobin再平衡过程:
flowchart TD
subgraph "初始状态"
IRC1["Consumer1: P0,P3,P6"]
IRC2["Consumer2: P1,P4"]
IRC3["Consumer3: P2,P5"]
end
subgraph "Consumer1故障"
FRC1["Consumer1: 离线"]
FRC2["Consumer2: P1,P4"]
FRC3["Consumer3: P2,P5"]
Note5["45s内: P0,P3,P6按轮询分配给其他消费者"]
end
subgraph "重新分配(45s后)"
RRC1["Consumer1: 已移除"]
RRC2["Consumer2: P0,P2,P4,P6"]
RRC3["Consumer3: P1,P3,P5"]
Note6["重新轮询分配所有分区"]
end
IRC1 --> FRC1
IRC2 --> FRC2
IRC3 --> FRC3
FRC1 --> RRC1
FRC2 --> RRC2
FRC3 --> RRC3
4.4 Sticky分配策略
粘性分区定义:分配结果带有"粘性",在执行新分配前考虑上次分配结果,尽量减少分配变动。
核心原则:
- Topic Partition的分配要尽量均衡
- 当Rebalance发生时,尽量与上一次分配结果保持一致
优势:
- 减少分区重新分配的开销
- 保持消费者的本地状态
- 提高整体性能
flowchart TD
subgraph "Sticky分配策略优势"
subgraph "传统策略(Range/RoundRobin)"
T1["Consumer1故障"]
T2["完全重新分配所有分区"]
T3["所有消费者重新建立连接"]
T4["性能开销大"]
T1 --> T2 --> T3 --> T4
end
subgraph "Sticky策略"
S1["Consumer1故障"]
S2["只重新分配故障消费者的分区"]
S3["其他消费者保持原有分区"]
S4["性能开销小"]
S1 --> S2 --> S3 --> S4
end
end
Sticky再平衡示例:
flowchart TD
subgraph "初始Sticky分配"
ISC1["Consumer1: P0,P1"]
ISC2["Consumer2: P2,P3"]
ISC3["Consumer3: P4,P5,P6"]
end
subgraph "Consumer1故障后重分配"
FSC1["Consumer1: 已移除"]
FSC2["Consumer2: P2,P3,P0"]
FSC3["Consumer3: P4,P5,P6,P1"]
Note7["尽量保持原有分配<br/>只重新分配P0,P1"]
end
ISC1 --> FSC1
ISC2 --> FSC2
ISC3 --> FSC3
4.5 再平衡触发条件
Rebalance触发的三种情况:
-
消费者组成员变化
- 新消费者加入组
- 现有消费者离开组
- 消费者崩溃
-
订阅的Topic分区数变化
- Topic增加分区
- 订阅新的Topic
-
消费者超时
- 超过
session.timeout.ms
未发送心跳 - 超过
max.poll.interval.ms
未调用poll()
- 超过
flowchart TD
subgraph "再平衡触发条件"
subgraph "成员变化"
MC1["消费者加入"]
MC2["消费者离开"]
MC3["消费者崩溃"]
end
subgraph "分区变化"
PC1["Topic增加分区"]
PC2["订阅新Topic"]
end
subgraph "超时情况"
TC1["心跳超时<br/>(session.timeout.ms)"]
TC2["处理超时<br/>(max.poll.interval.ms)"]
end
Rebalance["触发Rebalance"]
MC1 --> Rebalance
MC2 --> Rebalance
MC3 --> Rebalance
PC1 --> Rebalance
PC2 --> Rebalance
TC1 --> Rebalance
TC2 --> Rebalance
end
4.6 再平衡过程详解
sequenceDiagram
participant C1 as Consumer1
participant C2 as Consumer2
participant C3 as Consumer3
participant Coord as Coordinator
Note over C1,Coord: 正常消费阶段
loop 正常消费
C1->>Coord: Heartbeat
C2->>Coord: Heartbeat
C3->>Coord: Heartbeat
end
Note over C1: Consumer1故障
Note over Coord: 检测到Consumer1心跳超时
Coord->>C2: 通知进入Rebalance
Coord->>C3: 通知进入Rebalance
Note over C2,C3: 停止消费,准备重新分配
C2->>Coord: JoinGroup请求
C3->>Coord: JoinGroup请求
Note over Coord: 重新选举Leader(C2)
Coord-->>C2: JoinGroup响应(Leader)
Coord-->>C3: JoinGroup响应(Follower)
Note over C2: 制定新的分区分配方案
C2->>Coord: SyncGroup(新分配方案)
C3->>Coord: SyncGroup(空)
Coord-->>C2: SyncGroup响应(新分配)
Coord-->>C3: SyncGroup响应(新分配)
Note over C2,C3: 开始按新分配消费
5. 最佳实践与性能优化
5.1 消费者组配置建议
# 核心配置参数
consumer:
# 心跳间隔:建议设置为session.timeout.ms的1/3
heartbeat.interval.ms: 3000
# 会话超时:根据网络环境调整,生产环境建议30-45s
session.timeout.ms: 30000
# 处理超时:根据业务处理时间调整
max.poll.interval.ms: 300000
# 分区分配策略:推荐使用CooperativeSticky
partition.assignment.strategy:
- org.apache.kafka.clients.consumer.CooperativeStickyAssignor
# 批量拉取大小:根据消息大小和处理能力调整
max.poll.records: 500
# 拉取数据大小:根据网络带宽调整
fetch.max.bytes: 52428800 # 50MB
6. 总结
6.1 核心要点回顾
- 消费模式:Kafka采用Poll模式,避免推模式的消费速率不匹配问题
- 消费者组:通过消费者组实现负载均衡和高可用,支持点对点和发布/订阅模式
- Leader选举:通过Coordinator协调消费者组的Leader选举和分区分配
- 分配策略:Range、RoundRobin、Sticky等策略各有特点,Sticky策略在Rebalance时性能最优
- 再平衡:虽然保证了高可用,但会影响性能,需要合理配置参数减少频率
6.2 架构优势
flowchart TD
subgraph "Kafka消费者组架构优势"
A1["高可用性<br/>消费者故障自动恢复"]
A2["负载均衡<br/>分区自动分配"]
A3["水平扩展<br/>动态增减消费者"]
A4["灵活性<br/>支持多种消费模式"]
A5["一致性<br/>精确的位移管理"]
Core["消费者组核心架构"]
A1 --> Core
A2 --> Core
A3 --> Core
A4 --> Core
A5 --> Core
end
通过深入理解Kafka消费者组的工作流程,我们可以更好地设计和优化基于Kafka的消息处理系统,确保系统的高性能、高可用和可扩展性。