線上服務(wù)如何優(yōu)雅停機?
前言
最近星球中有位小伙伴問了我一個問題:如何優(yōu)雅的停機?
我覺得這個問題挺有代表性的。
今天這篇文章跟大家一下優(yōu)雅停機的一些常見方案,希望對你會有所幫助。
1.什么是優(yōu)雅停機?
優(yōu)雅停機(Graceful Shutdown) 指在服務(wù)終止前,系統(tǒng)能:
- 拒絕新請求進入
- 完成存量請求處理
- 釋放所有資源
- 通知上下游服務(wù)
非優(yōu)雅停機的慘痛代價:
圖片
真實案例:支付回調(diào)丟失。
// 支付回調(diào)處理
@PostMapping("/callback")
public void handleCallback(Payment payment) {
// 1. 更新訂單狀態(tài)
orderService.updateStatus(payment.getOrderId(), PAID);
// 2. 發(fā)放權(quán)益(kill發(fā)生時此處未執(zhí)行)
benefitService.grantVip(payment.getUserId());
}當kill發(fā)生在步驟1和2之間時,導(dǎo)致訂單狀態(tài)已更新但權(quán)益未發(fā)放,引發(fā)用戶投訴。
2.優(yōu)雅停機三大核心流程
2.1 信號捕獲層
圖片
2.2 流量控制層
圖片
2.3 資源釋放層
圖片
3.Spring Boot優(yōu)雅停機的實現(xiàn)
3.1 基礎(chǔ)配置
在SpringBoot項目的application.yml文件中增加如下配置:
server:
shutdown: graceful # 開啟優(yōu)雅停機
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最長等待時間3.2 線程池優(yōu)雅關(guān)閉
在線程池中實現(xiàn)優(yōu)雅關(guān)閉功能:
@Bean
public ExecutorService threadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待任務(wù)完成
executor.setAwaitTerminationSeconds(60); // 最大等待時間
return executor.getThreadPoolExecutor();
}在shutdown之前,先等待任務(wù)完成。
3.3 分布式鎖釋放攔截器
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object handleRequest(ProceedingJoinPoint pjp) {
Lock lock = redisson.getLock("order_lock");
try {
lock.lock();
return pjp.proceed();
} finally {
if (!isShuttingDown()) {
lock.unlock(); // 非停機時正常釋放
}
// 停機時由鎖管理器統(tǒng)一釋放
}
}使用統(tǒng)一的攔截器釋放分布式鎖,防止出現(xiàn)異常有釋放遺漏的地方。
4.Kubernetes環(huán)境下的優(yōu)雅停機
4.1 關(guān)鍵配置
STOPSIGNAL SIGTERM # 使用SIGTERM替代SIGKILL# Deployment配置
spec:
terminationGracePeriodSeconds:60# 寬限期
containers:
-lifecycle:
preStop:
exec:
command:["/bin/sh","-c","sleep 20;"]# 預(yù)留緩沖時間在部署配置中增加預(yù)留緩沖時間。
4.2 就緒探針自動摘流
圖片
5.中間件連接優(yōu)雅關(guān)閉
5.1 數(shù)據(jù)庫連接池
@PreDestroy
public void close() {
HikariPool pool = dataSource.getHikariPoolMXBean();
pool.suspendPool(); // 停止借出連接
pool.softEvictConnections(); // 驅(qū)逐空閑連接
while (pool.getActiveConnections() > 0) {
Thread.sleep(500); // 等待活動連接完成
}
pool.shutdown(); // 徹底關(guān)閉
}使用@PreDestroy在服務(wù)銷毀之前關(guān)閉數(shù)據(jù)庫連接池。
5.2 RabbitMQ消費者
@PreDestroy
public void stop() {
channel.basicCancel(consumerTag); // 取消訂閱
while (unackedMessages.get() > 0) {
Thread.sleep(100); // 等待ACK完成
}
connection.close();
}@PreDestroy在服務(wù)銷毀之前取消訂閱,需要先等待ACK完成。
3. Redis分布式鎖
public class LockManager implements DisposableBean {
@Override
public void destroy() {
lockMap.forEach((key, lock) -> {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 強制釋放未解鎖的鎖
}
});
}
}實現(xiàn)DisposableBean接口,在服務(wù)銷毀之前強制釋放未解鎖的鎖。
6.全鏈路優(yōu)雅停機
6.1 停機事件傳播機制
圖片
6.2 狀態(tài)機管理
public enum ShutdownState {
RUNNING, // 正常運行
PRE_SHUTDOWN, // 拒絕新請求
DRAINING, // 排空存量請求
TERMINATED // 完全終止
}6.3 停機監(jiān)控面板
圖片
7.生產(chǎn)環(huán)境避坑指南
7.1 必須避免的四大陷阱
陷阱 | 后果 | 解決方案 |
死鎖等待 | 無法完成停機 | 設(shè)置鎖超時時間 |
第三方服務(wù)不可用 | 資源無法釋放 | 添加熔斷機制 |
長周期任務(wù) | 超過寬限期被強殺 | 拆分任務(wù)+保存中間狀態(tài) |
文件寫入未完成 | 數(shù)據(jù)損壞 | 使用原子文件替換 |
7.2 停機檢查清單
# 停機前執(zhí)行
curl -X POST http://localhost:8080/actuator/shutdown-prepare
# 驗證項:
1. 新請求返回503
2. 活動線程數(shù)持續(xù)下降
3. 數(shù)據(jù)庫連接數(shù)歸零
4. MQ無未ACK消息7.3 黃金法則:二段式停機
圖片
總結(jié)
- 基礎(chǔ)層:處理HTTP請求Spring Boot Graceful Shutdown + 線程池等待
- 進階層:管理中間件連接數(shù)據(jù)庫連接池排空 + MQ消費者取消訂閱
- 高級層:分布式協(xié)同停機事件廣播 + 分布式鎖釋放
- 終極層:全鏈路狀態(tài)管理停機狀態(tài)機 + 智能超時控制
停機策略對比表
策略 | 實現(xiàn)難度 | 停機時間 | 數(shù)據(jù)安全 | 適用場景 |
直接kill -9 | ☆ | 秒級 | 極低 | 開發(fā)環(huán)境 |
Spring Boot | ☆☆ | 10-30s | 中 | 常規(guī)Web應(yīng)用 |
容器化方案 | ☆☆☆ | 可配置 | 高 | K8S環(huán)境 |
全鏈路管理 | ☆☆☆☆ | 分鐘級 | 極高 | 金融核心系統(tǒng) |































