[TOC]
DelayedOperationPurgatory是一个相对独立的组件,它的主要功能是管理延迟操作。DelayedOperationPurgatory的底层依赖于Kafka提供的时间轮实现。有的读者可能会感到奇怪,我们可以使用JDK本身提供的java.util.Timer或是DelayQueue轻松实现定时任务的功能,为什么Kafka还要专门开发DelayedOperationPurgatory组件呢?这主要是因为像Kafka这种分布式系统的请求量巨大,性能要求也很高,JDK提供java.util.Timer和DelayedQueue底层实现使用的是堆这种数据结构,存取操作的复杂度都是O(nlog(n)),无法支持大量的定时任务。在高性能的框架中,为了将定时任务的存取操作以及取消操作的时间复杂度降为O(1),一般会使用其他方式实现定时任务组件,例如,使用时间轮的方式。这种做法还是比较多见的,例如,ZooKeeper中使用“时间桶”的方式处理Session过期,Netty也提供了HashedWheelTimer这种时间轮的实现,Quartz框架中也有时间轮的身影。
TimingWheel
Kafka的时间轮实现是TimingWheel,它是一个存储定时任务的环形队列,底层使用数组实现,数组中的每个元素可以存放一个TimerTaskList对象。TimerTaskList是环形双向链表,在其中的链表项TimerTaskEntry中封装了真正的定时任务TimerTask。TimerTaskList使用expiration字段记录了整个TimerTaskList的超时时间。TimerTaskEntry中的expirationMs字段记录了超时时间戳,timerTask字段指向了对应的TimerTask任务。TimeTask中的delayMs记录了任务的延迟时间,,timerTaskEntry字段记录了对应的TimerTaskEntry对象。这三个对象是TimingWheel实现的基础。
TimingWheel提供了层级时间轮的概念,如图4-32所示,第一层时间轮的时间跨度比较小,而第二层时间轮的时间跨度比较大。存放在同一TimerTaskList中的TimerTask到期时间可能不同,但是都由一个时间格覆盖。现假设图中第二层时间轮中编号为2的时间格保存的TimerTaskList到期时间为t,其中保存的任务的到期时间只能是[t~t+20ms]这个范围,例如T1的过期时间可以为t+10ms,任务T2的过期时间可以为t+15ms,T3的过期时间可以为t+12ms。如果任务到期时间不在[t~t+20ms]这个时间段,则只能放到其他的时间格对应的TimerTaskList中保存。
当任务到期时间超出了当前时间轮所表示的时间范围时,会尝试添加到上层时间轮。依然以图4-32为例,其中第一层时间轮的每个时间格是1ms,整个时间轮的跨度是20ms,其表针currentTime当前表示的时间ct,则该时间轮的跨度为[ct~ct+20ms],只有到期时间在这段范围内的任务才能添加到该时间轮中等待到期。到期时间超出[ct~ct+20ms]这个时间范围的任务会尝试添加到其上级时间轮中,通过逐层向上尝试,最终找到合适的时间轮层级。
整个时间轮表示的时间跨度是不变的,随着表针currentTime的后移,当前时间轮能处理时间段也在不断后移,新来的TimerTaskEntry会复用原来已经到期的TimerTaskList。如图4-32所示,第一层时间轮的时间跨度始终为20ms,表针currentTime表示的时间随着时间的流逝不断后移,指向了第三个时间格,此时表针currentTime表示的时间为ct+3ms,整个时间轮表示的时间段是[ct+3ms~ct+23ms],但是该时间轮的时间跨度依然是20ms。此时该时间轮中编号为2的时间格表示的时间范围不再是[ct+1ms~ct+2ms],而是[ct+22ms~ct+23ms]。
最后用一个示例详细介绍时间轮降级场景,如图4-33所示。
现在有一个任务是在445ms后执行,默认情况下,各个层级的时间轮的时间格个数为20,第一层时间轮每个时间格的跨度为1ms,整个时间轮的跨度为20ms,跨度不够。第二层时间轮时间格的跨度为20ms,整个时间轮的跨度为400ms,跨度依然不够。第三层时间轮时间格的跨度为400ms,整个时间轮的跨度为8000ms,跨度足够,此任务存放在第三层时间轮的第一个时间格对应的TimerTaskList中等待执行,此TimerTaskList到期时间是400ms。随着时间的流逝,当此TimerTaskList到期时,距离该任务的到期时间还有45ms,不能执行任务。我们将其重新提交到层级时间轮中,此时第一层时间轮跨度依然不够,但是第二层时间轮的跨度足够,该任务会被放到第二层时间轮第三个时间格中等待执行。如此往复几次,高层时间轮的任务会慢慢移动到低层时间轮上,最终任务到期执行。
介绍完了时间轮的基本概念之后,下面开始分析TimingWheel的具体实现,其核心字段的含义如下所述。
- ·buckets:Array.tabulate[TimerTaskList]类型,其每一个项都对应时间轮中的一个时间格,用于保存TimerTaskList的数组。在TimingWheel中,同一个TimerTaskList中的不同定时任务的到期时间可能不同,但是相差时间在一个时间格的范围内。
- ·tickMs:当前时间轮中一个时间格表示的时间跨度。
- ·wheelSize:当前时间轮的格数,也是buckets数组的大小
- ·taskCounter:各层级时间轮中任务的总数。
- ·startMs:当前时间轮的创建时间。
- ·queue:DelayQueue类型,整个层级时间轮共用的一个任务队列,其元素类型是TimerTaskList(实现了Delayed接口)。
- ·currentTime:时间轮的指针,将整个时间轮划分为到期部分和未到期部分。在初始化时,currentTime被修剪成tickMs的倍数,近似等于创建时间,但并不是严格的创建时间。
- ·interval:当前时间轮的时间跨度,即tickMswheelSize。当前时间轮只能处理时间范围在currentTime~currentTime+tickMsWheelSize之间的定时任务,超过这个范围,则需要将任务添加到上层时间轮中。
- ·overflowWheel:上层时间轮的引用。
在TimeWheel中提供了add()、advanceClock()、addOverflowWheel()三个方法,这三个方法实现了时间轮的基础功能。add()方法实现了向时间轮中添加定时任务的功能,它同时也会检测待添加的任务是否已经到期。
addOverflowWheel()方法会创建上层时间轮,默认情况下,上层时间轮的tickMs是当前整个时间轮的时间跨度interval。
advanceClock()方法会尝试推进当前时间轮的表针currentTime,同时也会尝试推进上层的时间轮的表针。随着当前时间轮的表针不断被推进,上层时间轮的表针也早晚会被推进成功。
SystemTimer
SystemTimer是Kafka中的定时器实现,它在TimeWheel的基础上添加了执行到期任务、阻塞等待最近到期任务的功能。下面来分析其核心字段。
- ·taskExecutor:JDK提供的固定线程数的线程池实现,由此线程池执行到期任务。
- ·delayQueue:各个层级的时间轮共用的DelayQueue队列,主要作用是阻塞推进时间轮表针的线程(ExpiredOperationReaper),等待最近到期任务到期。
- ·taskCounter:各个层级时间轮共用的任务个数计数器
- ·timingWheel:层级时间轮中最底层的时间轮。
- ·readWriteLock:用来同步时间轮表针currentTime修改的读写锁。
SystemTimer.add()方法在添加过程中如果发现任务已经到期,则将任务提交到taskExecutor中执行;如果任务未到期,则调用TimeWheel.add()方法提交到时间轮中等待到期后执行。SystemTimer.add()方法的实现如下:
SystemTimer.advanceClock()方法完成了时间轮表针的推进,同时对到期的TimerTaskList中的任务进行处理。如果TimerTaskList到期,但是其中的某些任务未到期,会将未到期任务进行降级,添加到低层次的时间轮中继续等待;如果任务到期了,则提交到taskExecutor线程池中执行。
DelayedOperation
在前面介绍ProducerRequest和FetchRequest两种请求时提到,服务端在收到这两种请求时并不是立即返回响应,可能会等待一段时间后才返回。对于ProducerRequest来说,其中的acks字段设置为-1表示ProducerRequest发送到Leader副本之后,需要ISR集合中所有副本都同步该请求中的消息(或超时)后,才能返回响应给客户端。ISR集合中的副本分布在不同Broker上,与Leader副本进行同步时就涉及网络通信,一般情况下我们认为网络传输是不可靠的而且是一个较慢的过程,通常采用异步的方式处理来避免线程长时间等待。当FetchRequest发送给Leader副本后,会积累一定量的消息后才返回给消费者或Follower副本,并不是Leader副本的HW后移一条消息就立即将其返回给消费者,这是为了实现批量发送消息,提高有效负载。
Kafka利用前面介绍的SystemTimer来定期检测请求是否超时,但是这些请求真正的目的并不是为了超时执行,而是为了满足其他条件后执行,例如ProducerRequest的响应条件ISR集合中所有副本都同步了请求中的消息,所以仅使用SystemTimer就无法满足需求了。Kafka使用DelayedOperation抽象类表示延迟操作,它对TimeTask进行了扩展,除了有定时执行的功能,还提供了检测其他执行条件的功能。我们可以认为DelayedOperation是一个延迟的、异步的操作。
如图4-34所示,DelayedOperation有四个实现类,分别表示四类不同的延迟操作,也对应了四种不同的请求。DelayedOperation是一个抽象类,completed字段是AtomicBoolean类型,标识了此DelayedOperation是否完成,初始值为false;delayMs记录了延迟操作的延迟时长。
DelayedOperation中各个方法的含义和功能如下所述。
- ·onComplete()方法:抽象方法,DelayedOperation的具体业务逻辑。例如DelayedProduce中该方法的实现就是向客户端返回ProduceResponse响应。此方法只能在forceComplete()方法中被调用,且在DelayedOperation的整个生命周期中只能被调用一次。
- ·forceComplete()方法:如果DelayedOperation没有完成,则先将任务从时间轮中删除掉,然后调用onComplete()方法执行其具体的业务逻辑。
可能有多个Handler线程并发检测DelayedOperation的执行条件,这就可能导致多个线程并发调用forceComplete()方法,但是onComplete()方法有只能调用一次的限制。因此在forceComplete方法中用AtomicBoolean的CAS操作进行限制,从而实现onComplete()方法只被调用一次。
- ·onExpiration()方法:抽象方法,DelayedOperation到期时执行的具体逻辑。
- ·run()方法:DelayedOperation到期时会提交到SystemTimer.taskExecutor线程池中执行。其中会调用forceComplete()方法完成延迟操作,然后调用onExpiration()方法执行延迟操作到期执行的相关代码。
- ·tryComplete()方法:抽象方法,在该方法中子类会根据自身的具体类型,检测执行条件是否满足,若满足则会调用forceComplete()完成延迟操作。
- ·isCompleted()方法:检测任务是否完成
DelayedOperation可能因为到期而被提交到SystemTimer.taskExecutor线程池中执行,也可能在其他线程检测其执行条件时发现已经满足执行条件,而将其执行。为了读者更好地理解这两条执行路线,给出图4-35供读者参考。
DelayedOperationPurgatory
DelayedOperationPurgatory是一个辅助类,提供了管理DelayedOperation以及处理到期DelayedOperation的功能。DelayedOperationPurgatory依赖的组件如图4-36所示。
DelayedOperationPurgatory中的watchersForKey字段用于管理DelayedOperation,它是Pool[Any, Watchers]类型,Pool的底层实现是ConcurrentHashMap。watchersForKey集合的key表示的是Watchers中的DelayedOperation关心的对象,其value是Watchers类型的对象,Watchers是DelayedOperationPurgatory的内部类,表示一个DelayedOperation的集合,底层使用LinkedList实现。
下面通过一个示例介绍watchersForKey字段以及Watchers的功能。DelayProduce关心的对象是TopicPartitionOperationKey对象,表示的是某个Topic中的某个分区。假设现在有一个ProducerRequest请求,它要向名为“test”的Topic中追加消息,分区的编号为0,此分区当前的ISR集合中有三个副本。该ProducerRequest的acks字段为-1表示需要ISR集合中所有副本都同步了该请求中的消息才能返回ProduceResponse。Leader副本处理此ProducerRequest时会为其生成一个对应的DelayedProduce对象,并交给DelayedOperationPurgatory管理,DelayedOperationPurgatory会将其存放到“test-0”(TopicPartitionOperationKey对象)对应的Watchers中,同时也会将其提交到SystemTimer中。之后,每当Leader副本收到Follower副本发送的对“test-0”的FetchRequest时,都会检测“test-0”对应的Watchers中的DelayedProduce是否已经满足了执行条件,如果满足执行条件就会执行DelayedProduce,向客户端返回ProduceResponse。最终,该DelayedProduce会因满足执行条件或时间到期而被执行。
Watchers的字段只有一个operations字段,它用于管理DelayedOperation的LinkedList队列,下面来分析Watchers其核心方法有三个:
·watch()方法:将DelayedOperation添加到operations队列中
-
·tryCompleteWatched()方法:遍历operations队列,对于未完成的DelayedOperation执行tryComplete()方法尝试完成,将已完成的DelayedOperation对象移除。如果operations队列为空,则将Watchers从DelayedOperationPurgatory. watchersForKey中删除。
·purgeCompleted()方法:负责清理operations队列,将已经完成的DelayedOperation从operations队列中移除,如果operations队列为空,则将Watchers从watchersForKey集合中删除。
DelayedOperationPurgatory中各个字段的含义如下所述。
- ·timeoutTimer:前面介绍的SystemTimer对象
- ·watchersForKey:管理Watchers的Pool对象
- ·removeWatchersLock:对watchersForKey进行同步的读写锁操作。
- ·estimatedTotalOperations:记录了该DelayedOperationPurgatory中的DelayedOperation个数。
-
·expirationReaper:此字段是一个ShutdownableThread线程对象,主要有两个功能,一是推进时间轮表针,二是定期清理watchersForKey中已完成的DelayedOperation,清理条件由purgeInterval字段指定。在DelayedOperationPurgatory初始化时会启动此线程。此线程的doWork()方法的代码如下:
DelayedOperationPurgatory的核心方法有两个:一个是checkAndComplete()方法,主要是根据传入的key尝试执行对应的Watchers中的DelayedOperation,通过调用Watchers.tryCompleteWatched()方法实现,不再赘述。另一个是tryCompleteElseWatch()方法,主要功能是检测DelayedOperation是否已经完成,若未完成则添加到watchersForKey以及SystemTimer中。具体的执行步骤如下:
有读者可能会问,为什么一个DelayedOperation可能会关心多个key呢?这里我们还是以DelayedProduce为例分析:在第2章介绍ProducerRequest时提到过,ProducerRequest中可以包含发送往同一节点的不同TopicAndPartition的消息,那么处理ProducerRequest时就涉及向多个分区中追加消息,所以需要关注多个TopicPartitionOperationKey,只有所有的分区都满足条件才能对客户端响应。
DelayedProduce
经过前面的介绍,我们已经了解了SystemTimer和DelayedOperationPurgatory的工作原理。在详细介绍DelayedProduce的相关实现之前,先来了解一下当ProducerRequest的acks字段为-1时,服务端的处理流程:在KafkaApis中处理ProducerRequest的方法是handleProducerRequest()方法,它会调用ReplicaManager.appendMessages()方法将消息追加到Log中,生成相应的DelayedProduce对象并添加到delayedProducePurgatory处理。delayedProducePurgatory是ReplicaManager中的字段,它是专门用来处理DelayedProduce的DelayedOperationPurgatory对象,其定义如下:
这里简略了解一下ReplicaManager.appendMessages()方法中与DelayedProduce处理相关的部分代码:
现在我们回到对DelayedProduce的分析,其中各个字段的含义和功能如下所述。
- delayMs:DelayedProduce的延迟时长。
- produceMetadata:ProduceMetadata对象。ProduceMetadata中为一个ProducerRequest中的所有相关分区记录了一些追加消息后的返回结果,主要用于判断DelayedProduce是否满足执行条件:
其中,produceRequiredAcks字段记录了ProduceRequest中acks字段的值,produceStatus记录了每个Partition的ProducePartitionStatus。ProducePartitionStatus的定义如下:
从上面的ReplicaManager.appendMessages()的代码可以看出,requiredOffset是ProducerRequest中追加到此分区的最后一个消息的offset,它会参与判断DelayedProduce是否符合执行条件,在DelayedProduce. tryComplete()方法中介绍。acksPending字段表示是否正在等待ISR集合中其他副本与Leader副本同步requiredOffset之前的消息,如果ISR集合中所有副本已经完成了requiredOffset之前消息的同步,则此值被设置为false。responseStatus字段主要用来记录Prod […]
·responseCallback:任务满足条件或到期执行时,在DelayedProduce.onComplete()方法中调用的回调函数。其主要功能是向RequestChannels中对应的responseQueue添加ProducerResponse,之后Processor线程会将其发送给客户端。
replicaManager:此DelayedProduce关联的ReplicaManager对象。
在DelayedProduce初始化时,首先会对produceMetadata字段中的produceStatus集合进行设置。初始化的代码如下:
DelayedProduce实现了DelayedOperation.tryComplete()方法,其主要逻辑是检测是否满足DelayedProduce的执行条件,并在满足执行条件时调用forceComplete()方法完成该延迟任务。满足下列任一条件,即表示此分区已经满足DelayedProduce的执行条件。只有ProducerRequest中涉及的所有分区都满足条件,DelayedProduce才能最终执行。
1)该分区出现了Leader副本的迁移。该分区的Leader副本不再位于此节点上,此时会更新对应ProducePartitionStatus中记录的错误码。
(2)正常情况下,ISR集合中所有副本都完成了同步后,该分区的Leader副本的HW位置已经大于对应的ProduceStatus.requiredOffset。此时会清空初始化中设置的超时错误码。
(3)如果出现异常,则更新分区对应的ProducePartitionStatus中记录的错误码。
DelayedProduce.tryComplete()方法的实现如下:
通过前面的分析,onComplete()方法才是DelayedProduce执行的真正逻辑,其代码如下:
请读者注意一个细节,如果DelayedProduce是到期执行,则返回的错误码是在其初始化过程中预先设置的Errors.REQUEST_TIMED_OUT.code。
最后,来看一下responseCallback这个回调函数的具体实现,它会向RequestChannels中对应的responseQueue队列添加ProducerResponse,最终Processor线程会将ProducerResponse返回给生产者。读者可以跟踪一下代码,发现此回调函数是在KafkaApis.handleProducerRequest()方法中定义sendResponseCallback()函数,其具体代码如下:
整个ProducerRequest以及DelayedProduce相关的处理流程到这里就已经介绍完了。为了让读者更好地理解这个流程,我们进行简单总结,并给出图4-37,读者可以结合前面的代码分析与此图深入理解。
(1)生产者发送ProducerRequest向某些指定分区追加消息。
(2)ProducerRequest经过网络层和API层的处理到达ReplicaManager,它会将消息交给日志存储子系统进行处理,最终追加到对应的Log中。同时还会检测delayedFetchPurgatory中相关key对应的DelayedFetch,满足条件则将其执行完成,这部分内容在下一节中介绍。
(3)日志存储子系统返回追加消息的结果。
(4)ReplicaManager为ProducerRequest生成DelayedProduce对象,并交由delayedProducePurgatory管理。
(5)delayedProducePurgatory使用SystemTimer管理DelayedProduce是否超时。
(6)ISR集合中的Follower副本发送FetchRequest请求与Leader副本同步消息。同时,也会检查DelayedProduce是否符合执行条件。
(7)DelayedProduce执行时会调用回调函数产生ProducerResponse,并将其添加到RequestChannels中。
(8)由网络层将ProducerResponse返回给客户端
DelayedFetch
DelayedFetch是FetchRequest对应的延迟操作,它的原理与DelayedProduce类似。我们先来粗略分析FetchRequest的处理流程:来自消费者或Follower副本的FetchRequest由KafkaApis.handleFetchRequest()方法处理,它会调用ReplicaManager.fetchMessages()方法从相应的Log中读取消息,并生成DelayedFetch添加到delayedFetchPurgatory中处理。delayedFetchPurgatory是ReplicaManager中的字段,它是专门用来处理DelayedFetch的DelayedOperationPurgatory对象。其定义如下:
这里简略分析一下ReplicaManager.fetchMessages()方法中与DelayedFetch相关的代码:
我们大致上可以了解到DelayedProduce和DelayedFetch之间的关联:在处理ProducerRequest的过程中会向Log中添加数据,可能会后移Leader副本的LEO,Follower副本就可以读取到足量的数据,所以会尝试完成DelayedFetch;在处理来自Follower副本的FetchRequest过程中,可能会后移HW,所以会尝试完成DelayedProduce,这样两者可以很好地协同工作了。
现在回到对DelayedFetch的分析,其中字段的含义如下所述。
- ·delayMs:延迟操作的延迟时长。
- ·fetchMetadata:FetchMetadata对象。FetchMetadata中为FetchRequest中的所有相关分区记录了相关状态,主要用于判断DelayedProduce是否满足执行条件。
其中,fetchMinBytes字段记录了需要读取的最小字节数,fetchPartitionStatus记录了每个分区的FetchPartitionStatus。FetchPartitionStatus的代码如下:
其中,startOffsetMetadata记录了在前面读取Log时已经读取到的offset位置。fetchInfo记录FetchRequest携带的一些信息,主要是请求的offset以及读取最大字节数。
- responseCallback:任务满足条件或到期执行时,在DelayedFetch.onComplete()方法中调用的回调函数,其主要功能是创建FetchResponse并添加到RequestChannels中对应的responseQueue队列中
DelayedFetch.tryComplete()方法主要负责检测是否满足DelayedFetch的执行条件,并在满足条件时调用forceComplete()方法执行延迟操作。满足下面任一条件,即表示此分区满足DelayedFetch的执行条件:
(1)发生Leader副本迁移,当前节点不再是该分区的Leader副本所在的节点。
(2)当前Broker找不到需要读取数据的分区副本。
(3)开始读取的offset不在activeSegment中,此时可能是发生了Log截断,也有可能是发生了roll操作产生了新的activeSegment。
(4)累计读取的字节数超过最小字节数限制。
与DelayedProduce不同,DelayedFetch中只要有一个Partition满足任一执行条件,DelayedFetch就会最终执行。下面是tryComplete()方法的代码:
分析到这里读者可能会问,为什么产生新的activeSegment时,就不用管fetchMinBytes这个最小读取字节数的限制而直接返回FetchResponse呢?请读者回顾前面对LogSegment.read()方法的介绍,read()方法返回的是分片的FileMessageSet对象,而并没有真正从文件中读取数据到内存中。如果这里的fetchOffset与endOffset分属不同FileMessageSet,read()方法的结果就无法使用一个分片的FileMessageSet对象表示了。读者可能又会问,LogSegment.read()方法的返回值为什么要设计成返回分片的FileMessage对象呢?这么设计的主要目的是为了使用“零拷贝(Zero Copy)”技术。
零拷贝
简单说,在消费者获取消息时,服务器先从硬盘读出数据到内存,然后将内存中的数据原封不动地通过Socket发送给消费者。虽然这个操作描述非常简单,但是其中涉及的步骤非常多,效率也比较差,尤其是当数据量较大时。按照这种设计,其底层执行步骤大致如下:首先,应用程序调用read()方法时需要从用户态切换到内核态,将数据从磁盘上读取出来保存到内核缓冲区中;然后,内核缓冲区中的数据传输到应用程序,此时read()方法调用结束,从内核态切换到用户态;之后,应用程序执行send()方法,需要从用户态切换到内核态,将数据传输给Socket Buffer;最后,内核会将Socket Buffer中的数据发送NIC Buffer(网卡缓冲区)进行发送,此时send()方法结束,从内核态切换到用户态。如图4-39所示,在这个过程中涉及四次上下文切换(Context switch)以及四次数据复制,并且其中有两次复制操作由CPU完成。但是在这个过程中,数据完成没有进行变化,仅仅是从磁盘复制到了网卡缓冲区中,会浪费大量的CPU周期。
通过“零拷贝”技术可以去掉这些无谓的数据复制操作,同时也会减少上下文切换的次数。大致步骤如下:首先,应用程序调用transferTo()方法,DMA会将文件数据发送到内核缓冲区;然后,Socket Buffer追加数据的描述信息;最后,DMA将内核缓冲区的数据发送到网卡缓冲区,这样就完全解放了CPU,实现了零拷贝,如图4-40所示。
我们回到LogSegment.writeTo()方法,其中与 “零拷贝”相关的代码如下:
最后来解释之前的问题,如果LogSegment.read()方法返回的数据可以分散在两个FileMessageSet中,就需要先将数据读取出来缓存到内存,然后再发送出去,无法使用“零拷贝”技术。
下面回到对DelayedFetch的分析,DelayedFetch.onComplete()方法的实现如下:
这里的responseCallback回调函数与DelayedProduce中介绍的功能类似。读者可以跟踪一下代码,发现此回调函数是在KafkaApis.handleFetchRequest()方法中定义sendResponseCallback()函数。
与DelayedProduce一样,我们对DelayedFetch的相关处理流程做一下总结,这里以来自Follower副本为例,如图4-41所示。
(1)Follower副本发送FetchRequest,从某些分区中获取消息。
(2)FetchRequest经过网络层和API层的处理,到达ReplicaManager,它会从日志存储子系统中读取数据,并检测是否要更新ISR集合、HW等,之后还会执行delayedProducePurgatory中满足条件的相关DelayedProduce。
(3)日志存储子系统返回读取消息以及相关信息,例如此次读取到的offset等。
(4)ReplicaManager为FetchRequest生成DelayedFetch对象,并交由delayedFetchPurgatory管理。
(5)delayedFetchPurgatory使用SystemTimer管理DelayedFetch是否超时。
(6)生产者发送ProducerRequest请求追加消息。同时也会检查DelayedFetch是否符合执行条件。
(7)DelayedFetch执行时会调用回调函数产生FetchResponse,添加到RequestChannels中。
(8)由网络层将FetchResponse返回给客户端。
这里主要介绍了DelayProduce和DelayFetch两种DelayedOperation实现细节和应用场景,剩余两种DelayedJoin和DelayedHeartbear在后面还会有详细介绍。DelayedOperationPurgatory组件到这里就介绍完了。
本节介绍了时间轮的相关概念以及在Kafka中的TimingWheel、SystemTimer的实现。之后,介绍了DelayedOperation接口,它是对TimeTask的扩展,实现了有条件的延迟操作。DelayedOperationPurgatory在SystemTimer的基础上实现了对DelayedOperation的管理。最后,介绍了DelayedProduce和DelayedFetch的相关处理流程及这两个类的具体实现,让读者清楚地了解DelayedOperation接口的实现和使用场景。