Kafka009——协调器之Group&Consumer

767 阅读14分钟

写在前面

在学习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的主要属性有:

  1. config:Broker的配置参数,用于获取一些与消费者组相关的配置值,如offsets.topic.num.partitions、offsets.retention.minutes等。
  2. replicaManager:副本管理器,用于与Broker上的其他组件进行交互,如读写数据、同步数据、分配副本等。
  3. groupManager:消费者组管理器,用于维护消费者组的元数据信息,如成员列表、状态、分区分配方案等,并将这些信息存储在Kafka内部主题(__consumer_offsets)中
  4. isActive:标志位,表示GroupCoordinator是否处于激活状态,只有在激活状态下才能处理消费者组相关的请求。

Group Coordinator的主要方法有:

状态相关:

  1. startup():启动方法,用于激活Group Coordinator,并启动一个后台线程来删除过期的消费者组元数据。
  2. shutdown():关闭方法,用于关闭Group Coordinator,并停止后台线程。

Group维护相关:

  1. handleJoinGroup():处理加入消费者组的请求,用于接收消费者发送的JoinGroupRequest,并根据消费者组的状态和成员变化,触发再均衡(Rebalance)操作,重新分配分区给消费者,并通知消费者更新自己的订阅状态。
  2. handleSyncGroup():处理同步分区分配方案的请求,用于接收消费者发送的SyncGroupRequest,并将分区分配结果返回给消费者。
  3. handleLeaveGroup():处理离开消费者组的请求,用于接收消费者发送的LeaveGroupRequest,并从消费者组中移除该消费者,并触发再均衡操作

Consumer管理相关:

  1. handleHeartbeat():处理心跳维持的请求,用于接收消费者发送的HeartbeatRequest,并更新该消费者的最后心跳时间戳,并检查是否需要再均衡。
  2. handleCommitOffsets():处理提交偏移量的请求,用于接收消费者发送的OffsetCommitRequest,并将偏移量信息存储在ZooKeeper或Kafka内部主题(__consumer_offsets)中。
  3. handleFetchOffsets():处理获取偏移量的请求,用于接收消费者发送的OffsetFetchRequest,并从ZooKeeper或Kafka内部主题(__consumer_offsets)中获取偏移量信息,并返回给消费者。

Consumer Coordinator

Consumer Coordinator是Kafka Consumer中的一个组件,它负责与Broker交互,也是GroupCoordinator的一个子类,继承了GroupCoordinator的功能。它主要有以下功能:

  1. 监听Consumer Group是否有成员关系新的变化,和Topic Partition数量是否有新的变化;
  2. 管理Consumer Group的Offset
  3. 与Group Coordinator交互,协助完成Rebalance

主要属性与方法

Consumer Coordinator的主要属性有:

  1. subscriptions:一个 SubscriptionState 对象,用于存储消费者的订阅信息,如订阅的主题、分区、offset 等。
  2. fetcher:一个 Fetcher 对象,用于从 broker 拉取数据。
  3. autoCommitTask:一个 AutoCommitTask 对象,用于定期提交 offset。

Consumer Coordinator的主要方法有:

Offset相关:

  1. autoCommitTask:一个 AutoCommitTask 对象,用于定期提交 Offset。
  2. commitOffsetsSync():一个同步提交 Offset 的方法,会阻塞直到提交成功或超时。
  3. commitOffsetsAsync():一个异步提交 Offset 的方法,可以传入一个回调函数。
  4. refreshCommittedOffsetsIfNeeded():一个刷新已提交 Offset 的方法,会从 Broker 获取最新的 Offset。
  5. resetOffset():一个重置 Offset 的方法,会根据消费者的重置策略(latest, earliest, none)来设置 Offset。

Consumer Group相关:

  1. onJoinPrepare():一个在加入组之前执行的方法,会提交 offset 和清空分配的分区。
  2. onJoinComplete():一个在加入组成功后执行的方法,会更新分配的分区和 offset。
  3. onJoinLeader():一个在成为组 leader 时执行的方法,会发送分区分配方案给组内其他成员。
  4. onJoinFollower():一个在成为组 follower 时执行的方法,会接收 leader 发送的分区分配方案。

在简单介绍Group 与 Consumer Coordinator基本信息之后,我想主要通过实际的问题来分析他们两个角色到底是如何使用的?

Rebalance

触发Rebalance

ConsumerGroup成员数量变化

之前有提到过,当ConsumerGroup成员数量有变化时(新增成员或者减少成员)会触发Rebalance。这个有两种情况:

新增成员

  1. 当一个新的消费者想要加入一个消费者组时,它会向 Group Coordinator 发送 JoinGroup 请求,并提供自己的 Group ID、订阅信息、会话超时时间、重平衡超时时间、协议类型和协议列表等参数;
  2. Group Coordinator会调用 handleJoinGroup() 方法来处理 JoinGroup 请求,处理的过程也就是 handleJoinGroup() 的主要逻辑,大致如下:
    1. 处理Group
      1. 根据 Group ID 找到或创建对应的 GroupMetadata 对象,并将其加锁。
      2. 检查 GroupMetadata 的状态,如果是 Dead 或 Empty 状态,就创建一个新的 generation id,并将状态设置为 PreparingRebalance。
    2. 处理Consumer
      1. 检查 Consumer 是否已经在组中,如果是,就更新其订阅信息和会话超时时间;如果不是,就创建一个新的 MemberMetadata 对象,并将其加入到组中。
      2. 检查是否有相同的 member id 或 group instance id 已经存在于组中,如果有,就移除旧的成员并触发 Rebalance 操作。
      3. 检查是否有相同的协议类型和协议列表的其他成员已经在组中,如果没有,就返回错误码 InconsistentGroupProtocol。
    3. 决定Consumer Leader
      1. 检查是否已经有 leader 成员存在于组中,如果没有,就将当前成员作为 leader;如果有,就将当前成员作为 follower。
    4. 等待所有成员加入
      1. 检查是否已经收到了所有成员的 JoinGroup 请求,如果是,就开始执行 Rebalance 操作;如果不是,就等待其他成员的请求或者超时发生(超时受 group.inital.rebalance.delay.msrebalance_timeout_ms影响)。

减少成员

  1. Group Coordinator通过心跳机制检测到Consumer失去连接、或者Consumer主动离开ConsumerGroup,向Group Coordinator发起LeaveGroup请求
  2. Group Coordinator会调用 handleLeaveGroup() 方法来处理 LeaveGroup 请求,其处理逻辑大致如下:
    1. handleLeaveGroup方法首先会从groupMetadataCache中获取对应的GroupMetadata对象;
    2. 调用removeMemberAndUpdateGroup方法删除组内的成员,并更新组的状态,该方法会根据组的当前状态,执行不同的操作:
      1. 如果组处于PreparingRebalance或者AwaitingSync状态,那么会触发一次新的rebalance操作;
      2. 如果组处于Stable或者Dead状态,那么会直接删除成员,并释放其占用的分区;
      3. 如果组处于Empty状态,那么会删除整个组,并清除其在__consumer_offsets中的位移信息

Topic Partition数量变化

在Kafka还依赖ZooKeeper的版本中,GroupCoordinator会监听ZooKeeper上的/brokers/topics节点的变化事件,从而感知到Topic的Partition数量变化。

在Kafka使用Raft协议的最新版本中,Kafka会通过Raft协议将Topic Partition新建或删除,以 ControllerChangeRecord 记录变化的元数据,而后将此数据送往所有的控制器节点。

Rebalance处理流程

详细的流程可以参考前面的文章,这里做一下简单的描述:

  1. 查找Coordinator
    1. Consumer在加入Group之前首先需要找到Group对应的Coordinator所在的Broker,并与Broker创建相互通信的网络连接。
      1. 如果Consumer已经保存了GroupCoordinator节点信息,并建立了网络,则可以直接进行重选JoinGroup的环节;
      2. 否则需要向集群中的某个节点(集群中负载最小的节点)发送FindCoordinatorRequest请求来查找对应的GroupCoordinator。
  2. 重新JoinGroup
    1. 当有新的消费者加入或退出消费者组时,或者订阅的主题分区数量发生变化时,GroupCoordinator会将消费者组的状态设置为PreparingRebalance,并要求所有消费者发送JoinGroup请求,重新加入消费者组。
    2. 消费者收到JoinGroup请求后,会停止消费,并向GroupCoordinator发送JoinGroup请求,包含自己的元数据信息,如订阅的主题、分区分配策略等。
  3. 重选Leader
    1. GroupCoordinator收集到所有消费者的JoinGroup请求后,会选择一个消费者作为Leader,并将其他消费者的元数据信息发送给Leader,让Leader负责制定分区分配方案,并将消费者组的状态设置为AwaitingSync,而将这些信息通过JoinGroup响应返回给ConsumerCoordinator。
  4. 重新SyncGroup
    1. Leader收到GroupCoordinator发送的元数据信息后,会根据分区分配策略,如Range、RoundRobin等,计算出每个消费者应该分配到哪些分区,并将分配方案通过SyncGroup请求发送给GroupCoordinator。
    2. ConsumerCoordinator会根据Leader发送的SyncGroup请求,将分配方案下发给所有消费者,并将消费者组的状态设置为Stable。
    3. ConsumerCoordinator会根据消费者发送的OffsetCommit请求,将消费者提交的位移信息存储到__consumer_offsets主题中,并返回OffsetCommit响应。
    4. ConsumerCoordinator会根据消费者发送的OffsetFetch请求,从__consumer_offsets主题中读取消费者请求的位移信息,并返回OffsetFetch响应。

穿插一些细节

  1. onJoinPrepare():
    1. Consumer在收到GroupCoordinator发送的JoinGroup请求时被调用。
    2. 主要作用是为了避免数据重复或丢失,在消费者加入消费者组之前,将当前消费的位移信息提交给GroupCoordinator,以便在Rebalance后能够从上次提交的位移处继续消费。
  2. onJoinCompelte():
    1. Consumer在收到GroupCoordinator发送的SyncGroup响应时被调用。
    2. 主要作用是为了避免数据重复或丢失,在消费者加入Group之后,将当前分配的分区与上次分配的分区进行比较,如果有变化,就根据位移重置策略,从__consumer_offsets主题中读取或重置位移,并从相应的位移处开始消费。
  3. onJoinLeader():
    1. Consumer在收到GroupCoordinator发送的JoinGroup响应时,且发现响应中是自己当选为Group Leader,这时onJoinLeader()被调用。
    2. 它会传入一个JoinGroupResponse对象,包含了消费者组的元数据信息,如Leader ID、Group Protocol、Member Metadata等。
    3. 主要作用是为了完成分区分配的协调工作,在消费者被选为消费者组的Leader时,它会根据分区分配策略,如Range、RoundRobin等,计算出每个消费者应该分配到哪些分区,并将分配方案通过SyncGroupRequest发送给GroupCoordinator。
  4. onJoinFollower():
    1. Consumer在收到GroupCoordinator发送的JoinGroup响应时,且发现响应中是自己不是Group Leader时,onJoinFollower()被调用。
    2. 和onJoinLeader()方法一样,它也需要传入一个JoinGroupResponse对象。
    3. 主要作用是为了完成分区分配的协调工作,在消费者被选为消费者组的Follower时,它会创建一个新的SyncGroupRequest.Builder(带上消费者组ID、生成ID、成员ID等信息),并通过sendSyncGroupRequest方法将其发送给GroupCoordinator。

Offset管理

关于Offset

Offset对于ConsumerGroup本质来说是一个Map:

  1. Key:Topic-Partition
  2. Value:Consumer消费该分区的最新Offset

Offset的分类

  1. LogStartOffset:一个Partition的起始位移,初始为0。随着消息的增加以及日志清除策略的影响,这个值会阶段性的增大。
  2. ConsumerOffset:消费位移,表示某个消费者消费某个Partition消费到的位移位置。
  3. HighWatermark:Consumer可以消费到的Partition的最高日志位移。HW>=ConsumerOffset。
  4. LogEndOffset:存在于Partition Replica中,代表这个Replica中最新的一条消息位移。
    1. 当Producer向Leader Replica中新写入一条消息时,Leader Replica的LogEndOffset+1;
    2. 当Follower Replica成功从Leader Replica同步一条消息时,Follower Replica的LogEndOffset+1;
    3. 计算HW:Leader Replica和所有与Leader Replica同步的Follower Replica集合(ISR)的所有LEO中的最小值;
  5. LogStableOffset:与Kafka的事务消息有关,这里按下不表。

Offset的保存

新版本的Kafka通过采用内部主题 __consumer_offsets来保存Offset,也就是将 Consumer 的Offset数据作为一条条普通的 Kafka 消息,提交到 __consumer_offsets 中。

Offset的提交

之前的文章也有讨论过这部分,这里做一下回顾:

  1. 自动Offset提交
    1. 通过配置Consumer端参数enable.auto.commit为true来开启Consumer自动提交Offset,并可通过 auto.commit.interval.ms 来设置自动提交间隔。
    2. Kafka 会保证在开始调用poll方法时,提交上次poll返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消息丢失的情况。但自动提交位移的一个问题在于,它可能会出现重复消费,如果处理失败了下次开始的时候就会从上一次提交的offset 开始处理。
  2. 手动Offset提交
    1. 通过配置Consumer端参数enable.auto.commit为false来进行手动Offset提交。
    2. commitOffsetsSync(), 同步提交Offset:同步操作,即该方法会一直等待,直到位移被成功提交才会返回。如果提交过程中出现异常,该方法会将异常信息抛出。因为同步阻塞,所以容易成为系统瓶颈;
    3. commitOffsetsASync(), 异步提交Offset:异步操作,调用该方法会直接返回,不会阻塞,同时可以通过Callback实现异步完成后的处理。但他的问题在于无法重试。
    4. 综合使用:
      1. 常规式程序正常运行,使用异步提交,不阻塞,提升系统吞吐;当关闭Consumer时,使用同步提交,确保Consumer关闭前能将最新消费的Offset提交成功。
      2. 分批次提交。再消费大批量消息时,可在批量消费消息逻辑中,通过异步实现更精细粒度的异步提交,这样也能尽可能减少因Offset提交失败导致的重复消费问题的影响。

Offset管理的大致过程如下:

OffsetCommit:

  1. Consumer在消费消息时,会定期向GroupCoordinator发送OffsetCommit请求,将自己消费的位移信息提交给GroupCoordinator。
  2. GroupCoordinator会调用handleCommitOffsets()处理OffsetCommit请求,具体来说:
    1. 会将消费者提交的位移信息存储到一个特殊的主题__consumer_offsets中,并返回OffsetCommit响应给消费者。
    2. __consumer_offsets主题配置了compact策略,使得它总是能够保存最新的位移信息,既控制了该主题总体的日志容量,也能实现保存最新offset的目的。
    3. GroupCoordinator除了将位移作为日志保存到磁盘上,还维护了一张能快速服务于offset抓取请求的consumer offsets表。这个表作为缓存,包含的仅仅是__consumer_offsets主题的分区中属于leader分区对应的条目(存储的是offset)。

OffsetFetch:

  1. 消费者在启动时或者重启后,会向GroupCoordinator发送OffsetFetch请求
  2. GroupCoordinator会调用handleFetchOffsets()方法处理OffsetFetch请求,具体来说:
    1. GroupCoordinator会从__consumer_offsets主题中读取对应的位移信息,并返回OffsetFetch响应给消费者。
  3. 消费者根据获取到的位移信息,从上次提交的位移处开始继续消费消息。

写在后面

之前的文章和这篇文章都聊到了Kafka的事务消息,而且Kafka里事务功能的时间也依赖于一个事务协调器。所以下一篇希望能对Kafka的事务功能进行研究。

参考资料

  1. Apache Kafka
  2. Apache Kafka
  3. kafka系列之Coordinator(14) - 掘金 (juejin.cn)
  4. blog.csdn.net/zhanyuanlin…
  5. blog.51cto.com/u_12279910/…