Kafka自身提供了三种分区分配策略,通过消费者端配置参数partition.assignment.strategy来控制。
1.RangeAssignor分配策略(kafka默认的分区策略)
通过配置partition.assignment.strategy=org.apache.kafka.clients.consumer.RangeAssignor来让此消费者使用RangeAssignor分配策略。
按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。
该策略会将消费者组内所有订阅这个主题的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的会被多分配一个分区。假设n=分区数/消费者数量,m=分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的消费者每个分配n个分区。
2.RoundRobinAssignor分配策略
为了解决上面的问题,Kafka还提供了这个分配策略
通过配置partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor来让此消费者使用RoundRobinAssignor分配策略。
这种策略是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。
这种分配策略可以解决1中的情况,这15个分区会轮询分配给这两个消费者,即第一个消费者分配到8个,第二个分配到7个。
3.StickyAssignor分配策略
通过配置partition.assignment.strategy=org.apache.kafka.clients.consumer.StickyAssignor来让此消费者使用StickyAssignor分配策略。
kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
(1)分区的分配要尽可能均匀。
(2)分区的分配尽可能与上次分配的保持相同。
当两者发生冲突时,第一个目标优先于第二个目标。
当发生分区重平衡,那么对于同一个分区而言,有可能之前的消费者和新指派的消费者不是同一个,之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。所以这种分配方式尽可能地让前后两次分配相同,进而减少系统资源的损耗及其他异常情况的发生。
消费者协调器和组协调器
现在有个问题,如果多个消费者,彼此配置的分配策略并不完全相同,那么以哪个为准?多个消费者之间的分区分配是需要协同的,这个协同的过程交由消费者协调器和组协调器来完成的,它们之间使用一套组协调协议进行交互。
消费者协调器(ConsumerCoodrinator)和组协调器(GroupCoodrinator)——是针对于新版的消费者客户端而言的,旧版的消费者客户端是使用ZooKeeper的监听器(Watcher)来实现这些功能的。这样有弊端,就是触发再均衡操作时,一个消费组下的所有消费者会同时进行再均衡操作,而消费者之间并不知道彼此操作的结果,这就可能导致Kafka工作在一个不正确的状态。(个人理解是每个消费者的信息都会保存在zookeeper节点下,相当于不同的分支,这些分支互相独立进行,不依赖彼此)
重平衡的过程
新版Kafka将全部消费组分成多个子集,每个消费组的子集在服务端对应一个GroupCoordinator对其进行管理,它是Kafka服务端中用于管理消费组的组件。而消费者客户端中的ConsumerCoordinator组件负责与GroupCoordinator进行交互。
有如下几个情况会触发再均衡。
(1)有新的消费者加入消费组(常见)
(2)消费者宕机下线,比如长时间GC,网络延迟导致消费者长时间未向GroupCoordinator发送心跳等情况,GroupCoordinator会认为消费者已经下线(超过了session.timeout.ms)
(3)消费组所对应的GroupCoordinator发生变更
(4)消费组内所订阅的任一主题或者主题的分区数量发生变化
案例分析:当有消费者加入消费者组时,触发再均衡
第一阶段:FIND_COORDINATOR
消费者需要确定它所属的消费组对应的GroupCoordinator所在的broker,并创建与该broker相互通信的网络连接。
如果消费者已经保存了与消费组对应的GroupCoordinator节点的信息,并且与它之间的网络连接是正常的,那么就可以进入第二阶段了。否则,就需要向集群中的某个节点发送FindCoordinatorRequest请求来查找对应的GroupCoordinator。request请求中包括coordinator_key(消费组的名称,就是groupId)、coordinator_type(值为0)
Kafka通过coordinator_key查找对应的GroupCoordinator节点,如果找到就会返回对应的node_id、host和port信息。
查找GroupCoordinator的方式是先根据消费组groupId的哈希值计算__consumer_offsets中的分区编号
找到对应的分区后,再找这个分区的leader副本所在的broker节点,该broker节点即为这个groupId所对应的GroupCoordinator节点。消费组最终的分区分配方案及组内消费者所提交的消费位移信息都会发送给此leader副本所在的broker节点,让此broker节点既扮演GroupCoordinator的角色,又扮演保存分区分配方案和组内消费者位移的角色,可以省去很多不必要的中间轮转所带来的开销。
第二阶段:JOIN_GROUP
找到消费组对应的GroupCoordinator之后就进入加入消费组的阶段,此阶段的消费者会向GroupCoordinator发送JoinGroupRequest请求,选举出leader消费者并将组内所有消费者的分区策略‘投票’结果相应给leader消费者。
如果是原来的消费者重新加入消费组,那么在发送JoinGroupRequest请求之前还要执行一些准备工作:
(1)如果消费端参数enable.auto.commit 设置为true,那么在请求加入消费组之前需要向GroupCoordinator提交消费位移。这个过程是阻塞执行的,要么成功提交消费位移,要么超时。
(2)如果消费者添加了自定义的再均衡监听器,那么会在重新加入消费组之前实施自定义的规则逻辑,比如清除一些状态,或者提交消费位移等。
(3)因为是重新加入消费组,之前与GroupCoordinator节点之间的心跳检测也就不需要了,所以在成功地重新加入消费组之前需要禁止心跳检测的运作。
消费者在发送JoinGroupRequest之后会阻塞等待Kafka服务端的响应。服务端在收到JoinGroupRequest请求后会交由GroupCoordinator来处理。它会对request做合法性校验。
选举消费组的leader
这个leader和leader副本的概念不一样
分两种情况,如果消费组内还没有leader,那么第一个加入消费组的消费者就是leader,如果某一时刻leader消费者由于某些原因退出了消费组,那么会重新选举一个新的leader,这个重新选举leader的算法,几乎是随机选择一个。
选举分区分配策略
每个消费者都可以设置自己的分区分配策略,对消费组而言需要从各个消费者呈报上来的各个分配策略中选择一个策略来进行整体的分区分配。这个分区分配是根据消费组内的各个消费者投票来决定的。选举过程:
(1)收集各个消费者支持的所有分配策略,组成候选集candidates
(2)每个消费者从候选集中找出第一个自身支持的策略,为这个策略投一票
(3)计算候选集中各个策略的选票数,选票数最多的策略即为当前消费组的分配策略
如果有消费者不支持最终选出的分配策略,就会报出异常。
之后,Kafka服务端就要发送JoinGroupResponse响应给各个消费者,leader消费者和其他普通消费者收到的响应内容并不相同。只有发给leader的response中的members包含有效数据。members包含各个成员信息。其中member_metadata为每个消费者的订阅信息。由此可见,GroupCoodinator并不参与消费者的分区分配策略,只是协调作用。
第三阶段:SYNC_GROUP
通过GroupCoordinator将leader消费者中的消费者组最终分区分配策略以及各消费者分区订阅情况同步给各个消费者。
leader消费者根据第二阶段选举出来的分区分配策略来实施具体的分区分配,在此之后需要将分配的方案同步给各个消费者,它是通过GroupCoordinator来负责转发同步分配方案的,第三个阶段也就是同步阶段,各个消费者会向GroupCoordinator发送SyncGroupRequest请求来同步分配方案。这个request中,只有leader包含具体的分区分配方案,在group_assignment中,其余消费者的group_assignment为空。goup_assignment是一个数组类型,里面包含了组内各个消费者对应的具体分配方案。
服务端收到消费者发送的SyncGroupRequest请求后会交由GroupCoordinator处理,首先会校验request是否合法,然后会将leader发送过来的分区分配策略提取出来,连同整个消费组的元数据信息一起存入__consumer_offsets主题中,最后发送响应给各个消费者以提供各自所属的分配方案。
之后开启心跳任务,消费者定期向服务端的GroupCoordinator发送HeartbeatRequest来确定彼此在线。
第四阶段:HEARTBEAT
这个阶段中,消费组中的所有消费者都会处于正常工作状态。
消费者在消费之前,需要确定拉取消息的起始位置。如果之前已经将最后的消费位移提交到了GroupCoordinator,并且GroupCoordinator已经将其保存到了__consumer_offsets中,此时消费者可以通过OffsetFetchRequest请求获取上次提交的消费位移并从此处继续消费。
只要GroupCoodinator在session.timeout.ms的时间之内没有收到消费者的心跳,就会认为消费者已经下线。
而heartbeat.interval.ms这个是消费者向GroupCoodinator发送心跳的频率,通常是session.timeout.ms的1/3,有可能因为网络延迟或者GC的原因某几次发送心跳的时间会有延迟,然后之后就正常了。
如果消费者开始了再均衡,消费者发送心跳包给GroupCoodinator,那么GroupCoodinator就会响应REBALANCE_IN_PROGRESS,consumer就能及时知道发生了rebalance,然后发送JoinGroupRequest开始重新加入组。
经历完四个阶段后,Kafka的重平衡就算是完成了,整个过程是Stop The World的。