热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

调用dubbo接口返回timeout_Dubbo中的时间轮(TimeWheel)算法应用

点击上方“JavaEdge”,关注公众号设为“星标”,好文章不错过!1定时任务Netty、Quartz、Kafka以及Linux都有定时任

  点击上方“JavaEdge”,关注公众号

设为“星标”,好文章不错过!

1 定时任务

Netty、Quartz、Kafka 以及 Linux 都有定时任务功能。

  • JDK 自带的 java.util.TimerDelayedQueue 可实现简单的定时任务,底层用的是堆,存取复杂度都是 O(nlog(n)),但无法支撑海量定时任务。

  • 在任务量大、性能要求高的场景,为了将任务存取及取消操作时间复杂度降为 O(1),会采用时间轮算法。

2 时间轮模型及其应用

一种高效批量管理定时任务的调度模型。一般会实现成一个环形结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务。

指针周期性跳动,跳动到一个槽位,就执行该槽位的定时任务。

  • Hashed Timing Wheel 结构示意图7e38a644e33f31d159ef171f343c1d76.png

适用场景

7db915469a57739682a7cc0fb8537113.png
  • 故障恢复

  • 流量控制

  • 调度算法

  • 控制网络中的数据包生命周期

计时器维护代价高,如果

  • 处理器在每个时钟滴答声中都会中断

  • 使用精细粒度计时器

  • 未完成的计时器很多

需要高效的定时器算法以减少总体中断的开销。

单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,通常会使用多级时间轮以及持久化存储与时间轮结合的方案。

模型和性能指标

7db915469a57739682a7cc0fb8537113.png3808b963f3f13ca3c9d4014f51b9211a.png

模型中的规则

80b02c35c4750266064c29fd7227c243.png
  • 客户端调用:

    • START_TIMER(时间间隔,Request_ID,Expiry_Action)

    • STOP_TIMER(Request_ID)

  • 计时器tick调用:

    • PER_TICK_BOOKKEEPING

    • EXPIRY_PROCESSING

3808b963f3f13ca3c9d4014f51b9211a.png

性能指标

80b02c35c4750266064c29fd7227c243.png
  • 空间
    数据结构使用的内存

  • 延迟
    开始和结束上述任何例程所需的时间

3 Dubbo的时间轮结构

Dubbo 时间轮实现位于 dubbo-common 模块的 org.apache.dubbo.common.timer 包,下面我们就来分析时间轮涉及的核心接口和实现。

核心接口

7db915469a57739682a7cc0fb8537113.png

5726024d7be7e9439015ad5c6dae3423.png

TimerTask

7db915469a57739682a7cc0fb8537113.png

在 Dubbo 中,所有定时任务都要实现 TimerTask 接口。只定义了一个 run() 方法,入参是一个 Timeout 接口对象。

Timeout

7db915469a57739682a7cc0fb8537113.png

Timeout 对象与 TimerTask 对象一一对应,类似线程池返回的 Future 对象与提交到线程池中的任务对象之间的关系。
通过 Timeout 对象,不仅可以查看定时任务的状态,还可以操作定时任务(例如取消关联的定时任务)。

  • Timeout 接口中的方法:615fc063dffa92ffb011c2fb2d8f95c1.png

Timer 接口定义了定时器的基本行为,核心是 newTimeout() :提交一个定时任务(TimerTask)并返回关联的 Timeout 对象,类似于向线程池提交任务。

HashedWheelTimeout

7db915469a57739682a7cc0fb8537113.png

HashedWheelTimeout 是 Timeout 接口的唯一实现,是 HashedWheelTimer 的内部类。HashedWheelTimeout 扮演了两个角色:

  • 时间轮中双向链表的节点,即定时任务 TimerTask 在 HashedWheelTimer 中的容器

  • 定时任务 TimerTask 提交到 HashedWheelTimer 之后返回的句柄(Handle),用于在时间轮外部查看和控制定时任务

3808b963f3f13ca3c9d4014f51b9211a.png

核心字段

80b02c35c4750266064c29fd7227c243.png
  • prev、next。通过双向链表被用来在HashedWheelTimerBucket链timeouts(定时任务),由于只在WorkerThread上行动,没有必要进行同步/volatile。1fd80616fb3731bcdd8ab0708fc78382.png

  • task,实际被调度的任务24b3bb6e0f72544fb18f445e511af6bd.png

  • deadline,定时任务执行的时间。在创建 HashedWheelTimeout 时指定
    计算公式:currentTime(创建 HashedWheelTimeout 的时间) + delay(任务延迟时间) - startTime(HashedWheelTimer 的启动时间),nse0e4276dc32e2cfb190fa2765cf01490.png

  • state,定时任务当前所处状态ca95c09a73afd22106a8db26d028ac02.png

    • 可选状态:54e2b907e0a61fe81352bc7eec327f4e.png

    • STATE_UPDATER 用于实现 state 状态变更的原子性。4ed4b90c424b07f47a8c90eae38da7c4.png

  • remainingRounds,当前任务剩余的时钟周期数。时间轮所能表示的时间长度有限,在任务到期时间与当前时刻的时间差,超过时间轮单圈能表示时长,就出现套圈,需要该字段值表示剩余的时钟周期。fb9c492e453fb5911db7aea766f86642.png

3808b963f3f13ca3c9d4014f51b9211a.png

核心API

80b02c35c4750266064c29fd7227c243.png
  • isCancelled()5c56dbb7489bca2dcc2ecdcdaf16c2f8.png

  • isExpired()a07bddd98b39b73e54246e0ec8ba42fd.png

  • state()
    检查当前 HashedWheelTimeout 状态8511a47f49de795294e225f15cc74579.png

  • cancel() 方法d921f86fbbe5d10575717d58b31b1310.png

  • expire() 方法26d6172f65dd1b6ccf1a2f652a1b49b8.png

  • remove()dfd1b78bb43ce1497e2de5e78813a2cc.png

HashedWheelBucket

7db915469a57739682a7cc0fb8537113.png

时间轮中的一个槽。
时间轮中的槽实际上就是一个用于缓存和管理双向链表的容器,双向链表中的每一个节点就是一个 HashedWheelTimeout 对象,也就关联了一个 TimerTask 定时任务。

HashedWheelBucket 持有双向链表的首尾两个节点 - head 和 tail,再加上每个 HashedWheelTimeout 节点均持有前驱和后继引用,即可正、逆向遍历整个链表。

3808b963f3f13ca3c9d4014f51b9211a.png

核心API

80b02c35c4750266064c29fd7227c243.png
  • addTimeout()277e74231170c7e772b8f5bd0dcc275a.png

  • pollTimeout()c3a63ac74c3c006575ae419af325bc26.png

  • remove()
    从双向链表中移除指定的 HashedWheelTimeout 节点。

  • clearTimeouts()
    循环调用 pollTimeout() 方法处理整个双向链表,并返回所有未超时或者未被取消的任务。

  • expireTimeouts()
    遍历双向链表中的全部 HashedWheelTimeout 节点。在处理到期的定时任务时,会通过 remove() 方法取出,并调用其 expire() 方法执行;对于已取消的任务,通过 remove() 方法取出后直接丢弃;对于未到期的任务,会将 remainingRounds 字段(剩余时钟周期数)减一。

HashedWheelTimer

7db915469a57739682a7cc0fb8537113.png

Timer 接口的实现,通过时间轮算法实现了一个定时器。

3808b963f3f13ca3c9d4014f51b9211a.png

职能

80b02c35c4750266064c29fd7227c243.png

根据当前时间轮指针选定对应 HashedWheelBucket 槽,从链表头部开始迭代,计算每个 HashedWheelTimeout 定时任务:

  • 属于当前时钟周期则取出运行

  • 不属于则将其剩余的时钟周期数减一

3808b963f3f13ca3c9d4014f51b9211a.png

核心域

80b02c35c4750266064c29fd7227c243.png
  • workerState
    时间轮当前所处状态,三个可选值,由 AtomicIntegerFieldUpdater 实现其原子地修改。b8cb466a2dd80cb0b42a600b2cfb03dd.png

  • startTime
    当前时间轮的启动时间,提交到该时间轮的定时任务的 deadline 字段值均以该时间戳为起点进行计算。43bac409ed7781b1673f1e18f81c6ef6.png

  • wheel
    时间轮环形队列,每个元素都是一个槽。当指定时间轮槽数为 n 时,会向上取最靠近 n 的 2 次幂值1b9e077d1c926ba3daf8f52a807f6ccd.png

  • timeouts、cancelledTimeouts
    HashedWheelTimer 会在处理 HashedWheelBucket 的双向链表前,先处理这俩队列的数据:

    • timeouts 队列
      缓冲外部提交时间轮中的定时任务

    • cancelledTimeouts 队列
      暂存取消的定时任务f5677de16b4905fe5ac9c30a0cfb087d.png

  • tick
    位于 HashedWheelTimer$Worker ,时间轮的指针,步长为 1 的单调递增计数器c4f945f6ccea7c12910de1ec21589afb.png

  • mask
    掩码, mask = wheel.length - 1,执行 ticks & mask 便能定位到对应的时钟槽92bf153d48d1a9d8bd99134af3dc1aee.png

  • ticksDuration
    时间指针每次加 1 所代表的实际时间,单位为纳秒。ea78cb9d2d295652779924f4c0ee5597.png

  • pendingTimeouts
    当前时间轮剩余的定时任务总数。289cccf810451dc00333c07f3e77bd76.png

  • workerThread
    时间轮内部真正执行定时任务的线程。1b803d825f27f81f224420a8a8dca8eb.png

  • worker
    真正执行定时任务的逻辑封装这个 Runnable 对象中。e7e739f23d28b94348c33474ca8b32c8.png

3808b963f3f13ca3c9d4014f51b9211a.png

newTimeout()

80b02c35c4750266064c29fd7227c243.png

提交定时任务,在定时任务进入到 timeouts 队列之前会先调用 start() 方法启动时间轮,其中会完成下面两个关键步骤:

  1. 确定时间轮的 startTime 字段

  2. 启动 workerThread 线程,开始执行 worker 任务。

之后根据 startTime 计算该定时任务的 deadline,最后才能将定时任务封装成 HashedWheelTimeout 并添加到 timeouts 队列。

4 时间轮指针一次转动的执行流程

HashedWheelTimer$Worker.run():

  1. 时间轮指针转动,时间轮周期开始

  2. 清理用户主动取消的定时任务,这些定时任务在用户取消时,记录到 cancelledTimeouts 队列中。在每次指针转动的时候,时间轮都会清理该队列

  3. 将缓存在 timeouts 队列中的定时任务转移到时间轮中对应的槽中

  4. 根据当前指针定位对应槽,处理该槽位的双向链表中的定时任务

  5. 检测时间轮的状态。如果时间轮处于运行状态,则循环执行上述步骤,不断执行定时任务。如果时间轮处于停止状态,则执行下面的步骤获取到未被执行的定时任务并加入 unprocessedTimeouts 队列:遍历时间轮中每个槽位,并调用 clearTimeouts() 方法;对 timeouts 队列中未被加入槽中循环调用 poll()

  6. 最后再次清理 cancelledTimeouts 队列中用户主动取消的定时任务。

5 定时任务应用

并不直接用于周期性操作,而是只向时间轮提交执行单次的定时任务,在上一次任务执行完成的时候,调用 newTimeout() 方法再次提交当前任务,这样就会在下个周期执行该任务。即使在任务执行过程中出现了 GC、I/O 阻塞等情况,导致任务延迟或卡住,也不会有同样的任务源源不断地提交进来,导致任务堆积。

Dubbo 时间轮应用主要在如下方面:

  1. 失败重试, 例如,Provider 向注册中心进行注册失败时的重试操作,或是 Consumer 向注册中心订阅时的失败重试等

  2. 周期性定时任务, 例如,定期发送心跳请求,请求超时的处理,或是网络连接断开后的重连机制

参考

  • https://zhuanlan.zhihu.com/p/32906730

往期推荐

DDD领域驱动实战-子域/核心域等核心概念

实战DDD领域驱动设计之限界上下文

DDD领域驱动设计实战(三)- 理解实体

DDD领域驱动设计实战(四)- 理解值对象

架构设计之高可扩展性

abb9b769c2197e5fa6763a17e6b3541e.gif

目前交流群已有 800+人,旨在促进技术交流,可关注公众号添加笔者微信邀请进群

47c1de5066db7b13ca230463246dd297.png

喜欢文章,点个“在看、点赞、分享”素质三连支持一下~




推荐阅读
  • 1、形成邻居条件:1)区域ID相同;2)hello,dead时间一致;3)认证&# ... [详细]
  • 在运行于MS SQL Server 2005的.NET 2.0 Web应用中,我偶尔会遇到令人头疼的SQL死锁问题。过去,我们主要通过调整查询来解决这些问题,但这既耗时又不可靠。我希望能找到一种确定性的查询模式,确保从设计上彻底避免SQL死锁。 ... [详细]
  • 为什么会崩溃? ... [详细]
  • 深入解析Java并发之ArrayBlockingQueue
    本文详细探讨了ArrayBlockingQueue,这是一种基于数组实现的阻塞队列。ArrayBlockingQueue在初始化时需要指定容量,因此它是一个有界的阻塞队列。文章不仅介绍了其基本概念和数据结构,还深入分析了其源码实现,包括各种入队、出队、获取元素和删除元素的方法。 ... [详细]
  • RTThread线程间通信
    线程中通信在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取& ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • Netty框架中运用Protobuf实现高效通信协议
    在Netty框架中,通过引入Protobuf来实现高效的通信协议。为了使用Protobuf,需要先准备好环境,包括下载并安装Protobuf的代码生成器`protoc`以及相应的源码包。具体资源可从官方下载页面获取,确保版本兼容性以充分发挥其性能优势。此外,配置好开发环境后,可以通过定义`.proto`文件来自动生成Java类,从而简化数据序列化和反序列化的操作,提高通信效率。 ... [详细]
  • 本文探讨了如何使Shell和程序同时响应Ctrl-C信号的方法,即通过将两者置于同一进程组并将其设为终端的前台进程组。 ... [详细]
  • 探讨Linux系统中PCI设备的I/O地址与内存映射的区别及其实现方式。 ... [详细]
  • 本文详细介绍了如何使用 Python 编程语言中的 Scapy 库执行 DNS 欺骗攻击,包括必要的软件安装、攻击流程及代码示例。 ... [详细]
  • 最新进展:作为最接近官方声明的信息源,本文吸引了大量关注。若需获取最新动态,请访问:lkhill.com/ccie-version-5-update ... [详细]
  • 利用Node.js实现PSD文件的高效切图
    本文介绍了如何通过Node.js及其psd2json模块,快速实现PSD文件的自动化切图过程,以适应项目中频繁的界面更新需求。此方法不仅提高了工作效率,还简化了从设计稿到实际应用的转换流程。 ... [详细]
  • 洛谷 P4009 汽车加油行驶问题 解析
    探讨了经典算法题目——汽车加油行驶问题,通过网络流和费用流的视角,深入解析了该问题的解决方案。本文将详细阐述如何利用最短路径算法解决这一问题,并提供详细的代码实现。 ... [详细]
  • 本文总结了近年来在实际项目中使用消息中间件的经验和常见问题,旨在为Java初学者和中级开发者提供实用的参考。文章详细介绍了消息中间件在分布式系统中的作用,以及如何通过消息中间件实现高可用性和可扩展性。 ... [详细]
  • 本文介绍了 Java 中 io.netty.channel.kqueue.KQueueStaticallyReferencedJniMethods.evfiltSock() 方法的使用及其代码示例,帮助开发者更好地理解和应用该方法。 ... [详细]
author-avatar
叶蕊2502860197
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有