github.com/benz9527/toy-box/algo@v0.0.0-20240221120937-66c0c6bd5abd/timer/README.md (about) 1 # Why Timing Wheels 2 3 ![时间轮动态图](./assets/1.gif) 4 5 常规(传统)的任务队列调度,需要不断的轮询任务队列,然后进行调度。这种方式会存在两个问题: 6 7 - 轮询的开销 8 - 调度的精度 9 10 尽可能不损失调度精度的前提下,减少调度的开销。 比如使用全局唯一的时间轮来进行批量化调度,充分利用协程/多线程资源,避免存在多个不同的调度中心,导致资源浪费。 11 主要的表现是,时间轮可以大批量地管理各种延时任务和周期任务。 12 13 # Timing Wheels Implementation vs Traditional Implementation 14 15 ## Traditional Implementation 16 17 - JDK Timer + DelayQueue(小根堆-优先级队列) 18 - Java ScheduledThreadPoolExecutor(DelayQueue + thread pool) 19 20 ## Timing Wheels Implementation 21 22 hash table + linked list + ticker(心跳信号、节拍器、555 信号发生器) 23 24 hash table + linked list + DelayQueue + channel + timer (轮询 DelayQueue, 在第一个快到期元素的差值大于 0 的时候,进行休眠,避免空转) 25 26 - Kafka 27 28 # 单层级时间轮 29 30 这种实现可能会存在调度时间不精确的现象。 31 32 # 多层级时间轮 33 34 通过对时间进行层级划分的方式来提高精确度。 35 36 # 实现细节讨论 37 38 ## slot/bucket 如何划分时间范围? 39 40 按照 kafka 的实现,timing wheel 的时间包括 startMs, tickMs, wheelSize, interval 和 currentTimeMs,到 slot 上就是 expirationMs。 41 42 但是 kafka 的时间是靠 DelayQueue 在 timeoutMs 时间内把所有过期的 buckets 取出来,再取出内部的 tasks 进行调度和 reinsert 操作。 43 44 在 golang 中,有 ticker 作为高精度的时间信号发生器来驱动时间轮的转动,所以不需要 DelayQueue 来进行过期的 bucket 的取出和 reinsert 操作。 45 46 ticker 的精度就是至关重要了,如果精度不够,那么就会导致调度时间不准确,比如跨度大,会导致在 slot 里面比较远离 expirationMs 的任务被延后调度。 47 48 例子: 49 假设 tickMs = 10ms,它相当于 kafka 中的 interval,那么 slot 的划分就是 [0, 10), [10, 20), [20, 30) 这样的。 50 51 如果 task 被设置的任务间隔是 2ms,那么它第一次应该是在 [0, 10) 的 slot 里面,但是触发调度的时间点却是在 10ms,这样就导致了调度时间不准确。 52 53 虽然可以通过限制提交进入时间轮的最小时间间隔来避免这个问题,但这样会导致时间轮可面向的场景变少,比如无法支持 1ms 的时间间隔。 54 55 ## slot/bucket 为什么要使用多层级? 56 多层级的时间轮是为了方便存放更多的不同时间跨度的任务且减少 slot 的数量,跨度大的任务放在时间间隔大的时间轮上面,这操作是逐级向上找的,直到找到合适的时间轮。 57 58 ## 时间轮的转动 59 时间在向前流逝,就会让最底层时间轮的 slot 不断向前移动,同时也在不断更新所有时间轮的基准时间点,形成时间和时间轮同时向前流逝的效果。 60 61 执行任务的时候,实际上只有最底层的 slot 里面的任务才会被执行,其他层级的 slot 里面的任务只是在时间轮转动的时候,把任务移动到下一层级合适时间间隔的 slot 里面。 62 63 ## 为什么只有最底层的 slot 里面的任务才会被执行? 64 因为最底层的 slot 里面的任务的时间间隔是最小的,所以它的时间间隔是最精确的,其他层级的 slot 里面的任务的时间间隔都是最底层的 slot 里面的任务的时间间隔的整数倍。 65 66 ## 为什么每一层的时间轮可以不断地承载任务? 67 68 使用(逻辑上/物理上)的环境数据结构,可以在 slot 移动过程中把已经执行的任务的 slot 变更为下一个周期可以使用的 slot(复用)。 69 70 这里有一个闭环逻辑,底层时间轮的实际执行时间是要加上上层时间轮的周期的。 71 72 时间轮就是靠空间换取一定的时间,查的快(时间短)就是要付出一定空间上的代价。 73 74 ![移动](./assets/2.gif) 75 76 ## slot/bucket 如何实现多层级? 77 78 任务的提交涉及到时间跨度大的问题,比如秒级,分钟级,小时级,天级,月级,年级,这样的时间跨度,如果都放在一个时间轮里面,那么就会导致时间轮的大小很大, 79 比如 365 * 24 * 60 * 60 * 1000 = 31536000000,这样的时间轮就会有 31536000000 个 slot,这样的时间轮是不现实的。 80 81 所以需要对时间轮进行分层,比如秒级的时间轮,分钟级的时间轮,小时级的时间轮,天级的时间轮,月级的时间轮,年级的时间轮。 82 83 但是这样又会导致时间轮的每一次层的 slot 数量不一致。 84 85 但是实际上,设置相同的即可,因为相同的数量,在时间跨度小的任务非常多的时间轮上,可以减少 slot 的数量,虽然会导致在时间跨度大的时间轮上,要增加 slot 的数量。 86 87 ![动态时间轮升级和降级](./assets/3.gif) 88 89 ## task 分类 90 - 一次性延迟执行任务。 91 这种必然有一个距离当前时间的延迟间隔,假设为 delayMs,那么它的过期时间就是 currentTimeMs + delayMs 92 - 小周期性执行任务。 93 这种任务有一个固定的周期,假设为 periodMs,那么它的过期时间就是 currentTimeMs + periodMs 94 - 大跨度的周期性执行任务。 95 这种需要计算它的下一次过期时间 expiredMs,因为大跨度的执行,需要考虑年,月,日的变化(极端的时候 96 还需要考虑到秒级别时间的补偿之类) 97 - 周期间隔不连续的,而且次数有限制的任务。 98 这种任务的周期间隔是不连续的,比如 1s,2s,3s,4s,5s,10s,20s,30s,40s,50s,60s 99 - 周期间隔不连续,但是循环执行的任务。 100 这种任务的周期间隔是不连续的,但是是循环执行的,比如 1s,2s,3s,4s,5s,10s,1s, 2s, 3s, 4s, 5s, 10s, 1s, 2s, 3s, 4s, 5s, 10s ... 101 102 ## 参数说明 103 - tickMs `u ms` 104 - slotSize `n` 105 - startMs 基准时间,时间轮被初始化的时间 106 - interval = tickMs * wheelSize 107 - slot (circle array,使用取余模拟循环效果) 108 - task list (linked list,每个 slot 下挂载着对应的任务) 109 - tasks map 110 记录任务信息,方便取消任务;全局唯一 111 - delay queue 112 用于存放过期的任务,方便调度器调度;全局唯一;主要是存放 slot;不是任务过期,而是 slot 过期要清空或者重新分配它里面的任务 113 114 ## slot 的切分 115 令 u 为时间单元,一个大小为 n 的时间轮有 n 个桶,能够持有 n * u 个时间间隔的定时任务。 116 那么每个 slot 都持有进入相应时间范围的定时任务。 117 118 - 第一个 slot 持有 [0, u) 范围的任务 119 - 第二个 slot 持有 [u, 2u) 范围的任务 120 - 第 n 个 slot 持有 [u * (n - 1), u * n) 范围的任务 121 随着时间单元 u 的数量持续增加,也就是 slot 不断地被移动,slot 移动之后其中所有的定时任务都会过期。 122 由于任务已经过期,此时就不会有任务被添加大当前的 slot 中,调度器应该立刻运行过期的任务。 123 因为空 slot 在下一轮是可用的,所以如果当前的 slot 对应时间 t,那么它会在 tick 后变成 [t + u * n, t + (n + 1) * u) 的 slot。 124 简单地说,当前 slot 中的任务会移动到下一个间隔了 t ms 的 slot 中。 125 126 slot 切分的间隔需要小一点,比如 2ms,5ms 这样的,防止间隔跨度大,导致调度时间偏移(不准确) 127 128 # 待续 129 130 ## 使用 SkipList + hash table 实现时间轮的 slot/bucket 的查找 (delay queue) 131 132 ## 是否能使用 RedBlackTree 实现时间轮的 slot/bucket 的查找