作者:徐靖峰

来源:Kirito的技术分享

开头先废话几句,有段时间没有更新博客了,除了公司项目比较忙之外,还有个原因就是开始思考如何更好地写作。

远的来说,我从大一便开始在 CSDN 上写博客,回头看那时的文笔还很稚嫩,一心想着反正只有自己看,所以更多的是随性发挥,随意吐槽,内容也很简陋:刷完一道算法题记录下解题思路,用 JAVA 写完一个 demo 之后,记录下配置步骤。

近的来看,工作之后开始维护自己的博客站点: www.cnkirito.moe,也会同步更新自己公众号。相比圈子里其他前辈来说,读者会少很多,但毕竟有人看,每次动笔之前便会开始思考一些事。除了给自己的学习经历做一个归档,还多了一些顾虑:会不会把知识点写错?会不会误人子弟?自己的理解会不会比较片面,不够深刻?等等等等。

但自己的心路历程真的发生了一些改变。在我还是个小白的时候,学习技术:第一个想法是百度,搜别人的博客,一步步跟着别人后面配置,把 demo run 起来。而现在,遇到问题的第一思路变成了:源码 debug,官方文档。我便开始思考官方文档和博客的区别,官方文档的优势除了更加全面之外,还有就是:“它只教你怎么做”,对于一个有经验有阅历的程序员来说,这反而是好事,这可以让你有自己的思考。而博客则不一样,如果这个博主特别爱 BB,便会产生很多废话(就像本文的第一段),它会有很多作者自己思考的产物,一方面它比官方文档更容易出错,更容易片面,一方面它比官方文档更容易启发人,特别是读到触动到我的好文时,会抑制不住内心的喜悦想要加到作者的好友,这便是共情我之后的文章也会朝着这些点去努力:不避重就轻,多思考不想当然,求精。

最近瞥了一眼项目的重启脚本,发现运维一直在使用 kill-9<pid> 的方式重启 springboot embedded tomcat,其实大家几乎一致认为: kill-9<pid> 的方式比较暴力,但究竟会带来什么问题却很少有人能分析出个头绪。这篇文章主要记录下自己的思考过程。

kill -9 和 kill -15 有什么区别?

在以前,我们发布 WEB 应用通常的步骤是将代码打成 war 包,然后丢到一个配置好了应用容器(如 Tomcat,Weblogic)的 Linux 机器上,这时候我们想要启动/关闭应用,方式很简单,运行其中的启动/关闭脚本即可。而 springboot 提供了另一种方式,将整个应用连同内置的 tomcat 服务器一起打包,这无疑给发布应用带来了很大的便捷性,与之而来也产生了一个问题:如何关闭 springboot 应用呢?一个显而易见的做法便是,根据应用名找到进程 id,杀死进程 id 即可达到关闭应用的效果。

上述的场景描述引出了我的疑问:怎么优雅地杀死一个 springboot 应用进程呢?这里仅仅以最常用的 Linux 操作系统为例,在 Linux 中 kill 指令负责杀死进程,其后可以紧跟一个数字,代表信号编号(Signal),执行 kill-l 指令,可以一览所有的信号编号。


  1. xu@ntzyz-qcloud ~ % kill -l

  2. HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

本文主要介绍下第 9 个信号编码 KILL,以及第 15 个信号编号 TERM 。

先简单理解下这两者的区别: kill-9pid 可以理解为操作系统从内核级别强行杀死某个进程, kill-15pid 则可以理解为发送一个通知,告知应用主动关闭。这么对比还是有点抽象,那我们就从应用的表现来看看,这两个命令杀死应用到底有啥区别。

  • 代码准备

由于笔者 springboot 接触较多,所以以一个简易的 springboot 应用为例展开讨论,添加如下代码。

1 增加一个实现了 DisposableBean 接口的类


  1. @Component

  2. public class TestDisposableBean implements DisposableBean{

  3. @Override

  4. public void destroy() throws Exception {

  5. System.out.println("测试 Bean 已销毁 ...");

  6. }

  7. }

2 增加 JVM 关闭时的钩子


  1. @SpringBootApplication

  2. @RestController

  3. public class TestShutdownApplication implements DisposableBean {

  4. public static void main(String[] args) {

  5. SpringApplication.run(TestShutdownApplication.class, args);

  6. Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {

  7. @Override

  8. public void run() {

  9. System.out.println("执行 ShutdownHook ...");

  10. }

  11. }));

  12. }

  13. }

  • 测试步骤

  1. 执行 java-jar test-shutdown-1.0.jar 将应用运行起来

  2. 测试 kill-9pid, kill-15pid, ctrl+c 后输出日志内容

  • 测试结果

kill-15pid & ctrl+c,效果一样,输出结果如下


  1. 2018-01-14 16:55:32.424 INFO 8762 --- [ Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2cdf8d8a: startup date [Sun Jan 14 16:55:24 UTC 2018]; root of context hierarchy

  2. 2018-01-14 16:55:32.432 INFO 8762 --- [ Thread-3] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown

  3. 执行 ShutdownHook ...

  4. 测试 Bean 已销毁 ...

  5. java -jar test-shutdown-1.0.jar7.46s user 0.30s system 80% cpu 9.674 total

kill-9pid,没有输出任何应用日志


  1. [1]8802 killed     java -jar test-shutdown-1.0.jar

  2. java -jar test-shutdown-1.0.jar7.74s user 0.25s system 41% cpu 19.272 total

可以发现,kill -9 pid 是给应用杀了个措手不及,没有留给应用任何反应的机会。而反观 kill -15 pid,则比较优雅,先是由 AnnotationConfigEmbeddedWebApplicationContext (一个 ApplicationContext 的实现类)收到了通知,紧接着执行了测试代码中的 Shutdown Hook,最后执行了 DisposableBean#destory() 方法。孰优孰劣,立判高下。

一般我们会在应用关闭时处理一下“善后”的逻辑,比如

  1. 关闭 socket 链接

  2. 清理临时文件

  3. 发送消息通知给订阅方,告知自己下线

  4. 将自己将要被销毁的消息通知给子进程

  5. 各种资源的释放

等等

而 kill -9 pid 则是直接模拟了一次系统宕机,系统断电,这对于应用来说太不友好了,不要用收割机来修剪花盆里的花。取而代之,便是使用 kill -15 pid 来代替。如果在某次实际操作中发现:kill -15 pid 无法关闭应用,则可以考虑使用内核级别的 kill -9 pid ,但请事后务必排查出是什么原因导致 kill -15 pid 无法关闭。

springboot 如何处理 -15 TERM Signal?

上面解释过了,使用 kill -15 pid 的方式可以比较优雅的关闭 springboot 应用,我们可能有以下的疑惑: springboot/spring 是如何响应这一关闭行为的呢?是先关闭了 tomcat,紧接着退出 JVM,还是相反的次序?它们又是如何互相关联的?

尝试从日志开始着手分析, AnnotationConfigEmbeddedWebApplicationContext 打印出了 Closing 的行为,直接去源码中一探究竟,最终在其父类 AbstractApplicationContext 中找到了关键的代码:


  1. @Override

  2. public void registerShutdownHook() {

  3. if (this.shutdownHook == null) {

  4. this.shutdownHook = new Thread() {

  5. @Override

  6. public void run() {

  7. synchronized (startupShutdownMonitor) {

  8. doClose();

  9. }

  10. }

  11. };

  12. Runtime.getRuntime().addShutdownHook(this.shutdownHook);

  13. }

  14. }

  15. @Override

  16. public void close() {

  17. synchronized (this.startupShutdownMonitor) {

  18. doClose();

  19. if (this.shutdownHook != null) {

  20. Runtime.getRuntime().removeShutdownHook(this.shutdownHook);

  21. }

  22. }

  23. }

  24. protected void doClose() {

  25. if (this.active.get() && this.closed.compareAndSet(false, true)) {

  26. LiveBeansView.unregisterApplicationContext(this);

  27. // 发布应用内的关闭事件

  28. publishEvent(new ContextClosedEvent(this));

  29. // Stop all Lifecycle beans, to avoid delays during individual destruction.

  30. if (this.lifecycleProcessor != null) {

  31. this.lifecycleProcessor.onClose();

  32. }

  33. // spring 的 BeanFactory 可能会缓存单例的 Bean

  34. destroyBeans();

  35. // 关闭应用上下文&BeanFactory

  36. closeBeanFactory();

  37. // 执行子类的关闭逻辑

  38. onClose();

  39. this.active.set(false);

  40. }

  41. }

为了方便排版以及便于理解,我去除了源码中的部分异常处理代码,并添加了相关的注释。在容器初始化时,ApplicationContext 便已经注册了一个 Shutdown Hook,这个钩子调用了 Close() 方法,于是当我们执行 kill -15 pid 时,JVM 接收到关闭指令,触发了这个 Shutdown Hook,进而由 Close() 方法去处理一些善后手段。具体的善后手段有哪些,则完全依赖于 ApplicationContext 的 doClose() 逻辑,包括了注释中提及的销毁缓存单例对象,发布 close 事件,关闭应用上下文等等,特别的,当 ApplicationContext 的实现类是 AnnotationConfigEmbeddedWebApplicationContext 时,还会处理一些 tomcat/jetty 一类内置应用服务器关闭的逻辑。

窥见了 springboot 内部的这些细节,更加应该了解到优雅关闭应用的必要性。JAVA 和 C 都提供了对 Signal 的封装,我们也可以手动捕获操作系统的这些 Signal,在此不做过多介绍,有兴趣的朋友可以自己尝试捕获下。

还有其他优雅关闭应用的方式吗?

spring-boot-starter-actuator 模块提供了一个 restful 接口,用于优雅停机。

  • 添加依赖


  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-actuator</artifactId>

  4. </dependency>

  • 添加配置


  1. #启用shutdown

  2. endpoints.shutdown.enabled=true

  3. #禁用密码验证

  4. endpoints.shutdown.sensitive=false

生产中请注意该端口需要设置权限,如配合 spring-security 使用。

执行 curl-X POST host:port/shutdown 指令,关闭成功便可以获得如下的返回:


  1. {"message":"Shutting down, bye..."}

虽然 springboot 提供了这样的方式,但按我目前的了解,没见到有人用这种方式停机,kill -15 pid 的方式达到的效果与此相同,将其列于此处只是为了方案的完整性。

如何销毁作为成员变量的线程池?

尽管 JVM 关闭时会帮我们回收一定的资源,但一些服务如果大量使用异步回调,定时任务,处理不当很有可能会导致业务出现问题,在这其中,线程池如何关闭是一个比较典型的问题。


  1. @Service

  2. public class SomeService {

  3. ExecutorService executorService = Executors.newFixedThreadPool(10);

  4. public void concurrentExecute() {

  5. executorService.execute(new Runnable() {

  6. @Override

  7. public void run() {

  8. System.out.println("executed...");

  9. }

  10. });

  11. }

  12. }

我们需要想办法在应用关闭时(JVM 关闭,容器停止运行),关闭线程池。

初始方案:什么都不做。在一般情况下,这不会有什么大问题,因为 JVM 关闭,会释放之,但显然没有做到本文一直在强调的两个字,没错----优雅。

方法一的弊端在于线程池中提交的任务以及阻塞队列中未执行的任务变得极其不可控,接收到停机指令后是立刻退出?还是等待任务执行完成?抑或是等待一定时间任务还没执行完成则关闭?

方案改进:发现初始方案的劣势后,我立刻想到了使用 DisposableBean 接口,像这样


  1. @Service

  2. public class SomeService implements DisposableBean{

  3. ExecutorService executorService = Executors.newFixedThreadPool(10);

  4. public void concurrentExecute() {

  5. executorService.execute(new Runnable() {

  6. @Override

  7. public void run() {

  8. System.out.println("executed...");

  9. }

  10. });

  11. }

  12. @Override

  13. public void destroy() throws Exception {

  14. executorService.shutdownNow();

  15. //executorService.shutdown();

  16. }

  17. }

紧接着问题又来了,是 shutdown 还是 shutdownNow 呢?这两个方法还是经常被误用的,简单对比这两个方法。

ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。

ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。

查看 shutdown 和 shutdownNow 的 java doc,会发现如下的提示:

shutdown() :Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.Invocation has no additional effect if already shut down.This method does not wait for previously submitted tasks to complete execution.Use {@link #awaitTermination awaitTermination} to do that.

shutdownNow():Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. These tasks are drained (removed) from the task queue upon return from this method.This method does not wait for actively executing tasks to terminate. Use {@link #awaitTermination awaitTermination} to do that.There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {@link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

两者都提示我们需要额外执行 awaitTermination 方法,仅仅执行 shutdown/shutdownNow 是不够的。

最终方案:参考 spring 中线程池的回收策略,我们得到了最终的解决方案。


  1. public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory

  2. implements DisposableBean{

  3. @Override

  4. public void destroy() {

  5. shutdown();

  6. }

  7. /**

  8. * Perform a shutdown on the underlying ExecutorService.

  9. * @see java.util.concurrent.ExecutorService#shutdown()

  10. * @see java.util.concurrent.ExecutorService#shutdownNow()

  11. * @see #awaitTerminationIfNecessary()

  12. */

  13. public void shutdown() {

  14. if (this.waitForTasksToCompleteOnShutdown) {

  15. this.executor.shutdown();

  16. }

  17. else {

  18. this.executor.shutdownNow();

  19. }

  20. awaitTerminationIfNecessary();

  21. }

  22. /**

  23. * Wait for the executor to terminate, according to the value of the

  24. * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.

  25. */

  26. private void awaitTerminationIfNecessary() {

  27. if (this.awaitTerminationSeconds > 0) {

  28. try {

  29. this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));

  30. }

  31. catch (InterruptedException ex) {

  32. Thread.currentThread().interrupt();

  33. }

  34. }

  35. }

  36. }

保留了注释,去除了一些日志代码,一个优雅关闭线程池的方案呈现在我们的眼前。

1 通过 waitForTasksToCompleteOnShutdown 标志来控制是想立刻终止所有任务,还是等待任务执行完成后退出。

2 executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)。

更多需要思考的优雅停机策略

在我们分析 RPC 原理的系列文章里面曾经提到,服务治理框架一般会考虑到优雅停机的问题。通常的做法是事先隔断流量,接着关闭应用。常见的做法是将服务节点从注册中心摘除,订阅者接收通知,移除节点,从而优雅停机;涉及到数据库操作,则可以使用事务的 ACID 特性来保证即使 crash 停机也能保证不出现异常数据,正常下线则更不用说了;又比如消息队列可以依靠 ACK 机制+消息持久化,或者是事务消息保障;定时任务较多的服务,处理下线则特别需要注意优雅停机的问题,因为这是一个长时间运行的服务,比其他情况更容易受停机问题的影响,可以使用幂等和标志位的方式来设计定时任务...

事务和 ACK 这类特性的支持,即使是宕机,停电,kill -9 pid 等情况,也可以使服务尽量可靠;而同样需要我们思考的还有 kill -15 pid,正常下线等情况下的停机策略。

最后再补充下整理这个问题时,自己对 jvm shutdown hook 的一些理解。

When the virtual machine begins its shutdown sequence it will start all registered shutdown hooks in some unspecified order and let them run concurrently. When all the hooks have finished it will then run all uninvoked finalizers if finalization-on-exit has been enabled. Finally, the virtual machine will halt.

shutdown hook 会保证 JVM 一直运行,知道 hook 终止 (terminated)。这也启示我们,如果接收到 kill -15 pid 命令时,执行阻塞操作,可以做到等待任务执行完成之后再关闭 JVM。同时,也解释了一些应用执行 kill -15 pid 无法退出的问题,没错,中断被阻塞了。

参考资料

[1] https://stackoverflow.com/questions/2921945/useful-example-of-a-shutdown-hook-in-java

[2] spring 源码

[3] jdk 文档

-END-

近期热文:

  • 搞个996或247,你的团队就是互联网团队了?

  • Java 11将于本月25日发布,新特性一览

  • 印象笔记终于支持Markdown了 !你还会再用其他笔记吗?

  • 这位闯进程序员界的维密天使,她到底可以编出什么?!

  • 如何从ActiveMQ平滑迁移到Kafka?

  • 系统优化总结—系统层面

  • NIO相关基础篇

  • 以Dubbo为例,聊聊如何为开源项目做贡献

  • 25个面试中最常问的问题和答案

关注我

点击“阅读原文”,看本号其他精彩内容

研究优雅停机时的一点思考相关推荐

  1. 【Java】优雅停机时的一点思考

    1.概述 转载:http://cxytiandi.com/blog/detail/15386 转载自:徐靖峰 Kirito的技术分享 最近瞥了一眼项目的重启脚本,发现运维一直在使用 kill -9 的 ...

  2. 研究 Dubbo 网卡地址注册时的一点思考

    1 如何选择合适的网卡地址 可能相当一部分人还不知道我这篇文章到底要讲什么,我说个场景,大家应该就明晰了.在分布式服务调用过程中,以 Dubbo 为例,服务提供者往往需要将自身的 IP 地址上报给注册 ...

  3. 锁失效_关于bigtable中chubby锁失效时的一点思考

    最近跟国内几家热门公司做分布式存储的大佬们聊了聊,过程十分愉快,但同时也有点小虐.说到底,自己在这个领域并没有很久的经验,很多东西仍停留在知其然而不知其所以然的地步.魔鬼藏在细节之处. 不过这也正好是 ...

  4. 一文聊透 Dubbo 优雅停机

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者 | kiritomoe 来源 | 公众号「Kirito的技术分享」 1 前言 一年之前 ...

  5. 处于停机等非正常状态_一文聊透 Dubbo 优雅停机

    1 前言 一年之前,我曾经写过一篇<研究优雅停机时的一点思考>,主要介绍了 kill -9,kill -15 两个 Linux 指令的含义,并且针对性的聊到了 Spring Boot 应用 ...

  6. 不懂优雅停机,搞挂了线上服务该咋办?

    作者丨陈树义 来源丨公众号:陈树义(ID:gh_b6f5025d4a8d) 公司项目是用 consul 进行注册的,在发布微服务的时候,总是会导致调用方出现一定几率的调用失败.一开始百思不得其解,后来 ...

  7. 不懂优雅停机,搞挂了线上服务,咋办?

    公司项目是用 consul 进行注册的,在发布微服务的时候,总是会导致调用方出现一定几率的调用失败.一开始百思不得其解,后来咨询了资深的同事才知道:原来是服务下线的时候没有优雅停机,没有去 consu ...

  8. Java应用的优雅停机

    一. 优雅停机的概念 优雅停机一直是一个非常严谨的话题,但由于其仅仅存在于重启.下线这样的部署阶段,导致很多人忽视了它的重要性,但没有它,你永远不能得到一个完整的应用生命周期,永远会对系统的健壮性持怀 ...

  9. 聊聊Spring Boot中的优雅停机

    大家好,我是推干货的DD! 今早在社区(spring4all.com)看到一个不错的问题: 感兴趣的小伙伴可以先自己思考一下,或者看一下源码. 关于优雅停机,网上的文章讲的比较透的不多,好多都是复制黏 ...

最新文章

  1. 维基百科创始人:将欧盟隐私规定推至全球将带来灾难
  2. 电气:需求响应:初始化一个调度周期的可转入转出负荷(python实现)
  3. 绕过mysql的id:32933 BUG 实现order by limit 正常取数据.
  4. python 模块 类 函数_Python17之函数、类、模块、包、库
  5. 在Windows中测试c语言单个函数运行时间方法
  6. Win7登录时出现Group policy client 未启动无法登陆问题的解决方法
  7. [转载] 用Tkinter打造GUI开发工具(45)用Tkinter做自己的中文代码编辑器
  8. UISegmentedControl
  9. Z-score标准化[转载]
  10. upgrade-insecure-requests
  11. 社交规则:饭后抢着买单到底是客气还是客套?大多并不是真心的
  12. 用户提交job后,abaqus的inp文件处理过程
  13. 腾讯会议看不到别人的共享屏幕,共享屏幕是黑色解决方法
  14. 基于SpringBoot+MybatisPlus+Vue+MySQL的体育用品商城设计
  15. 【房间墙上凿个洞,看你在干嘛~】安全攻防内网渗透-绕过防火墙和安全检测,搭建DNS隐蔽隧道
  16. 毕业后的档案问题——详细流程
  17. 初中教师资格证计算机试题,2017上半年初中语文教师资格证面试试题(精选)第二批...
  18. echarts做企业关系图谱_echarts交叉关系图一
  19. 怎样建设自组织的团队
  20. 2020寒假【gmoj1747】【马蹄印】【DFS】

热门文章

  1. linux acl 权限 给任何用户或用户组设置任何文件/目录的访问权限
  2. centos7 安装postgresql11
  3. linux agetty 登录框进程简介
  4. linux shell 特殊变量 符号 $0, $#, $*, $@, $?, $$ 命令行参数
  5. 多目录cmake工程 CmakeLists.txt编写
  6. 为学Linux,我看了这些书
  7. 直接内存访问(DMA)
  8. Linux内核的Oops
  9. Linux 内核中的 Device Mapper 机制
  10. matlab dsearchn,cKDTree与dsearchn