1 消费组
多个消费者可以组成一个消费组,每个消费者只属于一个消费组。消费组订阅主题的每个分区只会分配给该消费组中的某个消费者处理,不同的消费组之间彼此隔离无依赖。同一个消息只会被消费组中的一个消费者消费,如果想要让同一个消息被多个消费者消费,那么每个消费者需要属于不同的消费组,且对应消费组中只有该一个消费者,消费组的引入可以实现消费的“独占”或“广播”效果。
-
消费组下可以有多个消费者,个数支持动态变化。
-
消费组订阅主题下的每个分区只会分配给消费组中的一个消费者。
-
group.id标识消费组,相同则属于同一消费组。
-
不同消费组之间相互隔离互不影响。
如图所示,消费组1中包含两个消费者,其中消费者1分配消费分区0,消费者2分配消费分区1与分区2。此外消费组的引入还支持消费者的水平扩展及故障转移,比如从上图我们可以看出消费者2的消费能力不足,相对消费者1来说消费进度比较落后,我们可以往消费组里面增加一个消费者以提高其整体的消费能力,如下图所示。
假设消费者1所在机器出现宕机,消费组会发送重平衡,假设将分区0分配给消费者2进行消费,如下图所示。同个消费组中消费者的个数不是越多越好,最大不能超过主题对应的分区数,如果超过则会出现超过的消费者分配不到分区的情况,因为分区一旦分配给消费者就不会再变动,除非组内消费者个数出现变动而发生重平衡。
2 消费位移
3.1 消费位移主题
Kafka 0.9开始将消费端的位移信息保存在集群的内部主题(__consumer_offsets)中,该主题默认为50个分区,每条日志项的格式都是:<TopicPartition, OffsetAndMetadata>,其key为主题分区主要存放主题、分区以及消费组信息,value为OffsetAndMetadata对象主要包括位移、位移提交时间、自定义元数据等信息。
只有消费组往kafka中提交位移才会往这个主题中写入数据,如果消费端将消费位移信息保存在外部存储,则不会有消费位移信息,下面可以通过kafka-console-consumer.sh
脚本查看主题消费位移信息。
# bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server localhost:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config config/consumer.properties --from-beginning
[consumer-group01,nginx_access_log,2]::OffsetAndMetadata(offset=17104625, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
[consumer-group01,nginx_access_log,1]::OffsetAndMetadata(offset=17103024, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
[consumer-group01,nginx_access_log,0]::OffsetAndMetadata(offset=17107771, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
2.2 消费位移自动提交
消费端可以通过设置参数enable.auto.commit
来控制是自动提交还是手动,如果值为true
则表示自动提交,在消费端的后台会定时的提交消费位移信息,时间间隔由auto.commit.interval.ms
(默认为5秒)。
但是如果设置为自动提交会存在以下几个问题:
-
可能存在重复的位移数据提交到消费位移主题中,因为每隔5秒会往主题中写入一条消息,不管是否有新的消费记录,这样就会产生大量的同key消息,其实只需要一条,因此需要依赖前面提到日志压缩策略来清理数据。
-
重复消费,假设位移提交的时间间隔为5秒,那么在5秒内如果发生了rebalance,则所有的消费者会从上一次提交的位移处开始消费,那么期间消费的数据则会再次被消费。
2.3 消费位移手动提交
手动提交需要将enable.auto.commit
值设置为false
,然后由业务消费端来控制消费进度,手动提交分为以下三种类型:
-
同步手动提交位移:如果调用的是同步提交方法
commitSync()
,则会将poll拉取的最新位移提交到kafka集群,提交成功前会一直等待提交成功。 -
异步手动提交位移:调用异步提交方法
commitAsync()
,在调用该方法之后会立刻返回,不会阻塞,然后可以通过回调函数执行相关的异常处理逻辑。 -
指定提交位移:指定位移提交也分为异步跟同步,传参为Map<TopicPartition, OffsetAndMetadata>,其中key为消息分区,value为位移对象。
3 分组协调者
分组协调者(Group Coordinator)是一个服务,kafka集群中的每个节点在启动时都会启动这样一个服务,该服务主要是用来存储消费分组相关的元数据信息,每个消费组均会选择一个协调者来负责组内各个分区的消费位移信息存储,选择的主要步骤如下:
-
首选确定消费组的位移信息存入哪个分区:前面提到默认的__consumer_offsets主题分区数为50,通过以下算法可以计算出对应消费组的位移信息应该存入哪个分区
partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)
其中groupId
为消费组的id,这个由消费端指定,groupMetadataTopicPartitionCount
为主题分区数。 -
根据partition寻找该分区的leader所对应的节点broker,该broker的Coordinator即为该消费组的Coordinator。
4 重平衡机制
4.1 重平衡发生场景
以下几种场景均会触发重平衡操作:
-
新的消费者加入到消费组中。
-
消费者被动下线。比如消费者长时间的GC、网络延迟导致消费者长时间未向Group Coordinator发送心跳请求,均会认为该消费者已经下线并踢出。
-
消费者主动退出消费组。
-
消费组订阅的任意一个主题分区数出现变化。
-
消费者取消某个主题的订阅。
4.2 重平衡操作流程
重平衡的实现可以分为以下几个阶段:
-
查找
Group Coordinator
:消费者会从kafka集群中选择一个负载最小的节点发送GroupCoorinatorRequest
请求,并处理返回响应GroupCoordinatorResponse
。其中请求参数中包含消费组的id,响应中包含Coordinator所在节点id、host以及端口号信息。 -
Join group
:当消费者拿到协调者的信息之后会往协调者发送加入消费组的请求JoinGroupRequest
,当所有的消费者都发送该请求之后,协调者会从中选择一个消费者作为leader角色,然后将组内成员信息、订阅等信息发给消费者(响应格式JoinGroupResponse
见下表),leader负责消费方案的分配。
JoinGroupRequest
请求数据格式
名称 | 类型 | 说明 |
---|---|---|
group_id | String | 消费者id |
seesion_timeout | int | 协调者超过session_timeout指定的时间没有收到心跳消息,则认为该消费者下线 |
member_id | String | 协调者分配给消费者的id |
protocol_type | String | 消费组实现的协议,默认为sonsumer |
group_protocols | List | 包含此消费者支持的全部PartitionAssignor类型 |
protocol_name | String | PartitionAssignor类型 |
protocol_metadata | byte[] | 针对不同PartitionAssignor类型序列化后的消费者订阅信息,包含用户自定义数据userData |
JoinGroupResponse
响应数据格式式
名称 | 类型 | 说明 |
---|---|---|
error_code | short | 错误码 |
generation_id | int | 协调者分配的年代信息 |
group_protocol | String | 协调者选择的PartitionAssignor类型 |
leader_id | String | Leader的member_id |
member_id | String | 协调者分配给消费者的id |
members | Map集合 | 消费组中全部的消费者订阅信息 |
member_metadata | byte[] | 对应消费者的订阅信息 |
-
Synchronizing Group State
阶段:当leader消费者完成消费方案的分配后会发送SyncGroupRequest
请求给协调者,其他非leader节点也会发送该请求,只是请求参数为空,然后协调者将分配结果作为响应SyncGroupResponse
发给各个消费者,请求及相应的数据格式如下表所示:
SyncGroupRequest
请求数据格式
名称 | 类型 | 说明 |
---|---|---|
group_id | String | 消费组的id |
generation_id | int | 消费组保存的年代信息 |
member_id | String | 协调者分配的消费者id |
member_assignment | byte[] | 分区分配结果 |
(左右滑动查看完整表格)
SyncGroupResponse
响应数据格式
名称 | 类型 | 说明 |
---|---|---|
error_code | short | 错误码 |
member_assignment | byte[] | 分配给当前消费者的分区 |
(左右滑动查看完整表格)
4.3 分区分配策略
Kafka提供了三个分区分配策略:RangeAssignor、RoundRobinAssignor以及StickyAssignor,下面简单介绍下各个算法的实现。
-
RangeAssignor:kafka默认会采用此策略进行分区分配,主要流程如下
假设一个消费组中存在两个消费者{C0,C1},该消费组订阅了三个主题{T1,T2,T3},每个主题分别存在三个分区,一共就有9个分区{TP1,TP2,...,TP9}。通过以上算法我们可以得到D=4,R=1,那么消费组C0将消费的分区为{TP1,TP2,TP3,TP4,TP5},C1将消费分区{TP6,TP7,TP8,TP9}。这里存在一个问题,如果不能均分,那么前面的几个消费者将会多消费一个分区。
-
将所有订阅主题下的分区进行排序得到集合
TP={TP0,Tp1,...,TPN+1}
。 -
对消费组中的所有消费者根据名字进行字典排序得到集合
CG={C0,C1,...,CM+1}
。 -
计算
D=N/M
,R=N%M
。 -
消费者Ci获取消费分区起始位置=D*i+min(i,R),Ci获取的分区总数=D+(if (i+1>R)0 else 1)。
-
-
RoundRobinAssignor:使用该策略需要满足以下两个条件:1) 消费组中的所有消费者应该订阅主题相同;2) 同一个消费组的所有消费者在实例化时给每个主题指定相同的流数。
-
对所有主题的所有分区根据主题+分区得到的哈希值进行排序。
-
对所有消费者按字典排序。
-
通过轮询的方式将分区分配给消费者。
-
-
StickyAssignor:该分配方式在0.11版本开始引入,主要是保证以下特性:1) 尽可能的保证分配均衡;2) 当重新分配时,保留尽可能多的现有分配。其中第一条的优先级要大于第二条。
5 Kafka Consumer 源码分析
- Consumer 消费模型的第一步——加入一个 group - Kafka 源码解析之 Consumer 如何加入一个 Group(六)
- Consumer poll 模型 - Kafka 源码解析之 Consumer Poll 模型(七)
- Consumer 两种消费订阅及两中 commit (同步、异步)机制 - Kafka 源码解析之 Consumer 两种订阅模式(八)& Kafka 源码解析之 Consumer 两种 commit 机制和 partition 分配机制(九)
- ConsumerCoordinator 与 GroupCoordinator 机制 - Kafka 源码解析之 GroupCoordinator 详解(十)