Kafka技术内幕 读书笔记之(五) 协调者——消费组状态机
协调者保存的消费组元数据中记录了消费组的状态机 , 消费组状态机的转换主要发生在“加入组请求”和“同步组请求”的处理过程中 。
协调者处理“离开消费组请求”“迁移消费组请求”“心跳请求” “提交偏移量请求”也会更新消费组的状态、机,或者依赖消费组的状态进行不同的
处理。消费者要加入消费组 , 需要依次发送“加入组请求”和“同步组请求”给协调者 。 消费者加入组的过程叫作再平衡,因为协调者处理这两
种请求会更新消费组状态,所以再平衡操作跟消费组状态也息息相关。
监听器回调方法和再平衡操作的执行顺序如下 。
(1)消费者准备加入组,调用“消费者再平衡监听器”的 onPartitionsRevoked ()方法 。
(2)消费者发送“加入组请求 ” , 协调者开始处理消费者的“加入组请求”,执行再平衡操作 。
(3)消费者完成加入组,调用“消费者再平衡监昕器”的 on PartitionsAssigned ()方法 。
1. 3个消费者,一次再平衡操作
3个消费者同时发送了“加入组请求” 。 因为在“消费组元数据上”加了锁的保护,只能一次处理一个消费者的“加入组请求"。即第一个消费者收到
加入组响应之前,第二个消费者的加入组请求不会被处理。 该场景下一共只发生了一次再平衡操作, 即只执行了一次分区分配工作, 3个消费者最
后都收到了分配的分区 。 只执行一次再平衡操作,并不意味着消费组的状态为“准备再平衡”只有一次 : 第一个消费者加入组,消费组状态
从“稳定”到“准备再平衡”再到“等待同步”;第二个消费者加入组时,消费组状态会从“等待同步”到“准备再平衡气
下面是再平衡操作时Kafka服务端相关的日志记录,日志对应的请求发送和处理步骤如下 。
(1) 3个消费者同时发送加入组请求, consumer- 3 (第一个消费者)的请求优先被协调者处理 。
(2) consumer- 3 (主消费者)完成延迟操作 , 消费组状态改为“等待同步”。
(3) 处理consumer- 2 的“加入组请求,消费组状态改为“准备再平衡”,延迟操作不能完成 。
(4) 处理consumer- 1 的“加入组请求”,延迟操作不能完成;处理consumer - 3的同步请求。
(5) 消费组状态为“再平衡”, consumer- 3重新发送“加入组请求” 。
(6) 延迟操作可以完成,返回 “加入组响应”给3个消费者 ; consumer - 3发送“同步组请求” 。
(7) 协调者保存 consumer - 3发送的分配结果时,也会处理普通消费者的“同步组请求” 。
(8) 消费组的分配结果保存完毕后,返回“同步组响应”给所有的消费者,消费组状态为“稳定” 。
(9) 关闭应用程序,消费组状态为“离开”, 3个消费者都会从消费组中移除 。
第一种场景“3个消费者同时发送加入组请求”有以下几个特点 。
- 每个消费者再平衡监听器的 onPartitionsRevoked ()方法和onPartitionsAssigned ()方法都只调用一次。 这说明3个消费者加入消费组,
总共只发生一次再平衡操作,并且只分配一次分区 。
- 第一个消费者和第二个消费者都会调用 prepareRebalance ()方法,它们都将消费组状态改为“准备再平衡” 。 第一个消费者更改前的状态
是“稳定”,第二个消费者更改前的状态是“等待同步” 。
- 在这一次再平衡操作中,消费组的状态变化依次是 :稳定→准备再平衡→等待同步→准备再平衡→等待同步→稳定 。 最后关闭应用程序,消费组状态为“离开” 。
- 第一个消费者发送了两次“加入组请求”,第二个和第三个消费者各向发送了一次“加入组请求” 。
2. 3个消费者,两次再平衡操作
第二种场景: 第一个消费者启动后,过一秒启动第二、三个消费者 。 第二个消费者启动后,再过一秒启动第三个消费者。 这么做的目的是让第一个消费者
收到“加入组响应”保证它发送“同步组请求” 、收到“同步组响应”之前,都不会有新消费者加入消费组(睡眠了一秒,足够第一个消费者来完成这些操作)。
第一种场景中消费组状态还是“等待同步” , 第一个消费者还没发送“同步组请求” 。 第二个消费者发送“加入组请求”,会等待第一个消费者重新发送
“加入组请求” 。 第二种场景下第一个消费者发送了“同步组请求”,并收到“同步组响应”,更改消费组状态、为“稳定” 。 在这之后,第二个消费者
才发送了“加入组请求”,和第一种场景一样,同样要完成延迟操作,协调者会等待第一个消费者重新发送“加入组请求” 。
第一个消费者调用了两次onPartltionsRevoked ()和 onPartitionsAssigned ()方法,说明第一个消费者参与了两次再平衡操作 。
第二个消费者和第三个消费者都只调用了一次回调方法,说明它们只参与了一次再平衡操作 。 第一次再平衡操作只有第一个消费者,
所以分配结果是3个分区都分配给了第一个消费者 。 第二次再平衡操作有3个消费者参与,所以每个消费者都分配到了一个分区 。
在第二个和第三个消费者调用onPartltionsRevoked()方法 ,协调者还没有开始处理它们的请求之前,即还没有开始第二次再平衡操作,
第一个消费者因为在第一次再平衡操作分配到了分区,所以它可以拉取到消息( Message_Z )。但是当协调者开始处理第二个和第三个
任何一个消费者的请求时,开始第二次再平衡操作,第一个消费者就不能再拉取消息了 。 因为消费组状态被更新为“准备再平衡”,
第一个需要重新发送“加入组请求”才能参与第二次再平衡操作 。 第二种场景消费组的状态变化过程:
稳定→准备再平衡→等待同步→稳定→准备再平衡→等待同步→稳定 。
下面是第二种场景的服务端日志,它和第一种场景的区别是发生了两次再平衡操作,具体步骤如下。
(1)协调者处理consumer- 1的加入组请求,并且完成第一次再平衡,分配到分区 。
(2)协调者处理consumer- 2 的加入组请求,消费组状态为“准备再平衡”,延迟操作不能完成 。
(3)协调者处理consumer- 3 的加入组请求,延迟操作还不能完成 。
(4 ) consumer- 1重新发送加入组请求,延迟操作完成,返回“加入组响应”给所有消费者。
(5 ) consumer- 1是主消费者,它发送“同步组请求,返回“同步组响应”给 consumer- 1 。
(6 ) consumer - 2和consumer - 3是普通消费者,协调者依次处理并返回“同步组响应”
(7)关闭应用程序,消费组状态为“离开”, 3个消费者都会从消费组中移除 。
3. 3个消费者, 3次再平衡操作
第三种场景: 第一个消费者启动后,过一秒启动第二个消费者,再过两秒启动第三个消费者 。 在第二个消费者加入消费组后,设置两秒的睡眠时间,
是为了保证前面两个消费者完成第二次再平衡操作之前,暂时不让第三个消费者提前加入消费组。 这种场景相比第二种又多了一次再平衡操作,即每
个消费者加入消费组,都会触发一次再平衡操作 。 第一个消费者经历了 3次再平衡,第二个消费者经 历了两次再平衡,第三个消费者经历了一次再平衡:
从第三种场景服务端的日志也可以看出,除去关闭应用程序, 3个消费者依次力加入消费组,总共发生3次再平衡操作 。消费组的状态变化过程:
稳定→准备再平衡→等待同步→稳定(第一次再平衡结束→准备再平衡→等待同步→稳定(第二次再平衡结束→准备再平衡→等待同步→稳定(第三
次再平衡结束) 。 具体步骤如下 。
(1)协调者处理 consumer- 1 的加入组请求,并且完成第一次再平衡,消费组状态为“稳定” 。
(2)协调者处理 consumer- 2 的加入组请求,消费组状态为“准备再平衡”,延迟操作不能完成 。
(3 ) consumer - 1重新发送加入组请求,延迟操作完成,返回“加入组响应”给两个消费者。
(4)两个消费者都发送了“同步组请求”,协调者返回“同步组响应”给两个消费者,状态为“稳定” 。
(5)协调者处理consumer- 3 的加入组请求,延迟操作不能完成, 等待前两个消费者重新加入组 。
(6 ) consumer- 1和consumer- 2重新发送“加入组请求”,延迟操作完成,状态为“等待同步” 。
(7) 所有消费者都发送了“同步组请求”,协调者返回“同步组响应”给所有消费者,状态为“稳定”。
(8) 关闭应用程序,消费组状态为“离开”, 3个消费者都会从消费组中移除 。
要理解不同消费者按照不同顺序加入消费组后,协调者处理请求的执行顺序,需要理解下面这些概念。
(1)协调者处理请求时,对消费组元数据加锁保护,保证不会同时处理多个请求 。
(2)消费组状态为“稳定”或“等待同步”,都可以转为“准备再平衡”,并创建延迟的操作 。
(3)消费组状态为“准备再平衡”时,主消费者发送“同步组请求”的分配结果是无效的 。
(4)如果消费组中有消费者,但它们还没有重新发送加入组请求,延迟操作不能完成 。
(5)一旦消费组中所有消费者的回调方法不为空,延迟操作可以完成,并返回加入组响应 。
消费组的状态转换
一次再平衡操作的正常消费组状态变化过程是:稳定→准备再平衡→等待同步→稳定,这个顺序是按顺时针转动的 。
每次再平衡操作,协调者都会创建一个延迟的操作对象。 协调者处理多个消费者的请求,并不会创建多个延迟的操作对象。
协调者在处理“加入组请求”和“同步组请求”时,会对“消费组元数据”进行加锁。 协调者只有释放了锁,才可以处理其他消费者的请求 。
协调者处理同一个消费者的“加入组请求”和“同步组请求”,这两个操作的顺序是固定的,锁的持有永远不会冲突 。 但协调者处理不同消费
者的不同请求时,则有可能发生锁被占用的情况,此时需要等待锁释放后才可以执行。 比如协调者处理第一个消费者的“加入组请求”时,
就不能处理其他消费者的“加入组请求”;同样,协调者如果正在处理第一个消费者的“同步组请求”,也不能处理其他消费者的“加入组请求”或者“同步组请求” 。
协调者处理“加入组请求”
第一个消费者重新发送“加入组请求”之前,第三个消费者先发送了“加入组请求” 。 协调者处理第三个消费者的人口状态是“准备再平衡”,但并不会
改变消费组的状态,这种情况比较特别,但也是允许的 。
消费组状态设置为“准备再平衡”时(不管是从“稳定”状态进入,还是从“等待同步”状态进入),会尝试完成一次延迟的操作。这是因为协调者处理“加入组请求”时,
不一定每次都会创建延迟的操作,所以不一定每次都会调用tryCompleteElseWatch ()方法 。 因为与延迟操作相关的外部事件有可能会完成它,所以只要有
触发它的外部事件,就应该调用 checkAndComplete ()尽快完成延迟的操作 。
协调者处理消费者的“加入组请求”还要考虑消费者是否已经在消费组中 。 如果消费者以“未知编号”发送 “加入组请求”,协调者会先创建对应的消费者
成员元数据,然后将其加入到消费组元数据中 。 在协调者返回给消费者第一次的“加入组响应”结果中,要带有分配的成员编号 , 这样下一次
消费者会以分配的成员编号发送“加入组请求”,协调者就可以从消费组元数据中找出对应编号的消费者成员元数据。
消费组到达“稳定”状态后,表示协调者处理了主消费者发送的“同步组请求”,但这并不意味着其他消费者也都发送了“同步组请求” 。
如果普通消费者发送了“加入组请求”,但 没有收到“加入组响应”,需要重新发送“加入组请求” 。 协调者处理这种消费者重新发送的“加入组
请求”时,因为消费组状态已经是“稳定”,所以它会立即返回“加入组响应”给消费者 。 具体步骤如下。
(1 ) 3个消费者发送“加入组请求”给协调者,消费组状态从“准备再平衡”到“等待同步” 。
(2)协调者返回“加入组响应”给 3个消费者,前两个消费者收到“加入组响应”,第三个没收到响应 。
(3)第二个消费者是普通消费者,它发送“同步组请求”,协调者处理时只是设置对应的回调方法 。
(4)第一个消费者是主消费者,它完成分区分配,发送“同步组请求”给协调者,消费组状态改为“稳定” 。
(5)协调者返回“同步组响应”给两个消费者,因为只有前两个消费者的回调方法不为空 。
(6)第三个消费者没收到“加入组响应,它重新发送“加入组请求”。
(7)协调者处理第三个消费者的“加入组请求”,消费组状态是“稳定”,立即返回“加入组响应” 。
(8)第三个消费者因为不是主消费者,它收到“加入组响应”,会立即发送“同步组请求” 。
(9)协调者处理第三个消费者的“同步组请求”,消费组状态是“稳定”,立即返回“同步组响应”。
上面的示例是普通消费者没有收到“加入组响应”,但如果是主消费者没有收到“加入组响应”就不同了 。 由于主消费者没有收到“加入组响应”,
它就不可能执行分区分配工作,也不会发送“同步组请求” 。 协调者也不会将消费组状态更改为“稳定”,因此仍然停留在“等待同步”,
只能等主消费者及时地重新发送“加入组请求” 。 这种场景下,整个过程的具体步骤如下 。
(1 ) 3个消费者发送“加入组请求”给协调者,消费组状态从“准备再平衡”到“等待同步” 。
(2)协调者返回“加入组响应”给 3个消费者,前两个消费者收到“加入组响应”。
(3)前两个消费者都是普通消费者,它发送“同步组请求”,协调者处理时只是设置对应的回调方法 。
(4) 主消费者没有收到“加入组响应”,重新发送“加入组请求” 。
(5)协调者处理主消费者的“加入组请求”,消费组状态是“等待同步”, 立即返回“加入组响应” 。
(6)主消费者收到“加入组响应”,执行分区分配工作,并发送“同步组请求” 。
(7)协调者处理主消费者的“同步组请求”,返回响应给所有消费者,更新消费组状态为“稳定” 。
协调者处理“同步组请求”
协调者处理消费者发送的“同步组请求”时,还会从“准备再平衡”“稳定”两种状态进入 。 先来看消费组状态为“准备再平衡”的处理步骤 。
(1)第一个消费者加入组,消费组状态为“等待同步”,第一个消费者同时作为主消费者还在执行分区分配工作 。
(2)第二个消费者作为新的消费者加入组,将消费组状态改为“准备再平衡”
(3)第一个消费者执行完分区分配工作,会发送“同步组请求”给协调者 。
(4)协调者处理第一个消费者的“同步组请求”,由于消费组状态是“准备再平衡”,它会返回“正在再平衡”的错误码给第一个消费者 。
(5)第一个消费者收到错误的“同步组响应”,会重新发送“加入组请求 ”。
再来看协调者处理“同步组请求”,从“稳定”状态进入场景。有三个消费者,第一个是主消费者,第二个和第三个都是普通消费者 。 它们都加入组后,
消费组状态为“等待同步” 。 下面的步骤是协调者处理这3个消费者发送“同步组请求”的不同执行顺序和流程。
(1)第二个消费者收到“加入组响应”后立即发送“同步组请求”,由于第一个消费者还没发送“同步组请求”,协调者处理第二个消费者的“同步组请求”
会设置第二个消费者成员元数据的回调方法。
(2)第一个消费者执行完分区分配,发送“同步组请求” 。 协调者处理第一个消费者的“同步组请求”,也会设置第一个消费者成员元数据的回调方法。
(3)协调者返回“同步组响应”给元数据中有回调方法的消费者,即第一个消费者和l第二个消费者 。 因为第三个消费者还没发送“同步组请求”,
所以它的元数据回调方法为空 ,协调者不会返回响应给它 。
(4)协调者发送完“同步组响应”后,更改消费组状态为“稳定” 。
(5)第三个消费者发送“同步组请求”,由于消费组状态为“稳定”,协调者直接返回“同步组响应”给它 。
前面我们分析了协调者处理“加入组请求”和“同步组请求”的流程,以及相关的消费组状态机转换。 除此之外,消费者也可能会离开消费组。 比如 消费
者应用程序被手动停掉,虽然消费者不会马上从消费组元数据中移除,但是协调者会对注册在消费组中的所有消费者进行监控, 一旦消费者没有反
应,就会将其从消费组中移除。 如果消费组中所有消费者成员都离开了,协调者也会把消费组删除掉。
协调者处理“离开组请求”
消费者离开消费组有多种情况 , 比如消费者应用程序被关闭,或者应用程序没有关闭,但消费者不订阅主题了 。 消费者离开消费组,表示协调者
不需要在消费组中管理这个消费者了 。 消费者客户端的工作如下 。
(1)取消定时心跳任务,因为离开组意味着不被协调者管理,就不需要向协调者发送心跳了 。
(2)通过消费者的协调者对象( ConsumerCoordinator)发送“离开组请求”给协调者。
(3)重置相关的信息,比如设置成员编号为“未知编号”、重置 rejolnNeeded变革t为 false 。
“离开组请求”的处理和“加入组请求”“同步组请求” 一样,都会对“消费组元数据”进行加锁,而且都涉及消费组的状态机转换。
第一种场景 : 消费组状态是 “准备再平衡”。由于这个状态一定存在延迟的操作,而且还没到“等待同步”,说明延迟的操作还没有完成
那么消费者的离开,有可能会导致延迟的操作可以完成,所以需要通过延迟缓存检查是否能完成延迟的操作。
第二种场景:消费在且状态是 “稳定 ” 或者 “等待同步”。这两个状态说明要离开的消费者在这之前,已经收到“加入组响应”或者收到“同步组响应” 。
消费组状态为“等待同步”,说明延迟操作已经完成,消费者已经在消费组中 。 主消费者在分配分区时,为消费组所有的消费者分配分区,当然也
包括这个即将离开的消费者 。 现在消费者要离开,原本分配给它的分区应该重新分配给其他消费者,所以需要执行再平衡操作。
再平衡超时与会话超时
协调者等待“延迟操作”完成有一个时间限制,它会选择消费组中所有消费者会话超时时间的最大值,作为“再平衡操作的超时时间”,也叫作“延迟
操作的超时时间” 。 如果是第一个消费者加入组,再平衡操作的时间等于第一个消费者的会话超时时间 。 但因为目前消费组中只有第一个消费者,
所以协调者刚刚创建的延迟操作可以马上完成 。 当第二个消费者加入组后,再平衡操作的时间会选择两个消费者的会话超时时间最大值。
比如第一个消费者的会话超时时间是 10秒,第二个消费者的会话超时时间是5秒,再平衡操作的超时时间等于 10秒,延迟的加入操作会最多等待 10秒,
等待第一个消费者在这段时间内可以重新发送“加入组请求” 。
为延迟操作设置超时时间是为了防止延迟操作一直无法完成。 假设原有的消费者迟迟没有重新发送“加入组请求”,协调者就无法确定何时才可以返
回“加入组响应”给已经发送了“加入组请求”的消费者 。 从创建延迟操作, 经过了“再平衡操作超时时间”之后,延迟操作会被强制完成。 在完成延迟操作时,
协调者会找出那些没有在规定时间内重新发送“加入组请求”的消费者,将它们从消费组中移除摊。
在发送“加入组响应”时,消费组中的所有消费者一定都在“再平衡操作超时时间” 内及时发送了“加入组请求” 。 协调者返回“加入组响应”给每个消费者后,
都会立即完成本次“延迟的心跳”,并调度下一次“延迟的心跳” 。 “延迟的心跳”和“延迟的加入组”概念上相同 ,前者因为是消费者级别,超时时间
是消费者自己的会话超时时间 ; 后者因为是消费组级别 ,超时时间是所有消费者的最大会话超时时间 。
在消费组的一次再平衡操作过程中,服务端的协调者只有一个延迟的加入对象( DelayedJoin ),并且它会为每个消费者保存一个延迟的心跳对象,
( DelayedHeartbeat )用来监控消费者是否及时地发送心跳,具体步骤如下 。
(1)消费者发送“加入组请求”时会指定会话的超时时间(简称“会话时间”)。
(2)协调者不能立即返回“加入组响应”给消费者,创建一个消费组级别的“延迟加入” 。
(3)“延迟加入”可以完成,协调者返回“加入组响应”给消费组中的每个消费者 。
(4)协调者为每个消费者都创建一个“延迟心跳”,并监控每个消费者是否存活 。
协调者在处理完消费者的 “加入组请求”后,会返回“加入组响应”给消费者 。 消费者收到“加入组响应”后 , 就应该在会话时间内及时发送“同步组请求”给协调者 ;
否则 ,协调者就会认为消费者出现了故障。 协调者在处理“同步组请求”时,有多个地方调用了“完成若调度下一次心跳”方法 。
(1)状态为“等待同步”,在设置成员元数据的回调方法后调用 。
(2)状态为“稳定 ” ,在发送“ 同步组响应”给消费者后调用 。
(3)状态为“等待同步”,收到主消费者的“同步组请求”,给每个消费者发送 “ 同步组响应”后调用 。
第三处的用法和协调者处理“加入组请求”时 , 给每个消费者发送“加入组H向应”后调用“完成并调度下一次心跳”方法类似。 它们都针对所有消费者,而不是单个消费者 。
但前面两个用法,只针对一个消费者 。
协调者创建完“延迟操作”对象后,一个很重要的步骤是:当“延迟操作”相关的外部事件发生时 , 就需要通过延迟缓存尝试完成延迟的操作 。
对于“延迟的加入组”,外部事件是消费者发送了“加入组请求” ;对