【微服务】【部署】 ② 优雅停机 - 从“关门打烊“到“无缝交接“的实战指南

📖目录

  • 前言
  • [1. 为什么需要优雅停机?------大白话解读](#1. 为什么需要优雅停机?——大白话解读)
  • [2. 优雅停机的核心要素](#2. 优雅停机的核心要素)
    • [2.1 优雅下线:提前切流 + ELB流量/IP流量](#2.1 优雅下线:提前切流 + ELB流量/IP流量)
    • [2.2 优雅上线:健康检测](#2.2 优雅上线:健康检测)
    • [2.3 Spring Boot 的 Shutdown Hook 机制](#2.3 Spring Boot 的 Shutdown Hook 机制)
  • [3. 优雅停机的实现原理](#3. 优雅停机的实现原理)
    • [3.1 核心思想:流量平稳切换](#3.1 核心思想:流量平稳切换)
    • [3.2 优雅停机的流程图](#3.2 优雅停机的流程图)
  • [4. Spring Boot 优雅停机实战](#4. Spring Boot 优雅停机实战)
    • [4.1 前置配置:Spring Boot + Spring Cloud LoadBalancer](#4.1 前置配置:Spring Boot + Spring Cloud LoadBalancer)
      • [4.1.1 添加依赖(pom.xml)](#4.1.1 添加依赖(pom.xml))
      • [4.1.2 配置健康检查(application.yml)](#4.1.2 配置健康检查(application.yml))
      • [4.1.3 实现优雅下线逻辑](#4.1.3 实现优雅下线逻辑)
    • [4.2. Ribbon 配置(如果使用Ribbon)](#4.2. Ribbon 配置(如果使用Ribbon))
  • [5. Kubernetes (K8S) 优雅停机配置](#5. Kubernetes (K8S) 优雅停机配置)
    • [5.1 K8S 滚动更新策略](#5.1 K8S 滚动更新策略)
    • [5.2 延长 Pod 下线总时长](#5.2 延长 Pod 下线总时长)
    • [5.3 K8S 中的 preStop 脚本(通知 Eureka)](#5.3 K8S 中的 preStop 脚本(通知 Eureka))
  • [6. RocketMQ 优雅停机配置](#6. RocketMQ 优雅停机配置)
    • [6.1 消费者优雅停机](#6.1 消费者优雅停机)
    • [6.2 生产者优雅停机](#6.2 生产者优雅停机)
  • [7. Spring Boot & Spring Cloud 的优雅停机/开机配置详解](#7. Spring Boot & Spring Cloud 的优雅停机/开机配置详解)
    • [7.1 Spring Boot 的优雅停机配置](#7.1 Spring Boot 的优雅停机配置)
      • [7.1.1 启用优雅停机模式](#7.1.1 启用优雅停机模式)
      • [7.1.2 优雅停机的触发方式](#7.1.2 优雅停机的触发方式)
    • [7. 2 Spring Cloud 的优雅下线配置(以 Eureka 为例)](#7. 2 Spring Cloud 的优雅下线配置(以 Eureka 为例))
      • [7.2.1 服务下线时主动注销](#7.2.1 服务下线时主动注销)
    • [7.3 优雅停机/开机的完整配置示例](#7.3 优雅停机/开机的完整配置示例)
  • [8. 配置效果验证](#8. 配置效果验证)
    • [8.1 验证优雅停机](#8.1 验证优雅停机)
    • [8.2 验证服务下线](#8.2 验证服务下线)
  • [9. 常见问题与解决方案](#9. 常见问题与解决方案)
  • [10. 结语](#10. 结语)

前言

为什么超市关门时,总要贴个"正在整理,稍后营业"的告示牌?因为要让正在结账的顾客先结完账,再关门。微服务系统同样需要"优雅停机"------不是突然断电,而是让服务在下线前,先将流量平稳转移到其他实例,确保每个请求都能完成,不丢失、不中断。


1. 为什么需要优雅停机?------大白话解读

想象一下:你正在超市结账,突然收银台"啪"地关机了,你刚付完钱,系统却提示"支付失败"。这种体验糟透了,对吧?微服务系统也一样,如果服务突然下线,正在处理的请求就会失败,导致用户看到"500错误",甚至可能造成数据丢失。

优雅停机(Graceful Shutdown) 就是让服务在下线前,先"告诉"负载均衡器"我马上要下线了",然后等待当前请求完成,再将流量切走。就像超市"打烊前"会先贴个告示,告诉顾客"我们10分钟后关门,正在结账的请继续"。


2. 优雅停机的核心要素

2.1 优雅下线:提前切流 + ELB流量/IP流量

大白话解释

就像你搬家前,会先通知快递公司"我下周要搬走,新地址是XXX",让快递员把后续包裹发到新地址。服务下线前,需要先通知负载均衡器"我即将下线",让流量慢慢转移到其他健康实例。

2.2 优雅上线:健康检测

大白话解释

就像新店开业,会先测试水电、网络是否正常,再正式开门营业。服务上线后,需要先通过健康检查,确认服务正常,再将流量引入。

2.3 Spring Boot 的 Shutdown Hook 机制

大白话解释

Spring Boot 提供了"关机钩子",就像手机的"关机提醒",在系统要关闭前,会先执行一段代码,做些收尾工作(比如清理资源、完成当前请求)。


3. 优雅停机的实现原理

3.1 核心思想:流量平稳切换

优雅停机的数学本质是:
流量切换率 = 当前在线实例数 − 下线实例数 总实例数 \text{流量切换率} = \frac{\text{当前在线实例数} - \text{下线实例数}}{\text{总实例数}} 流量切换率=总实例数当前在线实例数−下线实例数

当实例准备下线时,系统会逐步降低其权重,直到流量完全转移到其他实例。这个过程类似于:

复制代码
初始流量分配:A:50%, B:50% → 服务A准备下线
逐步调整:A:40%, B:60% → A:30%, B:70% → A:20%, B:80% → ...
最终:A:0%, B:100%

3.2 优雅停机的流程图

plaintext 复制代码
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ 服务实例 A  │      │ 服务实例 B  │      │ 服务实例 C  │
└─────────────┘      └─────────────┘      └─────────────┘
        │                    │                    │
        ▼                    ▼                    ▼
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  负载均衡器  │      │  负载均衡器  │      │  负载均衡器  │
└─────────────┘      └─────────────┘      └─────────────┘
        │                    │                    │
        ▼                    ▼                    ▼
┌─────────────────────────────────────────────────────┐
│                流量监控与切换系统                  │
└─────────────────────────────────────────────────────┘
        │
        ▼
┌─────────────┐
│  用户请求   │
└─────────────┘

4. Spring Boot 优雅停机实战

4.1 前置配置:Spring Boot + Spring Cloud LoadBalancer

注意:Ribbon 已被 Spring Cloud LoadBalancer 替代,推荐使用 LoadBalancer。

4.1.1 添加依赖(pom.xml)

xml 复制代码
<!-- Spring Cloud LoadBalancer -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Boot Actuator(健康检查) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

4.1.2 配置健康检查(application.yml)

yaml 复制代码
# 启用健康检查端点
management:
  endpoints:
    web:
      exposure:
        include: health
  # 健康检查超时时间(秒)
  health:
    liveness:
      initial-delay: 10s
      timeout: 5s
    readiness:
      initial-delay: 10s
      timeout: 5s

4.1.3 实现优雅下线逻辑

java 复制代码
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GracefulShutdownConfig {

    // 优雅下线配置:设置等待时间
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> {
            factory.addProtocolHandlers(
                new TomcatProtocolHandlerCustomizer() {
                    @Override
                    public void customize(ProtocolHandler handler) {
                        // 设置等待时间(秒)
                        handler.setWaitTime(30); // 30秒等待时间
                    }
                }
            );
        };
    }

    // Spring Boot Shutdown Hook:服务下线前执行
    @Bean
    public ApplicationListener<ContextClosedEvent> gracefulShutdownListener() {
        return event -> {
            // 在服务下线前,先等待30秒,让当前请求完成
            try {
                Thread.sleep(30000); // 等待30秒
                System.out.println("服务已优雅下线,等待当前请求完成");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
    }
}

4.2. Ribbon 配置(如果使用Ribbon)

注意:Ribbon 已被 Spring Cloud LoadBalancer 替代,但如果你仍在使用 Ribbon,需要添加以下配置:

yaml 复制代码
# application.yml
ribbon:
  ServerListRefreshInterval: 10000 # 每10秒刷新服务列表,确保能及时获取最新实例

# Spring Boot 线程池配置
spring:
  mvc:
    async:
      request-timeout: 30000 # 30秒超时
  task:
    execution:
      thread-pool:
        core-size: 10
        max-size: 50
        queue-capacity: 1000
        wait-for-tasks-to-complete-on-shutdown: true # 等待所有任务完成
        await-termination-seconds: 10 # 等待10秒

5. Kubernetes (K8S) 优雅停机配置

5.1 K8S 滚动更新策略

K8S 滚动更新需要根据可用 IP 池来设置 maxSurgemaxUnavailable

yaml 复制代码
# deployment.yaml
spec:
  strategy:
    rollingUpdate:
      maxSurge: 50% # 默认50%的扩容比例
      maxUnavailable: 25% # 默认25%的不可用比例

大白话解释

就像超市搬家,如果新店有足够位置(50%以上IP池剩余),就一次搬50%的货品;如果只剩1个位置(IP不足),就"先搬1个,再搬1个"(maxSurge: 0maxUnavailable: 1)。

具体规则

  • 如果 IP 足够(50%以上剩余):maxSurge: 50%
  • 如果 IP 不足(只剩 N 个):maxSurge: N
  • 如果 IP 极度不足:maxSurge: 0maxUnavailable: 1(逐台滚动)

5.2 延长 Pod 下线总时长

yaml 复制代码
# deployment.yaml
spec:
  containers:
  - name: app
    image: my-app:latest
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 10"] # 停止前等待10秒
  terminationGracePeriodSeconds: 120 # 默认30秒,延长至120秒

大白话解释

就像超市打烊前,不会立刻关门,而是会等待120秒,确保所有正在结账的顾客都能完成交易。terminationGracePeriodSeconds 就是这个"等待时间"。


5.3 K8S 中的 preStop 脚本(通知 Eureka)

bash 复制代码
# preStop.sh(存储在存储卷中)
#!/bin/bash
# 通知 Eureka 注册中心,将当前实例状态改为 DOWN
curl -X POST http://eureka-server:8761/eureka/apps/${APP_NAME}/${INSTANCE_ID}/status?value=DOWN
sleep 10 # 确保请求完成
yaml 复制代码
# deployment.yaml
spec:
  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: prestop-script
      mountPath: /prestop.sh
      subPath: prestop.sh
      readOnly: true
    lifecycle:
      preStop:
        exec:
          command: ["/prestop.sh"]
  volumes:
  - name: prestop-script
    configMap:
      name: prestop-script-config

大白话解释

就像超市打烊前,会先给顾客发个短信"我们马上要关门了,请尽快结账"。preStop.sh 就是这个"短信",在 Pod 停止前通知 Eureka 将实例状态设为 DOWN。


6. RocketMQ 优雅停机配置

6.1 消费者优雅停机

java 复制代码
import org.springframework.context.SmartLifecycle;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.springframework.stereotype.Component;

@Component
public class RocketMQConsumer implements SmartLifecycle {

    private DefaultMQPushConsumer consumer;
    
    public RocketMQConsumer() {
        consumer = new DefaultMQPushConsumer("consumerGroup");
        // 设置消费参数
    }

    @Override
    public void start() {
        try {
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void stop() {
        // 先停止消费,等待当前消息处理完成
        consumer.shutdown();
    }

    @Override
    public boolean isRunning() {
        return consumer.isRunning();
    }
}

大白话解释

就像超市收银台,不会在顾客结账时突然关闭,而是先让当前顾客结完账,再关闭收银台。SmartLifecycle 确保消费者在 Spring 容器关闭前停止消费。


6.2 生产者优雅停机

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import javax.annotation.PreDestroy;

public class RocketMQProducer {
    
    private DefaultMQProducer producer;
    
    public RocketMQProducer() {
        producer = new DefaultMQProducer("producerGroup");
        // 设置生产者参数
    }
    
    @PreDestroy
    public void shutdown() {
        // 延后到 Spring 容器销毁后再关闭
        producer.shutdown();
    }
}

大白话解释

就像超市发货员,不会在顾客还没收到包裹前就停止发货,而是等到所有包裹都处理完再休息。@PreDestroy 确保生产者在 Spring 容器关闭后才停止。


7. Spring Boot & Spring Cloud 的优雅停机/开机配置详解

核心配置server.shutdown.graceful 是 Spring Boot 2.3+ 引入的优雅停机开关。


7.1 Spring Boot 的优雅停机配置

7.1.1 启用优雅停机模式

yaml 复制代码
# application.yml
server:
  shutdown: graceful # 启用优雅停机模式(Spring Boot 2.3+)
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s # 设置优雅停机的最大等待时间(默认30秒)

大白话解释

就像超市打烊前会贴告示"请结完账的顾客离开后,我们再关门"。这个配置告诉 Spring Boot:"等当前正在处理的请求都完成后,再关闭服务"。

7.1.2 优雅停机的触发方式

bash 复制代码
# 方式一:通过 kill 命令触发
kill <PID> # 发送 SIGTERM 信号(默认触发优雅停机)

# 方式二:通过 Actuator 端点触发
curl -X POST http://<服务地址>/actuator/shutdown

7. 2 Spring Cloud 的优雅下线配置(以 Eureka 为例)

7.2.1 服务下线时主动注销

yaml 复制代码
# application.yml
eureka:
  instance:
    # 自定义健康检查路径(Spring Cloud 默认使用 /actuator/health)
    health-check-url-path: /actuator/health
    # 设置服务下线时的等待时间(单位:毫秒)
    status-page-url-path: /actuator/info
    # 是否允许服务实例主动注销
    non-secure-port-enabled: true
    secure-port-enabled: false

大白话解释

就像快递站搬家前会先在系统里标记"这家站点即将关闭,请派件到新站点"。Eureka 会根据健康检查结果,自动将下线服务从注册中心移除,避免其他服务调用到已下线的实例。


7.3 优雅停机/开机的完整配置示例

yaml 复制代码
# application.yml
server:
  shutdown: graceful # 启用优雅停机
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s # 停机最大等待时间
  mvc:
    async:
      request-timeout: 30000 # 30秒超时
  task:
    execution:
      thread-pool:
        wait-for-tasks-to-complete-on-shutdown: true # 等待所有任务完成
        await-termination-seconds: 10 # 等待10秒
eureka:
  instance:
    health-check-url-path: /actuator/health # 健康检查路径
    status-page-url-path: /actuator/info # 状态页面路径
management:
  endpoints:
    web:
      exposure:
        include: health,info,shutdown # 暴露关键端点
  health:
    liveness:
      timeout: 5s # 活跃性检查
    readiness:
      timeout: 10s # 就绪性检查

8. 配置效果验证

8.1 验证优雅停机

  1. 向服务发送 kill <PID> 命令

  2. 观察日志输出:

    log 复制代码
    2025-12-02 10:00:00.000  INFO 12345 --- [main] o.s.b.w.e.t.TomcatWebServer : Graceful shutdown started
    2025-12-02 10:00:30.000  INFO 12345 --- [main] o.s.b.w.e.t.TomcatWebServer : Graceful shutdown completed

8.2 验证服务下线

  1. 在 Eureka Dashboard 中观察服务实例状态
  2. 下线时状态应从 UPOUT_OF_SERVICEDOWN
  3. 确认其他服务不再调用该实例

9. 常见问题与解决方案

问题 原因 解决方案
服务下线后仍有请求失败 未配置优雅下线等待时间 增加 handler.setWaitTime(30)
健康检查失败导致流量不分配 健康检查路径配置错误 确认路径为 /actuator/health
流量切换过快导致服务抖动 负载均衡器权重调整过快 降低 fail_timeoutmax_fails
服务启动后无法加入负载均衡 健康检查超时时间过短 增加 health.liveness.timeout
K8S 中 Pod 下线过快 未设置 terminationGracePeriodSeconds 设置为 120 秒

10. 结语

优雅停机不是技术的终点,而是微服务系统高可用的起点。正如超市关门时的"温馨提示",它让系统在下线时保持用户体验的连续性,避免"突然断电"的尴尬。

"优雅停机不是让服务完美退出,而是确保每个请求都能完成。" ------ 这句话,正是我们每个微服务开发者应该铭记的。


上一篇参考【后端】蓝绿发布全链路改造详解:从配置到生产环境的完整实践

下一篇预告:【微服务】【部署】 ③ 深度解密:从"快递分拣"到"智能路由"的全链路流量调度


✨ 本文为原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

相关推荐
梁萌1 小时前
微服务任务调度XXL-JOB实战(docker)
docker·微服务·xxl-job·定时任务
晚霞的不甘1 小时前
深度解析:Flutter 与 OpenHarmony 融合架构下的跨平台渲染机制与系统级集成
flutter·架构
喜欢你,还有大家1 小时前
k8s——日志采集方案
云原生·容器·kubernetes
做运维的阿瑞1 小时前
K8s 1.28.2 + Containerd + CentOS7.9 集群部署
云原生·容器·kubernetes
Molesidy1 小时前
【Embedded Development】【ARM】ARM架构的初步认识
arm开发·架构
晚霞的不甘2 小时前
架构演进与生态共建:构建面向 OpenHarmony 的 Flutter 原生开发范式
flutter·架构
Mintopia2 小时前
🚀 Supabase:强力的服务端助手
数据库·架构·全栈
曾经的三心草2 小时前
微服务的编程测评系统-修改登录逻辑为邮箱登录
微服务·云原生·架构
青春不流名2 小时前
docker build -t mytomcat:10.1-jdk17 -f Dockerfile-MyTomcat .
云原生·eureka