zoukankan      html  css  js  c++  java
  • 别再用 kill -9 了,这才是微服务上下线的正确姿势!

    作者: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一下,观察日志就可知, 它在 AnnotationConfigEmbeddedWebApplicationContext 这个类中打印出了疑似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(false, true)) {
            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,在发生事件的时候做下线逻辑,对微服务来说即是从注册中心中注销掉服务。

    Spring Boot 系列教程和示例源码看这里:https://github.com/javastacks/spring-boot-best-practice

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

    可能会有疑问的是,微服务中一般来说,注销服务往往是优雅下线的第一步,接着才会执行停机操作,那么这个时候流量进来怎么办呢?

    个人会建议是,在注销服务之后就可开启请求挡板拒绝流量了,通过微服务框架本身的故障转移功能去处理被拒绝的流量即可。

    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内置容器优雅上线

    这个就很简单了,并且业界在应用层面的优雅上线均是在内置容器的前提下实现的,并且还可以配合一些列健康检查做文章。

    参看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脚本中,保证应用在容器启动之后在上线。容器执行类似启动容器 -> 健康检查 -> 上线服务逻辑 -> 健康上线服务直至完成 的流程。

    最后,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Java 微服务系列面试题和答案,非常齐全。

    近期热文推荐:

    1.600+ 道 Java面试题及答案整理(2021最新版)

    2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!

    3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

    4.Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    118/119. Pascal's Triangle/II
    160. Intersection of Two Linked Lists
    168. Excel Sheet Column Title
    167. Two Sum II
    172. Factorial Trailing Zeroes
    169. Majority Element
    189. Rotate Array
    202. Happy Number
    204. Count Primes
    MVC之Model元数据
  • 原文地址:https://www.cnblogs.com/javastack/p/14805265.html
Copyright © 2011-2022 走看看