关于java:别再用-kill-9-了这才是微服务上下线的正确姿势

97次阅读

共计 4770 个字符,预计需要花费 12 分钟才能阅读完成。

作者:fredalxin\
地址:https://fredal.xin/graceful-s…

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

就上线来说,如果组件或者容器没有启动胜利,就不应该对外裸露服务,对于下线来说,如果机器曾经停机了,就应该保障服务已下线,如此可防止上游流量进入不衰弱的机器。

优雅下线

根底下线(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…

@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 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0