延时消息实现原理
Kafka中存在大量的延时操作,比如延时生产、延时拉取和延时删除等。Kafka 并没有使用JDK自带的Timer 或DelayQueue来实现延时的功能,而是基于时间轮的概念自定义实现了一个用于延时功能的定时器(SystemTimer)。JDK中Timer和DelayQueue的插入和删除操作的平均时间复杂度为O (nlogn) 并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(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);
任务推进
有了定时任务,Kafka是如何来推进这些任务的呢?Kafka中的定时器借了JDK中的DelayQueue 来协助推进时间轮。 具体做法是:
- 对于每个使用到的TimerTaskList 调用delayQueue.offer加入DelayQueue,超时时间为TimerTaskList对应的expired;
- DelayQueue会根据TimerTaskList 对应的超时时间expiration来排序, 最短expiration 的TimerTaskList会被排在DelayQueue的队头。
- Kafka 中会有一个线程通过调用delayQueue.take来获取DelayQueue中到期的任务列表,这个线程叫作“ExpiredOperationReaper”,可以直译为“过期操作收割机”。
- 对获取到的任务列表,执行具体的任务