概要
时间轮是一种非常惊艳的数据结构。其在Linux内核中使用广泛,是Linux内核定时器的实现方法和基础之一。Netty内部基于时间轮实现了一个HashedWheelTimer来优化I/O超时的检测,本文将详细分析HashedWheelTimer的使用及原理。
背景
由于Netty动辄管理100w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源。
在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连接,释放资源。得益于Netty NIO的优异性能,基于Netty开发的服务器可以维持大量的长连接,单台8核16G的云主机可以同时维持几十万长连接,及时掐掉不活跃的连接就显得尤其重要。
看看官方文档说明:
A optimized for approximated I/O timeout scheduling.
You can increase or decrease the accuracy of the execution timing by
- specifying smaller or larger tick duration in the constructor. In most
- network applications, I/O timeout does not need to be accurate. Therefore,
- the default tick duration is 100 milliseconds and you will not need to try
- different configurations in most cases.
大概意思是一种对“适当”I/O超时调度的优化。因为I/O timeout这种任务对时效性不需要准确。
这种方案也不是Netty凭空造出来的,而是根据George Varghese和Tony Lauck在1996年的论文实现的,有兴趣的可以阅读一下。
论文下载
论文PPT
应用场景
HashedWheelTimer本质是一种类似延迟任务队列的实现,那么它的特点就是上述所说的,适用于对时效性不高的,可快速执行的,大量这样的“小”任务,能够做到高性能,低消耗。
例如:
- 心跳检测
- session、请求是否timeout
业务场景则有: - 用户下单后发短信
- 下单之后15分钟,如果用户不付款就自动取消订单
简单使用
如果之前没用过,先看看用法有一个大体的感受,
@Slf4j
public class HashedWheelTimerTest {
private CountDownLatch countDownLatch = new CountDownLatch(2);
@Test
public void test1() throws Exception {
//定义一个HashedWheelTimer,有16个格的轮子,每一秒走一个一个格子
HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 16);
//把任务加到HashedWheelTimer里,到了延迟的时间就会自动执行
timer.newTimeout((timeout) -> {
log.info("task1 execute");
countDownLatch.countDown();
}, 500, TimeUnit.MILLISECONDS);
timer.newTimeout((timeout) -> {
log.info("task2 execute");
countDownLatch.countDown();
}, 2, TimeUnit.SECONDS);
countDownLatch.await();
timer.stop();
}
}
需要引入netty-all.jar
包
使用上跟ScheduledExecutorService
差不多。
实现原理
源码基于netty-all.4.1.34.Final
数据结构
时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代码一段时间(这个时间越短,Timer的精度越高)。并用一个链表报错在该格子上的到期任务,同时一个指针随着时间一格一格转动,并执行相应格子中的到期任务。任务通过取摸决定放入那个格子。如下图所示:
假设一个格子是1秒,则整个wheel能表示的时间段为8s,假如当前指针指向2,此时需要调度一个3s后执行的任务,显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s后执行,应该等指针走完一个round零2格再执行,因此应放入4,同时将round(1)保存到任务中。检查到期任务时应当只执行round为0的,格子上其他任务的round应减1。
再回头看看构造方法的三个参数分别代表
- tickDuration
每一tick的时间 - timeUnit
tickDuration的时间单位 - ticksPerWheel
就是轮子一共有多个格子,即要多少个tick才能走完这个wheel一圈。
对于HashedWheelTimer的数据结构在介绍完源码之后有图解。
初始化
HashedWheelTimer整体代码不难,慢慢看应该都可以看懂
我们从HashedWheelTimer的构造方法入手,先说明一下
构造方法
//附上文档说明,自行阅读
/**
* Creates a new timer.
*
* @param threadFactory a {@link ThreadFactory} that creates a
* background {@link Thread} which is dedicated to
* {@link TimerTask} execution.
* @param tickDuration the duration between tick
* @param unit the time unit of the {@code tickDuration}
* @param ticksPerWheel the size of the wheel
* @param leakDetection {@code true} if leak detection should be enabled always,
* if false it will only be enabled if the worker thread is not
* a daemon thread.
* @param maxPendingTimeouts The maximum number of pending timeouts after which call to
* {@code newTimeout} will result in
* {@link java.util.concurrent.RejectedExecutionException}
*