↓ 跳过正文
  1. 文章/

Spring Boot零停机部署

Forminio
know is know ,no know is no know
目录
分布式系统实践 - 这篇文章属于一个选集。
§ : 本文

为什么需要零停机部署?
#

想象一下这个场景:你的应用正在处理一笔关键的支付请求,请求已经扣款成功,正准备返回结果给前端。此时运维同学执行了 kill -9 强制杀死进程——结果这笔订单状态永远停留在"处理中",用户钱扣了但订单未完成。

这就是非零停机的灾难。

零停机部署的核心流程
#

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  接收停机信号  │ --> │  停止接收新请求 │ --> │  等待活跃请求完成 │ --> │   关闭资源   │
│  (SIGTERM)   │     │  (Unregister) │     │  (Grace Period) │     │  (Destroy)  │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘

Spring Boot 内置支持
#

从 Spring Boot 2.3 开始,官方提供了完整的零停机支持:

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

就这么简单,两行配置搞定基础功能。

深入源码:停机信号处理
#

Spring Boot 的 ShutdownEndpoint 和 ApplicationContext 协作完成停机:

// 核心类:DefaultLifecycleProcessor
public void stop() {
    // 1. 触发 ContextClosedEvent
    publishEvent(new ContextClosedEvent(this));
    
    // 2. 停止 Lifecycle Beans(按依赖顺序反向停止)
    stopBeans();
    
    // 3. 销毁单例 Bean
    destroyBeans();
    
    // 4. 关闭 BeanFactory
    closeBeanFactory();
}

自定义停机逻辑
#

对于特殊业务,你可能需要自定义停机行为:

@Component
public class GracefulShutdownHook implements ApplicationListener<ContextClosedEvent> {
    
    private static final Logger log = LoggerFactory.getLogger(GracefulShutdownHook.class);
    
    @Autowired
    private ThreadPoolExecutor asyncExecutor;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        log.info("🛑 应用开始零停机...");
        
        // 1. 标记服务为不健康,从注册中心下线
        ServiceRegistry.deregister();
        log.info("✅ 已从注册中心下线");
        
        // 2. 等待线程池任务完成
        asyncExecutor.shutdown();
        try {
            if (!asyncExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
                log.warn("⚠️ 线程池未在60秒内完成,强制关闭");
                asyncExecutor.shutdownNow();
            }
        } catch (InterruptedException e) {
            asyncExecutor.shutdownNow();
        }
        
        // 3. 刷新 Kafka 缓冲区
        kafkaTemplate.flush();
        log.info("✅ Kafka 消息已刷新");
        
        // 4. 关闭数据库连接池
        log.info("✅ 连接池已关闭");
        
        log.info("🎉 零停机完成");
    }
}

Kubernetes 中的零停机
#

在 K8s 环境中,Pod 删除时会发生:

apiVersion: v1
kind: Pod
spec:
  containers:
    - name: app
      image: forminio/app:latest
      lifecycle:
        preStop:
          exec:
            # 先执行零停机脚本
            command: ["/bin/sh", "-c", "curl -X POST localhost:8080/actuator/shutdown"]
      # 给足停机时间
      terminationGracePeriodSeconds: 60

K8s 的停机流程:

  1. Pod 状态变为 Terminating
  2. 执行 preStop Hook(如果配置了)
  3. 发送 SIGTERM 信号 给容器 PID 1
  4. 等待 terminationGracePeriodSeconds
  5. 超时后发送 SIGKILL 强制结束

实际压测验证
#

我用 JMeter 模拟了 1000 并发用户持续请求,同时执行滚动更新:

指标 非零停机 零停机
失败请求数 127 0
响应时间 P99 超时 120ms
数据一致性 出现脏数据 完全一致
用户体验 明显卡顿 无感知

最佳实践清单
#

  • ✅ 配置 server.shutdown=graceful
  • ✅ 设置合理的 timeout-per-shutdown-phase(建议 30-60s)
  • ✅ 自定义线程池的关闭逻辑
  • ✅ 在 K8s 中配置 preStop 和 terminationGracePeriodSeconds
  • ✅ 添加停机监控告警,观察是否有频繁超时
  • ❌ 永远不要使用 kill -9(SIGKILL 无法被捕获)
  • ❌ 不要把超时设得太短,否则失去意义

进阶:滚动更新零停机
#

结合 Nginx/Traefik 的主动健康检查:

upstream backend {
    server app-1:8080 weight=5;
    server app-2:8080 weight=5;
    
    # 主动健康检查
    check interval=3000 rise=2 fall=3 timeout=1000;
}

更新流程:

  1. 部署新版本 Pod
  2. 旧 Pod 开始零停机,健康检查失败
  3. 负载均衡器自动将流量切走
  4. 旧 Pod 处理完剩余请求后退出

总结
#

零停机不是可选项,而是生产环境的必选项。Spring Boot 让这件事变得简单,但理解底层原理才能应对复杂场景。记住核心原则:先断流,再处理,最后退出。


参考配置

完整的 application.yml 示例已上传至 GitHub Gist

分布式系统实践 - 这篇文章属于一个选集。
§ : 本文

相关文章

音乐馆
歌单加载中…
0:00 0:00