乐趣区

关于java: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 外面有服务注册与发现性能,注册核心的作用就是用来治理服务的状态,当服务敞开时,会先告诉注册核心进行下线,而后通过注册核心移除节点信息,这样不就能够保障服务不被调用吗?

    那咱们来看下敞开的流程:

    整个敞开过程中依赖了两次 RPC 调用,一次是服务提供方告诉注册核心下线操作,一次是注册核心告诉服务调用方下线节点操作。并且注册核心告诉服务调用方都是异步的,并不能保障齐全实时性,通过服务发现并不能做到利用的无损敞开。

    有没有好的解决方案呢?

    服务提供方曾经进入敞开流程,那么很多对象曾经被销毁了,这个时候咱们能够设置一个申请“挡板”,挡板的作用就是通知调用方,服务提供方曾经开始进入敞开流程了,不能再解决其余申请了。

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

    解决流程:

    当服务提供方正在敞开,如果还收到了新的业务申请,服务提供方间接返回一个特定的异样给调用方。这个异样就是通知调用方“我正在敞开,不能解决这个申请”,而后调用方收到这个异样响应后,RPC 框架把这个节点从衰弱列表挪出,并把其余申请主动重试到其余节点,因为这个申请是没有被服务提供方解决过,所以能够平安地重试到其余节点,这样就能够实现对业务简直无损的解决。如果要更为欠缺,咱们还能够加上被动告诉机制,这样既能够保障实时性,也能够防止客户端呈现重试状况。

    如何捕捉敞开事件呢?

    操作系统的过程的敞开,如果不是强制完结,过程会接管到一个完结信号,Java 应用程序,在接管到完结信号时,会调用 Runtime.addShutdownHook 办法触发敞开钩子。咱们在 RPC 服务启动的时候,提前注册敞开钩子,在外面增加处理程序,先开启挡板,而后告诉调用方服务已下线。当接管到新来的申请时,挡板会进行拦挡,抛出特定异样。为了尽可能地实现正在解决的申请,咱们能够退出计数器机制,把残余申请纳入计数器当中,每解决完一个申请,就缩小一个计数,将所有残余申请解决实现之后,再真正完结服务。

    在 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 创作分享,感激大家的反对,心愿对大家有所播种!
入群申请,请加 WX 号:woodblock99

退出移动版