RPC实现原理之核心技术-优雅启动与关闭

一. 优雅启动
  1. 什么是启动预热

    启动预热就是让刚启动的服务,不直接承担全部的流量,而是让它随着时间的移动慢慢增加调用次数,最终让流量缓和运行一段时间后达到正常水平。

  2. 如何实现

    首先对于调用方来说,我们要知道服务提供方的启动时间,这里有两种获取方法:
    一种是服务提供方在启动的时候,主动将启动的时间发送给注册中心;
    另一种就是注册中心来检测, 将服务提供方的请求注册时间作为启动时间。这两者时间会有一些差异, 但并没有关系, 因为整个预热过程的时间是一个粗略值,即使多个机器节点之间存在 1 分钟的误差也不会影响,并且在真实环境中机器都会开启 NTP 时间同步功能,来保证所有机器时间的一致性。

    调用方通过服务发现,除了可以拿到 IP 列表,还可以拿到对应的启动时间。根据基于权重的负载均衡策略, 动态调整权重,随着时间的推移慢慢增加到服务提供方的调用次数。

    通过这种机制, 对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。

    在Dubbo框架中也引入了"warmup"特性,核心源码是在com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.java中:

    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        // 先得到Provider的权重
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        if (weight > 0) {
            // 得到provider的启动时间戳
            long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                // provider已经运行时间
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                // 得到warmup的值,默认为10分钟
                int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
                // provider运行时间少于预热时间,那么需要重新计算权重weight(即需要降权)
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        return weight;
    }
    
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        // 随着provider的启动时间越来越长,慢慢提升权重weight
        int ww = (int) ( (float) uptime / ( (float) warmup / (float) weight ) );
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }
    
    

    Dubbo2.7.3版本, 参考源码“org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance”

    根据calculateWarmupWeight()方法实现可知,随着provider的启动时间越来越长,慢慢提升权重weight,且权重最小值为1,具体执行策略:
    1)如果provider运行了1分钟,那么weight为10,即只有最终需要承担的10%流量;
    2)如果provider运行了2分钟,那么weight为20,即只有最终需要承担的20%流量;
    3)如果provider运行了5分钟,那么weight为50,即只有最终需要承担的50%流量;

二. 优雅关闭
  1. 为什么需要优雅关闭

    对于调用方来说,服务关闭的时候可能会存在以下几种情况:

    • 调用方发送请求时,目标服务已经下线。对于调用方来说,是可以立即感知的,并且在其健康列表里面会把这个节点挪掉,也就不会纳入负载均衡选中。

    • 调用方发请求时,目标服务正在关闭中,但调用方并不知道它正处于关闭状态,而且两者之间的连接也没有断开,所以这个节点还会存在健康列表里面,所以这个节点仍一定概率会被调用, 从而导致调用失败问题。

  2. 如何实现优雅关闭

    大家可能存有疑问,RPC 里面有服务注册与发现功能, 注册中心的作用就是用来管理服务的状态, 当服务关闭时, 会先通知注册中心进行下线, 然后通过注册中心移除节点信息,这样不就可以保障服务不被调用吗?

    那我们来看下关闭的流程:

    file

    整个关闭过程中依赖了两次 RPC 调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。并且注册中心通知服务调用方都是异步的,并不能保证完全实时性,通过服务发现并不能做到应用的无损关闭。

    有没有好的解决方案呢?

    服务提供方已经进入关闭流程,那么很多对象已经被销毁了,这个时候我们可以设置一个请求“挡板”,挡板的作用就是告诉调用方,服务提供方已经开始进入关闭流程了,不能再处理其他请求了。

    这就好比我们去超市结账,在交接班或者下班的时候, 收银员会放一个提示牌在柜台, 提示“该通道已关闭”,不能进行结账, 这个时候客户只能转移到其他可用的柜台上进行结账。

    处理流程:

    当服务提供方正在关闭,如果还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方。这个异常就是告诉调用方“我正在关闭,不能处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把其他请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务几乎无损的处理。如果要更为完善, 我们还可以加上主动通知机制,这样既可以保证实时性,也可以避免客户端出现重试情况。

    如何捕获关闭事件呢?

    操作系统的进程的关闭,如果不是强制结束,进程会接收到一个结束信号,Java应用程序,在接收到结束信号时, 会调用Runtime.addShutdownHook 方法触发关闭钩子。 我们在 RPC服务启动的时候,提前注册关闭钩子,在里面添加处理程序,先开启挡板, 然后通知调用方服务已下线。当接收到新来的请求时,挡板会进行拦截,抛出特定异常。为了尽可能地完成正在处理的请求, 我们可以加入计数器机制,把剩余请求纳入计数器当中, 每处理完一个请求, 就减少一个计数, 将所有剩余请求处理完成之后, 再真正结束服务。

    file

    在Dubbo框架中, 在以下场景中会触发优雅关闭:

    JVM主动关闭(System.exit(int)

    JVM由于资源问题退出(OOM);

    应用程序接受到进程正常结束信号:SIGTERMSIGINT信号。

    优雅停机是默认开启的,停机等待时间为10秒。可以通过配置dubbo.service.shutdown.wait来修改等待时间。

    基于ShutdownHook方式的优雅停机无法确保所有关闭流程一定执行完成,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。在关闭应用前,首先通过 QOS(在线运维命令) 的offline指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM 或SIGINT)流程,就能保证服务无损。

    Dubbo优雅关闭的源码:

    • DubboShutdownHook.register方法

      注册关闭钩子:

      /**
       * 注册关闭钩子,在服务关闭时触发执行
       */
      public void register() {
          if (!registered.get() && registered.compareAndSet(false, true)) {
              Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());
          }
      }
      
    • DubboShutdownHook.doDestroy方法

      销毁所有相关资源:

      /**
       * 关闭注销所有资源, 包括注册器和协议处理器。
       */
      public void doDestroy() {
          if (!destroyed.compareAndSet(false, true)) {
              return;
          }
          // 销毁所有注册器,包括Zookeeper、etcd、Consul等等。
          AbstractRegistryFactory.destroyAll();
          // 销毁所有协议处理器,包括Dubbo、Hessian、Http、Jsong等。
          destroyProtocols();
      }
      

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

麦神-mirson

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

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

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

打赏作者

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

抵扣说明:

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

余额充值