时间轮与延时任务
消费者在拉取消息时,如果设置了超时时间和最小消息数,某个partition上的消息如果没能超过最小消息数,响应会被阻塞,直到满足到达超时时间/达到最小消息数着两个条件之一。
那么kafka如何实现上述功能的呢?
上述功能的两个条件,第一是到达等待时间,这个很容易想到用延时队列完成;另一个条件的实现则是每当highwater移动,就追加返回消息,并判断是否满足最少消息数。
延时请求的实现,kafka最初是用DelayQueue实现,后来由于性能问题,改为用时间轮TimingWheel实现。我们先看时间轮的实现细节,之后再考虑时间轮方案的优势。
时间轮比较直观的描述,是你有一堆事要做,你按照星期/月/年进行划分,把事情依次划分到了最近一周/最近一年/最近几年的维度里,注意这几个纬度是不重叠的。比如今天是一月的第一周,当时间到了一月的第二周,你就需要把一月中剩余属于第二周的事写到最近的todo项中,而三四两周的事不动,依旧放到一月份的todo项中。当到了二月的第一周,你需要将二月todo项属于第一周要做的事放到最近的todo中。除此之外,由于一月维度已经全部过期,此时你需要将年维度中第十三个月的数据放到一月维度中。这就像一个个刻度不同的钟表齿轮,由最小刻度的秒针不断向前推进。
有了这个概念,我们看kafka中的定义的概念。首先kafka定义了一个叫时间轮的类,TimingWheel。这个类上有20个齿轮(bucket),也有齿轮的刻度(interval),每个齿轮还有指向更大刻度的齿轮(overflowWheel),这样齿轮的基本定义就算完成了。
仅有齿轮是不够的,时间轮的目的是完成延迟请求,因此绕不开一个叫TimerTask的类,这是个抽象类,继承了Runnable接口,我们可以往时间轮上添加任务,也可以取消任务。
当往时间轮上添加任务时,由于每个任务都有超时时间,对应着每个齿轮的刻度。所以实际是往每个齿轮上挂任务,每个齿轮上的任务就是TimerTaskList。
而所有的TimerTaskList我们用一个延迟队列来存储,并记录这个list里最早应该完成任务的时间。这个延时队列是实现延迟任务的关键。每次时针往前走一格时,会从该阻塞队列中拿到满足时间条件的TimeTaskList(时间不到的话会阻塞),并将其中的满足时间要求的Task提交到执行线程中执行。
时针往前拨一格的操作,则是由内部的线程完成,该线程是个死循环,只要不改变其执行标记,就会一直执行,这个操作函数是advanceClock。
kafka用SystemTimer封装了TimingWheel和TimerTask,SystemTimer提供了advanceClock的方法。
到此时间轮的逻辑完成了闭环,但仅仅时间轮是无法满足kafka的诉求,因为kafka的一些延迟任务,除了超时时间到达时必须执行,还要在满足业务条件时立即执行,比如消息数满足最小消息数时,需要立即给客户端相应。
kafka在时间轮之上,又做了封装,这次的封装是一个叫DelayOperation抽象类,提供forceComplete的方法,该方法是抽象类的具体实现,内部分别调用了cancel()从时间轮上摘下任务和onComplete()执行任务完成后的回调操作,其中onComplete()是抽象类,交由子类根据业务需求实现。比如我们这个例子中,需要将消息发送给客户端;或者写请求时如果ack是-1,一旦副本间同步成功,也需要立即告知生产者消息写入成功。DelayOperation提供的另一个方法是tryCompleteElseWatch(),这个方法会先调用tryComplete(),如果失败,将任务挂在时间盘上,被称作watch。而tryComplete()则是接口,交由业务自己实现。业务需要实现的另外一个接口是onComplete(),表示任务执行完后需要进行的操作,比如fetch任务拿到了满足数量的消息,后续操作应当是封装响应并返回。
至此,kafka完成了延迟请求相关的所有操作,这部分源码写的非常干净,同时相对独立,代码层次清晰,还有大量的并发操作(比如CountDownLock、ReenterLock等),而且整体代码并不多,推荐大家精度。接下来我已实现类以DelayedFetch为例,画几张图,一张是某些场景下主动触发完成任务,另一张是时间轮内部的定时任务,循环触发延时任务,最后一张是类之间的关系图,帮助大家更好理解这块的逻辑。
主动触发

定时任务

实体关系

一些思考
Q:为什么使用时间轮
A:我们在执行延时任务时,更多关注的是临期任务,对于间隔较远的任务,我们并不关心,因此我们频繁操作的实际上是列表中头部的一些数据。时间轮遵循了这样一个思路,时间越近,索引的颗粒度越细,减少不必要的操作,从而提高操作效率。索引其实就是时间轮上的bucket,从而达到了这个目的。当然从另外一个层面说,时间轮利用了空间换时间的设计思路,但这并不典型。
Q:这段代码有哪些可优化的点
A:可优化的地方很多,一是SystemTimer和TimingWheel类的功能有很多重叠的地方,可以合并,TimerTask和TimerTaskEntry也可以合并;其次对于高层的时间轮,存储数据并不需要用DelayedQueue,只需要用一个线程安全的队列存储任务即可,这样能大大提高插入高层任务的效率;另外,对于一些关键操作,比如定时器执行任务、把任务提交给异步线程,这种关键性的操作,应当提供一个单独的函数并以明确的命名方式表达出来,从而提高阅读性。
PS:原创不易,转载请注明出处和作者
PPS:能力有限,欢迎大佬指正

浙公网安备 33010602011771号