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能处理三种场景下的停机:
  1. 代码主动停机,Runtime.exit()
  1. kill指令kill进程
  1. 用户线程全部结束
可以说,唯一无法处理的就是kill -9强制停机命令,所以我们尽量不要使用kill -9
而监听到停机信号之后,应用级别的ShutdownHook是并行执行的,无法控制顺序。如果不同的ShutdownHook之间存在依赖关系,只能通过注册在同一个ShutdownHook里面然后内部实现顺序。
比如使用Spring容器的应用,一般都是通过Spring实现的ShutdownHook内部的一些扩展点(destory回调ContextClosedEvent监听等等)来做的。
如果你想对ShutdownHook机制有一个更深入的了解,推荐你去看看这篇文章

SpringShutdownHook

SpringBoot在启动的过程中注册了一个应用级别的ShutdownHook,这个方法是ConfigurableApplicationContext接口定义的,也可以手动显式调用,但是一个Spring上下文只能注册一个shutdownHook
notion image
核心流程都在doClose()方法里
整个停机流程还是比较清晰的,这里再简单总结一下:
  1. 最先广播ContextClosedEvent事件,此时所有监听了ContextClosedEvent事件的Bean会destory
  1. 第二步再destory实现了Lifecycle接口的Bean,可以看到Lifecycle还支持定义不同的phase,关于Lifecycle的停机流程我们后面会深入分析
  1. 第三步就是destory单例Bean。这个范围可能比你认知的要大一些,因为这里还会包含实现了AutoCloseable接口未指定destory-method但是有close或者shutdown方法的Bean
    1. 关闭容器本身
    1. 提供一个扩展点(onClose)给子类扩展用的

    SmartLifecycle

    这里重点介绍一下Spring停机流程的第二步,Lifecycle相关Bean的stop,后面要讲的SpringBoot内嵌WebServer的优雅停机就是基于SmartLifecycle来实现的。如果你不想看枯燥的代码分析,也可以直接跳到这一段的结尾看文字总结。
    关于Lifecycle和SmartLifecycle的停机流程,这里再总结一下:
    1. 把所有实现了Lifecycle接口的Bean按照phase值做groupBy,形成phase->LifecycleGroup的Map(没有实现Phased接口的phase按照0来处理)
    1. 按照phase值从大到小的顺序,依次stop LifecycleGroup
    1. LifecycleGroup组内按照order值从大到小stop里面的LifecycleBean,确保每个LifecycleBean只被stop一次,并且如果存在依赖关系,被依赖的Bean总是优先被stop
    1. 每个LifecycleGroup内的SmartLifecycleBean可以通过多线程来并行stop,最终会通过CountDownLatch来等待这个phase里的所有SmartLifecycleBean成功stop或者是超过spring.lifecycle.timeout-per-shutdown-phase配置的时长

    需要优雅停机的场景

    看完了上面的知识点铺垫,相信你对ShutdownHook、SpringShutdownHook以及Spring的LifecycleBean应该有一个大概的了解了。下面我们会结合实际的场景来看看它们是怎么处理优雅停机的。
    一般来说,停机不优雅可以分为两类:
    1. 服务注册/发现:涉及到服务注册/发现的,在停机的时候没有及时去注册中心摘除节点,而是先做了应用的destory,导致有的请求路由到正在停机或者已经停机的实例(服务不可用)
    1. Spring容器内部依赖:Spring容器内部多种类型的Bean destory顺序导致。比如先destory的Bean实际上会被后destory的Bean依赖,那么在destory期间进来的请求就有可能报错。
    两类问题的解决方案也比较简单:
    1. 针对服务注册/发现类的问题我们要按照下面的顺序处理
      1. 从注册中心下线
      2. 关闭服务端口
      3. 相关bean的销毁,回收
    1. 而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:webServerGracefulShutdownwebServerStartStop,从名字上不难判断出都跟停机相关,并且这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为例来分析:
    我们看看如果配置了优雅停机,会做哪些动作
    这里会开启一个新线程,主要做几件事情
    1. 先把端口停掉,不再接收新的请求
    1. 会等待已经进来的请求全部处理完,最大等待时长为spring.lifecycle.timeout-per-shutdown-phase。假设超过最大等待时长WebServerGracefulShutdownLifecycle对应的phase还未完成stop,那么Spring的停机流程会往下推进到WebServerStartStopLifecycle对应phase的stop,此时WebServerStartStopLifecycle会更新aborted字段,通知WebServerGracefulShutdownLifecycle立即shutdown
    1. 如果说已经进来的请求在超时时间内被消费完,那么本phase正常关闭,进入WebServerStartStopLifecycle对应phase的stop

    SpringBoot2.3之前自己实现的优雅停机

    在2.3版本发布之前,我们也尝试过自己来实现优雅停机。整体思路也是一样的:
    1. 先停止connector,不再接收新请求
    1. 替换tomcat线程池,主要是处理已经进来的但是还未提交到线程池的请求,这部分请求直接忽略掉
    1. 等待原tomcat线程池里的任务处理完,超时时间为5s
    上面这个springboot 2.3之前版本的优雅停机逻辑是我在2019年的时候实现的,印象里当时我们使用的还是spring-boot 1.5.x。当时是因为应用在停机过程中总是会打印ERROR日志,所以我们的初衷是为了在停机时不要老是无谓的“骚扰”自己。而按上面的做法,还是会让外界有感知,打印了多少个drop request就是影响了多少个外部请求。
    这里还有一点值得思考的是,服务端处理完就算是结束了吗?那如果客户端还没有读取完响应体呢?

    参考

    1. ShutdownHook原理
    1. Allow the embedded web server to be shut down gracefully #4657
    1. 研究优雅停机时的一点思考
    Relate Posts
    SpringBoot开启gzip压缩min-response-size不生效
    Lazy loaded image
    优雅停机之Spring Cloud Gateway
    Lazy loaded image
    优雅停机之Dubbo
    Lazy loaded image
    SpringBoot是如何做到一个jar包就可以直接运行的
    Lazy loaded image
    spring-cloud-gateway跨域配置两三事
    Lazy loaded image
    带你透过源码理解SpringBoot配置文件加载流程
    Lazy loaded image
    优雅停机之DubboSentinel之计数统计及限流逻辑
    Loading...
    黑微狗
    黑微狗
    一只普通的干饭汪🍚
    Latest posts
    RocketMQ 4.6.0 Message Trace 功能异常排查
    2025-4-8
    browser-use 项目核心原理
    2025-3-28
    关于怎么搭建一个这样的blog
    2025-3-28
    关于怎么给blog搞一个自定义的域名
    2025-3-28
    Excel导入需求升级——支持内嵌图片导入
    2025-3-28
    mysql流式查询中的一个坑
    2025-3-28