bigywater.tech
Spring · 76

优雅停机,以及和 K8s preStop 的配合

发布时为什么还会有 502?server.shutdown=graceful 到底保护了什么?preStop 睡几秒才够?阶段超时是多少?这篇用 SIGTERM 加一个会慢的接口,再用 Python 模拟负载均衡的摘除延迟,逐项实测。

graceful 是默认值
Boot 4.0.8 不设 server.shutdown 时,在途请求完成,日志是 Commencing graceful shutdown
149 ~ 162 / 0
模拟负载均衡 2 秒摘除延迟:不睡时失败请求数在 149 到 162 之间;preStop 睡 3 秒 + graceful 失败 0
30.1 秒
没设 timeout-per-shutdown-phase 时,在途 40 秒请求下进程的退出用时

SIGTERM 之后的行为

实测 · 本机执行输出
  什么都不设(默认值)
    正在处理的 /slow?ms=3000(SIGTERM 前 0.5 秒发出):200 done after 3000 ms;客户端等了 3.0 秒
    SIGTERM 之后 0.3 秒:新建连接请求 /fast → ConnectionRefusedError: [Errno 61] Connection refused;已有的 keep-alive 连接再请求 /fast → RemoteDisconnected: Remote end closed connection without response
    进程在 SIGTERM 之后 2.6 秒退出,退出码 143
    日志:Commencing graceful shutdown. Waiting for active requests to complete
    日志:Graceful shutdown complete
    AvailabilityChangeEvent CORRECT
    AvailabilityChangeEvent ACCEPTING_TRAFFIC
    AvailabilityChangeEvent REFUSING_TRAFFIC
    ContextClosedEvent 到达,readiness=REFUSING_TRAFFIC
    @PreDestroy 执行
  server.shutdown=immediate
    正在处理的 /slow?ms=3000(SIGTERM 前 0.5 秒发出):RemoteDisconnected: Remote end closed connection without response;客户端等了 0.5 秒
    SIGTERM 之后 0.3 秒:新建连接请求 /fast → ConnectionRefusedError: [Errno 61] Connection refused;已有的 keep-alive 连接再请求 /fast → RemoteDisconnected: Remote end closed connection without response
    进程在 SIGTERM 之后 0.3 秒退出,退出码 143
    AvailabilityChangeEvent CORRECT
    AvailabilityChangeEvent ACCEPTING_TRAFFIC
    AvailabilityChangeEvent REFUSING_TRAFFIC
    ContextClosedEvent 到达,readiness=REFUSING_TRAFFIC
    @PreDestroy 执行
  server.shutdown=graceful(阶段超时默认 30 秒)
    正在处理的 /slow?ms=3000(SIGTERM 前 0.5 秒发出):200 done after 3000 ms;客户端等了 3.0 秒
    SIGTERM 之后 0.3 秒:新建连接请求 /fast → ConnectionRefusedError: [Errno 61] Connection refused;已有的 keep-alive 连接再请求 /fast → RemoteDisconnected: Remote end closed connection without response
    进程在 SIGTERM 之后 2.5 秒退出,退出码 143
    日志:Commencing graceful shutdown. Waiting for active requests to complete
    日志:Graceful shutdown complete
    AvailabilityChangeEvent CORRECT
    AvailabilityChangeEvent ACCEPTING_TRAFFIC
    AvailabilityChangeEvent REFUSING_TRAFFIC
    ContextClosedEvent 到达,readiness=REFUSING_TRAFFIC
    @PreDestroy 执行
  server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase=1s(比请求短)
    正在处理的 /slow?ms=3000(SIGTERM 前 0.5 秒发出):RemoteDisconnected: Remote end closed connection without response;客户端等了 1.5 秒
    SIGTERM 之后 0.3 秒:新建连接请求 /fast → ConnectionRefusedError: [Errno 61] Connection refused;已有的 keep-alive 连接再请求 /fast → RemoteDisconnected: Remote end closed connection without response
    进程在 SIGTERM 之后 1.1 秒退出,退出码 143
    日志:Commencing graceful shutdown. Waiting for active requests to complete
    日志:Graceful shutdown aborted with one or more requests still active
    AvailabilityChangeEvent CORRECT
    AvailabilityChangeEvent ACCEPTING_TRAFFIC
    AvailabilityChangeEvent REFUSING_TRAFFIC
    ContextClosedEvent 到达,readiness=REFUSING_TRAFFIC
    @PreDestroy 执行

graceful 是默认值;在途请求完成,新连接立刻被拒绝;immediate 会断开在途请求;阶段超时比请求短也会断开。

摘除延迟与 preStop

实测 · 本机执行输出
  immediate,不睡(preStop 睡 0 秒)
    摘除前共发出 149 个请求,成功 0 个,失败 149 个(ConnectionResetError × 1、ConnectionRefusedError × 148);摘除前 0.1 秒发出的 3 秒请求 → ConnectionRefusedError: [Errno 61] Connection refused
  graceful,不睡
    摘除前共发出 162 个请求,成功 0 个,失败 162 个(ConnectionResetError × 1、ConnectionRefusedError × 161);摘除前 0.1 秒发出的 3 秒请求 → ConnectionRefusedError: [Errno 61] Connection refused
  什么都不设(默认值),不睡
    摘除前共发出 157 个请求,成功 0 个,失败 157 个(ConnectionResetError × 1、ConnectionRefusedError × 156);摘除前 0.1 秒发出的 3 秒请求 → ConnectionRefusedError: [Errno 61] Connection refused
  immediate,preStop 睡 3 秒
    摘除前共发出 122 个请求,成功 122 个,失败 0 个;摘除前 0.1 秒发出的 3 秒请求 → RemoteDisconnected: Remote end closed connection without response
  graceful,preStop 睡 3 秒
    摘除前共发出 124 个请求,成功 124 个,失败 0 个;摘除前 0.1 秒发出的 3 秒请求 → 200 done after 3000 ms

graceful 只保护已经进来的请求;负载均衡还在转发时,新请求会撞上已关闭的端口;preStop 要睡得比摘除传播时间长。

阶段超时

实测 · 本机执行输出
  server.shutdown=graceful,没有设置 spring.lifecycle.timeout-per-shutdown-phase
    正在处理的 /slow?ms=40000(SIGTERM 前 0.5 秒发出):TimeoutError: timed out;客户端等了 20.0 秒
    SIGTERM 之后 0.3 秒:新建连接请求 /fast → ConnectionRefusedError: [Errno 61] Connection refused;已有的 keep-alive 连接再请求 /fast → RemoteDisconnected: Remote end closed connection without response
    进程在 SIGTERM 之后 30.1 秒退出,退出码 143
    日志:Commencing graceful shutdown. Waiting for active requests to complete
    日志:Graceful shutdown aborted with one or more requests still active
    AvailabilityChangeEvent CORRECT
    AvailabilityChangeEvent ACCEPTING_TRAFFIC
    AvailabilityChangeEvent REFUSING_TRAFFIC
    ContextClosedEvent 到达,readiness=REFUSING_TRAFFIC
    @PreDestroy 执行

默认最长等 30 秒,之后日志 aborted 并断开请求。

分步演示

优雅停机
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。被测应用是 spring-boot-starter-webmvc(内嵌 Tomcat)的单个程序,一个 /slow?ms=N 接口;客户端是 Python http.client(超时 20 秒);SIGTERM 用 Popen.send_signal 直接发给 java 进程;第二节是模拟:负载均衡的「摘除延迟」就是让发请求的线程在 t0+2.0 秒停止,preStop 睡眠就是推迟发 SIGTERM,没有真正的 K8s、Service 或 Endpoints
    • 没有验证:真实的 K8s(Endpoints 摘除的实际传播时间、kube-proxy / Ingress 的缓存、terminationGracePeriodSeconds 与 SIGKILL 的配合);Actuator 的 readiness 探针在停机期间的表现(这里只看了 ApplicationAvailability 的状态);Jetty 等其他容器的行为;HTTP/2、WebSocket、SSE 等长连接在停机时的表现;@Async 线程池、消息消费者、定时任务的优雅停机;JDK 21 之外版本的 SIGTERM 处理
    京ICP备2026067502号-1京公网安备11010502063487号