别再用 kill -9 了,这才是微服务上下线的正确姿势!

共 9001字,需浏览 19分钟

 ·

2021-06-01 19:24


上一篇:3600万中国人在抖音“上清华”

作者:fredalxin
地址:https://fredal.xin/graceful-soa-updown

对于微服务来说,服务的优雅上下线是必要的。

就上线来说,如果组件或者容器没有启动成功,就不应该对外暴露服务,对于

下线来说,如果机器已经停机了,就应该保证服务已下线,如此可避免上游流

量进入不健康的机器。

优雅下线

基础下线(Spring/SpringBoot/内置容器)

首先JVM本身是支持通过shutdownHook的方式优雅停机的。

Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
        close();
    }
});

此方式支持在以下几种场景优雅停机:

  1. 程序正常退出

  2. 使用System.exit()

  3. 终端使用Ctrl+C

  4. 使用Kill pid干掉进程

那么如果你偏偏要kill -9 程序肯定是不知所措的。

而在Springboot中,其实已经帮你实现好了一个shutdownHook,支持响应

Ctrl+c或者kill -15 TERM信号。

随便启动一个应用,然后Ctrl+c一下,观察日志就可知, 它在 Annotation

ConfigEmbeddedWebApplicationContext 这个类中打印出了疑似

Closing...的日志,真正的实现逻辑在其父类

AbstractApplicationContext 中(这个其实是spring中的类,意味着什

么呢,在spring中就支持了对优雅停机的扩展)。

public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        this.shutdownHook = new Thread() {
            public void run() {
                synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                    AbstractApplicationContext.this.doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
 
}
 
public void destroy() {
    this.close();
}
 
public void close() {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
        this.doClose();
        if (this.shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
            } catch (IllegalStateException var4) {
                ;
            }
        }
 
    }
}
 
protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(falsetrue)) {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Closing " + this);
        }
 
        LiveBeansView.unregisterApplicationContext(this);
 
        try {
            this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
        } catch (Throwable var3) {
            this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
        }
 
        if (this.lifecycleProcessor != null) {
            try {
                this.lifecycleProcessor.onClose();
            } catch (Throwable var2) {
                this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
            }
        }
 
        this.destroyBeans();
        this.closeBeanFactory();
        this.onClose();
        this.active.set(false);
    }
 
}

我们能对它做些什么呢,其实很明显,在doClose方法中它发布了一个
ContextClosedEvent的方法,不就是给我们扩展用的么。

于是我们可以写个监听器监听ContextClosedEvent,在发生事件的时候做

下线逻辑,对微服务来说即是从注册中心中注销掉服务。


@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent{
    
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
       //注销逻辑
       zookeeperRegistry.unregister(mCurrentServiceURL);
       ...
    }
}

可能会有疑问的是,微服务中一般来说,注销服务往往是优雅下线的第一步,

接着才会执行停机操作,那么这个时候流量进来怎么办呢?

个人会建议是,在注销服务之后就可开启请求挡板拒绝流量了,通过微服务框

架本身的故障转移功能去处理被拒绝的流量即可。另外,关注公众号互联网架

构师,在后台回复:2T,可以获取我整理的 Java、Spring Boot 系列面试

题和答案,非常齐全。

Docker中的下线

好有人说了,我用docker部署服务,支不支持优雅下线。

那来看看docker的一些停止命令都会干些啥:

一般来说,正常人可能会用docker stop或者docker kill 命令去关闭容

器(当然如果上一步注册了USR2自定义信息,可能会通过docker exec

kill -12去关闭)。

对于docker stop来说,它会发一个SIGTERM(kill -15 term信息)给容

器的PID1进程,并且默认会等待10s,再发送一个SIGKILL(kill -9 信息)

给PID1。

那么很明显,docker stop允许程序有个默认10s的反应时间去做一下优雅停

机的操作,程序只要能对kill -15 信号做些反应就好了,如上一步描述。那

么这是比较良好的方式。

当然如果shutdownHook方法执行了个50s,那肯定不优雅了。可以通过

docker stop -t 加上等待时间。

外置容器的shutdown脚本(Jetty)

如果非要用外置容器方式部署(个人认为浪费资源并提升复杂度)。那么能不

能优雅停机呢。

可以当然也是可以的,这里有两种方式:

首先RPC框架本身提供优雅上下线接口,以供调用来结束整个应用的生命周期,

并且提供扩展点供开发者自定义服务下线自身的停机逻辑。同时调用该接口的

操作会封装成一个preStop操作固化在jetty或者其他容器的shutdown脚本

中,保证在容器停止之前先调用下线接口结束掉整个应用的生命周期。

shutdown脚本中执行类发起下线服务 -> 关闭端口 -> 检查下线服务直至完成 ->

关闭容器的流程。

而更简单的另一种方法是直接在脚本中加入kill -15命令。



优雅上线

优雅上线相对来说可能会更加困难一些,因为没有什么默认的实现方式,但是

总之呢,一个原则就是确保端口存在之后才上线服务。

springboot内置容器优雅上线

这个就很简单了,并且业界在应用层面的优雅上线均是在内置容器的前提下实

现的,并且还可以配合一些列健康检查做文章。Spring Boot 优雅关闭新姿

势,看看这篇。

参看sofa-boot的健康检查的源码,它会在程序启动的时候先对springboot

的组件做一些健康检查,然后再对它自己搞得sofa的一些中间件做健康检查,

整个健康检查的流程完毕之后(sofaboot 目前是没法对自身应用层面做健康

检查的,它有写相关接口,但是写死了port is ready...)才会暴露服务或

者说优雅上线,那么它健康检查的时机是什么时候呢:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    healthCheckerProcessor.init();
    healthIndicatorProcessor.init();
    afterHealthCheckCallbackProcessor.init();
    publishBeforeHealthCheckEvent();
    readinessHealthCheck();
}

可以看到它是监听了ContextRefreshedEvent这个事件。在内置容器模式中

,内置容器模式的start方法是在refreshContext方法中,方法执行完成之

后发布一个ContextRefreshedEvent事件,也就是说在监听到这个事件的时

候,内置容器必然是启动成功了的。

但ContextRefreshedEvent这个事件,在一些特定场景中由于种种原因,

ContextRefreshedEvent会被监听到多次,没有办法保证当前是最后一次

event,从而正确执行优雅上线逻辑。

在springboot中还有一个更加靠后的事件,叫做

ApplicationReadyEvent,它的发布藏在了afterRefresh还要后面的

那一句listeners.finished(context, null)中,完完全全可以保证内置容

器 端口已经存在了,所以我们可以监听这个事件去做优雅上线的逻辑,甚至可

以把中间件相关的健康检查集成在这里。

@Component
public class GracefulStartupListener implements ApplicationListener<ApplicationReadyEvent{    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent){
       //注册逻辑 优雅上线
       apiRegister.register(urls);
       ...
    }
}

外置容器(Jetty)优雅上线

目前大多数应用的部署模式不管是jetty部署模式还是docker部署模式(同样

使用jetty镜像),本质上用的都是外置容器。那么这个情况就比较困难了,至

少在应用层面无法观察到外部容器的运行状态,并且容器本身没有提供什么

hook给你实现。

那么和优雅上线一样,需要RPC框架提供优雅上线接口来初始化整个应用的生命

周期,并且提供扩展点给开发者供执行自定义的上线逻辑(上报版本探测信息等

)同样将调用这个接口封装成一个postStart操作,固化在jetty等外置容器

的startup脚本中,保证应用在容器启动之后在上线。容器执行类似启动容器

-> 健康检查 -> 上线服务逻辑 -> 健康上线服务直至完成 的流程。



看完这篇文章,你有什么收获?欢迎在留言区与10w+Java开发者一起讨论~

关注微信公众号:互联网架构师,在后台回复:2T,可以获取我整理的教程,都是干货。


猜你喜欢

1、GitHub 标星 3.2w!史上最全技术人员面试手册!FackBoo发起和总结

2、如何才能成为优秀的架构师?

3、从零开始搭建创业公司后台技术栈

4、程序员一般可以从什么平台接私活?

5、37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

6、滴滴业务中台构建实践,首次曝光

7、不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事

8、15张图看懂瞎忙和高效的区别

9、2T架构师学习资料干货分享


浏览 59
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报