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

时间轮的golang实践浅析

引言

  • 下列代码模仿一段RPC请求的执行过程,执行后会有哪些问题:
    RPC代码示例
  • 答案:因为超时控制后未阻断后续请求,导致并发读写产生Panic
  • 思考:客户端发起 HTTP 请求后,如果在指定时间内没有收到服务器的响应,则自动断开连接,超时控制是如何工作的?

什么是时间轮

  • 思考:一定有一个类似于定时器的工具在执行,到时间后,中断任务。那么这个定时器是什么样的数据结构?又是如何实现这个定时功能的?
  • 理论上
    • 客户端发起请求后,立即创建(启动)一个 Timer:到期间隔为 d,到期后执行 “断开连接” 的操作。
    • 如果到期间隔 d 以内收到了服务器的响应,客户端就删除(停止)这个 Timer。
    • 如果一直没有收到响应,则 Timer 最终会到期,然后执行 “断开连接” 的操作。
  • 实际上
    • 现代的 Web 服务动辄管理 100w+ 的连接,每个连接都会有很多超时任务(比如发送超时、心跳检测等),如果每个超时任务都对应一个 Timer,性能会比较低下
  • 破解之法:采用时间轮实现的 Timer来管理连接任务,使得创建和删除连接任务的时间复杂度为 O(1)

时间轮种类和设计思路

  • 常见的时间轮实现有两种:
    • 简单时间轮(Simple Timing Wheel)—— 比如 Netty4 的 HashedWheelTimer
    • 层级时间轮(Hierarchical Timing Wheels)—— 比如 Kafka 的 Purgatory

简单时间轮

简单时间轮的设计思路

  • 一个 简单时间轮就是一个循环列表,列表中的每一格包含一个定时任务列表(双向链表)。一个时间单位为 u、大小为 n 的简单时间轮,可以包含的定时任务的最大到期间隔为 n*u。
  • 以 u 为 1ms、n 为 3 的简单时间轮为例,可以包含的定时任务的最大到期间隔为 3ms

简单时间轮示例

在这里插入图片描述

简单时间轮的实现

    index := 0timingWheels := make([]interface{}, 10)for { // 循环消费任务time.Sleep(1 * time.Second)index = index % len(timingWheels)task := timingWheels[index]fmt.Println(task) // exec taskindex++}{ // 增加任务x := 2 // 2s 后执行task_i := "任务i"timingWheels[(index+x)%len(timingWheels)] = task_i}

简单时间轮的缺陷

  • 一旦选定 n,就不能包含到期间隔超过 n*u 的定时任务
    • 解决办法:选择较大的n
  • 引申问题:如果定时任务的到期时间跨度较大,就会选择较大的 n,在定时任务较少时会造成很大的空间浪费
    • 引申问题的解决办法:在定时任务中增加记录 round 轮次信息,可以有效弥补上述两个缺点。同样以上面 u 为 1ms、n 为 3 的简单时间轮为例,初始时间指向第 1 格;此时如果要创建到期时间为 4ms 的定时任务,可以在该任务中设置 round 为 1(4/3 取整),剩余到期时间用 4ms 减去 round*3ms 等于 1ms,因此放到第 2 格;等到当前时间指向第 2 格时,判断任务中的 round 大于 0,所以不会删除并执行该任务,而是对其 round 减一(于是 round 变为 0);等到再过 3ms 后,当前时间再次指向第 2 格,判断任务中的 round 为 0,进而删除并执行该任务
    index := 0timingWheels := make([]timingWheelsTask, 10)for { // 循环消费任务time.Sleep(1 * time.Second)index = index % len(timingWheels)cur := timingWheels[index]if cur.round != 0 {cur.round--} else {fmt.Println(cur.task) // exec task}index++}{ // 增加任务x := 20 // 2s 后执行round_i := x / len(timingWheels)index_i := x % len(timingWheels)task_i := timingWheelsTask{round: round_i,task:  "任务i",}timingWheels[index_i] = task_i}type timingWheelsTask struct {round inttask  interface{}}
  • 再次引申问题:每格轮子只能存放一个task,如果在同一时间,需要执行多个任务,怎么办?
    • 再次引申问题的解决办法:将timingWheelsTask结构体修改为:
	type timingWheelsTask struct {taskList []TaskList}type TaskList struct {round inttask  interface{}}
  • 再一次引申问题:TaskList的处理时间是O(n),如果定时任务数量很大,分摊到每一格的定时任务列表就会很长,这样的处理性能显然是让人无法接受的,特别是对于时间精度要求比较高的任务,另外就是由于list过长,导致for循环完list后,当前index的时间已经过了,长此以往,会导致整体时间轮的精度不准确,延误后面的task执行。
    • 问题到此看似无解

层级时间轮

层级时间轮的设计思路

  • 层级时间轮 通过使用多个时间轮,并且对每个时间轮采用不同的 u,可以有效地解决简单时间轮及其变体实现的问题
  • 理论上
    • 每一层时间轮的大小都固定为 n,第一层时间轮的时间单位为 u,那么第二层时间轮(我们称之为第一层时间轮的溢出时间轮 Overflow Wheel)的时间单位就为 n*u,以此类推。
    • 除了第一层时间轮是固定创建的,其他层的时间轮(均为溢出时间轮)都是按需创建的。
    • 原先插入到高层时间轮(溢出时间轮)的定时任务,随着时间的流逝,会被降级重新插入到低层时间轮中

层级时间轮实例

  • 以 u 为 1ms、n 为 3 的层级时间轮为例,第一层时间轮的时间单位为 1ms、大小为 3,第二层时间轮的时间单位为 3ms、大小为 3,以此类推
    在这里插入图片描述
  • 运行原理
    • 初始时,只有第一层(Level 1)时间轮,假设当前时间(蓝色箭头)指向第 1 格(此时:到期间隔为 [0ms, 1ms) 的定时任务放第 1 格,[1ms, 2ms) 的放第 2 格,[2ms, 3ms) 的放第 3 格)。
    • 此时我们创建一个到期间隔为 2ms 的定时任务 task1,按规则该任务会被插入到第一层时间轮的第 3 格。
    • 同一时刻,我们再次创建一个到期间隔为 4ms 的定时任务 task2,因为到期间隔超过了第一层时间轮的间隔范围,所以会创建第二层(Level 2)时间轮;第二层时间轮中的当前时间(蓝色箭头)也指向第 1 格,按规则该任务会被插入到第二层时间轮的第 2 格。
    • 随着时间的流逝,过了 2ms 后,第一层时间轮中的当前时间指向第 3 格,这一格包含的任务 task1 会被删除并执行;此时,第二层时间轮的当前时间没有变化,依然指向第 1 格。
    • 随着时间的流逝,又过了 1ms 后,第一层时间轮中的当期时间指向第 1 格,这一格中没有任务;此时,第二层当前时间指向第 2 格,这一格包含的任务 task2 会被删除并重新插入时间轮,因为剩余到期时间为 1ms,所以 task2 会被插入到第一层时间轮的第 2 格。
    • 随着时间的流逝,又过了 1ms 后,第一层时间轮中的当前时间指向第 2 格,这一格包含的定时任务 task2 会被删除并执行;此时,第二层时间轮的当前时间没有变化,依然指向第 2 格。

层级时间轮的实现

  • timingwheel源码
  • Kafka 的变体实现【指针不动,桶往前走】
    在这里插入图片描述
    • 使用大小为 wheelSize 的数组来表示一层时间轮,其中每一格是一个 bucket,每个 bucket 的时间单位为 tick。
    • 这个时间轮数组并没有模拟循环列表的行为(如图左所示),而是模拟了哈希表的行为。具体而言(如图右所示),这个时间轮数组会随着 currentTime 的流逝而移动,也就是说 currentTime 永远是指向第一个 bucket 的,每个落到该时间轮的定时任务,都会根据哈希函数 (expiration/tick)%wheelSize 散列到对应的 bucket 中。
  • Kafka Timer 实现源码
  • 时钟驱动方式
    • 常规的时间轮实现中,会在一个线程中每隔一个时间单位 tick 就醒来一次,并驱动时钟走向下一格,然后检查这一格中是否包含定时任务。如果时间单位 tick 很小(比如 Kafka 中 tick 为 1ms)并且(在最低层时间轮的)定时任务很少,那么这种驱动方式将会非常低效
    • Kafka 的层级时间轮实现中,利用了 Java 内置的 DelayQueue 结构,将每一层时间轮中所有 “包含有定时任务的 bucket” 都加入到同一个 DelayQueue 中,然后 等到有 bucket 到期后再驱动时钟往前走,并逐个处理该 bucket 中的定时任务。
    • 图解
      在这里插入图片描述
  • 往层级时间轮中添加一个定时任务 task1 后,会将该任务所属的 bucket2 的到期时间设置为 task1 的到期时间 expiration(= 当前时间 currentTime + 定时任务到期间隔 duration),并将这个 bucket2 添加(Offer)到 DelayQueue 中。
  • DelayQueue(内部有一个线程)会等待 “到期时间最早(earliest)的 bucket” 到期,图中等到的是排在队首的 bucket2,于是经由 poll 返回并删除这个 bucket2;随后,时间轮会将当前时间 currentTime 往前移动到 bucket2 的 expiration 所指向的时间(图中是 1ms 所在的位置);最后,bucket2 中包含的 task1 会被删除并执行。
  • 上述 Kafka 层级时间轮的驱动方式是非常高效的。虽然 DelayQueue 中 offer(添加)和 poll(获取并删除)操作的时间复杂度为 O(log n),但是相比定时任务的个数而言,bucket 的个数其实是非常小的(也就是 O(log n) 中的 n 很小),因此性能也是没有问题的

时间轮源码分析

  • PriorityQueue 优先队列
    • Push
    • Pop
    • PeekAndShift
  • DelayQueue 延时队列
    • Offer
    • Poll
  • Timer 定时器 event
    • getBucket
    • setBucket
  • bucket 时间轮的桶
    • Expiration
    • SetExpiration
    • Add
    • Remove
    • Flush
  • TimingWheel 时间轮本轮
    • add
    • addOrRun
    • advanceClock
    • Start
    • AfterFunc
  • Scheduler 调度时间轮
    • ScheduleFunc

参考

  • 层级时间轮的 Golang 实现

相关文章:

时间轮的golang实践浅析

引言 下列代码模仿一段RPC请求的执行过程,执行后会有哪些问题: RPC代码示例答案:因为超时控制后未阻断后续请求,导致并发读写产生Panic思考:客户端发起 HTTP 请求后,如果在指定时间内没有收到服务器的响应…...

Linux命令_stress 快速模拟CPU、内存、磁盘消耗

ping的安装命令:apt-get install -y inetutils-ping 会遇到Unable to locate package inetutils-ping问题 正确的操作是: ** 这时候需要敲:apt-get update,这个命令的作用是:同步 /etc/apt/sources.list 和 /etc/apt/…...

可视化绘图技巧100篇分析篇(二)-生存曲线(LM曲线)

目录 前言 几个高频面试题目 roc曲线和生存曲线区别 生存曲线模型 生存曲线组件讲解...

UP主发车啦!撩人仙侠文系列,谁来管管这个反派啊!

本人书龄4年,平时很爱看小说,阅遍无数经典修仙文,熬夜党的最爱啊!!!!我心中的仙侠top,都是我的心头爱。 一般我都会跟朋友说这六本五星级仙侠好文,如果她们不看&#xf…...

K8S使用持久化卷存储到NFS(NAS盘)

参考文章:K8S-v1.20中使用PVC持久卷 - 知乎 目录 1、概念: 1.1 基础概念 1.2 PV的配置 1.2.1 静态PV配置 1.2.2 动态PV配置 1.2.3 PVC与PV的绑定 1.2.4 PVC及PV的使用 2 部署PV及PVC 2.1 所有K8S机器都需要安装NFS程序 2.2 仅针对需要暴露文件…...

一图看懂 multidict 模块:类似于字典的键值对集合,键可以多次出现,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创,转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 multidict 模块:类似于字典的键值对集合,键可以多次出现,资料整理笔记(大全) 🧊摘要🧊模…...

django CBV 与 DRF APIView源码分析

django CBV源码分析 在django框架中,视图层中的逻辑即可以使用函数处理也可以使用类进行处理,如果在视图层中使用函数处理请求,就是FBV(function base views),如果在视图层中使用类处理请求,就是CBV(class base views…...

沃尔玛入驻教程:中国卖家如何免费、快速入驻沃尔玛walmart.com?

作为一家全球知名的零售巨头,沃尔玛(Walmart)的在线商城walmart.com拥有庞大的消费者基础和巨大的商机。对于中国的卖家来说,入驻沃尔玛的平台是一个很好的机会,但是有没有什么方法可以免费、快速入驻呢?有…...

《花雕学AI》Poe 上的四种 AI 机器人,你该怎么选?ChatGPT、Sage、Claude 和 Dragonfly对比

虽然 ChatGPT 是一项革命性的技术,但它作为一个消费产品却有点失败。你可能会花很长时间等待 OpenAI 的聊天机器人加载,或者根本无法使用它,因为它太大了。就算你能用上它,它也很缓慢,而且它的界面也很丑陋。它甚至没有…...

localStorage

目录 localStorage与sessionStorage localStorage的Set与Get localStorage传递参数 localStorage与sessionStorage 现代浏览器提供了一种被称为"Web Storage APIs"(Web 存储接口)的机制,允许在同一浏览器的不同标签页之间共享数…...

二十五、SQL 数据分析实战(9个中等难度的SQL题目)

文章目录 题目1: App 使用频率分析题目2: App 下载情况统计题目3: 寻找活跃学习者题目4: 商品分类整理题目5: 商品销售分析题目6: 网约车司机收益统计题目7: 网站登录时间间隔统计题目8: 不同区域商品收入统计题目9: 信贷逾期情况统计 题目1: App 使用频率分析 现有一张用户使…...

JavaSE_02基本语法-编程单词词汇

boolean [bʊlɪən] 变量的基本数据类型之一:布尔型const [kɒnst] n. 常量,常数constant [kɒnst(ə)nt] n. [数] 常数;恒量continue [kən’tɪnjuː] vi. 继续,连续;default [dɪ’fɔːlt; diːfɔːlt] 默认的,缺…...

区间预测 | MATLAB实现QRDNN深度神经网络分位数回归时间序列区间预测

区间预测 | MATLAB实现QRDNN深度神经网络分位数回归时间序列区间预测 目录 区间预测 | MATLAB实现QRDNN深度神经网络分位数回归时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 MATLAB实现QRDNN深度神经网络分位数回归时间序列区间预测。QRDNN模型…...

如何使用aframe.js构建一个简单的VR播放器

在当今这个信息化的时代,虚拟现实(VR)已经开始逐渐成为一种新的生活方式。作为一名前端开发工程师,在学习和探索VR技术方面,aframe.js是一个非常有趣和有用的工具。在本文中,我将介绍如何使用aframe.js构建…...

Fiddler抓包工具常见功能介绍,还不会的进来看

目录 Fiddler的功能面板 一、Statistics数据统计面板,性能分析 二、Inspectors查看请求与响应 三、Filters过滤器 1、User Filters启用 2、Action 3、过滤器实际应用 四、AutoResponder请求重定向 1、什么是请求重定向? 2、为什么要用这个功能&…...

基于海鸥算法优化的核极限学习机(KELM)分类算法-附代码

基于海鸥算法优化的核极限学习机(KELM)分类算法 文章目录 基于海鸥算法优化的核极限学习机(KELM)分类算法1.KELM理论基础2.分类问题3.基于海鸥算法优化的KELM4.测试结果5.Matlab代码 摘要:本文利用海鸥算法对核极限学习机(KELM)进行优化,并用于分类 1.KE…...

JAVA代码规范审查

JAVA代码规范审查 1. 添加必要的注释 所有的类都必须添加创建者和创建日期,以及简单的注释描述 方法内部的复杂业务逻辑或者算法,需要添加清楚的注释 一般情况下,注释描述类、方法、变量的作用 任何需要提醒的警告或TODO,也要注…...

Centos8安装redis7

​ 1.下载: 官网下载:Download | Redis 把安装包上传至服务器: 2.安装: 解压redis: [rootnode202 ~]# cd /usr/local/soft/ [rootnode202 soft]# tar -zxvf redis-7.0.11.tar.gz 安装: 进入redis-7.0.1…...

RabbitMQ详解(一):Linux安装

消息队列概念 消息队列是在消息的传输过程中保存消息的容器。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。 常见的消息队列 RabbitMQ 基于AMQP(高级消息队列协议)基础上…...

Mojo:比 Python 快 35000 倍的 AI 编程语言

Mojo:比 Python 快 35000 倍的 AI 编程语言 Mojo是一门刚刚发布的面向 AI 开发人员的编程语言。 Mojo 被设计为 Python 的超集,所以如果你已经掌握了 Python,学习 Mojo 会很容易。关键是 Mojo 将 Python 的易用性与 C 语言的性能相结合&am…...

1703_LibreOffice常用功能使用体验

全部学习汇总: GreyZhang/windows_skills: some skills when using windows system. (github.com) 首先需要说明的是我不是一个重度Office用户,甚至算不上一个重度的Office用户。我使用的Office软件最多的功能就是文档编辑,绝大多数时候还是文…...

Postgres:Win/Linux环境安装及一键部署脚本

1.Win安装Postgres (1)下载安装包 (2)开始安装 修改安装目录 选择要安装的组件 data也就是库表及数据的.dba文件存放目录 密码设置 端口设置 next next 开始安装 安装完成,Stack Builder 根据需要选择是否安装。仅仅是…...

每日一题144——数组大小减半

给你一个整数数组 arr。你可以从中选出一个整数集合,并删除这些整数在数组中的每次出现。 返回 至少 能删除数组中的一半整数的整数集合的最小大小。 示例 1: 输入:arr [3,3,3,3,5,5,5,2,2,7] 输出:2 解释:选择 {3,…...

运维必懂的13条高效工作秘诀

正确做事,更要做正确的事 “正确地做事”强调的是效率,重视做一件工作的最好方法;“做正确的事”强调的是效能,重视时间的最佳利用——这包括是做或者不做某项工作。 实际上,第一重要的却是效能而非效率,…...

【牛客刷题专栏】0x26:JZ25 合并两个排序的链表(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转),它登陆后会保存刷题记录进度,重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏:个人CSDN牛客刷题专栏。 题目来自:牛客/题库 / 在线编程 / 剑指offer: 目录 前言问…...

5/5~5/7总结

把socket通信改成了分成短连接和长连接,登录前的所有操作都是短连接,每次都关闭连接,如果登录成功了就保持该socket连接,登录成功之后的所有操作,修改资料,发信息等都用该socket, 服务端和客户…...

重要通知|Sui测试网将于5月11日重置

致Sui社区成员们: 正如之前公告所述,部分社区成员发现测试网可能会定期清除数据并重新启动。鉴于此,我们宣布计划将于2023年5月11日进行首次测试网清除。 对于想要继续读取和访问当前测试网络的社区成员,请使用由Mysten Labs在…...

教你快速把heic格式转化jpg,4种方法操作简单

教你快速把heic格式转化jpg的方法,因为HEIC格式图片通常出现在苹果公司的iOS 11操作系统及之后的版本中,这是因为苹果公司在这些版本中采用了HEIF(高效图像格式)作为默认的照片格式来替代JPEG格式。同时,需要注意的是&…...

交互式数据分析和处理新方法:pandas-ai =Pandas + ChatGPT

Python Pandas是一个为Python编程提供数据操作和分析功能的开源工具包。这个库已经成为数据科学家和分析师的必备工具。它提供了一种有效的方法来管理结构化数据(Series和DataFrame)。 在人工智能领域,Pandas经常用于机器学习和深度学习过程的预处理步骤。Pandas通…...

FIR滤波

参考来源: https://www.zhihu.com/question/323353814 本节主要围绕以下几个问题进行描述: 什么是FIR滤波器时域的卷积频域的相乘 关于FIR FIR滤波就是在时域上卷积的过程。将含噪声信号与低通滤波器的傅里叶逆变换值进行卷积,这个过程就是…...