写在前面
在学习Kafka Rebalance(这篇文章)的时候,就注意到了Group Coordinator在整个过程中的重要性,而且在介绍Broker的整体架构之后,也可继续深入研究具体功能的实现原理。所以这篇文章就研究Group Coordinator与和它强相关的Consumer Coordinator。
这篇文章首先介绍Group Coordinator,因为Group Coordinator是Broker组件的一部分,和前文较好联系。并且在Rebalance过程中,也是Group Coordinator做主要逻辑控制。而后介绍Consumer Coordinator,因为会思考平时使用只关注消费的Consumer是如何参与到复杂的Rebalance过程中的呢?
Group Coordinator
Group Coordinator是Broker上的一个组件,用于管理和协调消费者组的成员、状态、分区分配、偏移量等信息。每个Broker都有一个Group Coordinator对象,负责管理多个消费者组,但每个消费者组只有一个Group Coordinator。
主要属性与方法
Group Coordinator的主要属性有:
- config:Broker的配置参数,用于获取一些与消费者组相关的配置值,如offsets.topic.num.partitions、offsets.retention.minutes等。
- replicaManager:副本管理器,用于与Broker上的其他组件进行交互,如读写数据、同步数据、分配副本等。
- groupManager:消费者组管理器,用于维护消费者组的元数据信息,如成员列表、状态、分区分配方案等,并将这些信息存储在Kafka内部主题(__consumer_offsets)中
- isActive:标志位,表示GroupCoordinator是否处于激活状态,只有在激活状态下才能处理消费者组相关的请求。
Group Coordinator的主要方法有:
状态相关:
- startup():启动方法,用于激活Group Coordinator,并启动一个后台线程来删除过期的消费者组元数据。
- shutdown():关闭方法,用于关闭Group Coordinator,并停止后台线程。
Group维护相关:
- handleJoinGroup():处理加入消费者组的请求,用于接收消费者发送的JoinGroupRequest,并根据消费者组的状态和成员变化,触发再均衡(Rebalance)操作,重新分配分区给消费者,并通知消费者更新自己的订阅状态。
- handleSyncGroup():处理同步分区分配方案的请求,用于接收消费者发送的SyncGroupRequest,并将分区分配结果返回给消费者。
- handleLeaveGroup():处理离开消费者组的请求,用于接收消费者发送的LeaveGroupRequest,并从消费者组中移除该消费者,并触发再均衡操作
Consumer管理相关:
- handleHeartbeat():处理心跳维持的请求,用于接收消费者发送的HeartbeatRequest,并更新该消费者的最后心跳时间戳,并检查是否需要再均衡。
- handleCommitOffsets():处理提交偏移量的请求,用于接收消费者发送的OffsetCommitRequest,并将偏移量信息存储在ZooKeeper或Kafka内部主题(__consumer_offsets)中。
- handleFetchOffsets():处理获取偏移量的请求,用于接收消费者发送的OffsetFetchRequest,并从ZooKeeper或Kafka内部主题(__consumer_offsets)中获取偏移量信息,并返回给消费者。
Consumer Coordinator
Consumer Coordinator是Kafka Consumer中的一个组件,它负责与Broker交互,也是GroupCoordinator的一个子类,继承了GroupCoordinator的功能。它主要有以下功能:
- 监听Consumer Group是否有成员关系新的变化,和Topic Partition数量是否有新的变化;
- 管理Consumer Group的Offset
- 与Group Coordinator交互,协助完成Rebalance
主要属性与方法
Consumer Coordinator的主要属性有:
- subscriptions:一个 SubscriptionState 对象,用于存储消费者的订阅信息,如订阅的主题、分区、offset 等。
- fetcher:一个 Fetcher 对象,用于从 broker 拉取数据。
- autoCommitTask:一个 AutoCommitTask 对象,用于定期提交 offset。
Consumer Coordinator的主要方法有:
Offset相关:
- autoCommitTask:一个 AutoCommitTask 对象,用于定期提交 Offset。
- commitOffsetsSync():一个同步提交 Offset 的方法,会阻塞直到提交成功或超时。
- commitOffsetsAsync():一个异步提交 Offset 的方法,可以传入一个回调函数。
- refreshCommittedOffsetsIfNeeded():一个刷新已提交 Offset 的方法,会从 Broker 获取最新的 Offset。
- resetOffset():一个重置 Offset 的方法,会根据消费者的重置策略(latest, earliest, none)来设置 Offset。
Consumer Group相关:
- onJoinPrepare():一个在加入组之前执行的方法,会提交 offset 和清空分配的分区。
- onJoinComplete():一个在加入组成功后执行的方法,会更新分配的分区和 offset。
- onJoinLeader():一个在成为组 leader 时执行的方法,会发送分区分配方案给组内其他成员。
- onJoinFollower():一个在成为组 follower 时执行的方法,会接收 leader 发送的分区分配方案。
在简单介绍Group 与 Consumer Coordinator基本信息之后,我想主要通过实际的问题来分析他们两个角色到底是如何使用的?
Rebalance
触发Rebalance
ConsumerGroup成员数量变化
之前有提到过,当ConsumerGroup成员数量有变化时(新增成员或者减少成员)会触发Rebalance。这个有两种情况:
新增成员
- 当一个新的消费者想要加入一个消费者组时,它会向 Group Coordinator 发送 JoinGroup 请求,并提供自己的 Group ID、订阅信息、会话超时时间、重平衡超时时间、协议类型和协议列表等参数;
- Group Coordinator会调用 handleJoinGroup() 方法来处理 JoinGroup 请求,处理的过程也就是 handleJoinGroup() 的主要逻辑,大致如下:
- 处理Group
- 根据 Group ID 找到或创建对应的 GroupMetadata 对象,并将其加锁。
- 检查 GroupMetadata 的状态,如果是 Dead 或 Empty 状态,就创建一个新的 generation id,并将状态设置为 PreparingRebalance。
- 处理Consumer
- 检查 Consumer 是否已经在组中,如果是,就更新其订阅信息和会话超时时间;如果不是,就创建一个新的 MemberMetadata 对象,并将其加入到组中。
- 检查是否有相同的 member id 或 group instance id 已经存在于组中,如果有,就移除旧的成员并触发 Rebalance 操作。
- 检查是否有相同的协议类型和协议列表的其他成员已经在组中,如果没有,就返回错误码 InconsistentGroupProtocol。
- 决定Consumer Leader
- 检查是否已经有 leader 成员存在于组中,如果没有,就将当前成员作为 leader;如果有,就将当前成员作为 follower。
- 等待所有成员加入
- 检查是否已经收到了所有成员的 JoinGroup 请求,如果是,就开始执行 Rebalance 操作;如果不是,就等待其他成员的请求或者超时发生(超时受
group.inital.rebalance.delay.ms与rebalance_timeout_ms影响)。
- 检查是否已经收到了所有成员的 JoinGroup 请求,如果是,就开始执行 Rebalance 操作;如果不是,就等待其他成员的请求或者超时发生(超时受
- 处理Group
减少成员
- Group Coordinator通过心跳机制检测到Consumer失去连接、或者Consumer主动离开ConsumerGroup,向Group Coordinator发起LeaveGroup请求
- Group Coordinator会调用 handleLeaveGroup() 方法来处理 LeaveGroup 请求,其处理逻辑大致如下:
- handleLeaveGroup方法首先会从groupMetadataCache中获取对应的GroupMetadata对象;
- 调用removeMemberAndUpdateGroup方法删除组内的成员,并更新组的状态,该方法会根据组的当前状态,执行不同的操作:
- 如果组处于PreparingRebalance或者AwaitingSync状态,那么会触发一次新的rebalance操作;
- 如果组处于Stable或者Dead状态,那么会直接删除成员,并释放其占用的分区;
- 如果组处于Empty状态,那么会删除整个组,并清除其在__consumer_offsets中的位移信息
Topic Partition数量变化
在Kafka还依赖ZooKeeper的版本中,GroupCoordinator会监听ZooKeeper上的/brokers/topics节点的变化事件,从而感知到Topic的Partition数量变化。
在Kafka使用Raft协议的最新版本中,Kafka会通过Raft协议将Topic Partition新建或删除,以 ControllerChangeRecord 记录变化的元数据,而后将此数据送往所有的控制器节点。
Rebalance处理流程
详细的流程可以参考前面的文章,这里做一下简单的描述:
- 查找Coordinator
- Consumer在加入Group之前首先需要找到Group对应的Coordinator所在的Broker,并与Broker创建相互通信的网络连接。
- 如果Consumer已经保存了GroupCoordinator节点信息,并建立了网络,则可以直接进行重选JoinGroup的环节;
- 否则需要向集群中的某个节点(集群中负载最小的节点)发送FindCoordinatorRequest请求来查找对应的GroupCoordinator。
- Consumer在加入Group之前首先需要找到Group对应的Coordinator所在的Broker,并与Broker创建相互通信的网络连接。
- 重新JoinGroup
- 当有新的消费者加入或退出消费者组时,或者订阅的主题分区数量发生变化时,GroupCoordinator会将消费者组的状态设置为PreparingRebalance,并要求所有消费者发送JoinGroup请求,重新加入消费者组。
- 消费者收到JoinGroup请求后,会停止消费,并向GroupCoordinator发送JoinGroup请求,包含自己的元数据信息,如订阅的主题、分区分配策略等。
- 重选Leader
- GroupCoordinator收集到所有消费者的JoinGroup请求后,会选择一个消费者作为Leader,并将其他消费者的元数据信息发送给Leader,让Leader负责制定分区分配方案,并将消费者组的状态设置为AwaitingSync,而将这些信息通过JoinGroup响应返回给ConsumerCoordinator。
- 重新SyncGroup
- Leader收到GroupCoordinator发送的元数据信息后,会根据分区分配策略,如Range、RoundRobin等,计算出每个消费者应该分配到哪些分区,并将分配方案通过SyncGroup请求发送给GroupCoordinator。
- ConsumerCoordinator会根据Leader发送的SyncGroup请求,将分配方案下发给所有消费者,并将消费者组的状态设置为Stable。
- ConsumerCoordinator会根据消费者发送的OffsetCommit请求,将消费者提交的位移信息存储到__consumer_offsets主题中,并返回OffsetCommit响应。
- ConsumerCoordinator会根据消费者发送的OffsetFetch请求,从__consumer_offsets主题中读取消费者请求的位移信息,并返回OffsetFetch响应。
穿插一些细节
- onJoinPrepare():
- Consumer在收到GroupCoordinator发送的JoinGroup请求时被调用。
- 主要作用是为了避免数据重复或丢失,在消费者加入消费者组之前,将当前消费的位移信息提交给GroupCoordinator,以便在Rebalance后能够从上次提交的位移处继续消费。
- onJoinCompelte():
- Consumer在收到GroupCoordinator发送的SyncGroup响应时被调用。
- 主要作用是为了避免数据重复或丢失,在消费者加入Group之后,将当前分配的分区与上次分配的分区进行比较,如果有变化,就根据位移重置策略,从__consumer_offsets主题中读取或重置位移,并从相应的位移处开始消费。
- onJoinLeader():
- Consumer在收到GroupCoordinator发送的JoinGroup响应时,且发现响应中是自己当选为Group Leader,这时onJoinLeader()被调用。
- 它会传入一个JoinGroupResponse对象,包含了消费者组的元数据信息,如Leader ID、Group Protocol、Member Metadata等。
- 主要作用是为了完成分区分配的协调工作,在消费者被选为消费者组的Leader时,它会根据分区分配策略,如Range、RoundRobin等,计算出每个消费者应该分配到哪些分区,并将分配方案通过SyncGroupRequest发送给GroupCoordinator。
- onJoinFollower():
- Consumer在收到GroupCoordinator发送的JoinGroup响应时,且发现响应中是自己不是Group Leader时,onJoinFollower()被调用。
- 和onJoinLeader()方法一样,它也需要传入一个JoinGroupResponse对象。
- 主要作用是为了完成分区分配的协调工作,在消费者被选为消费者组的Follower时,它会创建一个新的SyncGroupRequest.Builder(带上消费者组ID、生成ID、成员ID等信息),并通过sendSyncGroupRequest方法将其发送给GroupCoordinator。
Offset管理
关于Offset
Offset对于ConsumerGroup本质来说是一个Map:
- Key:Topic-Partition
- Value:Consumer消费该分区的最新Offset
Offset的分类
- LogStartOffset:一个Partition的起始位移,初始为0。随着消息的增加以及日志清除策略的影响,这个值会阶段性的增大。
- ConsumerOffset:消费位移,表示某个消费者消费某个Partition消费到的位移位置。
- HighWatermark:Consumer可以消费到的Partition的最高日志位移。HW>=ConsumerOffset。
- LogEndOffset:存在于Partition Replica中,代表这个Replica中最新的一条消息位移。
- 当Producer向Leader Replica中新写入一条消息时,Leader Replica的LogEndOffset+1;
- 当Follower Replica成功从Leader Replica同步一条消息时,Follower Replica的LogEndOffset+1;
- 计算HW:Leader Replica和所有与Leader Replica同步的Follower Replica集合(ISR)的所有LEO中的最小值;
- LogStableOffset:与Kafka的事务消息有关,这里按下不表。
Offset的保存
新版本的Kafka通过采用内部主题 __consumer_offsets来保存Offset,也就是将 Consumer 的Offset数据作为一条条普通的 Kafka 消息,提交到 __consumer_offsets 中。
Offset的提交
在之前的文章也有讨论过这部分,这里做一下回顾:
- 自动Offset提交
- 通过配置Consumer端参数
enable.auto.commit为true来开启Consumer自动提交Offset,并可通过auto.commit.interval.ms来设置自动提交间隔。 - Kafka 会保证在开始调用
poll方法时,提交上次poll返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消息丢失的情况。但自动提交位移的一个问题在于,它可能会出现重复消费,如果处理失败了下次开始的时候就会从上一次提交的offset 开始处理。
- 通过配置Consumer端参数
- 手动Offset提交
- 通过配置Consumer端参数
enable.auto.commit为false来进行手动Offset提交。 - commitOffsetsSync(), 同步提交Offset:同步操作,即该方法会一直等待,直到位移被成功提交才会返回。如果提交过程中出现异常,该方法会将异常信息抛出。因为同步阻塞,所以容易成为系统瓶颈;
- commitOffsetsASync(), 异步提交Offset:异步操作,调用该方法会直接返回,不会阻塞,同时可以通过Callback实现异步完成后的处理。但他的问题在于无法重试。
- 综合使用:
- 常规式程序正常运行,使用异步提交,不阻塞,提升系统吞吐;当关闭Consumer时,使用同步提交,确保Consumer关闭前能将最新消费的Offset提交成功。
- 分批次提交。再消费大批量消息时,可在批量消费消息逻辑中,通过异步实现更精细粒度的异步提交,这样也能尽可能减少因Offset提交失败导致的重复消费问题的影响。
- 通过配置Consumer端参数
Offset管理的大致过程如下:
OffsetCommit:
- Consumer在消费消息时,会定期向GroupCoordinator发送OffsetCommit请求,将自己消费的位移信息提交给GroupCoordinator。
- GroupCoordinator会调用handleCommitOffsets()处理OffsetCommit请求,具体来说:
- 会将消费者提交的位移信息存储到一个特殊的主题__consumer_offsets中,并返回OffsetCommit响应给消费者。
- __consumer_offsets主题配置了compact策略,使得它总是能够保存最新的位移信息,既控制了该主题总体的日志容量,也能实现保存最新offset的目的。
- GroupCoordinator除了将位移作为日志保存到磁盘上,还维护了一张能快速服务于offset抓取请求的consumer offsets表。这个表作为缓存,包含的仅仅是__consumer_offsets主题的分区中属于leader分区对应的条目(存储的是offset)。
OffsetFetch:
- 消费者在启动时或者重启后,会向GroupCoordinator发送OffsetFetch请求
- GroupCoordinator会调用handleFetchOffsets()方法处理OffsetFetch请求,具体来说:
- GroupCoordinator会从__consumer_offsets主题中读取对应的位移信息,并返回OffsetFetch响应给消费者。
- 消费者根据获取到的位移信息,从上次提交的位移处开始继续消费消息。
写在后面
之前的文章和这篇文章都聊到了Kafka的事务消息,而且Kafka里事务功能的时间也依赖于一个事务协调器。所以下一篇希望能对Kafka的事务功能进行研究。