You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

465 lines
28 KiB
Markdown

2 years ago
# 20 | DelayedOperationBroker是怎么延时处理请求的
你好,我是胡夕。
上节课我们学习了分层时间轮在Kafka中的实现。既然是分层时间轮那就说明源码中构造的时间轮是有多个层次的。每一层所表示的总时长等于该层Bucket数乘以每个Bucket涵盖的时间范围。另外该总时长自动成为下一层单个Bucket所覆盖的时间范围。
举个例子目前Kafka第1层的时间轮固定时长是20毫秒interval即有20个BucketwheelSize每个Bucket涵盖1毫秒tickMs的时间范围。第2层的总时长是400毫秒同样有20个Bucket每个Bucket 20毫秒。依次类推那么第3层的时间轮时长就是8秒因为这一层单个Bucket的时长是400毫秒共有20个Bucket。
基于这种设计每个延迟请求需要根据自己的超时时间来决定它要被保存于哪一层时间轮上。我们假设在t=0时创建了第1层的时间轮那么该层第1个Bucket保存的延迟请求就是介于\[01之间第2个Bucket保存的是介于\[12)之间的请求。现在如果有两个延迟请求超时时刻分别在18.5毫秒和123毫秒那么第1个请求就应该被保存在第1层的第19个Bucket序号从1开始而第2个请求则应该被保存在第2层时间轮的第6个Bucket中。
这基本上就是Kafka中分层时间轮的实现原理。Kafka不断向前推动各个层级的时间轮的时钟按照时间轮的滴答时长陆续接触到Bucket下的各个延迟任务从而实现了对请求的延迟处理。
但是如果你仔细查看的话就会发现到目前为止这套分层时间轮代码和Kafka概念并无直接的关联比如分层时间轮里并不涉及主题、分区、副本这样的概念也没有和Controller、副本管理器等Kafka组件进行直接交互。但实际上延迟处理请求是Kafka的重要功能之一。你可能会问到底是Kafka的哪部分源码负责创建和维护这套分层时间轮并将它集成到整体框架中去的呢答案就是接下来要介绍的两个类Timer和SystemTimer。
## Timer接口及SystemTimer
这两个类的源码位于utils.timer包下的Timer.scala文件。其中**Timer接口定义了管理延迟操作的方法而SystemTimer是实现延迟操作的关键代码**。后续在学习延迟请求类DelayedOperation时我们就会发现调用分层时间轮上的各类操作都是通过SystemTimer类完成的。
### Timer接口
接下来我们就看下它们的源码。首先是Time接口类代码如下
```
trait Timer {
// 将给定的定时任务插入到时间轮上,等待后续延迟执行
def add(timerTask: TimerTask): Unit
// 向前推进时钟,执行已达过期时间的延迟任务
def advanceClock(timeoutMs: Long): Boolean
// 获取时间轮上总的定时任务数
def size: Int
// 关闭定时器
def shutdown(): Unit
}
```
该Timer接口定义了4个方法。
* add方法将给定的定时任务插入到时间轮上等待后续延迟执行。
* advanceClock方法向前推进时钟执行已达过期时间的延迟任务。
* size方法获取当前总定时任务数。
* shutdown方法关闭该定时器。
其中,最重要的两个方法是**add**和**advanceClock**,它们是**完成延迟请求处理的关键步骤**。接下来我们结合Timer实现类SystemTimer的源码重点分析这两个方法。
### SystemTimer类
SystemTimer类是Timer接口的实现类。它是一个定时器类封装了分层时间轮对象为Purgatory提供延迟请求管理功能。所谓的Purgatory就是保存延迟请求的缓冲区。也就是说它保存的是因为不满足条件而无法完成但是又没有超时的请求。
下面我们从定义和方法两个维度来学习SystemTimer类。
#### 定义
首先是该类的定义,代码如下:
```
class SystemTimer(executorName: String,
tickMs: Long = 1,
wheelSize: Int = 20,
startMs: Long = Time.SYSTEM.hiResClockMs) extends Timer {
// 单线程的线程池用于异步执行定时任务
private[this] val taskExecutor = Executors.newFixedThreadPool(1,
(runnable: Runnable) => KafkaThread.nonDaemon("executor-" + executorName, runnable))
// 延迟队列保存所有Bucket即所有TimerTaskList对象
private[this] val delayQueue = new DelayQueue[TimerTaskList]()
// 总定时任务数
private[this] val taskCounter = new AtomicInteger(0)
// 时间轮对象
private[this] val timingWheel = new TimingWheel(
tickMs = tickMs,
wheelSize = wheelSize,
startMs = startMs,
taskCounter = taskCounter,
delayQueue
)
// 维护线程安全的读写锁
private[this] val readWriteLock = new ReentrantReadWriteLock()
private[this] val readLock = readWriteLock.readLock()
private[this] val writeLock = readWriteLock.writeLock()
......
}
```
每个SystemTimer类定义了4个原生字段分别是executorName、tickMs、wheelSize和startMs。
tickMs和wheelSize是构建分层时间轮的基础你一定要重点掌握。不过上节课我已经讲过了而且我在开篇还用具体数字带你回顾了它们的用途这里就不重复了。另外两个参数不太重要你只需要知道它们的含义就行了。
* executorNamePurgatory的名字。Kafka中存在不同的Purgatory比如专门处理生产者延迟请求的Produce缓冲区、处理消费者延迟请求的Fetch缓冲区等。这里的Produce和Fetch就是executorName。
* startMs该SystemTimer定时器启动时间单位是毫秒。
除了原生字段SystemTimer类还定义了其他一些字段属性。我介绍3个比较重要的。这3个字段与时间轮都是强相关的。
1. **delayQueue字段**。它保存了该定时器下管理的所有Bucket对象。因为是DelayQueue所以只有在Bucket过期后才能从该队列中获取到。SystemTimer类的advanceClock方法正是依靠了这个特性向前驱动时钟。关于这一点一会儿我们详细说。
2. **timingWheel**。TimingWheel是实现分层时间轮的类。SystemTimer类依靠它来操作分层时间轮。
3. **taskExecutor**。它是单线程的线程池,用于异步执行提交的定时任务逻辑。
#### 方法
说完了类定义与字段我们看下SystemTimer类的方法。
该类总共定义了6个方法add、addTimerTaskEntry、reinsert、advanceClock、size和shutdown。
其中size方法计算的是给定Purgatory下的总延迟请求数shutdown方法则是关闭前面说到的线程池而addTimerTaskEntry方法则是将给定的TimerTaskEntry插入到时间轮中。如果该TimerTaskEntry表征的定时任务没有过期或被取消方法还会将已经过期的定时任务提交给线程池等待异步执行该定时任务。至于reinsert方法它会调用addTimerTaskEntry重新将定时任务插入回时间轮。
其实SystemTimer类最重要的方法是add和advanceClock方法因为**它们是真正对外提供服务的**。我们先说add方法。add方法的作用是将给定的定时任务插入到时间轮中进行管理。代码如下
```
def add(timerTask: TimerTask): Unit = {
// 获取读锁。在没有线程持有写锁的前提下,
// 多个线程能够同时向时间轮添加定时任务
readLock.lock()
try {
// 调用addTimerTaskEntry执行插入逻辑
addTimerTaskEntry(new TimerTaskEntry(timerTask, timerTask.delayMs + Time.SYSTEM.hiResClockMs))
} finally {
// 释放读锁
readLock.unlock()
}
}
```
add方法就是调用addTimerTaskEntry方法执行插入动作。以下是addTimerTaskEntry的方法代码
```
private def addTimerTaskEntry(timerTaskEntry: TimerTaskEntry): Unit = {
// 视timerTaskEntry状态决定执行什么逻辑
// 1. 未过期未取消:添加到时间轮
// 2. 已取消:什么都不做
// 3. 已过期:提交到线程池,等待执行
if (!timingWheel.add(timerTaskEntry)) {
// 定时任务未取消,说明定时任务已过期
// 否则timingWheel.add方法应该返回True
if (!timerTaskEntry.cancelled)
taskExecutor.submit(timerTaskEntry.timerTask)
}
}
```
TimingWheel的add方法会在定时任务已取消或已过期时返回False否则该方法会将定时任务添加到时间轮然后返回True。因此addTimerTaskEntry方法到底执行什么逻辑取决于给定定时任务的状态
1. 如果该任务既未取消也未过期那么addTimerTaskEntry方法将其添加到时间轮
2. 如果该任务已取消,则该方法什么都不做,直接返回;
3. 如果该任务已经过期,则提交到相应的线程池,等待后续执行。
另一个关键方法是advanceClock方法。顾名思义它的作用是**驱动时钟向前推进**。我们看下代码:
```
def advanceClock(timeoutMs: Long): Boolean = {
// 获取delayQueue中下一个已过期的Bucket
var bucket = delayQueue.poll(
timeoutMs, TimeUnit.MILLISECONDS)
if (bucket != null) {
// 获取写锁
// 一旦有线程持有写锁其他任何线程执行add或advanceClock方法时会阻塞
writeLock.lock()
try {
while (bucket != null) {
// 推动时间轮向前"滚动"到Bucket的过期时间点
timingWheel.advanceClock(bucket.getExpiration())
// 将该Bucket下的所有定时任务重写回到时间轮
bucket.flush(reinsert)
// 读取下一个Bucket对象
bucket = delayQueue.poll()
}
} finally {
// 释放写锁
writeLock.unlock()
}
true
} else {
false
}
}
```
由于代码逻辑比较复杂,我再画一张图来展示一下:
![](https://static001.geekbang.org/resource/image/31/89/310c9160f701082ceb90984a7dcfe089.jpg)
advanceClock方法要做的事情就是遍历delayQueue中的所有Bucket并将时间轮的时钟依次推进到它们的过期时间点令它们过期。然后再将这些Bucket下的所有定时任务全部重新插入回时间轮。
我用一张图来说明这个重新插入过程。
![](https://static001.geekbang.org/resource/image/53/ef/535e18ad9516c90ff58baae8cfc9b9ef.png)
从这张图中我们可以看到在T0时刻任务①存放在Level 0的时间轮上而任务②和③存放在Level 1的时间轮上。此时时钟推进到Level 0的第0个Bucket上以及Level 1的第0个Bucket上。
当时间来到T19时刻时钟也被推进到Level 0的第19个Bucket任务①会被执行。但是由于一层时间轮是20个Bucket因此T19时刻Level 0的时间轮尚未完整走完一圈此时Level 1的时间轮状态没有发生任何变化。
当T20时刻到达时Level 0的时间轮已经执行完成Level 1的时间轮执行了一次滴答向前推进一格。此时Kafka需要将任务②和③插入到Level 0的时间轮上位置是第20个和第21个Bucket。这个将高层时间轮上的任务插入到低层时间轮的过程是由advanceClock中的reinsert方法完成。
至于为什么要重新插入回低层次的时间轮,其实是因为,随着时钟的推进,当前时间逐渐逼近任务②和③的超时时间点。它们之间差值的缩小,足以让它们被放入到下一层的时间轮中。
总的来说SystemTimer类实现了Timer接口的方法**它封装了底层的分层时间轮,为上层调用方提供了便捷的方法来操作时间轮**。那么它的上层调用方是谁呢答案就是DelayedOperationPurgatory类。这就是我们建模Purgatory的地方。
不过在了解DelayedOperationPurgatory之前我们要先学习另一个重要的类DelayedOperation。前者是一个泛型类它的类型参数恰恰就是DelayedOperation。因此我们不可能在不了解DelayedOperation的情况下很好地掌握DelayedOperationPurgatory。
## DelayedOperation类
这个类位于server包下的DelayedOperation.scala文件中。它是所有Kafka延迟请求类的抽象父类。我们依然从定义和方法这两个维度去剖析它。
### 定义
首先来看定义。代码如下:
```
abstract class DelayedOperation(override val delayMs: Long,
lockOpt: Option[Lock] = None)
extends TimerTask with Logging {
// 标识该延迟操作是否已经完成
private val completed = new AtomicBoolean(false)
// 防止多个线程同时检查操作是否可完成时发生锁竞争导致操作最终超时
private val tryCompletePending = new AtomicBoolean(false)
private[server] val lock: Lock = lockOpt.getOrElse(new ReentrantLock)
......
}
```
DelayedOperation类是一个抽象类它的构造函数中只需要传入一个超时时间即可。这个超时时间通常是**客户端发出请求的超时时间**,也就是客户端参数**request.timeout.ms**的值。这个类实现了上节课学到的TimerTask接口因此作为一个建模延迟操作的类它自动继承了TimerTask接口的cancel方法支持延迟操作的取消以及TimerTaskEntry的Getter和Setter方法支持将延迟操作绑定到时间轮相应Bucket下的某个链表元素上。
除此之外DelayedOperation类额外定义了两个字段**completed**和**tryCompletePending**。
前者理解起来比较容易,它就是**表征这个延迟操作是否完成的布尔变量**。我重点解释一下tryCompletePending的作用。
这个参数是在1.1版本引入的。在此之前只有completed参数。但是这样就可能存在这样一个问题当多个线程同时检查某个延迟操作是否满足完成条件时如果其中一个线程持有了锁也就是上面的lock字段然后执行条件检查会发现不满足完成条件。而与此同时另一个线程执行检查时却发现条件满足了但是这个线程又没有拿到锁此时该延迟操作将永远不会有再次被检查的机会会导致最终超时。
加入tryCompletePending字段目的就是**确保拿到锁的线程有机会再次检查条件是否已经满足**。具体是怎么实现的呢下面讲到maybeTryComplete方法时我会再带你进行深入的分析。
关于DelayedOperation类的定义你掌握到这个程度就可以了重点是学习这些字段是如何在方法中发挥作用的。
### 方法
DelayedOperation类有7个方法。我先介绍下它们的作用这样你在读源码时就可以心中有数。
* forceComplete强制完成延迟操作不管它是否满足完成条件。每当操作满足完成条件或已经过期了就需要调用该方法完成该操作。
* isCompleted检查延迟操作是否已经完成。源码使用这个方法来决定后续如何处理该操作。比如如果操作已经完成了那么通常需要取消该操作。
* onExpiration强制完成之后执行的过期逻辑回调方法。只有真正完成操作的那个线程才有资格调用这个方法。
* onComplete完成延迟操作所需的处理逻辑。这个方法只会在forceComplete方法中被调用。
* tryComplete尝试完成延迟操作的顶层方法内部会调用forceComplete方法。
* maybeTryComplete线程安全版本的tryComplete方法。这个方法其实是社区后来才加入的不过已经慢慢地取代了tryComplete现在外部代码调用的都是这个方法了。
* run调用延迟操作超时后的过期逻辑也就是组合调用forceComplete + onExpiration。
我们说过DelayedOperation是抽象类对于不同类型的延时请求onExpiration、onComplete和tryComplete的处理逻辑也各不相同因此需要子类来实现它们。
其他方法的代码大多短小精悍你一看就能明白我就不做过多解释了。我重点说下maybeTryComplete方法。毕竟这是社区为了规避因多线程访问产生锁争用导致线程阻塞从而引发请求超时问题而做的努力。先看方法代码
```
private[server] def maybeTryComplete(): Boolean = {
var retry = false // 是否需要重试
var done = false // 延迟操作是否已完成
do {
if (lock.tryLock()) { // 尝试获取锁对象
try {
tryCompletePending.set(false)
done = tryComplete()
} finally {
lock.unlock()
}
// 运行到这里的线程持有锁其他线程只能运行else分支的代码
// 如果其他线程将maybeTryComplete设置为true那么retry=true
// 这就相当于其他线程给了本线程重试的机会
retry = tryCompletePending.get()
} else {
// 运行到这里的线程没有拿到锁
// 设置tryCompletePending=true给持有锁的线程一个重试的机会
retry = !tryCompletePending.getAndSet(true)
}
} while (!isCompleted && retry)
done
}
```
为了方便你理解,我画了一张流程图说明它的逻辑:
![](https://static001.geekbang.org/resource/image/35/a3/35bd69c5aa46d52a358976152508daa3.jpg)
从图中可以看出,这个方法可能会被多个线程同时访问,只是不同线程会走不同的代码分支,分叉点就在**尝试获取锁的if语句**。
如果拿到锁对象就依次执行清空tryCompletePending状态、完成延迟请求、释放锁以及读取最新retry状态的动作。未拿到锁的线程就只能设置tryCompletePending状态来间接影响retry值从而给获取到锁的线程一个重试的机会。这里的重试是通过do…while循环的方式实现的。
好了DelayedOperation类我们就说到这里。除了这些公共方法你最好结合一两个具体子类的方法实现体会下具体延迟请求类是如何实现tryComplete方法的。我推荐你从DelayedProduce类的**tryComplete方法**开始。
我们之前总说acks=all的PRODUCE请求很容易成为延迟请求因为它必须等待所有的ISR副本全部同步消息之后才能完成你可以顺着这个思路研究下DelayedProduce的tryComplete方法是如何实现的。
## DelayedOperationPurgatory类
接下来我们补上延迟请求模块的最后一块“拼图”DelayedOperationPurgatory类的源码分析。
该类是实现Purgatory的地方。从代码结构上看它是一个Scala伴生对象。也就是说源码文件同时定义了DelayedOperationPurgatory Object和Class。Object中仅仅定义了apply工厂方法和一个名为Shards的字段这个字段是DelayedOperationPurgatory监控列表的数组长度信息。因此我们还是重点学习DelayedOperationPurgatory Class的源码。
前面说过DelayedOperationPurgatory类是一个泛型类它的参数类型是DelayedOperation的具体子类。因此通常情况下每一类延迟请求都对应于一个DelayedOperationPurgatory实例。这些实例一般都保存在上层的管理器中。比如与消费者组相关的心跳请求、加入组请求的Purgatory实例就保存在GroupCoordinator组件中而与生产者相关的PRODUCE请求的Purgatory实例被保存在分区对象或副本状态机中。
### 定义
至于怎么学,还是老规矩,我们先从定义开始。代码如下:
```
final class DelayedOperationPurgatory[T <: DelayedOperation](
purgatoryName: String,
timeoutTimer: Timer,
brokerId: Int = 0,
purgeInterval: Int = 1000,
reaperEnabled: Boolean = true,
timerEnabled: Boolean = true) extends Logging with KafkaMetricsGroup {
......
}
```
定义中有6个字段。其中很多字段都有默认参数比如最后两个参数分别表示是否启动删除线程以及是否启用分层时间轮。现在源码中所有类型的Purgatory实例都是默认启动的因此无需特别留意它们。
purgeInterval这个参数用于控制删除线程移除Bucket中的过期延迟请求的频率在绝大部分情况下都是1秒一次。当然对于生产者、消费者以及删除消息的AdminClient而言Kafka分别定义了专属的参数允许你调整这个频率。比如生产者参数producer.purgatory.purge.interval.requests就是做这个用的。
事实上,需要传入的参数一般只有两个:**purgatoryName**和**brokerId**它们分别表示这个Purgatory的名字和Broker的序号。
而timeoutTimer就是我们前面讲过的SystemTimer实例我就不重复解释了。
### Wathcers和WatcherList
DelayedOperationPurgatory还定义了两个内置类分别是Watchers和WatcherList。
**Watchers是基于Key的一个延迟请求的监控链表**。它的主体代码如下:
```
private class Watchers(val key: Any) {
private[this] val operations =
new ConcurrentLinkedQueue[T]()
// 其他方法......
}
```
每个Watchers实例都定义了一个延迟请求链表而这里的Key可以是任何类型比如表示消费者组的字符串类型、表示主题分区的TopicPartitionOperationKey类型。你不用穷尽这里所有的Key类型你只需要了解Watchers是一个通用的延迟请求链表就行了。Kafka利用它来**监控保存其中的延迟请求的可完成状态**。
既然Watchers主要的数据结构是链表那么它的所有方法本质上就是一个链表操作。比如tryCompleteWatched方法会遍历整个链表并尝试完成其中的延迟请求。再比如cancel方法也是遍历链表再取消掉里面的延迟请求。至于watch方法则是将延迟请求加入到链表中。
说完了Watchers我们看下WatcherList类。它非常短小精悍完整代码如下
```
private class WatcherList {
// 定义一组按照Key分组的Watchers对象
val watchersByKey = new Pool[Any, Watchers](Some((key: Any) => new Watchers(key)))
val watchersLock = new ReentrantLock()
// 返回所有Watchers对象
def allWatchers = {
watchersByKey.values
}
}
```
WatcherList最重要的字段是**watchersByKey**。它是一个PoolPool就是Kafka定义的池对象它本质上就是一个ConcurrentHashMap。watchersByKey的Key可以是任何类型而Value就是Key对应类型的一组Watchers对象。
说完了DelayedOperationPurgatory类的两个内部类Watchers和WatcherList我们可以开始学习该类的两个重要方法tryCompleteElseWatch和checkAndComplete方法。
前者的作用是**检查操作是否能够完成**如果不能的话就把它加入到对应Key所在的WatcherList中。以下是方法代码
```
def tryCompleteElseWatch(operation: T, watchKeys: Seq[Any]): Boolean = {
assert(watchKeys.nonEmpty, "The watch key list can't be empty")
var isCompletedByMe = operation.tryComplete()
// 如果该延迟请求是由本线程完成的直接返回true即可
if (isCompletedByMe)
return true
var watchCreated = false
// 遍历所有要监控的Key
for(key <- watchKeys) {
// 再次查看请求的完成状态如果已经完成就说明是被其他线程完成的返回false
if (operation.isCompleted)
return false
// 否则将该operation加入到Key所在的WatcherList
watchForOperation(key, operation)
// 设置watchCreated标记表明该任务已经被加入到WatcherList
if (!watchCreated) {
watchCreated = true
// 更新Purgatory中总请求数
estimatedTotalOperations.incrementAndGet()
}
}
// 再次尝试完成该延迟请求
isCompletedByMe = operation.maybeTryComplete()
if (isCompletedByMe)
return true
// 如果依然不能完成此请求,将其加入到过期队列
if (!operation.isCompleted) {
if (timerEnabled)
timeoutTimer.add(operation)
if (operation.isCompleted) {
operation.cancel()
}
}
false
}
```
该方法的名字折射出了它要做的事情先尝试完成请求如果无法完成则把它加入到WatcherList中进行监控。具体来说tryCompleteElseWatch调用tryComplete方法尝试完成延迟请求如果返回结果是true就说明执行tryCompleteElseWatch方法的线程正常地完成了该延迟请求也就不需要再添加到WatcherList了直接返回true就行了。
否则的话代码会遍历所有要监控的Key再次查看请求的完成状态。如果已经完成就说明是被其他线程完成的返回false如果依然无法完成则将该请求加入到Key所在的WatcherList中等待后续完成。同时设置watchCreated标记表明该任务已经被加入到WatcherList以及更新Purgatory中总请求数。
待遍历完所有Key之后源码会再次尝试完成该延迟请求如果完成了就返回true否则就取消该请求然后将其加入到过期队列最后返回false。
总的来看,你要掌握这个方法要做的两个事情:
1. 先尝试完成延迟请求;
2. 如果不行就加入到WatcherList等待后面再试。
那么代码是在哪里进行重试的呢这就需要用到第2个方法checkAndComplete了。
该方法会**检查给定Key所在的WatcherList中的延迟请求是否满足完成条件**,如果是的话,则结束掉它们。我们一起看下源码:
```
def checkAndComplete(key: Any): Int = {
// 获取给定Key的WatcherList
val wl = watcherList(key)
// 获取WatcherList中Key对应的Watchers对象实例
val watchers = inLock(wl.watchersLock) { wl.watchersByKey.get(key) }
// 尝试完成满足完成条件的延迟请求并返回成功完成的请求数
val numCompleted = if (watchers == null)
0
else
watchers.tryCompleteWatched()
debug(s"Request key $key unblocked $numCompleted $purgatoryName operations")
numCompleted
}
```
代码很简单就是根据给定Key获取对应的WatcherList对象以及它下面保存的Watchers对象实例然后尝试完成满足完成条件的延迟请求并返回成功完成的请求数。
可见,非常重要的步骤就是**调用Watchers的tryCompleteWatched方法去尝试完成那些已满足完成条件的延迟请求**。
## 总结
今天我们重点学习了分层时间轮的上层组件包括Timer接口及其实现类SystemTimer、DelayedOperation类以及DelayedOperationPurgatory类。你基本上可以认为它们是逐级被调用的关系即**DelayedOperation调用SystemTimer类DelayedOperationPurgatory管理DelayedOperation**。它们共同实现了Broker端对于延迟请求的处理基本思想就是**能立即完成的请求马上完成否则就放入到名为Purgatory的缓冲区中**。后续DelayedOperationPurgatory类的方法会自动地处理这些延迟请求。
我们来回顾一下重点。
* SystemTimer类Kafka定义的定时器类封装了底层分层时间轮实现了时间轮Bucket的管理以及时钟向前推进功能。它是实现延迟请求后续被自动处理的基础。
* DelayedOperation类延迟请求的高阶抽象类提供了完成请求以及请求完成和过期后的回调逻辑实现。
* DelayedOperationPurgatory类Purgatory实现类该类定义了WatcherList对象以及对WatcherList的操作方法而WatcherList是实现延迟请求后续自动处理的关键数据结构。
总的来说延迟请求模块属于Kafka的冷门组件。毕竟大部分的请求还是能够被立即处理的。了解这部分模块的最大意义在于你可以学习Kafka这个分布式系统是如何异步循环操作和管理定时任务的。这个功能是所有分布式系统都要面临的课题因此弄明白了这部分的原理和代码实现后续我们在自行设计类似的功能模块时就非常容易了。
## 课后讨论
DelayedOperationPurgatory类中定义了一个Reaper线程用于将已过期的延迟请求从数据结构中移除掉。这实际上是由DelayedOperationPurgatory的advanceClock方法完成的。它里面有这样一句
```
val purged = watcherLists.foldLeft(0) {
case (sum, watcherList) => sum + watcherList.allWatchers.map(_.purgeCompleted()).sum
}
```
你觉得这个语句是做什么用的?
欢迎在留言区写下你的思考和答案,跟我交流讨论,也欢迎你把今天的内容分享给你的朋友。