分布式环境下,RPC框架自身以及服务提供方的业务逻辑实现,都应该对异常进行合理地封装,让使用方可以根据异常快速地定位问题;而在依赖关系复杂且涉及多个部门合作的分布式系统中,我们也可以借助分布式链路跟踪系统,快速定位问题。

1 Future超时处理案例

以调用端请求超时处理为例,RPC框架如何处理超时请求。

无论同步、异步调用,调用端内部都是异步,调用端在向服务端发消息前会创建一个Future,存储消息标识与Future的映射,当服务端收到消息并处理完毕后向调用端发响应消息,调用端在接收到消息后会根据标识找到这Future,并将结果注给这Future。

这过程中,若服务端没及时响应消息给调用端咋办?调用端如何处理超时请求?

可利用定时任务。每次创建一个Future,记录这Future的创建时间与这个Future的超时时间,并有一个定时任务进行检测,当该Future到达超时时间&&未被处理,就对该Future执行超时逻辑。

2 定时任务实现

2.1 sleep

最简单的。每创建一个Future,都启动一个线程,sleep,到达超时时间后就触发请求超时的处理逻辑。

高并发下,请求超时时间设5s,要创建多少个线程执行超时任务呢?超过10万个线程!

2.2 用一个线程处理所有的定时任务

假设启动一个线程,每100ms扫一遍所有的处理Future超时的任务,当发现一个Future超时,就执行这任务,对这个Future执行超时逻辑。

用得最多,也解决了方式一线程过多的问题。

缺陷

高并发的请求,扫描任务的线程每隔100ms要扫描多少个定时任务?若调用端刚好在1s内发送1万次请求,这1万次请求要在5s后才超时,扫描线程在这5s内就会不停对这1万个任务进行扫描遍历,要额外扫描40多次(每100ms扫描一次,5秒内要扫描近50次),浪费CPU!

使用这些简陋定时任务方案,缺陷就是让CPU做太多额外的轮询遍历操作,浪费CPU。

3 时间轮

只要减少额外扫描操作即可。

如我的一批定时任务是5s后执行,我在4.9s后才开始扫描这批定时任务,就大大节省CPU。可利用时钟轮。

3.1 生活的时钟

时钟轮在RPC中的应用_定时任务

秒针跳动一周=跳动60个刻度后,分针跳动1次;分针跳动60个刻度,时针走动一步。时钟轮实现原理就参考这原理。时钟轮示意图:

时钟轮在RPC中的应用_请求超时_02

时钟轮有:

  • 时间槽
    就相当于时钟的刻度。将每个任务放到对应的时间槽位。
  • 时钟轮
    相当于秒针与分针等跳动的一个周期

时钟轮的运行机制和生活中的时钟也是一样的,每隔固定的单位时间,就会从一个时间槽位跳到下一个时间槽位,这就相当于我们的秒针跳动了一次;时钟轮可以分为多层,下一层时钟轮中每个槽位的单位时间是当前时间轮整个周期的时间,这就相当于1分钟等于60秒钟;当时钟轮将一个周期的所有槽位都跳动完之后,就会从下一层时钟轮中取出一个槽位的任务,重新分布到当前的时钟轮中,当前时钟轮则从第0槽位从新开始跳动,这就相当于下一分钟的第1秒。

4 案例讲解

假设时钟轮10个槽位,时钟一轮周期1s,则每个槽位单位时间100ms,而下一层时间轮的周期就是10s,每个槽位的单位时间也就是1s,并且当前的时钟轮刚初始化完成,即第0跳,当前在第0个槽位。

时钟轮示意图:

时钟轮在RPC中的应用_请求超时_03

任务分配

现有3个任务:

  • 任务A(90ms后执行)
  • 任务B(610ms后执行)
  • 任务C(1秒610ms后执行)

任务添加到时钟轮,任务A被放到第0槽,B第6槽,C放到下一层时间轮的第1槽位,时钟轮任务分布示意图:

时钟轮在RPC中的应用_请求超时_04

执行过程

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

时钟轮在RPC中的应用_定时任务_05

该案例的时钟轮的扫描周期仍是100ms,但其中的任务并没有被过多重复扫描,完美解决CPU浪费过多问题。

5 时钟轮在RPC中的应用

时钟轮就是用来执行定时任务,在RPC框架中只要涉及到定时相关的操作,就可使用时钟轮。

调用端请求超时处理,就可用到时钟轮,每发一次请求,都创建一个处理请求超时的定时任务放到时钟轮,在高并发、高访问量时,时钟轮每次只轮询一个时间槽位中的任务,节省大量CPU。

调用端与服务端启动超时也可应用时钟轮,以调用端为例,假设我们想要让应用可以快速地部署,例如1分钟内启动,如果超过1分钟则启动失败。我们可以在调用端启动时创建一个处理启动超时的定时任务,放到时钟轮里。

定时心跳

RPC框架调用端定时向服务端发送心跳,来维护连接状态,可将心跳的逻辑封装为一个心跳任务,放到时钟轮。

心跳是要定时重复执行的,而时钟轮中的任务执行一遍就被移除了,对于这种需要重复执行的定时任务我们该如何处理呢?在定时任务的执行逻辑的最后,我们可以重设这个任务的执行时间,把它重新丢回时钟轮。

6 总结

这很好地解决了定时任务中,因每个任务都创建一个线程,导致的创建过多线程的问题,以及一个线程扫描所有的定时任务,让CPU做了很多额外的轮询遍历操作而浪费CPU的问题。

时钟轮的实现机制就是模拟现实生活中的时钟,将每个定时任务放到对应的时间槽位上,这样可以减少扫描任务时对其它时间槽位定时任务的额外遍历操作。

  • 时间槽位的单位时间越短,时间轮触发任务的时间就越精确。例如时间槽位的单位时间是10毫秒,那么执行定时任务的时间误差就在10毫秒内,如果是100毫秒,那么误差就在100毫秒内。
  • 时间轮槽位越多,一个任务被重复扫描的概率就越小,因为只有在多层时钟轮中的任务才会被重复扫描。比如一个时间轮的槽位有1000个,一个槽位的单位时间是10ms,那么下一层时间轮的一个槽位的单位时间就是10s,超过10秒的定时任务会被放到下一层时间轮中,也就是只有超过10s的定时任务会被扫描遍历两次,但若槽位是10个,则超过100ms的任务,就会被扫描遍历两次

视具体业务对时钟轮的周期和时间槽数设置。

只要涉及定时任务,都可应用时钟轮,如调用端的超时处理、调用端与服务端的启动超时以及定时心跳等。

FAQ

时钟轮可以实现延时消息的功能,比如让一个任务几分钟之后发送一条消息出去。在比如可以实现订单过期功能,用户下单10分钟没付款,就取消订单,可以通过时钟轮实现。

时钟轮存取任务的时间复杂度是O(1),相比之下优先队列的时间复杂度是O(logN)。

如果并发线程比较多,单位时间是不是划分很细啊。例如我同时有5个线程几乎之间间隔3到5毫秒,又有3个线程10到100毫秒的,我时钟轮也得调整具体怎么划分的,这么短时间内如何保证时钟轮准确性?

一般都是统一划分,时间轮主要解决是长时间没有触发的问题,不解决实时性。