RPC实现原理之核心技术-时间轮

1. 为什么需要时间轮?

在Dubbo中,为增强系统的容错能力,会有相应的监听判断处理机制。比如RPC调用的超时机制的实现,消费者判断RPC调用是否超时,如果超时会将超时结果返回给应用层。

在Dubbo最开始的实现中,是将所有的返回结果(DefaultFuture)都放入一个集合中,并且通过一个定时任务,每隔一定时间间隔就扫描所有的future,逐个判断是否超时。

这样的实现方式虽然比较简单,但是存在一个问题就是会有很多无意义的遍历操作开销。比如一个RPC调用的超时时间是10秒,而设置的超时判定的定时任务是2秒执行一次,那么可能会有4次左右无意义的循环检测判断操作。

为了解决上述场景中的类似问题,Dubbo借鉴Netty,引入了时间轮算法,减少无意义的轮询判断操作。

2. 时间轮原理

对于以上问题, 目的是要减少额外的扫描操作就可以了。比如说一个定时任务是在5 秒之后执行,那么在 4.9 秒之后才扫描这个定时任务,这样就可以极大减少 CPU开销。这时我们就可以利用时钟轮的机制了。
file

时钟轮的实质上是参考了生活中的时钟跳动的原理,那么具体是如何实现呢?

在时钟轮机制中,有时间槽和时钟轮的概念,时间槽就相当于时钟的刻度;而时钟轮就相当于指针跳动的一个周期,我们可以将每个任务放到对应的时间槽位上。

如果时钟轮有 10 个槽位,而时钟轮一轮的周期是 10 秒,那么我们每个槽位的单位时间就是 1 秒,而下一层时间轮的周期就是 100 秒,每个槽位的单位时间也就是 10 秒,这就好比秒针与分针, 在秒针周期下, 刻度单位为秒, 在分针周期下, 刻度为分。
file

假设现在我们有 3 个任务,分别是任务 A(0.9秒之后执行)、任务 B(2.1秒后执行)与任务 C(12.1秒之后执行),我们将这 3 个任务添加到时钟轮中,任务 A 被放到第 0 槽位,任务 B 被放到第 2槽位,任务 C 被放到下一层时间轮的第2个槽位,如下图所示:
file

通过这个场景我们可以了解到,时钟轮的扫描周期仍是最小单位1秒,但是放置其中的任务并没有反复扫描,每个任务会按要求只扫描执行一次, 这样就能够很好的解决CPU 浪费的问题。

这样可能会出现一个问题, 如果不断叠加时钟轮, 无限增长, 效率是会呈现下降,那么该如何解决?

针对于设定三个时钟轮, 小时轮, 分钟轮, 秒级轮。

3. Dubbo源码剖析

主要是通过Timer,Timeout,TimerTask几个接口定义了一个定时器的模型,再通过HashedWheelTimer这个类实现了一个时间轮定时器(默认的时间槽的数量是512,可以自定义这个值)。它对外提供了简单易用的接口,只需要调用newTimeout接口,就可以实现对只需执行一次任务的调度。通过该定时器,Dubbo在响应的场景中实现了高效的任务调度。

时间轮核心类HashedWheelTimer结构:
file

4. 时间轮在RPC的应用

  1. 调用超时与重试处理: 上面所讲的客户端调用超时的处理,就可以应用到时钟轮,我们每发一次请求,都创建一个处理请求超时的定时任务放到时钟轮里,在高并发、高访问量的情况下,时钟轮每次只轮询一个时间槽位中的任务,这样会节省大量的 CPU。

    源码 FailbackRegistry, 代码片段:

    	// 构造方法
        public FailbackRegistry(URL url) {
                super(url);
                this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);    
                // since the retry task will not be very much. 128 ticks is enough.
            	// 重试器的时间槽数量, 设定为128
                retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
            }
        
        // 失败时间任务注册器
        private void addFailedRegistered(URL url) {
                FailedRegisteredTask oldOne = failedRegistered.get(url);
                if (oldOne != null) {
                    return;
                }
                FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
                oldOne = failedRegistered.putIfAbsent(url, newTask);
                if (oldOne == null) {
                    // never has a retry task. then start a new task for retry.
                    // 旧任务不存在, 则放置时间轮,开启新一个任务
                    retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
               }
            }
    
  2. 定时心跳检测: RPC 框架调用端定时向服务端发送的心跳检测,来维护连接状态,我们可以将心跳的逻辑封装为一个心跳任务,放到时钟轮里。心跳是要定时重复执行的,而时钟轮中的任务执行一遍就被移除了,对于这种需要重复执行的定时任务我们该如何处理呢?我们在定时任务逻辑结束的最后,再加上一段逻辑, 重设这个任务的执行时间,把它重新丢回到时钟轮里。这样就可以实现循环执行。

    源码HeaderExchangeServer代码片段:

    ...
        // 建立心跳时间轮, 槽位数默认为128
        private static final HashedWheelTimer IDLE_CHECK_TIMER = new HashedWheelTimer(new NamedThreadFactory("dubbo-server-idleCheck", true), 1,
                    TimeUnit.SECONDS, TICKS_PER_WHEEL);
        ...
            // 启动心跳任务检测
            private void startIdleCheckTask(URL url) {
                if (!server.canHandleIdle()) {
                    AbstractTimerTask.ChannelProvider cp = () -> unmodifiableCollection(HeaderExchangeServer.this.getChannels());
                    int idleTimeout = getIdleTimeout(url);
                    long idleTimeoutTick = calculateLeastDuration(idleTimeout);
                    CloseTimerTask closeTimerTask = new CloseTimerTask(cp, idleTimeoutTick, idleTimeout);
                    this.closeTimerTask = closeTimerTask;
        
                    // init task and start timer.
                    // 开启心跳检测任务
                    IDLE_CHECK_TIMER.newTimeout(closeTimerTask, idleTimeoutTick, TimeUnit.MILLISECONDS);
                }
            }
        ...
    

    连接检测, 会不断执行, 加入时间轮中。
    AbstractTimerTask源码:

    @Override
    public void run(Timeout timeout) throws Exception {
        Collection<Channel> c = channelProvider.getChannels();
        for (Channel channel : c) {
            if (channel.isClosed()) {
                continue;
            }
            // 调用心跳检测任务
            doTask(channel);
        }
        // 重新放入时间轮中
        reput(timeout, tick);
    }
    

    还可以参考HeartbeatTimerTask、ReconnectTimerTask源码实现。


本文由mirson创作分享,如需进一步交流,请加QQ群:19310171或访问www.softart.cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦神-mirson

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值