如果在使用生产者客户端发送消息的时候将acks参数设置为-1,那么就意味着需要等待ISR 集合中的所有副本都确认收到消息之后才能正确地收到响应的结果,或者捕获超时异常。
那么这里等待消息写入follower副本井返回相应的响应结果给生产者客户端的动作是由谁来执行的呢?在将消息写入leader副本的本地日志文件之后,Kafka会创建一个延时的生产操作(DelayedProduce),用来处理消息正常写入所有副本或超时的情况,以返回相应的响应结果给生产者客户端。
延时消息分为延时生产消息和延时拉取消息。
1. 延时生产消息
生产者客户端生产消息的时候,消息写入leader副本,如果acks参数设置为-1,Kafka会创建一个延时的生产操作,并将延时生产操作加入延时操作管理器,触发事件事件包括外部事件触发和超时触发。
1.1 外部事件触发 – HW增长
介绍HW之前,先介绍一下LEO – Log End Offse,它标识当前日志文件中下一条待写入消息的offset,分区ISR集合中的每个副本都会维护自身的LEO。HW(高水位标记)对应的是ISR集合中的最小LEO,所以HW增长则表示分区中多数副本已经拉取到了最新消息,此时延时生产操作便被触发,返回成功给生产者客户端。
1.2 超时触发
超时比较容易理解,当超过指定时间,HW未发生变化,则认为消息仍未被follower拉取,便同样触发延时生产操作,只不过这里返回错误给生产者客户端。
2. 延时拉取消息
延时拉取操作同样由延时操作管理器管理,触发事件包括外部事件触发和超时触发。
2.1 外部事件触发
延时拉取操作的外部事件不同于延时生产操作,因为拉取操作的请求方可能是消费者客户端和follower副本,所以需要考虑两种场景。
- 消费者客户端拉取:
当消费者客户端发出延时拉取操作时,broker并不是立即返回,而是将延时拉取操作添加到延时操作管理器管理,并等待外部事件(HW增长)的到来。随着生产者客户端消息的写入,HW增长,延时拉取操作被触发,broker随即返回成功给消费者客户端。 - follower副本拉取:
follower副本通过从leader副本消费消息的方式保持和leader副本的消息同步。当follower副本和leader副本的消息保持一致,而leader副本又无新消息写入时,follower副本向leader副本发出拉取操作将无意义,并且消耗资源,显然不合理。此时leader副本是如何做的呢?
原来,Kafka在处理拉取请求时,会先读取一次日志文件,如果收集不到足够多的消息,那么就会创建一个延时拉取操作以等待拉取到足够数量的消息。延时操作创建完成同样被添加到延时操作管理器,等待外部事件(leader副本的日志文件新增消息)到来。随着生产者客户端消息的写入,消息追加到leader副本的日志文件,延时拉取操作被触发,此时,会再读取一次日志文件,然后将拉取结果返回给follower副本。
2.2 超时触发
超时触发比较简单,就是等到超时时间触发第二次读取日志文件的操作。
3. 延时消息实现原理
Kafka中存在大量的延时操作,比如延时生产、延时拉取和延时删除等。Kafka 并没有使用JDK自带的Timer 或DelayQueue来实现延时的功能,而是基于时间轮的概念自定义实现了一个用于延时功能的定时器(SystemTimer)。JDK中Timer和DelayQueue的插入和删除操作的平均时间复杂度为O (nlogn) 并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。
3.1 时间轮
时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList见下图) 。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务。下图是一个包括三层结构的时间轮,中心的tickMs、wheelSize、interval分别表示该层的时间的刻度、格数、总跨度。三层时间轮的时间区间分别是[0,20)、[20,400)、[400,160000)。
举个例子,比如现在有一个450ms的任务,那么,该任务最终将被插入第三层时间轮中时间格0(时间区间[400,800))的TimerTaskList。注意到在该时间格内的可能存在多个任务(比如446ms、455ms和473ms的定时任务),时间格0对应的TimerTaskList的超时时间expired为400ms。随着时间的流逝,当此TimerTaskList到期之时,原本定时为450ms的任务还剩下50ms的时间,还不能执行这个任务的到期操作。这里就有一个时间轮降级的操作,会将这个剩余时间为50ms的定时任务重新提交到层级时间轮中,此时,该任务被放到第二层时间轮中时间格1到期时间为[40ms, 60ms)的时间格中。再经历40ms之后,此时这个任务又被“ 察觉”,过还剩余10ms,还是不能立即执行到期操作。 所以还要再有一次时间轮的降级,此任务被添加到第一层时间轮的时间格10(时间区间[10ms,11ms))中,之后再经历10ms后,此任务真正到期,最终执行相应的到期操作。
时间轮的设计源于生活,生活中常见的钟表就是一个三层结构的时间轮。第一层:秒针(tickMs=1s、wheelSize=60、interval=1min);第二层:分针(tickMs=1min、wheelSize=60、interval=1hour);第三层:时针(tickMs=1hour、wheelSize=12、interval=12hour);
3.2 任务推进
有了定时任务,Kafka是如何来推进这些任务的呢?Kafka中的定时器借了JDK中的DelayQueue1
- 对于每个使用到的TimerTaskList 调用delayQueue.offer加入DelayQueue,超时时间为TimerTaskList对应的expired;
- DelayQueue会根据TimerTaskList 对应的超时时间expiration来排序, 最短expiration 的TimerTaskList会被排在DelayQueue的队头。
- Kafka 中会有一个线程通过调用delayQueue.take来获取DelayQueue中到期的任务列表,这个线程叫作“ExpiredOperationReaper”,可以直译为“过期操作收割机”。
- 对获取到的任务列表,执行具体的任务。