type
status
date
slug
summary
tags
category
icon
password
AI summary
优雅停机(上)
优雅停机是指以受控方式终止应用程序的过程,允许它完成任何正在进行的任务,释放资源并确保数据完整性。比如当前正在处理的请求、正在运行的定时任务、正在消费的消息等等。我们不能每次停机或者发布的时候都让外部有明显的感知。优雅停机的目标就是让外界(你的服务调用方)无感。
由于篇幅过长,文章分为上下两篇:
- 上篇主要铺垫相关知识点以及介绍SpringBoot内嵌webServer的优雅停机实现
- 下篇主要介绍几种常见的中间件(Dubbo、RocketMQ)的优雅停机实现
PS:如未做特殊说明,本文的代码都是基于spring-boot 2.3.4-RELEASE版本
Shutdown Hook
ShutdownHook是我们实现优雅停机的基石,它是JVM监听shutdown/kill信号的回调接口。我们通过注册应用级别的ShutdownHook来实现优雅停机。
ShutdownHook能处理三种场景下的停机:
- 代码主动停机,
Runtime.exit()
- kill指令kill进程
- 用户线程全部结束
可以说,唯一无法处理的就是
kill -9
强制停机命令,所以我们尽量不要使用kill -9
。而监听到停机信号之后,应用级别的ShutdownHook是并行执行的,无法控制顺序。如果不同的ShutdownHook之间存在依赖关系,只能通过注册在同一个ShutdownHook里面然后内部实现顺序。
比如使用Spring容器的应用,一般都是通过Spring实现的ShutdownHook内部的一些扩展点(
destory回调
、ContextClosedEvent监听
等等)来做的。如果你想对ShutdownHook机制有一个更深入的了解,推荐你去看看这篇文章
SpringShutdownHook
SpringBoot在启动的过程中注册了一个应用级别的ShutdownHook,这个方法是
ConfigurableApplicationContext
接口定义的,也可以手动显式调用,但是一个Spring上下文只能注册一个shutdownHook

核心流程都在
doClose()
方法里整个停机流程还是比较清晰的,这里再简单总结一下:
- 最先广播ContextClosedEvent事件,此时所有监听了ContextClosedEvent事件的Bean会destory
- 第二步再destory实现了Lifecycle接口的Bean,可以看到Lifecycle还支持定义不同的phase,关于Lifecycle的停机流程我们后面会深入分析
- 第三步就是destory单例Bean。这个范围可能比你认知的要大一些,因为这里还会包含实现了AutoCloseable接口、未指定destory-method但是有close或者shutdown方法的Bean
- 关闭容器本身
- 提供一个扩展点(onClose)给子类扩展用的
SmartLifecycle
这里重点介绍一下Spring停机流程的第二步,Lifecycle相关Bean的stop,后面要讲的SpringBoot内嵌WebServer的优雅停机就是基于SmartLifecycle来实现的。如果你不想看枯燥的代码分析,也可以直接跳到这一段的结尾看文字总结。
关于Lifecycle和SmartLifecycle的停机流程,这里再总结一下:
- 把所有实现了Lifecycle接口的Bean按照phase值做groupBy,形成phase->LifecycleGroup的Map(没有实现Phased接口的phase按照0来处理)
- 按照phase值从大到小的顺序,依次stop LifecycleGroup
- LifecycleGroup组内按照order值从大到小stop里面的LifecycleBean,确保每个LifecycleBean只被stop一次,并且如果存在依赖关系,被依赖的Bean总是优先被stop
- 每个LifecycleGroup内的SmartLifecycleBean可以通过多线程来并行stop,最终会通过CountDownLatch来等待这个phase里的所有SmartLifecycleBean成功stop或者是超过
spring.lifecycle.timeout-per-shutdown-phase
配置的时长
需要优雅停机的场景
看完了上面的知识点铺垫,相信你对ShutdownHook、SpringShutdownHook以及Spring的LifecycleBean应该有一个大概的了解了。下面我们会结合实际的场景来看看它们是怎么处理优雅停机的。
一般来说,停机不优雅可以分为两类:
- 服务注册/发现:涉及到服务注册/发现的,在停机的时候没有及时去注册中心摘除节点,而是先做了应用的destory,导致有的请求路由到正在停机或者已经停机的实例(服务不可用)
- Spring容器内部依赖:Spring容器内部多种类型的Bean destory顺序导致。比如先destory的Bean实际上会被后destory的Bean依赖,那么在destory期间进来的请求就有可能报错。
两类问题的解决方案也比较简单:
- 针对服务注册/发现类的问题我们要按照下面的顺序处理
- 从注册中心下线
- 关闭服务端口
- 相关bean的销毁,回收
- 而Spring容器内部依赖,我们要厘清依赖关系,基于Spring容器停机时的不同回调组件的执行顺序,配置好各个Bean之间的destory顺序
SpringBoot 2.3内嵌WebServer的优雅停机
SpringBoot 2.3版本开始支持内嵌WebServer的优雅停机。通过配置
server.shutdown=graceful
,在应用停机时,WebServer将不再允许新请求,并等待一定时长(通过spring.lifecycle.timeout-per-shutdown-phase
配置)来处理之前已经进来的请求。这个优雅停机就是通过我们前面介绍的LifecycleBean的stop回调来实现的。在此我强烈推荐你仔细阅读前文LifecycleBean的停机流程。我们找到启动webServer的源码,可以看到此时注册了2个Bean:
webServerGracefulShutdown
和webServerStartStop
,从名字上不难判断出都跟停机相关,并且这2个Bean都实现了SmartLifecycle接口WebServerStartStopLifecycle
负责webServer的启停,容器启动阶段会启动webServer,而容器停机阶段会销毁webServer,phase为Integer.MAX_VALUE - 1
WebServerGracefulShutdownLifecycle
负责优雅停机,phase为Integer.MAX_VALUE
停机时,是按phase从大到小的顺序依次stop的。所以
WebServerGracefulShutdownLifecycle
的stop早于WebServerStartStopLifecycle
。先优雅停机,再销毁webServer。让我们来重点看看优雅停机的逻辑:具体的逻辑都在WebServer的实现类里,重点是shutDownGracefully和stop这2个方法,我们以TomcatWebServer为例来分析:
我们看看如果配置了优雅停机,会做哪些动作
这里会开启一个新线程,主要做几件事情
- 先把端口停掉,不再接收新的请求
- 会等待已经进来的请求全部处理完,最大等待时长为
spring.lifecycle.timeout-per-shutdown-phase
。假设超过最大等待时长WebServerGracefulShutdownLifecycle
对应的phase还未完成stop,那么Spring的停机流程会往下推进到WebServerStartStopLifecycle
对应phase的stop,此时WebServerStartStopLifecycle
会更新aborted字段,通知WebServerGracefulShutdownLifecycle
立即shutdown
- 如果说已经进来的请求在超时时间内被消费完,那么本phase正常关闭,进入
WebServerStartStopLifecycle
对应phase的stop
SpringBoot2.3之前自己实现的优雅停机
在2.3版本发布之前,我们也尝试过自己来实现优雅停机。整体思路也是一样的:
- 先停止connector,不再接收新请求
- 替换tomcat线程池,主要是处理已经进来的但是还未提交到线程池的请求,这部分请求直接忽略掉
- 等待原tomcat线程池里的任务处理完,超时时间为5s
上面这个springboot 2.3之前版本的优雅停机逻辑是我在2019年的时候实现的,印象里当时我们使用的还是spring-boot 1.5.x。当时是因为应用在停机过程中总是会打印ERROR日志,所以我们的初衷是为了在停机时不要老是无谓的“骚扰”自己。而按上面的做法,还是会让外界有感知,打印了多少个
drop request
就是影响了多少个外部请求。这里还有一点值得思考的是,服务端处理完就算是结束了吗?那如果客户端还没有读取完响应体呢?
参考
- Author:黑微狗
- URL:https://blog.hwgzhu.com/article/graceful-shutdown-part1
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts