← 返回题目列表

什么是优雅停机(Graceful Shutdown)?Spring Boot 怎么做到「停机不丢请求」?

高频 中等 第 6 / 25 题 更新于 2026/08/03
优雅停机Graceful Shutdown停机PreDestroy

简化版

优雅停机(Graceful Shutdown)是指「应用关闭时,先处理完手头正在执行的请求、拒绝新请求,再真正退出」,而不是「一刀切立刻杀死进程、把正在处理的请求全丢掉」。为什么需要:如果直接 kill 进程,正在处理的请求会被强行中断——用户看到报错、订单可能处理一半、数据可能不一致。Spring Boot 怎么做(2.3+ 内置):配置 server.shutdown=graceful,收到停机信号(SIGTERM)时:① 停止接收新请求(web 服务器不再接受新连接);② 等待正在处理的请求执行完(有一个超时 spring.lifecycle.timeout-per-shutdown-phase,默认 30 秒,超时就强制结束);③ 关闭线程池、释放资源、执行 @PreDestroy④ 进程退出触发前提:要收到 SIGTERMkill 默认信号、K8s 停 Pod 发的信号)——JVM 的**关闭钩子(ShutdownHook)**捕获它触发优雅停机流程;但 kill -9(SIGKILL)无法捕获,会直接杀死,优雅停机失效。所以优雅停机 + K8s 的 preStop + 就绪探针配合,才能实现「滚动发布不丢请求」。

详细版

优雅停机 vs 强制停机

维度强制停机(kill -9)优雅停机(graceful)
正在处理的请求直接中断、丢失等它处理完
新请求拒绝接收
资源清理不执行执行 @PreDestroy、关连接池
数据一致性可能损坏保证处理完整
能否被捕获SIGKILL 不可捕获SIGTERM 可捕获

开启配置

server:
  shutdown: graceful              # 开启优雅停机(默认 immediate)
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s   # 等待正在处理请求的最长时间

优雅停机的流程

收到 SIGTERM(kill / K8s 停 Pod)

JVM 关闭钩子(ShutdownHook)被触发

① web 服务器停止接收新连接/新请求
② 等待正在处理的请求执行完(最多等 timeout-per-shutdown-phase)
      ↓ (超时则强制结束剩余请求)
③ 关闭 Spring 容器:执行 @PreDestroy、销毁 Bean、关线程池/连接池

④ 进程正常退出

⚠️ 优雅停机不是「配一个参数就万事大吉」,它依赖一整条链路① 必须收到可捕获的信号——kill(SIGTERM)、kill -15、K8s 删 Pod(先发 SIGTERM)可以;kill -9(SIGKILL)不可捕获,直接杀死,优雅停机完全失效。② K8s 环境还有「摘流量」的时间差问题——Pod 收到 SIGTERM 的同时,K8s 才开始把它从 Service 的 Endpoints 里摘掉,这期间可能还有新流量进来。所以要配 preStop 钩子(sleep 几秒,等 K8s 摘流量完成)+ 就绪探针(提前置为 not ready),配合应用的优雅停机,才能真正做到「滚动发布零丢请求」。单靠 server.shutdown=graceful 只解决了「应用侧不丢正在处理的请求」,摘流量是运维侧的事。

完整版教学

一、为什么需要优雅停机

先理解「强制停机会出什么问题」:

场景:滚动发布(发新版本,要停掉旧实例)
  或 缩容(减少实例)、重启

如果直接 kill 进程(强制停机):
  - 正在处理的请求被拦腰斩断 → 用户看到 502/连接重置
  - 请求可能执行到一半:订单创建了但没返回、扣了款没发货
  - 数据库事务可能未提交或未回滚干净
  - 消息可能消费了一半没 ack
  → 用户体验差 + 数据可能不一致

优雅停机想要的:
  停机时,"温柔地"收尾:
  - 不再接新请求(新流量导向其他实例)
  - 把手头正在处理的请求处理完(不丢)
  - 清理资源(关连接、提交/回滚事务、释放锁)
  - 再退出
  → 用户无感知、数据一致

优雅停机的动机是「停机时不丢正在处理的请求、不破坏数据一致性」——强制 kill 会拦腰斩断正在处理的请求(用户报错、订单处理一半、事务不干净)。优雅停机「温柔收尾」:不接新请求、处理完手头请求、清理资源再退出。在滚动发布、缩容、重启等频繁停机的云原生场景尤其重要。理解「强制停机丢正在处理的请求、破坏数据一致性;优雅停机不接新请求+处理完手头请求+清理资源再退出」,就理解了为什么需要优雅停机。

二、信号:SIGTERM vs SIGKILL

优雅停机的前提是「收到一个可捕获的信号」,这里 SIGTERM 和 SIGKILL 的区别是关键:

Linux 进程停止信号:
  SIGTERM(15):礼貌的"请你退出"信号
    - kill <pid>(默认)、kill -15、K8s 删 Pod 都先发它
    - ★ 可以被进程捕获 → 应用能在退出前做优雅收尾
  SIGKILL(9):强制的"立刻死"信号
    - kill -9 <pid>
    - ★ 不可捕获、不可忽略 → 进程被内核直接杀死,来不及做任何事
    - 优雅停机对它完全无效

所以优雅停机的第一前提:
  停机要用 SIGTERM(可捕获),不能用 kill -9
  → 应用捕获 SIGTERM → 触发优雅停机流程

K8s 的默认行为(很贴心):
  删 Pod 时先发 SIGTERM,等 terminationGracePeriodSeconds(默认30秒)
  如果还没退出,才发 SIGKILL 强杀
  → 给了应用优雅停机的时间窗

优雅停机的前提是「收到可捕获的 SIGTERM」——SIGTERM(15,kill 默认、K8s 删 Pod 先发)可被捕获,应用能在退出前收尾;SIGKILL(9,kill -9不可捕获,进程被内核直接杀死,优雅停机无效。K8s 删 Pod 时先发 SIGTERM、等 terminationGracePeriodSeconds(默认 30 秒)后才 SIGKILL,给了优雅停机的时间窗。理解「SIGTERM 可捕获(kill 默认/K8s 先发)触发优雅停机、SIGKILL(kill -9)不可捕获直接杀死、K8s 先 SIGTERM 等宽限期再 SIGKILL」,就理解了优雅停机的信号前提。

三、JVM 关闭钩子:捕获信号的机制

应用是怎么「捕获 SIGTERM 并触发收尾」的?靠 JVM 关闭钩子

JVM 关闭钩子(ShutdownHook):
  Runtime.getRuntime().addShutdownHook(thread)
  注册的线程,会在 JVM 正常关闭时(收到 SIGTERM、或 System.exit)执行

Spring Boot 利用它:
  ApplicationContext 注册了一个关闭钩子
  收到 SIGTERM → JVM 触发关闭钩子 → 调 context.close()
    → 触发容器关闭:执行 @PreDestroy、销毁 Bean、关闭 web 服务器...
    → 优雅停机的整套流程就是挂在这里

关键:
  - SIGTERM/System.exit → 触发关闭钩子 → 能优雅收尾
  - SIGKILL(kill -9)→ 不触发关闭钩子 → 无法收尾
  - 所以 @PreDestroy、优雅停机 都依赖"进程被优雅地要求退出"

应用捕获信号靠 JVM 关闭钩子(ShutdownHook)——注册的线程在 JVM 正常关闭(收到 SIGTERM 或 System.exit)时执行。Spring Boot 的 ApplicationContext 注册了关闭钩子,收到 SIGTERM 时触发 context.close(),进而执行 @PreDestroy、销毁 Bean、关闭 web 服务器——优雅停机整套流程挂在关闭钩子上。而 kill -9 不触发关闭钩子,所以 @PreDestroy 和优雅停机都失效。理解「JVM 关闭钩子捕获 SIGTERM 触发 context.close()、优雅停机和 @PreDestroy 都挂在关闭钩子上、kill -9 不触发钩子」,就理解了捕获信号的机制。

四、Spring Boot 优雅停机的完整流程

Spring Boot 2.3+ 内置优雅停机,配 server.shutdown=graceful 后的完整流程:

配置:
  server.shutdown=graceful
  spring.lifecycle.timeout-per-shutdown-phase=30s

流程(收到 SIGTERM 后):
  1. 关闭钩子触发,开始关闭容器
  2. ★ web 服务器进入"优雅关闭"模式:
     - 停止接收新连接/新请求
     - 但保持正在处理的请求继续执行
  3. 等待正在处理的请求执行完
     - 最多等 timeout-per-shutdown-phase(默认 30 秒)
     - 期间新请求被拒绝(连接被关闭或返回错误)
     - 所有在途请求处理完 → 立刻进入下一步
     - 超时仍有未完成的 → 强制结束它们
  4. 关闭 Spring 容器:@PreDestroy、销毁 Bean、
     关闭线程池、数据库连接池等
  5. 进程退出

支持的容器:Tomcat、Jetty、Undertow、Netty(Reactor) 都支持

Spring Boot 优雅停机流程(server.shutdown=graceful):① web 服务器停接新请求、保持在途请求;② 等在途请求处理完(最多 timeout-per-shutdown-phase,默认 30 秒,超时强制结束);③ 关闭容器执行 @PreDestroy、关线程池/连接池;④ 退出。Tomcat/Jetty/Undertow/Netty 都支持。这样保证「正在处理的请求不丢」。理解「优雅停机流程:停接新请求→等在途请求完成(超时强制)→关容器执行 @PreDestroy 关连接池→退出、默认 30 秒超时」,就掌握了 Spring Boot 优雅停机的完整流程。

五、K8s 环境:摘流量的时间差

生产(尤其 K8s)里,优雅停机还有个「摘流量时间差」问题必须解决:

问题:Pod 收到 SIGTERM 时,K8s 才"同时"开始把它从 Service 摘掉
  但"摘除"是异步的、有延迟(要更新 Endpoints、各节点 kube-proxy 同步)
  → 这期间(可能几百 ms~几秒),仍有新流量被路由到这个正在停的 Pod!
  → 如果应用此时已停止接收新请求 → 这些流量被拒绝 → 用户报错

解决:preStop 钩子 + 就绪探针
  ① preStop 钩子:Pod 停止前先执行(如 sleep 5~15 秒)
     lifecycle:
       preStop:
         exec: command: ["sh","-c","sleep 10"]
     → 给 K8s 时间把流量摘干净,再让应用真正开始停机
     (preStop 执行期间,SIGTERM 还没发给应用,应用还在正常服务)
  ② 就绪探针:停机开始时先置为 not ready,K8s 停止转发新流量

完整链路(零丢请求):
  K8s 删 Pod → 执行 preStop(sleep,等摘流量)
  → 发 SIGTERM → 应用优雅停机(处理完在途请求)→ 退出

K8s 环境的额外问题是「摘流量的时间差」——Pod 收到 SIGTERM 时 K8s 才异步开始摘流量(有延迟),这期间仍有新流量进来但应用已拒绝接收,导致用户报错。解决靠 preStop 钩子(停机前 sleep 几秒,等 K8s 摘流量完成,期间应用还在正常服务)+ 就绪探针(提前置 not ready)。完整链路:删 Pod → preStop sleep 等摘流量 → SIGTERM → 应用优雅停机 → 退出。理解「K8s 摘流量是异步有延迟、这期间新流量进来会被拒、用 preStop sleep 等摘流量+就绪探针配合应用优雅停机才能零丢请求」,就理解了生产环境优雅停机的完整方案。

六、其他资源的优雅关闭

优雅停机不只是 web 请求,还要考虑「其他正在进行的工作」:

除了 web 请求,停机时还有这些要"优雅":
  ① 线程池里正在执行的任务:
     - ThreadPoolTaskExecutor 设 setWaitForTasksToCompleteOnShutdown(true)
       + setAwaitTerminationSeconds(N) → 等任务执行完再关
  ② 消息消费者(Kafka/RabbitMQ):
     - 停止拉取新消息,把正在处理的消息处理完并 ack,再断开
     - Spring 的消息容器一般在容器关闭时会优雅停止
  ③ 定时任务(@Scheduled):
     - 正在执行的定时任务要能被优雅等待/中断
  ④ 数据库连接池:
     - 等正在用的连接归还、事务提交/回滚,再关闭池

统一思路:
  @PreDestroy / DisposableBean 里做资源收尾
  或用 SmartLifecycle(可控制关闭阶段和顺序)
  Spring 容器关闭时会按顺序触发这些,配合优雅停机一起完成

优雅停机要覆盖「所有正在进行的工作」,不只是 web 请求:线程池任务setWaitForTasksToCompleteOnShutdown)、消息消费者(处理完在途消息并 ack)、定时任务数据库连接池(等连接归还、事务提交)。这些通过 @PreDestroy/DisposableBean/SmartLifecycle(可控关闭顺序)在容器关闭时收尾。所以优雅停机是「应用整体的优雅收尾」。理解「优雅停机要覆盖线程池任务/消息消费/定时任务/连接池、通过 @PreDestroy/SmartLifecycle 收尾、是应用整体的优雅收尾」,就理解了优雅停机的完整范围。

记忆钩子:「优雅停机 = 停机时先处理完在途请求、拒绝新请求、清理资源再退出(不像 kill -9 一刀切丢请求);Spring Boot 2.3+ 配 server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase(默认30s);流程:停接新请求→等在途请求完成(超时强制)→关容器执行 @PreDestroy 关连接池→退出;前提:收到可捕获的 SIGTERM(kill 默认/K8s 先发)→JVM 关闭钩子触发,kill -9(SIGKILL)不可捕获直接杀死优雅停机失效;K8s 摘流量异步有延迟,要 preStop sleep 等摘流量+就绪探针才能零丢请求」

七、常见误区与追问

  • 误区:配了 server.shutdown=graceful 就能零丢请求。 只解决了「应用侧不丢在途请求」;K8s 环境还有摘流量的时间差(Pod 收到 SIGTERM 时 K8s 才异步摘流量),要配 preStop 钩子(sleep 等摘流量)+ 就绪探针,才能真正零丢请求。
  • 误区:kill -9 也能触发优雅停机。 不能——SIGKILL(kill -9)不可捕获、不触发 JVM 关闭钩子,进程被内核直接杀死,优雅停机和 @PreDestroy 全部失效;优雅停机必须用可捕获的 SIGTERM(kill 默认信号)。
  • 误区:优雅停机会无限等待在途请求。 有超时(spring.lifecycle.timeout-per-shutdown-phase,默认 30 秒);等待期内在途请求处理完就立刻进入下一步,超时仍未完成的会被强制结束——避免某个卡住的请求让停机永远完不成。
  • 误区:优雅停机只管 HTTP 请求。 还要管线程池正在执行的任务、消息消费者的在途消息、定时任务、数据库连接池等;这些通过 @PreDestroy/SmartLifecycle 在容器关闭时优雅收尾,是应用整体的优雅关闭。
  • 追问:@PreDestroy 和优雅停机是什么关系? @PreDestroy 挂在 JVM 关闭钩子触发的容器关闭流程里——收到 SIGTERM → 关闭钩子 → context.close() → 执行 @PreDestroy;优雅停机在此基础上先做「停接新请求、等在途请求完成」,再走到 @PreDestroy 这步;两者都依赖收到可捕获信号(kill -9 时都不执行)。
  • 追问:K8s 里为什么要配 preStop sleep? 因为 Pod 收到 SIGTERM 的同时,K8s 才异步开始把它从 Service Endpoints 摘除(有延迟),这期间仍有新流量路由过来;preStop 里 sleep 几秒让应用先「多服务一会」,等 K8s 摘流量完成后再收 SIGTERM 开始停机,避免摘流量窗口期的请求被拒。
  • 追问:terminationGracePeriodSeconds 和 timeout-per-shutdown-phase 的关系? 前者是 K8s 给 Pod 的总宽限时间(SIGTERM 后等多久才 SIGKILL,默认 30 秒);后者是 Spring 等待在途请求的时间(默认 30 秒);要保证 K8s 的宽限期 ≥ preStop 时间 + Spring 优雅停机时间,否则应用还没优雅停完就被 K8s SIGKILL 强杀了。

八、加强记忆

优雅停机(Graceful Shutdown)= 应用关闭时先处理完在途请求、拒绝新请求、清理资源再退出,而非 kill -9 一刀切丢请求(会导致用户报错、订单处理一半、数据不一致)。Spring Boot 2.3+ 内置:配 server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase(默认 30s)。流程(收到信号后):① web 服务器停接新请求、保持在途请求;② 等在途请求执行完(最多等超时,超时强制结束);③ 关闭容器执行 @PreDestroy、关线程池/连接池;④ 退出前提是收到可捕获的 SIGTERMkill 默认、K8s 删 Pod 先发)——JVM 关闭钩子(ShutdownHook) 捕获它触发 context.close()kill -9SIGKILL不可捕获、直接杀死,优雅停机和 @PreDestroy 全失效。K8s 环境还有「摘流量时间差」:Pod 收 SIGTERM 时 K8s 才异步摘流量(有延迟),期间新流量会被拒,要配 preStop 钩子sleep 等摘流量)+ 就绪探针,配合应用优雅停机才能「滚动发布零丢请求」。优雅停机还要覆盖线程池任务、消息消费、定时任务、连接池(@PreDestroy/SmartLifecycle 收尾)。一句话「优雅停机=停机先处理完在途请求、拒新请求、清资源再退出;Spring Boot 配 server.shutdown=graceful(默认30s超时);靠 JVM 关闭钩子捕获 SIGTERM(kill -9 不可捕获失效);K8s 还要 preStop sleep 等摘流量+就绪探针才能零丢请求」。