消费组发生再平衡时分区会被分配给新的消费者,为了保证新消费者能够从分区的上一次消费位置继续拉取并处理消息,每个消费者需要将分
区的消费进度,定时地同步给消费组对应的协调者节点 。新AP I为客户端提供了两种提交偏移盐的方式:异步模式和同步模式 。
另外,如果消费者客户端设置了向动提交( enable . auto . commit=true ,默认开启)的选项,会在客户端的轮询操作中调度定时任务,
定时任务也属于异步模式提交偏移量的一种运用场景
自动提交任务
如果消费者开启自动提交 消费者会通过“消费者的协调者”对象的向动提交任务( AutoCommitTask )定时将分区的拉取偏移量( position )
保存到服务端,然后更新分区状态的“提交偏移量:( commited )。 自动提交任务定义的 run ()方法只会执行一次,在每次任务完成后,
也要像发送心跳请求一样创建下一次的延迟任务 。
另外,创建第一个延迟任务也非常关键,在心跳任务中也提到了:如果没有创建第一个延迟任务,就永远不会有定时任务产生 。
只有创建了第一个延迟任务并放入队列,当延迟任务超时后,会从队列弹出这个超时任务并执行;任务执行完毕后会创建
一个新的延迟任务放入队列 。 通过这种方式,队列中只会存在一个延迟任务,并且确保一直有一个延迟任务。 心跳任务是在
消费者连接上协调者或者消费者加入消费组后,调用心跳任务的重置方法创建第一个延迟任务。 自动提交任务也是在
消费者加入消费组后,调用自动提交任务的enable ()方法创建第一个延迟的自动提交任务 。
如下图 所示,假设定时提交任务的时间间隔为5秒,消费者轮询的时间为 1秒。 但并不是说消费者发送心跳的时间点是 : 0秒、5秒、10秒。
下一次定时任务的调度时间点,要根据上一次定时任务完成时的时间来决定 。 比如第一次定时任务第8秒完成,下一次定时任务的时间点就是第 13秒,
具体步骤如下 。
(1) 开启定时任务,第一个延迟任务的时间=当前时间+时间间隔 = 0秒+ 5秒 = 5秒。
(2) 前面几次轮询都不会弹出延迟任务,第五次轮询弹出延迟任务,执行异步的提交偏移业任务 。
(3) 异步提交任务在第 8秒完成,新延迟任务的时间=当前时间 + 时间间隔 = 8秒 + 5秒 = 13秒。
自动提交任务运行时,调用 commitOffsetsAsync ()方法,采用异步模式提交偏移盘 。 消费者发送OFFSET_COMMIT请求给协调者,
“提交偏移量请求”定义的响应处理回调器( OffsetCommitResponseHandler)实现了 CoordlnatorResponseHandler抽象父类 。
提交偏移量的逻辑定义了多个回调方法,在回调方法的不同阶段都会处理不同的业务逻辑。
(1)消费者收到“提交偏移量请求”的响应结果,更新分区状态的“提交偏移量"变量。
(2)组合模式异步请求对象的监听器回调方法中,调用 OffsetCommitCallback的回调方法 。
(3)偏移量提交的最后一个回调方法,会创建新的延迟任务 。
异步发送请求,没有使用阻塞式的client.poll(future )轮询, 而是使用无阻塞的client . quickPoll () 。由于向动提交任务不需要关心结果,
也不会通过future.value ()获取异步请求的结果 。 但发送完请求后不关心结果,并不代表着不处理结果,客户端请求一定都有对应的客户端响应结果 。
客户端提交偏移量后,如果没有响应结果返回 ,或者客户端不处理响应结果。 那么仅仅将分区状态中拉取偏移量( position )代表的提交偏移量
( consumedOffset ) 保存到服务端 ,而没有更新分区状态的提交偏移量( committed),就会导致服务端保存的 “ 己提交偏移量”和消费者本地订阅状态的
“提交偏移量”数据不一致。
将拉取偏移量作为提交偏移量
旧API 中 , 当客户端迭代消费消息时会更新分区信息的已消费偏移量 ,并且有一个后台线程定时将分区信息的已消费偏移量作为已提交偏移量发送给
协调者节点 。新API中,订阅状态的分区状态有拉取偏移量( position ) 和提交偏移量( committed ) 两个变量。 客户端的轮询方法会在返回拉取的记录集之前,
更新分区状态的拉取偏移量,为下一次轮询操作中的拉取做准备 。 但客户端在迭代消费者记录集时,并没有更新分区状态的提交偏移量 。 所以拉取偏移量
变量也要能够代表分区的消费进度 ,即新API会使用拉取偏移量的值作为分区的提交偏移量发送给协调者节点 。
新API在迭代消息时没有更新订阅状态的任何变量,可以认为并不存在且消费偏移量这个变量 。分区状态还要保存提交偏移量这个变量的原因是 :
在轮询时 ,如果分区没有拉取偏移量,需要从协调者获取其他消费者提交的分区偏移量 , 然后保存到分区状态对象的提交偏移量 , 再将提交偏移量赋值
给拉取偏移量,这样分区状态的拉取偏移量就有数据了,客户端才可以发送拉取请求拉取消息 。
因为延迟任务的调度是在客户端的轮询中触发,而客户端的轮询又是在Kafka消费者的轮询方法中调用的,所以如果Kafka消费者没有轮询,
就不会执行延迟的任务 。 即使任务超时了,它也没有机会从延迟队列中移除 出去并执行。Kafka的轮询除了客户端的轮询
(在客户端轮询之前,还有发送拉取请求),还有一个步骤是拉取器获取记录集,客户端应用程序调用一次Kafka的轮询 ,
会返回一批消费者记录集 。 拉取器在返回获取的记录集给客户端应用程序处理之前,会更新本次拉取记录集后的订阅状态,
即分区的拉取偏移量。综合上面两点的背景知识,再结合拉取器拉取消息、 Kafka轮询的流程,具体步骤如下。
(1)拉取器发送拉取请求;客户端轮询,会把拉取请求发送出去 。
(2) 客户端轮询还有可能弹出超时的延迟任务,比如定时提交任务的调度时间到 了,应该立即执行 。
(3)拉取器的拉取请求完成后,通过回调处理器暂存拉取结果 。
(4) 拉取器调用获取记录集方法,更新订阅状态中分区的拉取偏移量,并返回结果给客户端应用程序 。
(5 ) 最后客户端应用程序开始处理Kafka轮询返回的消费者记录集 。
从上面的步骤中可以得出的结论是 : 延迟的提交任务超时后会被立即执行,它会比获取记录集时更新分区状态的拉取偏移量要早 。 Kafka轮询到结果集后,
前面这两个步骤都执行完后,客户端应用程序才会真正处理拉取的消费者记录集 。
现在来回答“定时提交任务为什么可以采用拉取偏移量作为提交偏移量”了 。 定时提交任务在超时后会立即执行,并且发生在本次轮询中拉取器
更新最新一批记录集的拉取偏移量之前。 而且这一次Kafka轮询中的定时提交任务一定发生在上一次的Kafka轮询都全部执行完成之后,而上一次Kafka轮询
一定成功更新了拉取偏移量,并且也成功处理了上一次拉取的那批记录集。 所以本次轮询中定时提交任务需要获取的提交偏移量,实际上等价于上一次轮询
更新后的拉取偏移量。
消费者拉取消息、心跳请求以及本节的定时提交任务都和轮询有关。 可见,轮询是消费者的入口,通过轮询,只要事件发生,就有对应的处理逻辑来接手,
后端的操作对于消费者都是透明的 。
同步提交偏移量
自动提交任务使用异步模式提交偏移量,调用client. qutckPoll ()后,可以立即回到主线程,所以异步模式是无阻塞的 。 而同步模式提交偏移量,
调用者必须等到提交偏移量完成后才回到主线程,所以同步模式是阻塞的 。
自动提交任务使用异步方式提交偏移量 , 因为任务是周期性运行的,没有什么依赖条件 , 不需要采用阻塞方式 ;而同步提交通常是因为存在某些依赖条件,
必须等待提交完成后才能往下进行。
异步提交偏移量是通过自动动提交任务触发的,那么同步提交偏移量是什么时候被调用的呢?
消费者在准备加入或重新加入消费组之前 , 如果开启了自动提交任务,要先暂停定时任务,执行一次同步模式的提交偏移量方法。
消费者调用 commitOffsetsSync ()方法后 ,必须等待消费者把偏移量提交到服务端并且收到响应结果,然后才允许进行下一步的操作 。
消费者内部的自动提交任务虽然是异步的, 但却是定时的 。 如果消费者想要更精确地控制提交偏移量的时机, 可以调用 KafkaConsumer暴露
出来的同步提交方法( commitSync() )或异步提交方法( commitAsync() )。 比如,处理每一条记录就提交一次偏移量,或者只有轮询一次才提交一次 。
消费者的消息处理语义
消费者从消息代理节点拉取到分区的消息后,对一条消息的处理语义有下面3种情况。
- 至多一次。消息最多被处理一次 , 可能会丢失,但绝不会重复传输 。
- 至少一次。消息至少被处理一次,不可能丢失,但可能会重复传输 。
- 正好一次。消息正好被处理一次,不可能丢失,也不可能重复传输 。
1 . 至多一次
消费者读取消息, 先保存消费进度,然后才处理消息。 这样有可能会出现:消费者保存完消费进度,但在处理消息之前挂了 。 新的消费者会从保存的
位置开始,但实际上在这个位置之前的消息可能并没有被真正处理。 这种场景对应了“至多一次”的语义, 即消息、有可能丢失(没有被处理)。
Kafka消费者实现“至多一次”的做法是 : 设置消费者自动提交偏移量,并且设置较短的提交时间间隔 。
2. 至少一次
消费者读取消息, 先处理消息,最后才保存消费进度。 这样有可能会出现 : 消费者处理完消息,但是在保存消费进度之前挂了 。 新的消费者从保存
的位置开始,有可能会重新处理上一个消费者已经处理过的消息 。 这种场景对应了“至少一次”的语义, 即消息有可能会被重复处理。
Kafka消费者实现至少一次的做法是 : 设置消费者自动提交偏移量,但设置很长的提交间隔(或者关闭向动提交偏移量)。 在处理完消息后,
手动调用同步模式的提交偏移量方法 。
3 . 正好一次
实现正好一次的消息处理语义有两种典型的解决方案 : 在保存消费进度和保存消费结果之间,引 人两阶段提交协议;或者让消费
者将消费进度和处理结果保存在同一个存储介质中 。 比如,将读取的数据和偏移盘一起存储到HDFS , 确保数据和偏移量要么一起被更新,要么都不会更新。
Kafka消费者实现正好一次的做法是 : 设置消费者不自动提交偏移量,订阅主题时设置自定义的消费者再平衡监听器( ConsumerRebalancelistener )。