手撸RPC---->优雅停机、优雅启动

一、优雅停机

首先,讨论一下暴力关闭的问题和解决方案:

要知道,rpc是服务于分布式系统的,到目前实现了基本的服务治理的能力,但是还有个问题

q: 我启动了多个服务提供方,同时启动了服务调用方,不断的进行方法调用,然后快速关闭几个提供方发现就出问题了,目前项目确实会有这样的问题。

快速关闭服务提供方时,注册中心感知、以及通过watcher机制通知调用方一定不能做到实时,一定会有延时,同时我们的心跳检测也会有一定的时间间隔。也就意味着当一个提供方实际上已经下线了,但是他依然在调用方的健康列表中,调用方依然认为他健康依然会给他发送消息,最后的结果就是超时等待,不断重试而已。所以如何在服务下线时快速的让调用方感知,很重要。

我们思考以下大概可以有以下几种解决方案:

1、通过控制台人工通知调用方,让他们手动摘除要下线的机器,这种方式很原始也很直接。但这样对于提供方上线的过程来说太繁琐了,每次上线都要通知到所有调用我接口的团队,整个过程既浪费时间又没有意义,显然不能被正常接受。

2、通过服务发现机制感知,这种方式我们探讨过,因为存在一定的时间差,所以会出现一定的问题。

3、不强依赖“服务发现”来通知调用方要下线的机器,由服务提供方自己来通知行不行。在rpc里面调用方跟服务提供方之间是长连接,可以在提供方应用内存里面维护一份调用方连接集合,当服务要关闭的时候,挨个去通知调用方去下线这台机器。

实时上第三种方式已经很好了,但是依旧会出现一些问题 ,如请求的时间点跟收到服务提供方关闭通知的时间点很接近,只比关闭通知的时间早不到1ms,如果再加上网络传输时间的话,那服务提供方收到请求的时候,它应该正在处理关闭逻辑。这就说明服务提供方关闭的时候,并没有正确处理关闭后接收到的新请求。

然而我们要明白,发生问题并不可怕,合理的解决掉问题就可以了,了解以上的问题我们可以看看下边的优雅关闭方案

优雅停机方案:

知道了根本原因,问题就很好解决了,因为服务提供方已经开始进入关闭流程,那么很多对象就可能已经被销毁了,关闭后再收到的请求按照正常业务请求来处理,肯定没法保证能处理的。所以我们可以在关闭的时候,设置一个请求”挡板”,挡板的作用就是告诉调用方,我已经开始进入关闭流程了,我不能再处理你这个请求了。

这种场景在生活中其实很常见,举一个例子:

银行办理业务,在交接班或者有其他要事情处理的时候,银行柜台工作人员会拿出一个纸板,放在窗口前,上面写到“该窗口已关闭”。在该窗口排队的人虽然有一万个不愿,也只能换到其它窗口办理业务,因为柜台工作人员会把当前正在办理的业务处理完后正式关闭窗口。

基于这个思路,可以这么处理:

1、调用方发起请求,给调用方一个特殊的响应,使用响应码标记即可,就是告诉调用方我已经收到这个请求了,但是我正在关闭,并没有处理这个请求。 2、调用方收到这个异常响应后,rpc框架把这个节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试至其他节点,这样就可以实现对业务无损。

**问题一:**那要怎么捕获到关闭事件呢?

我们可以通过捕获操作系统的进程信号来获取,在java 语言里面,可以使用Runtime的addShutdownHook方法,可以注册关闭的钩子。在rpc启动的时候,我们提前注册关闭钩子,并在里面添加处理程序,负责开启关闭标识和安全关闭服务,服务在关闭的时候会通知调用方下线节点。同时需要在我们调用链里面加上挡板处理器,当新的请求来的时候,会判断关闭标识,如果正在关闭,则抛出特定异常,返回特定结果。

看到这里,感觉问题已经比较好地被解决了。但细心的同学可能还会提出问题,关闭过程中已经在处理的请求会不会受到影响呢?

如果进程结束过快会造成这些请求还没有来得及应答, 此时调用方会也会抛出异常。为了尽可能地完成正在处理的请求,首先我们要把这些请求识别出来,需要有一个标识来判断是否还有正在处理的请求,如果没有了再关闭服务。

再举一个例子,我们经常看见停车场指示牌上提示还有多少剩余车位,这个是如何做到的呢?如果仔细观察一下,你就会发现它是每进入一辆车,剩余车位就减一,每出来一辆车剩余车位就加一。我们也可以利用这个原理,引入一个全局计数器,每开始处理请求之前加一完成请求处理减一,通过该计数器我们就可以快速判断是否有正在理的请求。

服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。但考虑到有些业务请求可能处理时间长,或者存在被挂住的情况,为了避免一直等待造成应用无法正常退出,我们可以在整个 ShutdowrHook里面,加上超时时间控制, 当超过了指定时间没有结束,则强制退出应用。超时时间我建议可以设定成 10s,基本可以确保请求都处理完了。整个流程如下图所示。关闭的钩子函数如下:

public class RpcShutdownHook extends Thread {
    
    @Override
    public void run() {
        // 1、打开挡板   (boolean 需要线程安全)
        ShutDownHolder.BAFFLE.set(true);
        
        // 2、等待计数器归零(正常的请求处理结束)  AtomicInteger
        // 等待归零,继续执行  countdownLatch, 最多等十秒
        long start = System.currentTimeMillis();
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (ShutDownHolder.REQUEST_COUNTER.sum() == 0L
                || System.currentTimeMillis() - start > 10000) {
                break;
            }
        }
        // 3、阻塞结束后,放行。执行其他操作,如释放资源
    }
}

 

二、优雅启动

刚刚介绍了优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能“安全”地切走流量,不再调用自己,从而做到对业务无损。其中实现的关键点就在于,让正在停机的服务提供方应用有状态,让调用方感知到服务提供方正在停机。

是不是很诧异?应用启动居然也要这么“讲究”吗?这就好比我们日常生活中的热车,行驶之前让发动机空跑一会,可以让汽车的各个部件都“热”起来,减小磨损。

换到应用上来看,原理也是一样的。运行了一段时间后的应用,执行速度会比刚启动的应用更快。这是因为在 Java 里面,在运行过程中,JVM 虚拟机会把高频的代码编译成机器码,被加载过的类也会被缓存到 JVM 缓存中,再次使用的时候不会触发临时加载,这样就使得“热点”代码的执行不用每次都通过解释,从而提升执行速度。

但是这些“临时数据”,都在我们应用重启后就消失了。重启后的这些“红利”没有了之后,如果让我们刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。

在上一讲我们说过,在微服务架构里面,上线肯定是频繁发生的,那我们总不能因为上线,就让过来的请求出现大面积超时吧?所以我们得想点办法。既然问题的关键是在于“刚重启的服务提供方因为没有预跑就承担了大流量”,那我们是不是可以通过某些方法,让应用一开始只接少许流量呢?这样低功率运行一段时间后,再逐渐提升至最佳状态。

这其实就是我今天要和你分享的重点,rpc 里面的一个实用功能——启动预热

1、启动预热

那什么叫启动预热呢?

简单来说,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。

那在 rpc 里面,我们该怎么实现这个功能呢?

我们现在是要控制调用方发送到服务提供方的流量。我们可以先简单地回顾下调用方发起的 rpc 调用流程是怎样的,调用方应用通过服务发现能够获取到服务提供方的 IP 地址,然后每次发送请求前,都需要通过负载均衡算法从连接池中选择一个可用连接。那这样的话,我们是不是就可以让负载均衡在选择连接的时候,区分一下是否是刚启动不久的应用?对于刚启动的应用,我们可以让它被选择到的概率特别低,但这个概率会随着时间的推移慢慢变大,从而实现一个动态增加流量的过程

2、延迟暴露

我们应用启动的时候都是通过 main 入口,然后顺序加载各种相关依赖的类。以 Spring 应用启动为例,在加载的过程中,Spring 容器会顺序加载 Spring Bean,如果某个 Bean 是 rpc 服务的话,我们不光要把它注册到 Spring-BeanFactory 里面去,还要把这个 Bean 对应的接口注册到注册中心。注册中心在收到新上线的服务提供方地址的时候,会把这个地址推送到调用方应用内存中;当调用方收到这个服务提供方地址的时候,就会去建立连接发请求。

但这时候是不是存在服务提供方可能并没有启动完成的情况?因为服务提供方应用可能还在加载其它的 Bean。对于调用方来说,只要获取到了服务提供方的 IP,就有可能发起 rpc 调用,但如果这时候服务提供方没有启动完成的话,就会导致调用失败,从而使业务受损。

那有什么办法可以避免这种情况吗?

在解决问题前,我们先看下出现上述问题的根本原因。这是因为服务提供方应用在没有启动完成的时候,调用方的请求就过来了,而调用方请求过来的原因是,服务提供方应用在启动过程中把解析到的 rpc 服务注册到了注册中心,这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。

这样的话,其实我们就可以**把接口注册到注册中心的时间挪到应用启动完成后。**具体的做法就是在应用启动加载、解析 Bean 的时候,如果遇到了 rpc 服务的 Bean,只先把这个 Bean 注册到 Spring-BeanFactory 里面去,而并不把这个 Bean 对应的接口注册到注册中心,只有等应用启动完成后,才把接口注册到注册中心用于服务发现,从而实现让服务调用方延迟获取到服务提供方地址。

这样是可以保证应用在启动完后才开始接入流量的,但其实这样做,我们还是没有实现最开始的目标。因为这时候应用虽然启动完成了,但并没有执行相关的业务代码,所以 JVM 内存里面还是冷的。如果这时候大量请求过来,还是会导致整个应用在高负载模式下运行,从而导致不能及时地返回请求结果。而且在实际业务中,一个服务的内部业务逻辑一般会依赖其它资源的,比如缓存数据。如果我们能在服务正式提供服务前,先完成缓存的初始化操作,而不是等请求来了之后才去加载,我们就可以降低重启后第一次请求出错的概率。

那具体怎么实现呢?

我们还是需要利用服务提供方把接口注册到注册中心的那段时间。我们可以在服务提供方应用启动后,接口注册到注册中心前,预留一个 Hook 过程,让用户可以实现可扩展的 Hook 逻辑。用户可以在 Hook 里面模拟调用逻辑,从而使 JVM 指令能够预热起来,并且用户也可以在 Hook 里面事先预加载一些资源,只有等所有的资源都加载完成后,最后才把接口注册到注册中心。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值