gitbook/RPC实战与核心原理/docs/219903.md
2022-09-03 22:05:03 +08:00

11 KiB
Raw Blame History

20 | 详解时钟轮在RPC中的应用

你好我是何小锋。上一讲我们学习了在分布式环境下如何快速定位问题简单回顾下重点。在分布式环境下RPC框架自身以及服务提供方的业务逻辑实现都应该对异常进行合理地封装让使用方可以根据异常快速地定位问题而在依赖关系复杂且涉及多个部门合作的分布式系统中我们也可以借助分布式链路跟踪系统快速定位问题。

现在切换到咱们今天的主题一起看看时钟轮在RPC中的应用。

定时任务带来了什么问题?

在讲解时钟轮之前我们先来聊聊定时任务。相信你在开发的过程中很多场景都会使用到定时任务在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个任务分别是任务A90毫秒之后执行、任务B610毫秒之后执行与任务C1秒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框架中只要涉及到定时任务我们都可以应用时钟轮比较典型的就是调用端的超时处理、调用端与服务端的启动超时以及定时心跳等等。

课后思考

在RPC框架中除了我说过的那几个例子你还知道有哪些功能的实现可以应用到时钟轮

欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!