前言
dubbo内部有比较多定时任务的管理功能,JDK也提供了Timer和DelayedQueue等工具类,可以实现简单的定时任务管理,其底层实现就是使用的堆这种数据结构,存取的时间复杂度是O(nlogN),无法支持大量的定时任务。dubbo内部采用了时间轮的方式来管理定时任务。应用场景比如:dubbo的心跳机制、dubbo客户端超时检测等。
时间轮是一种高效的、批量管理的定时任务的调度模型。时间轮一般会实现一个环形结构,类似于时钟,分为很多槽,每个槽代表一个时间间隔,每个槽使用双向链表来存储定时任务;指针周期性地跳动,跳动到一个槽位,执行对应的定时任务。
注意下单层时间轮的容量和精度是有限的,如果时间跨度特比大,精度要求很高,或者海量定时任务需要调度的场景,通常会使用多级时间轮以及持久化存储的方案。
比如多级时间轮以及持久化存储与时间轮结合,每级时间轮的时钟周期不一样,比如年级别时间轮、月级别时间轮、日级别时间轮、毫秒级别时间轮,定时任务在创建时先持久化,在时钟指针接近时预读到内存,并且需要定期清理磁盘上的过期任务。
Dubbo中,时间轮的实现方式是主要在dubbo-common的org.apache.dubbo.common.timer包中。dubbo和netty的实现基本一致,netty时间轮一个应用场景简单提下,Redisson实现分布式锁提供了watchdog锁续期的功能,为了避免每加锁一次起一个线程去扫描是否需要续期以及执行续期逻辑带来的压力,采用了netty的时间轮来注册续期任务,只用一个线程和合适的时间周期完成了续期逻辑。
核心接口
Timer接口:定义了定时器的基本行为。核心方法是newTimeout方法,提交一个定时任务(TimerTask)返回关联的Timeout对象。
1 | /** |
HashedWheelTimer实现类:Timer接口的实现类。通过时间轮算法实现了一个定时器。执行过程为:
- 根据当前时间轮指针选定对应的槽。
- 遍历槽上的定时任务(HashedWheelTimeout),对每个定时任务进行计算,是当前时钟周期则去除,如果不是则将任务中的剩余时钟周期-1,代表距离执行又接近了一圈。
TimerTask接口:所有定时任务都要继承TimerTask接口。
1 | /** |
Timeout接口:TimerTask中run()方法的参数,可以查看定时任务的状态,还可以操作取消定时任务
HashedWheelTimeout:Timeout接口的唯一实现,是HashedWheelTimer的内部类。扮演两个角色:
- 第一个,时间轮中双向链表的节点,即定时任务TimerTask在HashedWheelTimer中的容器
- 第二个,定时任务TimerTask提交到HashedWheelTimer之后的句柄,用于在时间轮外查看和控制定时任务。
HashedWheelTimeout的核心字段
1 | /** |
HashedWheelTimeout的核心方法
- isCancelled()、isExpired()、state()方法:主要用来检查HashedWheelTimeout的状态。
- cancel()方法:将当前HashedWheelTimeout状态设置为CANCELLED,将当前HashedWheelTimeout添加到canceledTimeouts队列等待销毁。
- expire()方法:当任务到期时,会调用该方法将当前HashedWheelTimeout设置为Expired状态,然后调用其中的TimerTask的run()方法执行定时任务。
- remove()方法:将当前的HashedWheelTimeout从时间轮中删除。
HashedWheelTimer 时间轮
上面有提到HashedWheelTimer实现类是时间轮的具体实现,工作原理是根据当前时间轮的指针选定对应的槽(HashedWheelBucket),从双向链表的头节点开始迭代,对每个HashedWheelTimeout进行计算,属于当前时钟周期则取出运行,不属于则将其时钟周期-1,等待下一圈的判断。
核心字段
1 | // 实现了Timer接口 通过时间轮算法实现了一个定时器 |
newTimeout方法提交定时任务
1 | // 时间轮 去加入一个新的任务 |
newTimeout主要做了几件事:
- 维护了时间轮中剩余定时任务数量字段
- start()方法
- 计算了时间轮的startTime方法,且修改时间轮的状态
- worker线程调用start()方法,开启执行扫描时间轮的线程。
- 根据startTime来计算定时任务要调度的deadline,当前时间+延时时间 - 启动时间。
- 封装task为HashedWheelTimeout添加到timeouts执行队列中。
worker线程扫描时间轮并执行任务
在worker线程的run()方法中:
1 | private final class Worker implements Runnable { |
时间轮一次转动的流程:
- 时间轮转动,时间周期开始
- 清理用户主动取消的任务,会记录在cancelledTimeouts队列中,在每次指针转动的时候,时间轮也会清理该队列。
- 将缓存在timeouts队列中的定时任务转移到时间轮对应的槽中。
- 根据当前指针定位槽,遍历双向链表,来执行对应的任务,方法实现在HashedWheelBucket.expireTimeouts方法中:
- 循环遍历双向链表,当定时任务的remainingRounds小于等于0,则说明是当前时钟周期内的任务,判断是否达到了deadline(满足时间的触发,兜底判断,一般在时钟周期内都会满足),如果满足调用expire()方法执行任务,内部会调用TimerTask的run方法,即真正的定时任务的逻辑。
- 如果用户取消,则直接remove()从链表上摘除。
- 继续遍历下一个Timeout节点。
- 时间轮一直是运行状态,则重复上述轮询的操作;如果时间轮为停止状态,则遍历每个槽位,来清除注册的定时任务。最后再清理cancelledTimeouts队列中用户取消的任务。
Dubbo中时间轮的应用
客户端的超时检查
客户端发起调用时会创建一个DefaultFuture,用于发起远程调用且阻塞同步等待结果。
1 | // DefaultFuture的newFuture方法 |
可以看到timeoutCheck方法中创建了一个TimeoutCheckTask(实现了TimerTask接口),然后利用时间轮调用newTimeout加入了一个定时任务。
客户端检查超时公用的时间轮:
1 | public static final Timer TIME_OUT_TIMER = new HashedWheelTimer( |
TimeoutCheckTask的逻辑:
1 | // 超时检查的任务 |
与注册中心交互的失败重试
1 | private void addFailedRegistered(URL url) { |
其中FailedRegisteredTask是TimerTask的实现,代表一个失败重新注册到注册中心的任务,内部就是调用了注册服务的逻辑(比如zk去创建临时节点);retryTimer是一个时间轮,30毫秒去转动指针扫描槽来执行任务。
1 | // Timer for failure retry, regular check if there is a request for failure, and if there is, an unlimited retry |