Spring Boot (RuoYi) Graceful Shutdown

文章目录

    用 RuoYi (Spring Boot) 开发了一套访客管理系统,由于要频繁的更新部署,我担心重启会影响到正在处理的请求。所以查了一下 Spring Boot 是否支持 Graceful Shutdown。因为 Spring Boot 这种非常成熟的框架,不能没有这种机制的,所以问题就变成了,如何配置来启用 Graceful Shutdown.

    Spring Boot 版本

    这个 RuoYi 版本中的 Spring Boot 版本为 2.5.15。 Spring Boot 从 2.3.0 起就已引入

    # application.yml
    server:
      shutdown: graceful # 开启优雅停机,默认为 immediate
    
    spring:
      lifecycle:
        timeout-per-shutdown-phase: 30s # 设置等待在途请求完成的最大超时时间,默认30秒
    

    具体说明:

    • server.shutdown: graceful —— Spring Boot 2.3.0 引入(2.3 之前只有 immediate 一种行为,配了也不认)
    • spring.lifecycle.timeout-per-shutdown-phase —— 同版本引入,Duration 类型,默认就是 30s

    Tomcat 9.0.106 的 NIO/NIO2/APR 连接器都支持 pause() 优雅停机(Tomcat 侧需要 9.0.33+),没有问题。应用用的是内嵌 Tomcat(spring-boot-starter-web),正好是这套配置支持的容器类型。

    两个属性的实际语义

    配置 作用
    server.shutdown: immediate(默认) JVM 收到 SIGTERM 直接销毁上下文,在途请求被硬切
    server.shutdown: graceful 先暂停 Tomcat 连接器(不再接新连接,新请求返回 503),等在途请求跑完再关上下文
    spring.lifecycle.timeout-per-shutdown-phase 同一个值同时用于:① Web 服务器等待在途请求的上限;② DefaultLifecycleProcessor 每个停机 phase 的超时。超时后会打 warn 并强行继续往下走

    所以不必去 Tomcat 里另找“grace period”属性 —— Spring Boot 就是复用这个 lifecycle 超时作为 Web 容器的等待上限。

    怎么验证生效

    重启时看日志,正常会出现类似:

    o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
    o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
    

    实测方式:起一个慢接口(sleep 10s),请求发出后立刻 kill(SIGTERM,不要 -9)进程,观察该请求是否返回正常响应、日志是否等到请求结束后才 Graceful shutdown complete。

    如果当前没有线程在处理请求,是否也要等 30 秒才能完成停止进程

    不会。30s 是「最长等待上限」,不是「固定等 20 秒」

    timeout-per-shutdown-phase 的语义是 deadline,不是 sleep。空闲时它会在毫秒级结束。

    它的实际工作机制

    Spring Boot 的 GracefulShutdown(org.springframework.boot.web.embedded.tomcat.GracefulShutdown)停机过程是:

    1. 先关闸:关闭 Connector 的接收,不再接受新连接/新请求(此时新请求被拒绝)。
    2. 然后轮询:循环检查当前 ServletContext 的活跃请求计数,轮询间隔约 50ms。
    3. 归零即走:计数一旦为 0,立刻跳出循环,打印 Graceful shutdown complete,继续往下做容器销毁。
    4. 超时才是保底:把 30s 通过 Connector#setStopTimeout 交给它,只有”等满 30s 还有请求没跑完”这种情况,才会强制中断那些请求。

    所以没有线程在处理请求时,它压根不进等待循环,第一个轮询周期就判定空闲走了。

    空闲停机的真实耗时构成

    阶段 耗时
    pause + 活跃计数检查 ~0 ms(≤ 1 个轮询周期)
    Context 销毁:Quartz shutdown、Druid 连接池 close、Lettuce close、ShutdownManager 等异步池(空闲立即返回) 通常 0.x ~ 2~3 s
    合计 一般 1~3 秒,与 30s 无关

    日志上表现为 Commencing graceful shutdown... 和 Graceful shutdown complete 两行时间戳几乎相同。

    一个常见的误会:空闲 keep-alive 连接不会被等

    浏览器 / 微信 webview 持有的长连接不算”活跃请求”,计数只统计正在被 Servlet 处理的请求。所以不会因为有几十个空闲长连接就多等——Tomcat 关 Connector 时会把它们直接关掉。