5.4.7 延迟的心跳

延迟操作有3个主要的方法:尝试完成方法(返回布尔值,表示是有可以完成)、超时的回调方法、完成的回调方法。对于“延迟加入”,尝试完成是判断消费组成员中是否还有消费者没有重新发送“加入组请求”,如果全部都发送了“加入组请求”,就认为“延迟加入”可以完成。“延迟加入”完成时的回调方法会发送“加入组响应”。

“延迟心跳”的尝试完成方法(tryCompleteHeartbeat())判断条件是:消费者成员是否存活。如果消费者存活,则可以调用完成时的回调方法(onCompleteHeartbeat())。但完成“延迟心跳”的方法实现中,并没有具体的处理代码:

延迟 python3 延迟心动_延迟 python3

判断消费者成员是否存活有下面的3种条件,只要任何一个条件满足,都认为消费者是存活的。

  • 消费者成员的awai.ti.ngJoi.nCallback回调方法不为空。
  • 消费者成员的awai.ti.ngSyncCallback回调方法不为空。
  • 消费者成员最近的心跳时间加上会话超时时间大于下一次心跳的截止时间。

先来看最后一个条件,因为截止时间是在创建“延迟心跳”时指定,那就来看创建“延迟心跳”时是怎么做的。

  1. 完成并调度下一次心跳

再来回顾下协调者调用“完成并调度下一次心跳”方法,创建“延迟心跳”的相关上下文。协调者先通过checkAndCoMplete()方法尝试完成已有的“延迟心跳”。通常来说,这一步一定能够完成“延迟的心跳”,否则就没有必要再创建新的“延迟的心跳”了。
注意:如果是协调者第一次调用checkAndCo~叭ete()方法,因为延迟缓存中还没有这个消费者的“延迟心跳”,所以第一次不会真正执行该方法。

在完成了上次的“延迟心跳”后,协调者会计算出下一次的心跳截止时间,并创建新的“延迟心跳”。这一次通过tryCompleteElseWatch()方法尝试完成刚刚创建的“延迟心跳”,则一定不能完成。因为判断能够完成的条件是:最新的心跳时间加上会话超时时间必须大于下一次心跳的截止时间。而刚刚创建的“延迟心跳”对象,在计算这个条件时,“最新条件时间加上会话超时时间等于下一次心跳的截止时间”,因此不满足完成的条件。相关代码如下:

延迟 python3 延迟心动_缓存_02

如图5-27所示,以协调者处理“加入组请求”和“同步组请求”时,调用“完成和调度下一次心跳”方法(下文简称“调度方法”)为例。有3个地方会调用该方法:协调者返回“加入组响应”给每个消费者之后、协调者处理消费者的“同步组请求”时、协调者返回“同步组响应”给每个消费者之后,具体步骤如下。

(1)协调者发送“加入组响应”给某个消费者后,当前时间为0秒。第一次调用调度方法,延迟缓存中没有“延迟心跳”,先创建“延迟的心跳”,而且它的截止时间为5秒,」之满足完成的条件,加入延迟缓存。
(2)消费者在l秒时发送了“同步组请求”,当前时间为l秒。协调者处理消费者的“同步组请求”,第二次调用调度方法,延迟缓存中有“延迟的心跳”,尝试完成它,可以完成。然后创建新的“延迟心跳”,截止时间为6秒。
(3)协调者发送“同步组响应”给某个消费者后,当前时间为3秒。第三次调用调度方法,延迟缓存中有“延迟的心跳”,尝试完成它,可以完成。然后创建新的“延迟心跳”,截止时间为8秒。

如图5-28所示,我们从延迟缓存的角度看调度方法。调度方法分3步:检查井完成延迟心跳、创建新的延迟心跳、尝试完成并监视延迟的心跳。如果缓存中已经存在延迟操作,第一步一定会完成延迟的心跳,并将延迟心跳从缓存中删除。第三步一定不会完成新创建的延迟心跳,并将刚创建的延迟心跳加入缓存。

延迟 python3 延迟心动_回调方法_03

如表5-8所示,当执行调度方法时,判断延迟心跳是杏可以完成。截止时间是上一次延迟心跳的截止时间,而最近心跳是当前时间,所以第三列的条件总是能够成立(第一次没有截止时间)。

延迟 python3 延迟心动_协调者_04

如图5-29所示,调度方法每次创建新的延迟心跳,都会更新截止时间。只要在下一次心跳截止时之前执行调度方法,都会完成延迟的心跳。但如果没有在截止时间内再次执行调度方法,延迟缓存中的延迟心跳就会超时,对应的消费者就有可能被协调者从消费组中移除(还有下面分析的其他条件限制)。

注意:协调者创建的每个延迟心跳都和消费者一一对应,延迟心跳的超时时间是消费者设直的会话超时时间。放入延迟缓存中的延迟,心跳被用来表示消费者是否存活。如果消费者在延迟心跳的截止时间之前再次调用了调度方法,旧的延迟,心跳满足完成的条件,会从缓存中弹出并执行。协调者会创建新的延迟心跳,新延边,\.;跳的截止时间也会被更新。通过这种方式,消费者只要存活,都对应缓存中的一个延迟心跳。

延迟 python3 延迟心动_延迟 python3_05

再来看判断消费者成员是否存活的另外两个条件:消费者成员的awai.ti.ngJoi.nCallback或awai.ti.ngSyncCallback回调方法不为空,这两个条件下即使超时了,也被认为是存活的。

  1. 判断消费者成员是否存活
    消费者成员元数据的回调方法有两个,先来看awai.ti.ngSyncCallback和延迟心跳示例。如图5-30所示,假设3个消费者设置的会话超时时间分别是:Cl=lO秒,C2=20秒,C3=40秒。协调者完成“延迟加入”,发送“加入组响应”的时间为10:00:00。对应每个消费者的下次心跳截止时间分别是:Cl=10:00:10,C2=10:00:2日,C3=10:00:40。Cl在10:00:03发送了“同步组请求”,协调者更新Cl的心跳截止时间为10:00:13。那么照理说,Cl在10:00:13后因为一直都没有机会再调用调度方法,所以“延迟的心跳”就会超时,Cl就会被协调者从消费组中移除。但实际上,协调者处理Cl的“同步组请求”时,设置了awai.ti.ngSyncCallback回调方法。即使“延迟的心跳”超时了,但“回调方法不为空”,消费者成员仍然被认为是存活的,Cl就不会从消费组中移除。

注意:“延迟的心跳”在超时后,还是会调用onExpi.reHeartbeat()方法,只不过对能够真正执行onMefTlberFai.lure.O方法再加上一层限制条件,防止消费者仍然存活,却被移除掉。

延迟 python3 延迟心动_延迟 python3_06

当主消费者C2直到10:00:20才发送“同步组请求”,而C3还没有发送“同步组请求”。协调者处理C2的“同步组请求”时,只会完成Cl和C2上一次的延迟心跳,并创建新的延迟心跳。假设协调者返回“同步组响应”给Cl和C2的时间是10:00:25,那么Cl下一次的心跳截止时间为10:00:35,C2下一次的心跳截止时间为10:00:45。下面的步骤是3个消费者延迟心跳的变化情况。

(1)协调者10:00:03处理Cl的“同步组请求”,Cl的下次心跳截止时间为10:00:13。
(2)协调者10:00:20处理C2的“同步组请求”,C2的下次心跳截止时间为10:00:40。
(3)协调者10:00:25返回“同步组响应”给Cl,Cl的下次心跳截止时间为10:00:35。
(4)协调者10:00:25返回“同步组响应”给C2,C2的下次心跳截止时间为10:00:“。
(5)协调者10:00:40处理。的“同步组请求”,C3的下次心跳截止时间为10:01:20。

协调者调用“完成并调度下一次心跳”的调度方法时,不管外部事件是哪一种(返回加入组响应、处理同步组请求、返回同步组响应),都会更新同一个延迟的心跳对象。在任何时刻,消费者在延迟缓存中的延迟心跳只有一个。也就是说,协调者在返回“加入组响应”时,也可能会更新返回“同步组响应”创建的延迟心跳。

再来看“判断消费者成员存活”的另一个条件:awa"i.t"i.ngJo"i.nCallback回调方法不为空。

协调者在处理普通消费者的“同步组请求”时,除了设置awa"i.t"i.ngJo"i.nCallback回调方法,也会调用调度方法更新延迟的心跳。如果延迟心跳超时,但主消费者还没有发送“同步组请求”,普通消费者仍然被认为是存活的。协调者在处理消费者的“加入组请求”时,也会设置awa"i.t"i.ngJ01.nCallback回调方法,但不会调用调度方法。如果对应的延迟心跳超时,但延迟的加入操作还不能完成,消费者也被认为是存活的。

如图5-31所示,仍然以前面的示例作为基础,不过这里假设最开始只有两个消费者,协调者在10:00:00返回同步组响应给Cl和C2,它们的下次心跳截止时间分别是口=10:00:10和口=10:00:20。下面几个步骤是协调者处理“加入组请求”、延迟加入、延迟心跳相关的事件顺序。

(1)新的消费者C3在10:00:02加入组,Cl和C2必须在心跳截止时间内重新发送“加入组请求”。
(2)Cl在10:00:03重新发送“加入组请求”,延迟加入不能完成,因为C2还没有发送“加入组请求”。
(3)当时间到10:00:10时(并不是10:00:13),Cl的延迟心跳超时了。但因为协调者处理“加入组请求”时,设置了awa"i.t"i.ngJo"i.nCallback回调方法,所以Cl还是存活的。
(4)C2在10:00:15重新发送了“加入组请求”,延迟加入可以完成。协调者返回响应给3个消费者,并且更新它们的下次心跳截止时间,分别是:C1=10:00:25、(2=10:00:35、ζ3=10:00:550

注意:协调者处理“加入纽请求”时,因为没有调用调度方法,并不会更新下次心跳的截止时间。所以图中协调者处理Cl,它的心跳截止时间还是上一次的10:00:1日,新加入的消费者C3则没有延迟心跳。

延迟 python3 延迟心动_回调方法_07

针对协调者处理“加入组请求”的awa"i.t"i.ngJo"i.nCallback回调方法,再举个异常的例子。如图5-32所示,假设消费者2并没有在下次心跳截止时间(1日:00:2日)之前重新发送“加入组请求”,对应的延迟心跳会判断到消费者2失败,从而将其从消费组中移除。另外,延迟的加入在完成时,协调者也不会返回“加入组响应”给消费者2,因为它已经不在消费组中了。

延迟 python3 延迟心动_回调方法_08

总结成员元数据的awai.ti.ngJoi.nCallback和lawai.ti.ngSyncCallback回调方法使用的地方。

  • 消费者处理“加入组请求”和“同步组请求”时先保存回调方法,在返回响应时调用回调方法。
  • awai.ti.ngJoi.nCallback还用来判断消费组中的消费者是否重新发送了“加入组请求”。
  • 即使消费者的延迟心跳超时了,如果元数据的两个回调方法不为空,消费者仍然被认为是存活的。

协调者会在消费者加入组的过程中创建延迟的心跳。消费者成功加入消费组(即消费组进入稳定状态)后,它会发送心跳请求给协调者。协调者处理消费者的心跳请求时,也会调用“完成并调度下一次心跳”方法。

  1. 协调者处理,心跳
    消费者成功加入组后,会在调用。『iJoi.nComplete()回调方法后重置心跳任务,重新开始调度发送心跳的定时任务。这里用“重置”是因为消费组会经常发生再平衡,每次再平衡过后,消费组状态变为“稳定”,每个消费者都需要重新发送心跳请求给协调者。

延迟 python3 延迟心动_协调者_09

协调者处理消费者发送的心跳请求,没有其他的依赖限制条件。比如,不像“延迟加入”那样需要等待其他消费者都发送了“加入组请求”,才会返回“加入组响应”;也不需要等待主消费者发送“同步组请求”后,才返回“同步组响应”。协调者处理心跳请求和加入组过程中调用调度方法一样,也会立即完成延迟缓存中已有的延迟心跳,并创建一个新的延迟心跳并重新放入延迟缓存。最后,心跳的处理没有产生结果数据,协调者直接返回没有错误码的“心跳响应”给消费者。


5.5 小结

第4章和本章主要分析了新消费者的客户端(KafkaConsumer)和服务端的协调者(GroupCoordinator)。消费者客户端的主要业务逻辑是拉取消息,而为了拉取消息必须分配到分区。同一个消费组的所有消费者通过向协调者发送“加入组请求”,最终获得分配给向己的分区。服务端的协调者负责消费组的再平衡操作,将集群所有的分区按照分配算法分配给消费组的每个消费者。

新的消费者将“消费组管理协议”和“分区分配策略”进行了分离。协调者仍然负责消费组的管理,包括消费者元数据、消费组元数据、消费组状态机等数据结构的维护。而分区分配的实现则会在消费组的一个主消费者中完成。由于分区分配交由主消费者客户端完成,但每个消费者为了获得分区分配结果,还是只能和协调者联系,因此主消费者在完成分区分配后,还要将分配结果发送回协调者。采用这种方式,每个消费者都需要发送下面两种请求给协调者。

  • 加入组请求。协调者收集消费组的所有消费者,并选举一个主消费者执行分区分配工作。
  • 同步组请求。主消费者完成分区分配,由协调者将分区的分配结果传播给每个消费者。

消费者发送“加入组请求”给协调者,是为了让协调者收集所有的消费者。协调者会把消费者成员列表发送给主消费者,这样主消费者才可以执行分区分配工作。每个消费者发送给协调者的“加入组请求”,都带有各自的消费者成员元数据。比如,消费者订阅的的主题、消费组编号、会话超时时间等。“加入组请求”和“加入组响应”的字段如下:

延迟 python3 延迟心动_缓存_10

主消费者收到的“加入组响应”带有所有的消费者成员,它在执行完分区分配工作后,发送给协调者的“同步组请求”带有分配给每个消费者的分区结果。协调者在收到主消费者的“同步组请求”后,会先将消费组的分配结果持久化,然后才返回“同步组响应”给每个消费者。每个消费者的“同步组响应”只包含分配给这个消费者的分区列表,分区分配算法保证了不同消费者的分区一定是不同的。“同步组请求”和“同步组响应”的字段如下:

延迟 python3 延迟心动_缓存_11

注意:“同步组请求”和“同步纽响应”中并没有消费者成员字段(MemberId,消费组状态中的不算)。这是因为消费者成员编号在“加入组请求”和“加入纽响应”中已经存在,所以就不需妥了。

消费者发送“同步组请求”,是在它收到协调者的“加入组响应”后才开始的,“加入组请求”和“同步组请求”链式依次调用。协调者处理不同消费者的这两种请求,用消费组状态机来维护不同的事件。消费组的状态主要有下面3个。

  • “准备再平衡”。新消费者加入组或者旧消费者离开组消费组都需要执行一次再平衡操作。
  • “等待同步”。所有消费者都加入组,协调者返回“加入组响应”给每个消费者前,更改状态为“等待同步飞它表示协调者等待接收主消费者发送的包含消费组分配结果的“同步组请求”。
  • “稳定”。协调者返回带有分区分配结果的“同步组响应”给每个消费者。

协调者除了管理消费者的负载均衡,并最终分配分区给每个消费者,还会接收每个消费者的心跳请求。协调者通过心跳监控消费者成员是否存活:如果消费者没有在指定的截止时间内发送心跳,协调者认为消费者失败,将其从消费组中移除,这样消费组就需要执行再平衡操作。另外,协调者在处理“加入组请求”和“同步组请求”过程中,为了保证参与加入组的消费者及时响应,也会用心跳来监控消费者成员是否还存活。