当前位置: 首页 > news >正文

时间轮算法理解、Kafka实现

概述

TimingWheel,时间轮,简单理解就是一种用来存储若干个定时任务的环状队列(或数组),工作原理和钟表的表盘类似。

关于环形队列,请参考环形队列。

时间轮由两个部分组成,一个环状数组,一个遍历环状数组的指针。

首先定义一个固定长度的环状数组,队列中的每一个元素代表一个时间格(可以精确到秒或毫秒。实际场景里,如Java或Linux下的cron定时任务,都是某一秒来触发。在实时处理领域,则一般用毫秒),一个时间格可存放若干个定时任务(真实业务开发场景下,同时触发多个任务),即任务列表。任务列表是一个环形的双向链表,链表中的每一项表示的都是定时任务项,其中封装真正的定时任务。
在这里插入图片描述
时间格代表时间轮的基本时间跨度或精度,假如一秒走一个时间格的话,则这个时间轮的精度就是1秒。当指针指向某个数组时,就会把这个数组中存储的任务取出来,然后遍历链表逐个运行里面的任务。

下图是一个有12个时间格的时间轮,转完一圈需要12s。当需要新建一个3s后执行的定时任务,只需要将定时任务放在下标为3的时间格中即可。
在这里插入图片描述
当需要创建一个15s后执行的定时任务怎么办呢?

此时可考虑引入圈数(也叫轮数)这一概念,即这个任务还是放在下标为3的时间格中,圈数为2。除增加圈数这种方法之外,还有种多层次时间轮,Kafka采用的就是这种方案。

时间轮的好处:

  • 减少定时任务添加和删除的时间复杂度,提升性能;
  • 可保证每次执行定时器任务都是O(1)复杂度,在定时器任务密集的情况下,性能优势非常明显

实现

在很多开源组件里可看到时间轮算法的实现:Kafka、Netty、Dubbo、Caffeine。

值得一提的是,网络上好多文章说ZooKeeper里也有时间轮算法的实现,并没有。

Kafka

Kafka中有很多延时操作,如耗时的网络请求(如Produce时等待ISR副本复制成功)会被封装成DelayOperation进行延迟处理操作,防止阻塞Kafka请求处理线程。

Kafka没有使用JDK自带的Timer和DelayQueue实现。底层都是个优先队列,即采用minHeap的数据结构,最快需要执行的任务排在队列第一个,不同的是Timer中有个线程去拉取任务执行,DelayQueue是个容器,需要配合其他线程工作。时间复杂度上这两者插入和删除操作都是O(logn),不满足性能要求。

ScheduledThreadPoolExecutor是JDK提供定时线程池,也是DelayQueue + 池化线程的一个实现。

Kafka基于时间轮实现延时操作,时间轮算法的插入删除操作的时间复杂度都是O(1),满足性能要求。

源码类为org.apache.kafka.server.util.timer.TimingWheel

public class TimingWheel {private final long tickMs;private final long startMs;private final int wheelSize;private final AtomicInteger taskCounter;private final DelayQueue<TimerTaskList> queue;private final long interval;private final TimerTaskList[] buckets;private long currentTimeMs;private volatile TimingWheel overflowWheel = null;
}

几个核心参数:

  • tickMs:时间跨度
  • startMs:开始时间
  • wheelSize:时间轮中bucket的个数
  • interval:时间轮的整体时间跨度 = tickMs * wheelSize
  • currentTimeMs:tickMs的整数倍,代表时间轮当前所处的时间。currentTimeMs可以将整个时间轮划分为到期部分和未到期部分,currentTimeMs当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的TimerTaskList中的所有任务

整个时间轮的总体跨度是不变的,随着指针currentTimeMs的不断推进,当前时间轮所能处理的时间段也在不断后移,总体时间范围在currentTimeMs和currentTimeMs+interval之间。

Kafka采用多层次时间轮来支持大跨度的定时任务,参考手表。
在这里插入图片描述
上图时间轮,第1层的时间精度为1,第2层的时间精度为20,第3层的时间精度为400。假如需要添加一个350s后执行的任务A的话(当前时间是0s),这个任务会被放在第2层(第二层的时间跨度为20*20=400>350)的第350/20=17个时间格子。

当第一层转17圈之后,时间过去340s,第2层的指针此时来到第17个时间格子。此时第2层第17个格子的任务会被移动到第1层。任务A当前是10s之后执行,因此它会被移动到第1层的第10个时间格子。

在层与层之间的移动,叫做时间轮的升降级。时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是O(1)

随着时间推进,也会有一个时间轮降级的操作,原本延时较长的任务会从高一层时间轮重新提交到时间轮中,然后会被放在合适的低层次的时间轮当中等待处理。

在Kafka中时间轮之间如何关联呢,如何展现这种高一层的时间轮关系?
一个内部对象的指针,指向自己高一层的时间轮对象。

如何推进时间轮的前进,让时间轮的时间往前走?
通过DelayQueue来推进,是一种空间换时间的思想;DelayQueue中保存着所有的TimerTaskList对象,根据时间来排序,这样延时越小的任务排在越前面。外部通过一个ExpiredOperationReaper线程从DelayQueue中获取超时的任务列表TimerTaskList,然后根据TimerTaskList的过期时间来精确推进时间轮的时间,这样就不会存在空推进的问题。

Kafka采用权衡的策略,把DelayQueue用在合适地方。DelayQueue只存放TimerTaskList,并不是所有的TimerTask,数量并不多,相比空推进带来的影响是利大于弊的。

总结

  • Kafka使用时间轮来实现延时队列,因为其底层是任务的添加和删除是基于链表实现的,时间复杂度为O(1),满足高性能的要求;
  • 对于时间跨度大的延时任务,引入层级时间轮,能更好控制时间粒度,可以应对更加复杂的定时任务处理场景;
  • 对于如何实现时间轮的推进和避免空推进影响性能,采用空间换时间的思想,通过DelayQueue来推进时间轮。

Netty

io.netty.util.HashedWheelTimer

Netty中的时间轮是通过工作线程按照固定的时间间隔tickDuration推进的,如果长时间没有到期任务,这种方案会带来空推进的问题,造成一定性能损耗;

Dubbo

org.apache.dubbo.common.timer.HashedWheelTimer,和Netty的源码实现几乎一样。

Caffeine

com.github.benmanes.caffeine.cache.TimerWheel

内部类Sentinel代表当前任务,两个内部类AscendingIterator和DescendingIterator分别表示从时间轮取任务的两个方式,

参考

  • Kafka时间轮算法设计
  • HashedWheelTimer使用及源码分析
  • 一个开源的时间轮算法介绍

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 数列分块<1>
  • vs2017/2019串口Qt Serial Port/modbus使用报错
  • 将vue项目整合到springboot项目中并在阿里云上运行
  • Linux yum-config-manager命令教程:轻松管理YUM软件源(附实例详解和注意事项)
  • js逆向第24例:FastMoss数据分析网站Fm-Sign加密字段破解
  • MATLAB激光通信和-积消息传递算法(Python图形模型算法)模拟调制
  • MAVSDK动态库与静态库及mavsdk_server程序macOS平台编译与安装
  • SQL Server端口配置指南
  • threadx 线程以及优先级调整案例
  • npm和yarn清理缓存命令
  • 【Delphi】webView2 开发中注意事项:Content-MD5
  • 社交网络图数据集
  • WGCLOUD的ping设备监测可以导入excel数据吗
  • Redis基本数据结构
  • 品牌产业出海指南如何搭建国际化架构的跨境电商平台?
  • [译] React v16.8: 含有Hooks的版本
  • 【407天】跃迁之路——程序员高效学习方法论探索系列(实验阶段164-2018.03.19)...
  • angular2 简述
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • ES2017异步函数现已正式可用
  • HashMap剖析之内部结构
  • JavaWeb(学习笔记二)
  • Java编程基础24——递归练习
  • Laravel 中的一个后期静态绑定
  • PHP 的 SAPI 是个什么东西
  • rc-form之最单纯情况
  • Sequelize 中文文档 v4 - Getting started - 入门
  • Spring核心 Bean的高级装配
  • SQLServer之创建显式事务
  • ucore操作系统实验笔记 - 重新理解中断
  • underscore源码剖析之整体架构
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 安装python包到指定虚拟环境
  • 飞驰在Mesos的涡轮引擎上
  • 配置 PM2 实现代码自动发布
  • 删除表内多余的重复数据
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • ## 1.3.Git命令
  • (16)UiBot:智能化软件机器人(以头歌抓取课程数据为例)
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (八)Spring源码解析:Spring MVC
  • (佳作)两轮平衡小车(原理图、PCB、程序源码、BOM等)
  • (十三)Maven插件解析运行机制
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (转)淘淘商城系列——使用Spring来管理Redis单机版和集群版
  • .Net 6.0--通用帮助类--FileHelper
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET I/O 学习笔记:对文件和目录进行解压缩操作
  • .NET 的程序集加载上下文