Rust 实现Netty HashedWheelTimer时间轮

时间:2021-08-09
本文章向大家介绍Rust 实现Netty HashedWheelTimer时间轮,主要包括Rust 实现Netty HashedWheelTimer时间轮使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、背景

近期在内网上看到一篇文章,文中提到的场景是 系统自动取消 15分钟内 未支付的订单。对于数据量比较少并且实时性要求不那么高的场景,一种简单的方式是轮询数据库,比如每秒轮询一下数据库中所有的数据,处理所有到期的数据。但是如果需要处理的数据量较大,高达百万甚至千万时,这时候如果还轮询数据库就不妥了。

文中给出了这类问题的解决思路,使用延时队列:Redis ZSort、RabbitMQ 死信队列、Netty 时间轮

二、延时队列-时间轮

上图几个名词的解释:

  • tickMs: 时间轮由多个时间格组成,每个时间格就是 tickMs,它代表当前时间轮的基本时间跨度。
  • wheelSize: 代表每一层时间轮的格数
  • interval: 当前时间轮的总体时间跨度,interval=tickMs × wheelSize
  • startMs: 构造当层时间轮时候的当前时间,第一层的时间轮的 startMs 是 TimeUnit.NANOSECONDS.toMillis(nanoseconds()),上层时间轮的 startMs 为下层时间轮的 currentTime。
  • currentTime: 表示时间轮当前所处的时间,currentTime 是 tickMs 的整数倍(通过 currentTime=startMs - (startMs % tickMs 来保正 currentTime 一定是 tickMs 的整数倍),这个运算类比钟表中分钟里 65 秒分钟指针指向的还是 1 分钟)。 currentTime 可以将整个时间轮划分为到期部分和未到期部分,currentTime 当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的 TimerTaskList 的所有任务。

时间轮工作原理:

如上图所示,时间轮是一个存储延迟消息的环形队列,每个元素对应一个延迟任务列表,这个列表是一个双向环形链表,链表中每一项都代表一个需要执行的延迟任务。时间轮会有表盘指针,表示时间轮当前所指时间,随着时间推移,该指针会不断前进,并处理对应位置上的延迟任务列表。

三、Netty 时间轮源码分析

3.1.主要的成员类:

  • HashedWheelTimer:调度器,服务启动 Worker 线程,投递新的 延迟任务。
  • Worker:工作线程,循环执行,每次sheep(tickMs),根据指针的位置,遍历对应的 延迟任务列表。
  • HashedWheelBucket:如上图所示,表盘中的每一个格子,记录着 延迟任务 列表的head、tail。
  • HashedWheelTimeout:对应延迟任务列表中的一个元素,当任务过期时,执行TimerTask中的run方法。
  • TimerTask:接口类,投递的延时任务 需要实践接口中的 run 方法

3.2.主要的类方法:

  • HashedWheelTimer#newTimeout:投递新的延时任务 到 timeouts 队列中(Netty 中使用的是 PlatformDependent 队列结构)
  • Worker#waitForNextTick: 等待下个ticks,线程sleep(tick)
  • Worker#transferTimeoutsToBuckets: 从 timeouts 队列中消费 100000 个 延迟任务,分配到不同的 HashedWheelBucket 的链表中。
  • HashedWheelBucket#expireTimeouts:执行到期的任务,然后从队列中剔除已经过期的任务。

四、Rust实现HashedWheelTimer

Rust 实现的 HashedWheelTimer,相比 Netty 只是语言上的区别,算法和数据结构上是一致。源码地址:https://github.com/ZuoFuhong/hashed_wheel_timer

过程中遇到的一些难点:

1.BucketTimeout 双向链表的实现比较费劲,在单线程下使用的 Rc + RefCell 的组合。

2.投递 延迟任务,使用标准库的 mpsc 队列,进行线程间的传递:

3.延迟任务的 run 方法,想过传递闭包,也想过用泛型,这些后续再实现:

4.为什么将WheelTimeout转换成 BucketTimeout:

因为 BucketTimeout  的内部元素 是一个 Rc + RefCell 的组合,不能进行线程传递。如果要支持线程间传递,就要加一个 Arc + Mutex,这样感觉好啰嗦,又得不停的加锁,所以做了一个转换。

5.为什么把 Worker#start 放在 WheelTimer 中执行启动,而观察Netty 是在 WheelTimer#new_timeout 中启动的:

如果不提前启动 Worker#start,使用WheelTimer#clone 后 sender 元素还是 None。

以上,真的不是换个语言那么容易的事,现在回过头发现 java 和 golang 这类有 GC 的语言,真实太爽了,啥都不用管。

五、总结思考

作为一个服务端开发人员,在面试过程中,或多或少会被人问到 “大流量、高并发” 的问题。想要解决这类问题,前提是要能遇到 大规模的用户量,如果没有真实的用户量,可以模拟用户量。当有了 大规模的用户量时,进行验证时,很多问题就会冒出来。这一点在工作中,越发感受深刻:

  • 1.系统的承载能力,需要通过 “模拟压测” 的方式去预估用户量。不能等到真正有了用户量的时候,用实践去评估。
  • 2.任何一个小问题,在用户量较小的时候,不会出现。一旦用户量上来了,就会成为大问题。
  • 3.系统是不断迭代的,要结合背景 因时制宜,早期为了适应市场,快速开发,不要过度的去追求设计上的完美。当下满足诉求即可,后续在迭代中逐步加强。

原文地址:https://www.cnblogs.com/marszuo/p/15120423.html