1
什么是再均衡?
再均衡(
Rebalance
)本质上是一种协议,规定了一个消费组中所有消费者如何达成一致来分配订阅主题的每个分区。
比如某个消费组有
20
个消费组,订阅了一个具有
100
个分区的主题。正常情况下,
Kafka
平均会为每个消费者分配5
个分区。这个分配的过程就叫再均衡。
2
什么时候再均衡?
再均衡的触发条件:
1.
组成员发生变更
(
新消费者加入消费组组、已有消费者主动离开或崩溃了
)
2.
订阅主题数发生变更。如果正则表达式进行订阅,则新建匹配正则表达式的主题触发再均衡。
3.
订阅主题的分区数发生变更
3 如何进行组内分区分配?
三种分配策略: RangeAssignor 和 RoundRobinAssignor 以及 StickyAssignor 。
三种分配策略: RangeAssignor 和 RoundRobinAssignor 以及 StickyAssignor 。
4 谁来执行再均衡和消费组管理?
Kafka 提供了一个角色: Group Coordinator来执行对于消费组的管理。
Kafka 提供了一个角色: Group Coordinator来执行对于消费组的管理。
Group Coordinator——
每个消费组分配一个消费组协调器用于组管理和位移管理。当消费组的第一个消费者启动的时候,它会去和Kafka Broker
确定谁是它们组的组协调器。之后该消费组内所有消费者和该组协调器协调通信。
5
如何确定coordinator
两步:
1.
确定消费组位移信息写入
__consumers_offsets
的哪个分区。具体计算公式: __consumers_offsets partition# = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount) 注意:
groupMetadataTopicPartitionCount由 offsets.topic.num.partitions
指定,默认是
50
个分区。
2.
该分区
leader
所在的
broker
就是组协调器。
6 Rebalance Generation
它表示
Rebalance
之后主题分区到消费组中消费者映射关系的一个版本,主要是用于保护消费组,隔离无效偏移量提交的。如上一个版本的消费者无法提交位移到新版本的消费组中,因为映射关系变了,你消费的或许已经不是原来的那个分区了。每次group
进行
Rebalance
之后,
Generation
号都会加1,表示消费组和分区的映射关系到了一个新版本,如下图所示:
Generation 1
时
group
有
3
个成员,随后成员2
退出组,消费组协调器触发
Rebalance
,消费组进入
Generation 2
,之后成员
4
加入,再次触发
Rebalance,消费组进入Generation 3.
![](https://img-blog.csdnimg.cn/20200826100055222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
7
协议
(protocol)
kafka
提供了
5
个协议来处理与消费组协调相关的问题:
Heartbeat
请求:
consumer
需要定期给组协调器发送心跳来表明自己还活着
LeaveGroup
请求:主动告诉组协调器我要离开消费组
SyncGroup
请求:消费组
Leader
把分配方案告诉组内所有成员
JoinGroup
请求:成员请求加入组
DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息等。通常该请求是给管理员使用
组协调器在再均衡的时候主要用到了前面 4 种请求。
组协调器在再均衡的时候主要用到了前面 4 种请求。
8 liveness
消费者如何向消费组协调器证明自己还活着? 通过定时向消费组协调器发送
Heartbeat
请求。如果超过了设定的超时时间,那么协调器认为该消费者已经挂了。一旦协调器认为某个消费者挂了,那么它就会开启新一轮再均衡,并且在当前其他消费者的心跳响应中
添加
“REBALANCE_IN_PROGRESS”
,告诉其他消费者:重新分配分区
9
再均衡过程
再均衡分为
2
步:
Join
和
Sync
1. Join
, 加入组。所有成员都向消费组协调器发送
JoinGroup
请求,请求加入消费组。一旦所有成员都发送了JoinGroup
请求,协调
i
器从中选择一个消费者担任
Leader
的角色,并把组成员信息以及订阅信息发给Leader
。
2. Sync
,
Leader
开始分配消费方案,即哪个消费者负责消费哪些主题的哪些分区。一旦完成分配,Leader
会将这个方案封装进
SyncGroup
请求中发给消费组协调器,非
Leader
也会发SyncGroup请求,只是内容为空。消费组协调器接收到分配方案之后会把方案塞进SyncGroup的
response
中发给各个消费者
![](https://img-blog.csdnimg.cn/20200826100905942.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
注意:在协调器收集到所有成员请求前,它会把已收到请求放入一个叫
purgatory(
炼狱
)
的地方。然后是分发分配方案的过程,即SyncGroup
请求:
![](https://img-blog.csdnimg.cn/20200826100951818.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
注意
:消费组的分区分配
方案在客户端执行
。
Kafka
交给客户端可以有更好的灵活性。
Kafka
默认提供三种分配策略:range
和
round-robin
和
sticky
。可以通过消费者的参数:partition.assignment.strategy 来实现自己分配策略。
10
消费组状态机
消费组组协调器根据状态机对消费组做不同的处理:
消费组组协调器根据状态机对消费组做不同的处理:
![](https://img-blog.csdnimg.cn/20200826101050477.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
说明:
1. Dead
:组内已经没有任何成员的最终状态,组的元数据也已经被组协调器移除了。这种状态响应各种请求都是一个response
:
UNKNOWN_MEMBER_ID
2. Empty
:组内无成员,但是位移信息还没有过期。这种状态只能响应
JoinGroup
请求
3. PreparingRebalance
:组准备开启新的
rebalance
,等待成员加入
4. AwaitingSync
:正在等待
leader consumer
将分配方案传给各个成员
5. Stable
:再均衡完成,可以开始消费。
11 RangeAssignor
PartitionAssignor
接口用于用户定义实现分区分配算法,以实现
Consumer
之间的分区分配。消费组的成员订阅它们感兴趣的Topic
并将这种订阅关系传递给作为订阅组协调者的
Broker
。协调者选择其中的一个消费者来执行这个消费组的分区分配并将分配结果转发给消费组内所有的消费者。
Kafka
默认采用
RangeAssignor
的分配算法
。
RangeAssignor
对每个
Topic
进行独立的分区分配。对于每一个
Topic
,首先对分区按照分区
ID
进行数值排序,然后订阅这个Topic
的消费组的消费者再进行字典排序,之后尽量均衡的将分区分配给消费者。这里只能是尽量均衡,因为分区数可能无法被消费者数量整除,那么有一些消费者就会多分配到一些分区.
![](https://img-blog.csdnimg.cn/20200826101516435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
assign(topic, consumers) {
// 对分区和Consumer进行排序
List<Partition> partitions = topic.getPartitions();
sort(partitions);
sort(consumers);
// 计算每个Consumer分配的分区数
int numPartitionsPerConsumer = partition.size() / consumers.size();
// 额外有一些Consumer会多分配到分区
int consumersWithExtraPartition = partition.size() % consumers.size();
// 计算分配结果
for (int i = 0, n = consumers.size(); i < n; i++) {
// 第i个Consumer分配到的分区的
index int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
// 第i个Consumer分配到的分区数
int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
// 分装分配结果
assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
}
}
RangeAssignor
策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个Topic
,RangeAssignor策略会将消费组内所有订阅这个
Topic
的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。
这种分配方式明显的一个问题是随着消费者订阅的
Topic
的数量的增加,不均衡的问题会越来越严重,比如上图中4
个分区
3
个消费者的场景,
C0
会多分配一个分区。如果此时再订阅一个分区数为
4
的Topic,那么
C0
又会比
C1
、
C2
多分配一个分区,这样
C0
总共就比
C1
、
C2
多分配两个分区了,而且随着Topic的增加,这个情况会越来越严重。
字典序靠前的消费组中的消费者比较
“
贪婪
”
。
![](https://img-blog.csdnimg.cn/20200826101905305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
12 RoundRobinAssignor
RoundRobinAssignor
的分配策略是将消费组内订阅的所有
Topic
的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor
是针对单个
Topic
的分区进行排序分配的)。如果消费组内,
消费者订阅的Topic列表是相同的
(每个消费者都订阅了相同的
Topic
),那么分配结果是尽量均衡的(消费者之间分配到的分区数的差值不会超过1
)。
如果订阅的Topic列表是不同的
,那么分配结果是不保证
“
尽量均衡”
的,因为某些消费者不参与一些
Topic
的分配。
![](https://img-blog.csdnimg.cn/20200826102020696.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
相对于
RangeAssignor
,在订阅多个
Topic
的情况下,
RoundRobinAssignor
的方式能消费者之间尽量均衡的分配到分区(分配到的分区数的差值不会超过1——RangeAssignor
的分配策略可能随着订阅的Topic
越来越多,差值越来越大)。
对于消费组内消费者订阅
Topic
不一致的情况:假设有两个个消费者分别为
C0
和
C1
,有
2
个
TopicT1、
T2
,分别拥有
3
和
2
个分区,并且
C0
订阅
T1
和
T2
,
C1
订阅
T2
,那么
RoundRobinAssignor
的分配结果如下:
![](https://img-blog.csdnimg.cn/20200826102101450.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
看上去分配已经尽量的保证均衡了,不过可以发现
C0
承担了
4
个分区的消费而
C1
订阅了
T2
一个分区,是不是把T2P0
交给
C1
消费能更加的均衡呢?
13 StickyAssignor
动机
尽管
RoundRobinAssignor
已经在
RangeAssignor
上做了一些优化来更均衡的分配分区,但是在一些情况下依旧会产生严重的分配偏差,比如消费组中订阅的Topic
列表不相同的情况下。 更核心的问题是无论是RangeAssignor
,还是
RoundRobinAssignor
,当前的分区分配算法都没有考虑上一次的分配结果
。显然,在执行一次新的分配之前,如果能考虑到上一次分配的结果,尽量少的
调整分区分配的变动,显然是能节省很多开销的
目标
从字面意义上看,
Sticky
是
“
粘性的
”
,可以理解为分配结果是带
“
粘性的
”
:
1.
分区的分配尽量的均衡
2. 每一次重分配的结果尽量与上一次分配结果保持一致
当这两个目标发生冲突时,优先保证第一个目标。第一个目标是每个分配算法都尽量尝试去完成的,而第二个目标才真正体现出StickyAssignor
特性的。
例如:
1.有
3
个
Consumer
:
C0
、
C1
、
C2
2.有
4
个
Topic
:
T0
、
T1
、
T2
、
T3
,每个
Topic
有
2
个分区
3.所有
Consumer
都订阅了这
4
个分区
StickyAssignor的分配结果如下图所示(增加 RoundRobinAssignor 分配作为对比):
StickyAssignor的分配结果如下图所示(增加 RoundRobinAssignor 分配作为对比):
![](https://img-blog.csdnimg.cn/2020082610235827.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20200826102408994.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)
按照
Sticky
的方式:
仅对消费者
1
分配的分区进行重分配,红线部分。最终达到均衡的目的。
![](https://img-blog.csdnimg.cn/20200826102426183.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpZGFuNzA2Mw==,size_16,color_FFFFFF,t_70)