消费者拉取数据是在拉取器中完成的,发送心跳是在消费者的协调者上完成的,但并不是说拉取器和消费者的协调者就没有关联关系 。
“消费者的协调者”的作用是确保客户端的消费者和服务端的协调者之间的正常通信,如果消费者没有连接上协调者(比如协调者认为消费者挂了,或者消费者认
或者消费者认为协调者挂了),那么拉取器的拉取工作以及后续的消息消费等工作就都无法正常进行。
每个消费者都需要定时地向协调者发送心跳,以表明向己是存活的 。 如果消费者一段时间内没有发送心跳,协调者就会认为消费者挂掉了 。
协调者还要能够对消费组成员失败进行处理,比如将失败消费者拥有的分区分配给其他消费者消费。 心跳通常会作为分布式系统的健康检查状况手段,通过让
每个节点都定时上报心跳信息、给某个中心节点,如果一段时间没有收到某个节点的心跳,中心节点就认为那个节点挂掉了 。
心跳状态
每个消费者客户端都只有一个心跳任务,心跳对象( Heartbeat )除了记录心跳任务的元数据——会话超时时间( timeout )、定时任务时间间隔( interval ),
还会记录当前心跳任务的状态一一最近的会话重置时间 、最近的心跳发送时间 、最近的心跳接收时间 。
心跳任务发送心跳请求的主要逻辑是:在发送心跳请求前,记录心跳状态的最近心跳发送时间( lastHeartbeatSend );在收到心跳响应结果后,
记录心跳状态的最近心跳接收时间( lastHeartbeatReceive );然后计算下一次心跳任务的发生时间,新创建一个延迟的心跳任务 。
处理心跳结果的示例
客户端启动时会创建调度时间为0秒的延迟任务加入队列 。 客户端轮询的时间为2秒,会弹出延迟任务(因为延迟任务的调度时间小于当前时间),
现在队列为空了 。 但是因为没有上一次心跳,只有上一次的会话重置时间,经过下面3个步骤的计算后,会重新创建一个调度时间为5秒
的延迟任务加入队列 。
(1) 距离上次心跳的时间间隔=当前轮询的时间-上次会话的重置时间= 2秒 - 0秒= 2秒 。
(2) 距离下次心跳的时间间隔=心跳间隔 -距离上次心跳的时间间隔= 5秒 - 2秒= 3秒 。
(3)下次心跳任务的时间=当前轮询的时间+距离下次心跳的时间间隔= 2秒 + 3秒= 5秒。
上面的步骤执行尾,队列中延迟任务的调度时间为5秒。 在这之后,如果轮询时间小子5秒,则不 会弹出队列的延迟任务,因为轮询的当前时间
小于延迟任务的调度时间 。 如下图所示,只有当轮询时间为5秒时,才会弹州调度时间为5秒的延迟任务,现在队列又为空 了 。 此时经过下面两个步骤计算
出来的“距离下次心跳时间间隔”为0秒,就会执行发送心跳请求的逻辑。
(1)距离上次心跳的时间间隔 = 当前轮询的时间-上次会话的重置时间= 5秒-0秒= 5秒 。
(2)距离下次心跳的时间间隔 = 心跳间隔 -距离上次心跳的时间间隔 = 5秒- 5秒= 0秒 。
在发送心跳请求之前,先记录上一次的心跳时间为当前时间即 5秒。 假设心跳在8秒时才完成(虚线部分), 经过下面3个步骤后,
会重新创建调度时间为 10秒的延迟任务放入队列中 。
(1) 距离上次心跳的时间间隔 = 心跳完成的时间 -上次心跳的时间= 8秒 - 5秒= 3秒 。
(2) 距离下次心跳的时间间隔 = 心跳间隔-距离上次心跳的时间间隔= 5秒 - 3秒= 2秒 。
(3)下次心跳任务的时间 = 心跳完成的时间+距离下次心跳的时间间隔= 8秒 + 2秒= 10秒
延迟任务并不是一个线程,它必须通过客户端的轮询来触发执行。 客户端刚启动时必须先创建一个延迟任务放入队列,这样客户端在轮询时,
才有可能获取出延迟的任务去执行。 如果客户端启动时没有创建延迟任务,那么队列中就永远不会有延迟任务 。 另外,轮询时如果弹出了需要执行的延迟
任务,不管有没有执行发送心跳请求的流程,都要重新创建新的延迟任务放入队列 。 那么第一次创建延迟任务是在客户端启动后的什么时候
发生呢?
心跳和协调者的关系
客户端调用心跳任务的 reset ()方法会创建第一个延迟任务,这个方法的调用链如下 。
- 确保协调者是已知的,即消费者客户端必须连接上管理消费组的协调者 。
- 确保消费组是活动的, 即消费者必须分配到分区 。
消费者和协调者进行交互操作,必须确保消费者已经知道并且连接上协调者所在的节点,如果都没有连接上协调者,心跳等其他操作都不会正常进行。
连接上协调者后,就可以立即向协调者发送一次心跳。 另外,如果消费者需要重新加入消费组,在分配到分区后,也要重置心跳任务 。
消费者发送心跳,正常来说应当只是通知一下服务端协调者而已 。 不过在分布式系统中,通信的双方可能都会存在一些问题,
比如协调者可能会突然挂掉。 这时服务端应该为每个消费组重新选择一个协调者,但如果此后消费者连接的还是原来的协调者就有问题了
(它应该连接最新的协调者节点),这种情况应该让消费者重新获取协调者。 服务端如果能够在客户端定时发送的心跳任务中附带这种信
息,客户端就能够及时知道应该再去找最新的协调者 。 消费者针对不同错误码的处理方式如下 。
- 协调者挂掉了,客户端设置“消费组的协调者”对象为空 ,消费者需要重新发送“获取消费组的协调者”请求获取新的协调者。
- 协调者没有挂掉,客户端设置“需要重新加入组”变量为 true ,消费者需要向协调者重新发送“加入组请求”加入消费组。
心跳的响应处理中并不执行具体的错误处理操作,比如让客户端连接新的协调者或者让消费者重新加入消费组,心跳只是更新这些后续错误处理操作
相关的条件变量 。 当客户端轮询时会监听到条件 变量发生了变化,从而让轮询操作主动地执行对应的操作 。 这里可以把心跳看作任务通知,必须依赖
客户端的轮询来确保能及时捕获到错误情况进行错误处理,不在心跳中处理任务是因为任务的操作时间可能比心跳间隔长得多 。 所以,客户端的轮询非常重要,
它不仅仅驱动了数据的不断拉取,还可以根据心跳结果执行不同的任务 。
消费者的协调者因为需要定时存储分区的消费进度,还有一个自动提交偏移量的定时任务。