前言
「明明只是做了一次 Deployment 更新,為什麼使用者就收到 502 了?」——這是很多團隊在 Kubernetes 上跑 Spring 微服務時都遇過的問題。問題的根源在於 Pod 關閉時的處理不當。本篇將從問題現象出發,拆解 K8s 刪除 Pod 的完整流程,並給出一套可以直接套用的解決方案。
Pod 關閉時到底發生了什麼?
當 Kubernetes 要終止一個 Pod(不管是滾動更新、手動刪除、還是節點資源不足),有兩種可能的結局:
理想情況:優雅關閉
容器在寬限期內完成以下流程:執行 preStop Hook → 接收 SIGTERM 訊號 → 程式正常結束 → Kubelet 從 API Server 清除 Pod 記錄。
最壞情況:強制終止
如果容器在寬限期內沒有自行結束,Kubelet 會直接發送 SIGKILL。可能原因:
- 程式沒有處理 SIGTERM 訊號
- preStop Hook 跑太久
- 應用程式清理資源超時
結果就是進行中的請求被硬切斷,使用者看到錯誤頁面。
兩個會影響使用者的問題
每次 Deployment 更新都是「建新 Pod + 刪舊 Pod」同時進行。如果沒有做好優雅關閉,會碰到兩個問題:
問題一:進行中的請求被中斷
Pod 被刪除時,如果程式沒有處理 SIGTERM,容器會立即退出。正在處理的 HTTP 請求直接斷掉。如果這個請求涉及資料庫寫入且不是冪等操作,可能造成資料不一致。
問題二:流量還在往已死的 Pod 送
這個問題更隱蔽。Pod 刪除過程中有兩條平行的時間線:
| 時間線一:網路規則更新 | 時間線二:Pod 終止 |
|---|---|
| 1. API Server 標記 Pod 為 Terminating | 1. API Server 標記 Pod 為 Terminating |
| 2. Endpoint Controller 移除 Pod IP | 2. Kubelet 清理容器資源 |
| 3. kube-proxy 更新 iptables 規則 | 3. Kubelet 發送 SIGTERM |
| 4. 30秒後若未退出,發送 SIGKILL |
關鍵在於:這兩條時間線是並行執行的,沒有先後順序保證。Pod 可能在 kube-proxy 還沒更新完 iptables 規則前就已經被殺掉了,這時候新進來的請求會被路由到一個已經不存在的 Pod。
三步解決方案
第一步:Spring Boot 開啟優雅關閉
讓應用程式本身能正確回應 SIGTERM 訊號:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
效果:收到 SIGTERM 後,Spring Boot 會拒絕新請求,等待所有進行中的請求處理完畢(最多等待設定的超時時間),然後才結束程序。
第二步:加入 preStop Hook
解決時間線競爭問題——讓 Pod 在開始關閉前先「睡一會」,給 kube-proxy 足夠時間更新網路規則:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
這 10 秒的等待,確保新流量不再被路由到這個 Pod 後,才開始真正的關閉流程。
第三步:調整 terminationGracePeriodSeconds
K8s 預設給 30 秒寬限期,但我們現在的流程是:preStop 10 秒 + Spring 優雅關閉 30 秒 = 40 秒。必須把寬限期調大:
terminationGracePeriodSeconds: 45
完整的 Deployment 範本
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-spring-app
spec:
replicas: 3
selector:
matchLabels:
app: my-spring-app
template:
metadata:
labels:
app: my-spring-app
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: my-spring-app:latest
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
各設定之間的關係
務必確保:
terminationGracePeriodSeconds >= preStop 等待秒數 + 應用優雅關閉超時秒數
以上例為例:45 >= 10 + 30,多出 5 秒作為緩衝。如果你的應用需要更長的關閉時間,各數值都要相應調整。
不同框架的對應做法
| 框架 | 優雅關閉設定方式 |
|---|---|
| Spring Boot 2.3+ | server.shutdown=graceful |
| Go (net/http) | 攔截 SIGTERM,呼叫 server.Shutdown(ctx) |
| Node.js (Express) | 監聽 process.on('SIGTERM'),呼叫 server.close() |
| Python (Gunicorn) | 設定 graceful_timeout 參數 |
核心邏輯都一樣:攔截終止訊號 → 停止接受新請求 → 等待進行中請求完成 → 乾淨退出。
總結
Pod 關閉時的使用者請求中斷,根本原因是「網路規則更新」和「Pod 終止」這兩條時間線的競爭條件。解決方案是三管齊下:應用層優雅關閉 + preStop Hook 延遲 + 充足的寬限期。這三個設定缺一不可,建議作為所有 Spring(或任何 Web 框架)微服務在 K8s 上的標準配置模板。

發佈留言