当前位置: 首页 > 科技观察

时钟轮在RPC中的应用_0

时间:2023-03-16 21:05:18 科技观察

今天的文章介绍如何在RPC中使用时钟轮实现定时任务,比如调用端的超时处理,定时心跳……定时任务带来了哪些问题?在讲解时钟轮之前,我们先来说说计时任务。相信大家在开发的过程中,会在很多场景中用到定时任务,在RPC框架中很多地方都会用到它。以调用方的请求超时处理逻辑为例,看看RPC框架是如何处理超时请求的。在解释Future的时候说过:无论是同步调用还是异步调用,调用者内部实现都是异步的,调用者在向服务端发送消息之前会创建一个Future,并存储消息ID之间的映射而Future,当服务端接收并处理消息并向调用者发送响应消息时,调用者收到消息后会根据消息的唯一标识找到Future,并将结果注入到Future中。在这个过程中,如果服务端没有及时响应调用者怎么办?调用者应该如何处理超时请求?没错,你可以使用定时任务。每次创建一个Future,我们记录下Future的创建时间和Future的超时时间,有定时任务检测。当Future达到超时未处理时,我们对Future执行超时逻辑。如何执行定时任务?有一种方法可以实现,也是最简单的一种。我们每创建一个Future,就启动一个线程,然后sleep,到达超时时间,触发请求超时处理逻辑。这种方式真的很简单,在一些场景下是可以使用的,但是缺点也很明显。就像刚才说的Future超时处理的例子,如果我们面对高并发请求,单机每秒发送几万个请求,请求超时时间设置为5秒,我们需要多少个线程创造?加班任务怎么办?十万多线程,这个数字真的够吓人的。但是等等,我们还有另一种方法。我们可以用一个线程来处理所有的定时任务,以刚才的Future超时处理为例。假设我们要启动一个线程,它会每隔100毫秒扫描所有处理Future超时的任务。当我们发现一个Future超时了,我们就会执行这个任务,执行这个Future的超时逻辑。这种方式我们用的最多,也解决了第一种方式线程过多的问题,但其实它也有明显的缺点。同样是高并发请求,那么扫描任务的线程每100毫秒扫描多少个定时任务呢?如果调用者在1秒内发送了10000个请求,而这10000个请求直到5秒后才超时,那么扫描线程将在5秒内连续处理这10000个任务。对于扫描遍历,需要额外扫描40次以上(每100毫秒扫描一次,5秒内扫描近50次),这是对CPU的浪费。我们在使用定时任务的时候,带来的问题就是让CPU做了很多额外的轮询和遍历操作,浪费了CPU。这种现象在定时任务很多的时候尤为明显。什么是钟轮?这个问题并不难解决,我们只需要想办法减少额外的扫描操作即可。比如我这批定时任务5秒后执行,我4.9秒后开始扫描这些定时任务,大大节省了CPU。这时候我们就可以利用时钟轮的机制。我们先来看看我们生活中使用的时钟。你对它很熟悉。时钟的时针、分针和秒针。秒针跳动一周后,即60刻度后,分针跳动一次,分针跳动60刻度,时针走一步。钟轮的实现原理参考了生活中时钟跳动的原理。在钟轮机构中,有时隙和钟轮的概念。时隙相当于时钟的刻度,钟轮相当于秒针和分针的一个周期跳动。我们会将每个任务放在相应的时间段。钟轮的运行机制与生活中的时钟是一样的。每固定一个时间单位,它就会从一个时隙跳到下一个时隙,相当于我们的秒针跳动一次;时钟轮可以分为多层,下一层时钟轮中每个槽位的单位时间为当前时间轮整个周期的时间,相当于1分钟等于60秒;当钟轮跳动一个周期的所有slot完成后,一个slot的任务会从下一层钟轮中取出,重新分配给当前钟轮,当前钟轮将从第0个slot,相当于下一分钟的第一个slot。1秒。为了让大家更容易理解时钟轮的运行机制,下面用一个示例场景来模拟一下,下面我们一起来看看这个场景。假设我们的时钟轮有10个槽,时钟轮的周期为1秒,那么每个槽的单位时间为100毫秒,下一层时间轮的周期为10秒,每个槽的单位时间为abit为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槽的任务取出并分发到当前时钟轮。从当前时钟轮中取出,放入当前时钟轮的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个slot,那么超过100毫秒的任务就会被扫描遍历两次。结合这些特点,我们可以根据具体业务场景设置时钟轮的周期和时隙数量。在RPC框架中,只要涉及到定时任务,我们都可以应用时钟轮。比较典型的是调用者的超时处理,调用者和服务端的启动超时,定时心跳。