📖目录
- 前言
- [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 池来设置 maxSurge 和 maxUnavailable:
yaml
# deployment.yaml
spec:
strategy:
rollingUpdate:
maxSurge: 50% # 默认50%的扩容比例
maxUnavailable: 25% # 默认25%的不可用比例
大白话解释 :
就像超市搬家,如果新店有足够位置(50%以上IP池剩余),就一次搬50%的货品;如果只剩1个位置(IP不足),就"先搬1个,再搬1个"(maxSurge: 0 且 maxUnavailable: 1)。
具体规则:
- 如果 IP 足够(50%以上剩余):
maxSurge: 50% - 如果 IP 不足(只剩 N 个):
maxSurge: N - 如果 IP 极度不足:
maxSurge: 0且maxUnavailable: 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 验证优雅停机
-
向服务发送
kill <PID>命令 -
观察日志输出:
log2025-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 验证服务下线
- 在 Eureka Dashboard 中观察服务实例状态
- 下线时状态应从
UP→OUT_OF_SERVICE→DOWN - 确认其他服务不再调用该实例
9. 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 服务下线后仍有请求失败 | 未配置优雅下线等待时间 | 增加 handler.setWaitTime(30) |
| 健康检查失败导致流量不分配 | 健康检查路径配置错误 | 确认路径为 /actuator/health |
| 流量切换过快导致服务抖动 | 负载均衡器权重调整过快 | 降低 fail_timeout 和 max_fails |
| 服务启动后无法加入负载均衡 | 健康检查超时时间过短 | 增加 health.liveness.timeout |
| K8S 中 Pod 下线过快 | 未设置 terminationGracePeriodSeconds |
设置为 120 秒 |
10. 结语
优雅停机不是技术的终点,而是微服务系统高可用的起点。正如超市关门时的"温馨提示",它让系统在下线时保持用户体验的连续性,避免"突然断电"的尴尬。
"优雅停机不是让服务完美退出,而是确保每个请求都能完成。" ------ 这句话,正是我们每个微服务开发者应该铭记的。
上一篇参考 :【后端】蓝绿发布全链路改造详解:从配置到生产环境的完整实践
下一篇预告:【微服务】【部署】 ③ 深度解密:从"快递分拣"到"智能路由"的全链路流量调度
✨ 本文为原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。