详解时钟轮在 RPC 中的应用

简介: 本文介绍了时钟轮机制及其在RPC框架中的应用。通过对比定时任务的多种实现方式,引出时钟轮如何高效处理超时问题。它仿照时钟结构,将任务分配到时间槽,减少CPU轮询开销,适用于高并发场景下的请求超时、启动超时和定时心跳等定时任务管理,显著提升性能。

在讲解时钟轮之前,我们先来聊聊定时任务。相信你在开发的过程中,很多场景都会使用到定时任务,在 RPC 框架中也有很多地方会使用到它。就以调用端请求超时的处理逻辑为例,下面我们看一下 RPC 框架是如果处理超时请求的。
回顾下 第 17 讲,我讲解 Future 的时候说过:无论是同步调用还是异步调用,调用端内部实行的都是异步,而调用端在向服务端发送消息之前会创建一个 Future,并存储这个消息标识与这个 Future 的映射,当服务端收到消息并且处理完毕后向调用端发送响应消息,调用端在接收到消息后会根据消息的唯一标识找到这个 Future,并将结果注入给这个 Future。
那在这个过程中,如果服务端没有及时响应消息给调用端呢?调用端该如何处理超时的请求?
没错,就是可以利用定时任务。每次创建一个 Future,我们都记录这个 Future 的创建时间与这个 Future 的超时时间,并且有一个定时任务进行检测,当这个 Future 到达超时时间并且没有被处理时,我们就对这个 Future 执行超时逻辑。
那定时任务该如何实现呢?
有种实现方式是这样的,也是最简单的一种。每创建一个 Future 我们都启动一个线程,之后 sleep,到达超时时间就触发请求超时的处理逻辑。
这种方式吧,确实简单,在某些场景下也是可以使用的,但弊端也是显而易见的。就像刚才我讲的那个 Future 超时处理的例子,如果我们面临的是高并发的请求,单机每秒发送数万次请求,请求超时时间设置的是 5 秒,那我们要创建多少个线程用来执行超时任务呢?超过 10 万个线程,这个数字真的够吓人了。
别急,我们还有另一种实现方式。我们可以用一个线程来处理所有的定时任务,还以刚才那个 Future 超时处理的例子为例。假设我们要启动一个线程,这个线程每隔 100 毫秒会扫描一遍所有的处理 Future 超时的任务,当发现一个 Future 超时了,我们就执行这个任务,对这个 Future 执行超时逻辑。
这种方式我们用得最多,它也解决了第一种方式线程过多的问题,但其实它也有明显的弊端。
同样是高并发的请求,那么扫描任务的线程每隔 100 毫秒要扫描多少个定时任务呢?如果调用端刚好在 1 秒内发送了 1 万次请求,这 1 万次请求要在 5 秒后才会超时,那么那个扫描的线程在这个 5 秒内就会不停地对这 1 万个任务进行扫描遍历,要额外扫描 40 多次(每 100 毫秒扫描一次,5 秒内要扫描近 50 次),很浪费 CPU。
在我们使用定时任务时,它所带来的问题,就是让 CPU 做了很多额外的轮询遍历操作,浪费了 CPU,这种现象在定时任务非常多的情况下,尤其明显。
什么是时钟轮?
这个问题也不难解决,我们只要找到一种方式,减少额外的扫描操作 就行了。比如我的一批定时任务是 5 秒之后执行,我在 4.9 秒之后才开始扫描这批定时任务,这样就大大地节省了 CPU。这时我们就可以利用时钟轮的机制了。我们先来看下我们生活中用到的时钟。

很熟悉了吧,时钟有时针、分针和秒针,秒针跳动一周之后,也就是跳动 60 个刻度之后,分针跳动 1 次,分针跳动 60 个刻度,时针走动一步。而时钟轮的实现原理就是参考了生活中的时钟跳动的原理。

在时钟轮机制中,有 时间槽 和 时钟轮 的概念,时间槽就相当于时钟的刻度,而时钟轮就相当于秒针与分针等跳动的一个周期,我们会将每个任务放到对应的时间槽位上。
时钟轮的运行机制和生活中的时钟也是一样的,每隔固定的单位时间,就会从一个时间槽位跳到下一个时间槽位,这就相当于我们的秒针跳动了一次;时钟轮可以分为多层,下一层时钟轮中每个槽位的单位时间是当前时间轮整个周期的时间,这就相当于 1 分钟等于 60 秒钟;当时钟轮将一个周期的所有槽位都跳动完之后,就会从下一层时钟轮中取出一个槽位的任务,重新分布到当前的时钟轮中,当前时钟轮则从第 0 槽位从新开始跳动,这就相当于下一分钟的第 1 秒。
为了方便你了解时钟轮的运行机制,我们用一个场景例子来模拟下,一起看下这个场景。
假设我们的时钟轮有 10 个槽位,而时钟轮一轮的周期是 1 秒,那么我们每个槽位的单位时间就是 100 毫秒,而下一层时间轮的周期就是 10 秒,每个槽位的单位时间也就是 1 秒,并且当前的时钟轮刚初始化完成,也就是第 0 跳,当前在第 0 个槽位。

好,现在我们有 3 个任务,分别是任务 A(90 毫秒之后执行)、任务 B(610 毫秒之后执行)与任务 C(1 秒 610 毫秒之后执行),我们将这 3 个任务添加到时钟轮中,任务 A 被放到第 0 槽位,任务 B 被放到第 6 槽位,任务 C 被放到下一层时间轮的第 1 槽位,如下面这张图所示。

当任务 A 刚被放到时钟轮,就被即刻执行了,因为它被放到了第 0 槽位,而当前时间轮正好跳到第 0 槽位(实际上还没开始跳动,状态为第 0 跳);600 毫秒之后,时间轮已经进行了 6 跳,当前槽位是第 6 槽位,第 6 槽位所有的任务都被取出执行;1 秒钟之后,当前时钟轮的第 9 跳已经跳完,从新开始了第 0 跳,这时下一层时钟轮从第 0 跳跳到了第 1 跳,将第 1 槽位的任务取出,分布到当前的时钟轮中,这时任务 C 从下一层时钟轮中取出并放到当前时钟轮的第 6 槽位;1 秒 600 毫秒之后,任务 C 被执行。

看完了这个场景,相信你对时钟轮的机制已经有所了解了。在这个例子中,时钟轮的扫描周期仍是 100 毫秒,但是其中的任务并没有被过多的重复扫描,它完美地解决了 CPU 浪费的问题。
这个机制其实不难理解,但实现起来还是很有难度的,其中要注意的问题也很多。具体的代码实现我们这里不展示,这又是另外一个比较大的话题了。有兴趣的话你可以自行查阅下相关源码,动手实现一下。
时钟轮在 RPC 中的应用
通过刚才对时钟轮的讲解,相信你可以看出,它就是用来执行定时任务的,可以说在 RPC 框架中只要涉及到定时相关的操作,我们就可以使用时钟轮。那么 RPC 框架在哪些功能实现中会用到它呢?
刚才我举例讲到的调用端请求超时处理,这里我们就可以应用到时钟轮,我们每发一次请求,都创建一个处理请求超时的定时任务放到时钟轮里,在高并发、高访问量的情况下,时钟轮每次只轮询一个时间槽位中的任务,这样会节省大量的 CPU。
调用端与服务端启动超时也可以应用到时钟轮,以调用端为例,假设我们想要让应用可以快速地部署,例如 1 分钟内启动,如果超过 1 分钟则启动失败。我们可以在调用端启动时创建一个处理启动超时的定时任务,放到时钟轮里。
除此之外,你还能想到 RPC 框架在哪些地方可以应用到时钟轮吗?还有定时心跳。RPC 框架调用端定时向服务端发送心跳,来维护连接状态,我们可以将心跳的逻辑封装为一个心跳任务,放到时钟轮里。
这时你可能会有一个疑问,心跳是要定时重复执行的,而时钟轮中的任务执行一遍就被移除了,对于这种需要重复执行的定时任务我们该如何处理呢?在定时任务的执行逻辑的最后,我们可以重设这个任务的执行时间,把它重新丢回到时钟轮里。
总结
今天我们主要讲解了时钟轮的机制,以及时钟轮在 RPC 框架中的应用。这个机制很好地解决了定时任务中,因每个任务都创建一个线程,导致的创建过多线程的问题,以及一个线程扫描所有的定时任务,让 CPU 做了很多额外的轮询遍历操作而浪费 CPU 的问题。
时钟轮的实现机制就是模拟现实生活中的时钟,将每个定时任务放到对应的时间槽位上,这样可以减少扫描任务时对其它时间槽位定时任务的额外遍历操作。在时间轮的使用中,有些问题需要你额外注意:
● 时间槽位的单位时间越短,时间轮触发任务的时间就越精确。例如时间槽位的单位时间是 10 毫秒,那么执行定时任务的时间误差就在 10 毫秒内,如果是 100 毫秒,那么误差就在 100 毫秒内。
● 时间轮的槽位越多,那么一个任务被重复扫描的概率就越小,因为只有在多层时钟轮中的任务才会被重复扫描。比如一个时间轮的槽位有 1000 个,一个槽位的单位时间是 10 毫秒,那么下一层时间轮的一个槽位的单位时间就是 10 秒,超过 10 秒的定时任务会被放到下一层时间轮中,也就是只有超过 10 秒的定时任务会被扫描遍历两次,但如果槽位是 10 个,那么超过 100 毫秒的任务,就会被扫描遍历两次。
结合这些特点,我们就可以视具体的业务场景而定,对时钟轮的周期和时间槽数进行设置。
在 RPC 框架中,只要涉及到定时任务,我们都可以应用时钟轮,比较典型的就是调用端的超时处理、调用端与服务端的启动超时以及定时心跳等等。

相关文章
|
存储 缓存 分布式计算
Spark任务OOM问题如何解决?
大家好,我是V哥。在实际业务中,Spark任务常因数据量过大、资源分配不合理或代码瓶颈导致OOM(Out of Memory)。本文详细分析了各种业务场景下的OOM原因,并提供了优化方案,包括调整Executor内存和CPU资源、优化内存管理策略、数据切分及减少宽依赖等。通过综合运用这些方法,可有效解决Spark任务中的OOM问题。关注威哥爱编程,让编码更顺畅!
1030 3
|
4月前
|
人工智能 自然语言处理 搜索推荐
2025国内AI数字人企业厂商权威推荐与综合对比选择指南
数字人企业崛起,像衍科技、阿里、华为引领技术与应用变革。从服务到社交,数字人多元发展,赋能政务、文旅、医疗等领域,推动降本增效与数实融合,开启智能交互新时代。
|
4月前
|
存储 编解码 JSON
RPC 实战:剖析 gRPC 源码,动手实现一个完整的 RPC
本讲通过剖析gRPC源码,实战实现RPC框架。利用Protocol Buffer定义接口,生成客户端和服务端代码,结合HTTP/2多路复用与PB序列化,详解请求发送、接收及编解码流程,揭示动态代理、序列化等技术在gRPC中的落地应用,帮助读者掌握RPC核心原理与实现。
|
4月前
|
Java 开发工具 数据安全/隐私保护
《中州养老》
《中州养老》是一个面向养老院的单体后台管理系统,涵盖员工管理端与家属小程序端。系统功能完善,包含预约参观、入住退住、计费、健康监测等模块。我主要负责核心模块设计开发,如护理等级、床位管理、权限控制或智能监测等。项目采用SpringBoot+Vue3技术栈,结合Redis缓存、Nginx部署、阿里云OSS与IoT平台,实现高效稳定的数据交互与实时健康监控。通过RBAC权限模型保障系统安全,利用定时任务、线程池、索引优化等手段提升性能,支持微信登录、小程序预约、设备报警等实用功能,全面助力智慧养老信息化建设。(238字)
|
4月前
|
存储 缓存 NoSQL
《神领物流》
本项目为基于微服务架构的智能物流系统,涵盖用户端、快递员端、司机端及管理端。采用GitFlow协作开发,结合Jenkins实现持续集成。通过Redis优化运费模板查询,利用Neo4j实现路线规划,MongoDB存储作业范围与物流轨迹,结合RabbitMQ保障消息可靠传输,使用Seata解决分布式事务,并引入多级缓存与布隆过滤器应对高并发场景,提升系统性能与稳定性。
|
10月前
|
前端开发 开发者 容器
使用CSS Grid实现响应式布局
使用CSS Grid实现响应式布局
|
10月前
|
人工智能 搜索推荐 API
“电商API数据赋能:实时分析,优化营销策略”
电商API通过实时数据交互赋能企业,优化营销策略与运营效率。其核心价值体现在动态定价、个性化推荐及促销效果追踪等场景,助力企业快速响应市场变化。技术上依赖数据聚合、实时计算框架与A/B测试,同时需应对数据延迟、接口稳定性及合规性挑战。未来,AI与API深度融合将推动预测性分析和智能决策,为企业带来更大竞争优势。
248 1
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
895 2
|
JSON API 开发者
1688 快递费用 API 接口的技术剖析与应用
1688快递费用API接口为企业和开发者提供自动化、高效化的快递费用查询服务,打破人工查询的繁琐局面。通过输入寄件与收件地址、商品重量、体积及选择快递公司等信息,接口精准计算费用并返回结果,支持中通、圆通等主流快递。输出内容包括快递费用、预估时效及附加费说明,助力电商精细化运营。Python示例代码展示了如何使用requests库发起POST请求并解析响应数据,实现费用查询自动化。
570 10
|
机器学习/深度学习 人工智能 自然语言处理
《当心!生成式AI正成为网络钓鱼的“帮凶”,这些防范要点你必须知道!》
生成式AI在数字化浪潮中迅速革新各领域,带来便利的同时也催生了隐蔽且危险的网络钓鱼威胁。它通过自然语言处理生成逼真文本,突破语言限制,甚至利用深度伪造技术合成人脸和声音,使攻击更加难以察觉。为应对这一威胁,个人和企业需强化安全意识教育、部署先进安全工具、完善安全策略,并巧用检测工具识别AI生成内容,以筑牢防范之堤,保护信息安全。
461 3

热门文章

最新文章