SpringNotes

第 29 章:优雅上下线

zjc 于 2026-01-29 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 优雅上线确保实例准备好才接流量;优雅下线确保实例停止前排空流量、处理完任务并释放资源。否则发布时会出现连接拒绝、502、请求中断和消息重复。

29.1 问题场景

上线:

Pod 启动完成
  -> 端口已监听
  -> 但缓存未预热 / 数据源未就绪
  -> 被导入流量
  -> 请求超时

下线:

Pod 收到 SIGTERM
  -> 立即关闭连接
  -> 在途请求失败
  -> 消费位移未提交
  -> 任务未完成

29.2 启动就绪

Spring Boot 就绪事件:

@Component
public class WarmUpRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        warmUpLocalCache();
    }
}

要求:

  1. readiness 在预热完成后通过;
  2. 慢预热异步化或分批;
  3. 外部依赖检查不阻塞过久;
  4. 启动超时合理;
  5. 失败策略明确。

29.3 Kubernetes 探针

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 2
  failureThreshold: 60
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 2

发布顺序:

1. 新 Pod 启动
2. startup 通过
3. readiness 通过
4. Service 导流量
5. 旧 Pod 缩容

maxUnavailable: 0 可以避免发布过程容量下降。

29.4 优雅停机配置

Spring Boot:

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

Kubernetes:

terminationGracePeriodSeconds: 45

流程:

SIGTERM
  -> Pod Endpoint 被移除
  -> readiness 变为 not ready
  -> 停止接收新请求
  -> 等待在途请求
  -> 关闭 Bean 和线程池
  -> 进程退出

应用总等待时间必须小于 terminationGracePeriodSeconds,留出余量。

29.5 preStop

由于 Endpoint 更新和负载均衡刷新有延迟,可增加:

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 5"]

作用:

  1. 等待服务发现摘流;
  2. 避免旧实例仍被调用;
  3. 给网关和客户端缓存同步时间。

sleep 只是简单方案,更完善方案由平台注入下线钩子。

29.6 关闭线程池

@PreDestroy
public void shutdown() throws InterruptedException {
    executor.shutdown();
    if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
}

任务要求:

  1. 支持中断;
  2. 有执行时长上限;
  3. 未完成任务持久化或记录;
  4. 幂等可恢复;
  5. 关闭日志记录剩余任务数。

29.7 消费者下线

流程:

1. 停止拉取新消息
2. 处理完当前批次
3. 提交 offset
4. 关闭 consumer

注意:

  1. 处理超时必须小于总停机时间;
  2. 位移提交失败会重复消费;
  3. 消费必须幂等;
  4. 不能在处理一半时 shutdownNow;
  5. 长事务要拆批。

29.8 定时任务下线

要求:

  1. 停止调度新任务;
  2. 正在执行的任务有最大时长;
  3. 分布式锁要能释放;
  4. 执行状态持久化;
  5. 未完成任务可恢复。

ShedLock 示例:

@Scheduled(cron = "0 */5 * * * *")
@SchedulerLock(name = "settlement", lockAtMostFor = "10m")
public void settle() {
}

停机时不要强杀,应等待当前分片完成或记录断点。

29.9 验证

发布压测:

1. 持续压测请求
2. 滚动发布服务
3. 观察错误率
4. 观察连接拒绝
5. 观察消息重复
6. 观察日志和 trace

验收标准:

  1. 发布期间 5xx 不增加;
  2. 无 Connection refused;
  3. 无未完成请求中断;
  4. 消息无丢失;
  5. 定时任务无重复执行;
  6. Pod 正常退出而不是 SIGKILL。

本章小结

优雅上下线是发布稳定性的基础。上线依赖 readiness 和预热策略,下线依赖 SIGTERM、摘流延迟、请求排空、线程池关闭、消息位移提交和任务状态持久化。总停机时间必须小于平台宽限期,并通过发布压测验证。

思考题

  1. 为什么端口可访问不等于应用可服务?
  2. preStop sleep 解决什么问题?
  3. 优雅停机时间如何设置?
  4. 消费者下线时为什么要先停止拉取?
  5. 如何验证发布过程零请求失败?