文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-03 04:38

关注

就上线来说,如果组件或者容器没有启动成功,就不应该对外暴露服务,对于下线来说,如果机器已经停机了,就应该保证服务已下线,如此可避免上游流量进入不健康的机器。

优雅下线

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

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

  1. Runtime.getRuntime().addShutdownHook(new Thread() {  
  2.     @Override  
  3.     public void run() {  
  4.         close();  
  5.     }  
  6. }); 

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

程序正常退出

使用System.exit()

终端使用Ctrl+C

使用Kill pid干掉进程

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

而在Springboot中,其实已经帮你实现好了一个shutdownHook,支持响应Ctrl+c或者kill -15 TERM信号。

随便启动一个应用,然后Ctrl+c一下,观察日志就可知, 它在 AnnotationConfigEmbeddedWebApplicationContext 这个类中打印出了疑似Closing...的日志,真正的实现逻辑在其父类 AbstractApplicationContext 中(这个其实是spring中的类,意味着什么呢,在spring中就支持了对优雅停机的扩展)。

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

  1. public void registerShutdownHook() {  
  2.     if (this.shutdownHook == null) {  
  3.         this.shutdownHook = new Thread() {  
  4.             public void run() {  
  5.                 synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {  
  6.                     AbstractApplicationContext.this.doClose();  
  7.                 }  
  8.             }  
  9.         };  
  10.         Runtime.getRuntime().addShutdownHook(this.shutdownHook);  
  11.     } 
  12.  
  13. public void destroy() {  
  14.     this.close();  
  15.  
  16. public void close() {  
  17.     Object var1 = this.startupShutdownMonitor; 
  18.      synchronized(this.startupShutdownMonitor) { 
  19.          this.doClose();  
  20.         if (this.shutdownHook != null) {  
  21.             try {  
  22.                 Runtime.getRuntime().removeShutdownHook(this.shutdownHook);  
  23.             } catch (IllegalStateException var4) {  
  24.                 ;  
  25.             }  
  26.         }  
  27.     }  
  28.  
  29. protected void doClose() {  
  30.     if (this.active.get() && this.closed.compareAndSet(false, true)) {  
  31.         if (this.logger.isInfoEnabled()) {  
  32.             this.logger.info("Closing " + this);  
  33.         }  
  34.         LiveBeansView.unregisterApplicationContext(this);  
  35.         try {  
  36.             this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));  
  37.         } catch (Throwable var3) {  
  38.             this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);  
  39.         }  
  40.         if (this.lifecycleProcessor != null) {  
  41.             try {  
  42.                 this.lifecycleProcessor.onClose();  
  43.             } catch (Throwable var2) {  
  44.                 this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);  
  45.             }  
  46.         }  
  47.         this.destroyBeans();  
  48.         this.closeBeanFactory();  
  49.         this.onClose();  
  50.         this.active.set(false);  
  51.     }   

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

于是我们可以写个监听器监听ContextClosedEvent,在发生事件的时候做下线逻辑,对微服务来说即是从注册中心中注销掉服务。 

  1. @Component  
  2. public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {  
  3.         @Override  
  4.     public void onApplicationEvent(ContextClosedEvent contextClosedEvent){  
  5.        //注销逻辑  
  6.        zookeeperRegistry.unregister(mCurrentServiceURL);  
  7.        ...  
  8.     }  

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

个人会建议是,在注销服务之后就可开启请求挡板拒绝流量了,通过微服务框架本身的故障转移功能去处理被拒绝的流量即可。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 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...)才会暴露服务或者说优雅上线,那么它健康检查的时机是什么时候呢: 

  1. @Override  
  2. public void onApplicationEvent(ContextRefreshedEvent event) {  
  3.     healthCheckerProcessor.init();  
  4.     healthIndicatorProcessor.init();  
  5.     afterHealthCheckCallbackProcessor.init();  
  6.     publishBeforeHealthCheckEvent();  
  7.     readinessHealthCheck();  

可以看到它是监听了ContextRefreshedEvent这个事件。在内置容器模式中,内置容器模式的start方法是在refreshContext方法中,方法执行完成之后发布一个ContextRefreshedEvent事件,也就是说在监听到这个事件的时候,内置容器必然是启动成功了的。

但ContextRefreshedEvent这个事件,在一些特定场景中由于种种原因,ContextRefreshedEvent会被监听到多次,没有办法保证当前是最后一次event,从而正确执行优雅上线逻辑。

在springboot中还有一个更加靠后的事件,叫做ApplicationReadyEvent,它的发布藏在了afterRefresh还要后面的那一句listeners.finished(context, null)中,完完全全可以保证内置容器 端口已经存在了,所以我们可以监听这个事件去做优雅上线的逻辑,甚至可以把中间件相关的健康检查集成在这里。 

  1. @Component  
  2. public class GracefulStartupListener implements ApplicationListener<ApplicationReadyEvent> {      
  3.     @Override  
  4.     public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent){  
  5.        //注册逻辑 优雅上线  
  6.        apiRegister.register(urls);  
  7.        ...  
  8.     }  

外置容器(Jetty)优雅上线

目前大多数应用的部署模式不管是jetty部署模式还是docker部署模式(同样使用jetty镜像),本质上用的都是外置容器。那么这个情况就比较困难了,至少在应用层面无法观察到外部容器的运行状态,并且容器本身没有提供什么hook给你实现。

那么和优雅上线一样,需要RPC框架提供优雅上线接口来初始化整个应用的生命周期,并且提供扩展点给开发者供执行自定义的上线逻辑(上报版本探测信息等)。同样将调用这个接口封装成一个postStart操作,固化在jetty等外置容器的startup脚本中,保证应用在容器启动之后在上线。容器执行类似启动容器 -> 健康检查 -> 上线服务逻辑 -> 健康上线服务直至完成 的流程。

 

 

来源:Java技术栈内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯