当前位置: 代码网 > it编程>编程语言>Java > Spring应用优雅下线的完整指南

Spring应用优雅下线的完整指南

2026年09月15日 Java 我要评论
1. 一次发布引发的线上事故:我们为什么需要“优雅下线”先看一个真实场景。假设你负责一个订单服务,深夜发布新版本。你直接在注册中心把实例下线,然后执行 kill 命令。监控却立

1. 一次发布引发的线上事故:我们为什么需要“优雅下线”

先看一个真实场景。假设你负责一个订单服务,深夜发布新版本。你直接在注册中心把实例下线,然后执行 kill 命令。监控却立刻弹出告警:该实例在停止过程中处理了 300 多笔订单,其中 40 笔因数据库连接被强制关闭而失败,用户收到“系统繁忙,请稍后重试”。更糟的是,下游支付回调打到该实例时连接被拒,引发了重试风暴。这次事故的根源是什么?因为你“粗暴地杀掉进程”,没有给应用一个缓冲期来完成它正在处理的请求,也没有提前通知网关和负载均衡器“我要离开了”。

这就是为什么需要“优雅下线”。它不是一个单一开关,而是一条从流量入口到应用内部的完整动作链。只有理解了这条链,你才能在发布、扩缩容、故障恢复时做到用户无感知。

2. 先记一个最小模型:摘流量 → 停新 → 处理存量 → 释放资源

你可以把优雅下线理解成一场有序的“退场仪式”。先记住四步主流程:

  1. 摘流量:让负载均衡器、网关、注册中心不再把新请求发给当前实例,并等待“在途通知”完成。
  2. 停新:拒绝或缓冲新进入的请求,避免处理一半时进程退出。
  3. 处理存量:给正在处理的请求一个宽限期,让它们完成事务、返回响应。
  4. 释放资源:关闭线程池、数据库连接池、消息消费者,最后退出进程。

这四个环节由不同的组件协作完成:负载均衡器 / 网关 / 注册中心负责“摘流量”,停机钩子(shutdown hook) 负责“停新”和“处理存量”,健康检查负责向外部汇报状态,而 spring 容器 负责释放资源。它们通过操作系统信号、http 探针、注册中心的元数据等手段连接起来。

整个过程的角色与连接关系可以用下面的 ascii 图表示:

客户端/用户
       |
       v
[负载均衡器/网关] ---- 健康检查探针 ----> spring 应用 (端口 8080)
       |                                      |
       | 摘除流量(注册中心反注册/api下线)        | 1.sigterm
       v                                      v
[注册中心 (nacos/eureka)]              [停机钩子] ---> 线程池停止接收新任务
                                             |         等待存量任务完成
                                             v
                                      [spring 容器销毁] ---> 关闭数据源/连接池
                                             |
                                             v
                                        进程退出

注意:健康检查并不直接参与停机流程,但它决定了负载均衡器何时开始摘除流量。所以,你必须把健康检查、负载均衡摘除和停机钩子放在一起设计,而不是各自为政。

3. 停机钩子:应用内部的“总指挥”

3.1 停机钩子是什么,它解决什么问题

当操作系统向 java 进程发送中断信号(通常是 sigterm,即 kill 命令默认信号)时,jvm 会触发已注册的关闭钩子(shutdown hook)。它允许你在进程退出前执行清理代码。spring boot 的 applicationcontext 本身也注册了一个钩子,用于调用 close() 方法,从而触发 bean 的销毁和容器关闭。

这个机制解决的问题是:让应用有机会“体面地”处理完正在进行的工作,而不是被系统强行终止。

3.2 最小示例:利用 spring@predestroy感知停机

先看一个能直接运行的最小示例。创建一个 spring boot 项目,添加 spring-boot-starter-web,然后写一个带 @predestroy 方法的 bean。目标:当进程收到 sigterm 时,观察控制台输出,证明钩子被调用。

代码 1: 最小停机钩子示例(完整可运行)

前置环境:jdk 8+,maven 3.6+,spring boot 2.7+。创建一个 maven 项目,pom.xml 中加入:

<parent>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-parent</artifactid>
    <version>2.7.18</version>
</parent>
<dependencies>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>
</dependencies>

主类 shutdowndemoapplication.java

import org.springframework.boot.springapplication;
import org.springframework.boot.autoconfigure.springbootapplication;
import org.springframework.context.annotation.bean;
import javax.annotation.predestroy;

@springbootapplication
public class shutdowndemoapplication {
    public static void main(string[] args) {
        springapplication.run(shutdowndemoapplication.class, args);
    }

    @bean
    public shutdownlistener shutdownlistener() {
        return new shutdownlistener();
    }

    public static class shutdownlistener {
        @predestroy
        public void onshutdown() {
            system.out.println("[优雅停机] 正在清理资源,当前时间: " + system.currenttimemillis());
        }
    }
}

操作步骤:

  1. 使用 mvn spring-boot:run 或打包后 java -jar target/demo.jar 启动应用。
  2. 找到进程 pid(例如 ps -ef | grep demo.jar)。
  3. 执行 kill <pid>(发送 sigterm)。

预期输出:在应用退出前,控制台打印 [优雅停机] 正在清理资源...

这个示例证明了 @predestroy 方法会被调用。但它并没有真正处理请求。你需要在真实场景中,在 @predestroy 里关闭线程池、等待任务完成。

常见误区:@predestroy 只在容器正常关闭时触发,如果你用 kill -9 强杀,钩子不会执行。另外,如果你在钩子里执行耗时的清理操作,而默认超时时间到了,jvm 会强制退出,所以不要做太多重活。

3.3 spring boot 2.3+ 的优雅停机配置:行为与取舍

从 spring boot 2.3 开始,内置了优雅停机支持,针对 web 服务器(tomcat、jetty、reactor netty)和消息监听器。你只需要在 application.properties 中设置:

# 开启优雅停机(默认 immediate)
server.shutdown=graceful
# 最长等待时间,默认 30 秒(注意:这是 spring boot 的全局限制,不同服务器有不同覆盖)
spring.lifecycle.timeout-per-shutdown-phase=20s

这里有两个概念需要区分:

  • server.shutdown=graceful 告诉 web 容器在收到停机信号后停止接受新请求,并等待已接收请求完成。
  • spring.lifecycle.timeout-per-shutdown-phase 是 spring 容器每个关闭阶段(如 bean 销毁)的超时时间,不是等请求的总时间,总时间以 web 服务器的实现为准,例如 tomcat 默认等待 30 秒(可配置)。

设计取舍:优雅停机能提高可用性,但也延长了实例的退出时间——如果请求处理很慢,你可能要等满超时。所以,你需要平衡“等待时长”和“发布速度”。一般设置 10~30 秒就够,如果业务经常有长事务,需要更长且配合流量摘除策略。

小例子:假设你的下单接口平均耗时 200ms,但偶尔有 5 秒的报表请求。设置 server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase=10s,在停机时 tomcat 会等所有活跃请求完成,但最多等多久?实际上 tomcat 自己也有超时,默认 30 秒。所以 10 秒是 spring 容器销毁阶段的限制,而 tomcat 等待请求完成可能超过 10 秒?这里需要看依赖:如果你的 tomcat 版本高于 9.0.33,spring boot 2.3+ 会设置 tomcat 的 gracefulshutdowntimeoutspring.lifecycle.timeout-per-shutdown-phase 的值?实际上 spring boot 把 server.shutdown 映射到 tomcat 的 gracefulshutdown 方法,等待时间为 spring.lifecycle.timeout-per-shutdown-phase?? 不,更准确的是:spring boot 2.3 中,tomcat 的优雅停机等待时间取 spring.lifecycle.timeout-per-shutdown-phase 的值作为每个阶段,但同时 tomcat 的 gracefulshutdown 也有它的超时配置(可通过 server.tomcat.graceful-shutdown-timeout 设置,默认 30 秒)。所以两者可能共同作用,你需要了解细节以避免陷阱。

为了避免版本混淆,建议测试你的环境。

4. 健康检查:让外部知道“我还能不能用”

4.1 健康检查的角色

健康检查是负载均衡器或编排平台(如 k8s)用来判断实例是否存活和就绪的机制。在引入 spring boot actuator 后,/actuator/health 暴露实例的健康状态。kubernetes 等平台通过探测这个端点决定是否将流量路由到该实例。

4.2 两种探针:存活与就绪

k8s 有两种探针:

  • 存活探针(livenessprobe):检测进程是否死锁或崩溃,失败会重启容器。
  • 就绪探针(readinessprobe):检测应用是否准备好接收流量,失败会将 pod 从 service 的 endpoint 中摘除,但不会重启。

优雅下线主要用就绪探针。当你收到停机信号时,希望先让就绪探针失败,这样 service 会停止向该 pod 转发新请求。

4.3 完整示例:基于 spring boot actuator 与 k8s 就绪探针的下线流程

示例 2:结合健康检查的优雅下线(贴近生产)

前置环境:需要 docker 和可用的 kubernetes 集群,或者你可以用 minikube。同样使用前面的 spring boot 项目,添加 actuator 依赖和业务模拟接口。

代码 2.1:pom.xml 添加依赖

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-actuator</artifactid>
</dependency>

代码 2.2:配置 health 端点开放和停机设置

# 开启优雅停机
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s
# 暴露健康端点
management.endpoints.web.exposure.include=health,info
# 让健康检查包含 db 和 diskspace(默认已有)
management.endpoint.health.show-details=always

代码 2.3:模拟长耗时请求的接口

在主类中增加一个 controller,模拟一个耗时 15 秒的处理:

import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.restcontroller;

@restcontroller
class longtaskcontroller {
    @getmapping("/slow")
    public string slow() throws interruptedexception {
        thread.sleep(15000); // 模拟长任务
        return "done";
    }
}

代码 2.4:定义就绪探针,并在停机信号时切换为 not_accepting_traffic

这里的关键是如何让就绪探针失败?spring boot 没有内置“停止接收流量”的状态,但我们可以自定义 healthindicator 来反映实例状态。实现思路:监听停机信号,设置一个标志位,让 /actuator/health 返回 down

更好的做法是注册一个停机钩子,在收到 sigterm 时先改变健康状态,再等待请求完成。但注意:钩子执行时 spring 容器还在,但 actuator 的 healthendpoint 可能已经失效?不,容器还没销毁,但端点可能还在。但更安全的方式是利用 k8s prestop 钩子,在容器停止前调用一个 api 来设置状态。

下面用一个简化方案:使用 spring boot 的 applicationlistener<applicationpreparedevent>?不,还是使用自定义 healthindicator 并用一个静态变量控制。

为了可运行,我们展示一个独立的完整示例:自定义 readinesshealthindicatorshutdownsignallistener

import org.springframework.boot.actuate.health.health;
import org.springframework.boot.actuate.health.healthindicator;
import org.springframework.stereotype.component;

@component
public class readinessindicator implements healthindicator {
    private volatile boolean acceptingtraffic = true;

    public void setacceptingtraffic(boolean acceptingtraffic) {
        this.acceptingtraffic = acceptingtraffic;
    }

    @override
    public health health() {
        if (acceptingtraffic) {
            return health.up().build();
        }
        return health.down().withdetail("reason", "shutting down").build();
    }
}

applicationrunner 或主类中注册一个线程监听信号,实际生产中可使用 sun.misc.signal,但为了跨平台更推荐使用 spring 的停机钩子?但 spring 钩子执行时机在容器关闭前,可能来不及把状态设为 down。事实上,k8s 发送 sigterm 后,同时执行 prestop 钩子(如果有)。更常见的做法是设置 prestop 调用一个接口:

假设你有一个 /internal/readiness/off 端点,仅供内部调用:

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.restcontroller;

@restcontroller
public class admincontroller {
    @autowired
    private readinessindicator readinessindicator;

    @postmapping("/internal/readiness/off")
    public string turnoff() {
        readinessindicator.setacceptingtraffic(false);
        return "off";
    }
}

在 k8s deployment 中,配置 prestop 钩子:

apiversion: apps/v1
kind: deployment
metadata:
  name: demo-app
spec:
  replicas: 1
  selector:
    matchlabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: demo:1.0
        ports:
        - containerport: 8080
        readinessprobe:
          httpget:
            path: /actuator/health
            port: 8080
          initialdelayseconds: 5
          periodseconds: 5
        lifecycle:
          prestop:
            exec:
              command: ["sh", "-c", "curl -x post http://localhost:8080/internal/readiness/off"]

当删除 pod 时,k8s 先执行 prestop(这里把就绪状态设为 down),然后等待一段时间(默认 30 秒)才发送 sigterm。这个等待期内,service 端点已经摘除该 pod,新流量不再进来,存量请求得以继续。然后 sigterm 触发 spring 优雅停机,继续等待存量请求完成。

可观察结果:在滚动发布期间,旧 pod 的 /actuator/health 变为 down,新 pod 就绪后,流量平滑切换,无 502 报错。

适用场景:适合 k8s 部署的微服务。

容易改错的地方:健康检查的 periodseconds 不要太短;prestop 执行时间受 terminationgraceperiodseconds 限制(默认 30 秒),如果你需要更长的存量处理时间,要同步增加该值。另外,prestop 是阻塞的,它执行完前 k8s 不会发 sigterm,所以不要在里面做太长时间的操作。

4.4 误区:健康检查等于就绪状态?

很多人误以为只要健康检查成功,流量就一直可达。实际上,健康检查分为存活和就绪,存活失败会重启容器,就绪失败才会摘除流量。如果你的健康检查只有 up 状态,而你的应用因为内存或线程池耗尽无法处理新请求,探针可能依然为 up,但流量雪崩。所以最好在健康检查中增加自定义指标,如线程池剩余容量、数据库连接池使用率。

5. 负载均衡摘除:从入口切断新流量

5.1 摘除的两种方式:被动摘除与主动摘除

负载均衡器(如 nginx、spring cloud gateway)或注册中心(如 nacos、eureka)需要在上线前将实例从候选列表中移除。有两种思路:

  • 被动摘除:靠健康检查失败让负载均衡器自动摘除。
  • 主动摘除:应用主动调用注册中心的下线 api,或者通过脚本通知网关。

被动摘除简单,但存在健康检查间隔(通常几秒到几十秒)导致流量仍可能打过来。主动摘除更即时,但需要额外的编排。

5.2 spring cloud 场景:通过注册中心实现灰度摘除

如果你的服务使用 nacos 或 eureka 做注册发现,可以在停机前调用 nacosserviceregistry.deregister() 将实例注册删除。下面是用 spring cloud alibaba nacos 的示例:

示例 3:spring cloud nacos 下的主动摘除(贴近生产)

前置环境:一个可运行的 nacos 服务(可用 docker 启动),spring boot 2.7 + spring cloud alibaba 2021.0.5.0。

代码 3.1:依赖

<dependency>
    <groupid>com.alibaba.cloud</groupid>
    <artifactid>spring-cloud-starter-alibaba-nacos-discovery</artifactid>
</dependency>

代码 3.2:配置 nacos 地址

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.service-name=demo-service

代码 3.3:调用注销 api 实现主动摘除(伪代码)

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.restcontroller;
import com.alibaba.cloud.nacos.registry.nacosserviceregistry;
import org.springframework.cloud.client.serviceregistry.registration;

@restcontroller
public class registrycontroller {
    @autowired
    private nacosserviceregistry nacosserviceregistry;
    @autowired
    private registration registration;

    @postmapping("/deregister")
    public string deregister() {
        // 注意:nacosserviceregistry 的 deregister 需要 registration 对象
        nacosserviceregistry.deregister(registration);
        return "deregistered";
    }
}

注意:deregister 调用后,实例在 nacos 中消失,但本地应用还在运行。因此,通常在发布脚本中先调用 /deregister,等待几秒(让 lb 同步),再发送 sigterm。

可观察结果:调用后,nacos 控制台该实例下线,客户端在下次拉取时不再获得该实例 ip,新请求不再分发到该实例。

适用场景:使用 spring cloud 且没有 k8s 时,手动控制发布流程。

容易改错的地方:不要误用 registration 类型,如果是 nacos 的 nacosregistration 必须正确注入。另外,如果在钩子里调用 deregister,需要确保网络可用,否则会因异常导致停机失败。

5.3 摘除时机的选择:先摘除,后停机,再等待

正确的顺序是:摘除流量 → 等待(例如 10 秒) → 发送 sigterm。其中等待时间取决于负载均衡器同步周期。对于 nacos,默认有 3 秒定时拉取?实际上客户端会缓存服务列表,最长可能延迟 30 秒。所以,如果你的负载均衡器是有状态的(如 ribbon 客户端缓存),摘除后必须等待足够长的时间。

6. 线程池与请求队列:停新与存量的核心

6.1 web 容器如何实现“停新等待完成”

在优雅停机开启后,spring boot 的 web 容器会停止接收新连接/新请求,但继续处理已经进入的请求。这一般通过“先关闭服务端套接字,不再 accept,然后等待活跃请求计数归零”实现。

6.2 自定义业务线程池:如何优雅停止

很多应用有自己的业务线程池(如执行异步任务)。当停机时,你需要停止提交新任务,并 shutdown() 等待任务结束。

示例 4:业务线程池优雅停止(可独立运行)

下面是一个纯 java 示例,模拟线程池停止。

代码 4.1:完整示例

import java.util.concurrent.*;

public class poolshutdowndemo {
    public static void main(string[] args) throws interruptedexception {
        executorservice pool = executors.newfixedthreadpool(4);

        // 提交 10 个耗时任务
        for (int i = 0; i < 10; i++) {
            final int id = i;
            pool.submit(() -> {
                try {
                    system.out.println("task " + id + " started");
                    thread.sleep(2000);
                    system.out.println("task " + id + " finished");
                } catch (interruptedexception e) {
                    thread.currentthread().interrupt();
                    system.out.println("task " + id + " interrupted");
                }
            });
        }

        // 模拟停机:停止接受新任务
        pool.shutdown();
        system.out.println("执行 shutdown, 拒绝新任务");

        // 等待已提交任务完成,最多等 5 秒
        if (!pool.awaittermination(5, timeunit.seconds)) {
            system.out.println("还有任务未完成,强制关闭");
            pool.shutdownnow(); // 中断剩余任务
        }
        system.out.println("线程池已完全停止");
    }
}

运行预期:看到任务按顺序开始、完成;如果有任务超过 5 秒,会看到“强制关闭”。

解释shutdown() 不再接受新任务,已提交的任务继续执行。awaittermination 轮询等待。如果超时,可调用 shutdownnow() 中断。

边界:如果你的任务不响应中断,shutdownnow 也无法终止它们,所以最好让任务支持中断。

6.3 常见错误:在@predestroy里直接调用system.exit

有些开发者会在钩子里写 system.exit(0),这会绕过 spring 的销毁顺序,导致资源没被释放。不要在钩子里调用 system.exit,应让容器正常关闭。

7. 完整过程:k8s 滚动更新的端到端时序

现在把前面的机制串起来,看一次典型的 k8s 滚动更新。

文本时序图:

k8s master          old pod           new pod         user traffic
   |                  |                  |                |
   |-- 触发滚动更新 --|                  |                |
   |                  |                  |                |
   |-- 启动新 pod ----|----------------->|                |
   |                  |             等待就绪           |
   |                  |                  |                |
   |-- 准备终止旧pod->|                  |                |
   |-- 执行 prestop ->| /health down     |                |
   |                  |--- 摘除流量 --------------------> x
   |-- 等待终止宽限->| 继续处理存量请求                   |
   |                  |                  |                |
   |-- 发送 sigterm->| spring 钩子       |                |
   |                  | 停止新请求        |                |
   |                  | 等待在途请求完成  |                |
   |-- 等待退出-------|                  |                |
   |<-- 进程退出 ------|                  |                |
   |                  |                  |                |
   |---------------- 流量已切到新pod -------------------->

步骤拆解:

  1. k8s 创建新 pod,等待它就绪(readiness 探针通过)。
  2. 此时旧 pod 仍在服务。
  3. 滚动更新策略(默认 rollingupdate)会选一个旧 pod 删除。
  4. 在删除过程中,k8s 先执行 prestop 钩子,把健康状态置 down,service 摘除该 pod。
  5. k8s 等待 terminationgraceperiodseconds(默认 30 秒)后发送 sigterm。
  6. sigterm 触发 spring 优雅停机:web 容器停止接收新请求,已接收请求快速完成,spring 容器释放资源。
  7. 进程退出,滚动更新继续下一台。

关键点prestopsigterm 之间有一段等待,这是典型的“优雅停机窗口”。你需要确保这段时间足够让流量列表同步,且存量请求能完成。如果你的服务需要长耗时处理,就应增加 terminationgraceperiodseconds 并相应调整 spring 的 timeout-per-shutdown-phase

8. 配置对比与选型指南

8.1 不同部署方式下的配置一览表

部署方式摘除流量手段健康检查优雅停机配置主要注意点
虚拟机/裸机 + nginxnginx upstream 手动剔除或脚本依赖 nginx 健康检查server.shutdown=graceful需脚本配合 nginx reload,停机钩子需等待 nginx 同步
docker + k8sservice endpoint 自动摘除(readiness 失败)k8s 探针server.shutdown=graceful, prestop需定义 prestop 生效,注意 terminationgraceperiodseconds
spring cloud + nacos调用 deregisterspring boot actuatorserver.shutdown=graceful需在停机前主动注销,并等待客户端缓存刷新
单机测试server.shutdown=graceful直接 kill 观察钩子

8.2 停机参数对比表

参数默认值作用建议值
server.shutdownimmediate是否优雅停机graceful
spring.lifecycle.timeout-per-shutdown-phase30sspring 容器各阶段超时10-20s
server.tomcat.graceful-shutdown-timeout30s (tomcat 9.0.33+)tomcat 等待请求完成的超时与 spring 超时相一致或更大
terminationgraceperiodseconds (k8s)30sk8s 发送 sigterm 前的等待足够覆盖 prestop 和请求处理

8.3 常见误区与陷阱

误区/陷阱说明正确做法
kill -9 也能优雅停机强杀不会执行钩子只有 kill (sigterm) 才能触发
健康检查一直为 up不会摘除流量需自定义状态,结合停机标记
只配置 server.shutdown=graceful 就够了若负载均衡未摘除,新流量仍可能涌入需设计完整的摘除流程
@predestroy 中可以执行大量清理钩子执行也受总超时限制将耗时操作移出或异步处理
停机钩子执行顺序与声明顺序相同实际上 spring 容器销毁有特定顺序了解 bean 生命周期
在线程池 shutdown 后调用 submit 会抛异常但应避免提交新任务统一停新接口

9. 工程落地:一步到位的发布脚本与配置示例

下面提供一个可复制的生产级发布脚本框架(适用于 vm + nacos)及配置。

9.1 停止应用前,先摘除流量并等待

#!/bin/bash
# 假设服务名和端口
export service_name=my-service
export port=8080
# 1. 调用内部摘除接口
curl -x post http://localhost:${port}/internal/readiness/off
# 或调用 nacos 注销接口(如果有)
# curl -x put "http://localhost:8848/nacos/v1/ns/instance?servicename=${service_name}&ip=127.0.0.1&port=${port}&enabled=false"
# 2. 等待负载均衡器同步(默认 10 秒)
sleep 10
# 3. 优雅停止应用
kill $(pgrep -f "java -jar my-service.jar")
# 等待进程退出
wait $(pgrep -f "java -jar my-service.jar")  # 注意:需要捕获 pid

9.2 验证健康检查的 curl 命令

# 检查应用健康状态
curl http://localhost:8080/actuator/health | jq .
# 模拟摘除后健康检查
curl -x post http://localhost:8080/internal/readiness/off
curl http://localhost:8080/actuator/health # 应返回 down

10. 生产实践建议

  • 先摘流量,后发 sigterm:确保没有新流量再来。
  • 预留缓冲时间:摘除操作后至少等待 10 秒,具体取决于注册中心/负载均衡器的同步周期。
  • 合理设置超时:根据你的业务处理时长设置 spring 和 k8s 的超时,避免过长拖慢发布。常见生产配置:server.shutdown=graceful, spring.lifecycle.timeout-per-shutdown-phase=20s, k8s terminationgraceperiodseconds=35s
  • 将健康检查与停机联动:用自定义 healthindicatorprestop 让探针失败。
  • 监控停机过程:在日志中记录请求数、活跃线程数、关闭阶段耗时,便于排障。
  • 使用新的 spring boot 版本:老版本(2.3 之前)需要自己实现,强烈建议升级。

10.1 各类异常对应的排查思路(排障清单)

现象可能原因排查和解决
停机时 502/504负载均衡未摘除或摘除太晚检查健康检查是否失败;缩短摘除等待;增加等待时间
停机时数据库连接异常在请求未完成时连接池已关闭确保容器关闭顺序正确;调整 spring.datasource.hikari... 的关闭顺序;在 @predestroy 中先关闭业务线程池再关闭数据源
钩子未执行使用了 kill -9 或钩子里有异常改用 sigterm;捕获异常并记录日志
进度时间长导致容器被杀超时设置过短增加 terminationgraceperiodseconds 和 spring 超时
摘除后仍有新请求客户端缓存了旧的实例列表增加等待时间;强制清空本地 dns 或 ribbon 缓存

11. 面试/复盘问题

  1. spring boot 2.3+ 优雅停机需要设置哪些参数?底层原理是什么?
  2. 什么是停机钩子?它在 jvm 中如何触发?与 @predestroy 的关系?
  3. 在 kubernetes 中,如何利用探针优雅摘除流量?prestop 和 sigterm 的配合?
  4. 如果业务中有一个 10 分钟的任务,你怎么设计优雅下线方案?
  5. 健康检查返回 down 一定会摘除流量吗?取决于什么?
  6. 为什么要在停机前先注销实例?直接 kill 会有什么风险?

12. 总结:优雅下线决策清单

最后,给你一张实用决策清单,当你要配置优雅下线时:

  • 是否使用了 spring boot 2.3+? → 是:开启 server.shutdown=graceful;否:升级或手动实现。
  • 是否部署在 k8s? → 是:配置 prestop 和就绪探针;并在 prestop 中使就绪状态为 down;再发送 sigterm。
  • 是否使用注册中心(nacos/eureka)? → 是:在发布脚本中先调用下线 api,并等待 10 秒。
  • 是否有长时间运行的任务? → 是:增加 terminationgraceperiodseconds 和 spring 超时,并确保任务可中断。
  • 是否需要自动恢复? → 如果是滚动发布,k8s 自动完成;否则需脚本等待进程退出。

记住最小模型:摘流量 → 停新 → 处理存量 → 释放资源。只要每一步都明确由哪个组件负责,执行顺序正确,就能让用户无感。优雅下线不是银弹,但它是微服务生命周期中不可省略的一环。

以上就是spring应用优雅下线的完整指南的详细内容,更多关于spring应用优雅下线的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com