前言
Kubernetes 的滾動更新(Rolling Update)號稱能實現零停機部署,但實際壓測後你會發現,預設配置下仍然會有少量請求失敗。這不是 K8s 的 bug,而是滾動更新過程中「移除 endpoint」和「終止 Pod」兩個動作的時序差異造成的。本篇將從壓測數據出發,深入分析問題根源,並給出經過驗證的零停機部署方案。
滾動更新策略回顧
Kubernetes Deployment 預設使用 RollingUpdate 策略。核心思想是:新版 Pod 啟動就緒後才終止舊版 Pod,確保過程中始終有足夠的實例提供服務。
兩個關鍵參數:
- maxSurge:更新過程中最多可以超額多少個 Pod(預設 25%)
- maxUnavailable:更新過程中最多允許多少個 Pod 不可用(預設 25%)
kind: Deployment
apiVersion: apps/v1
metadata:
name: demo
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
上述配置表示:每次只多建一個新 Pod,等它就緒後再終止一個舊 Pod,逐一替換。從 kubectl get pods 的輸出看起來確實很平滑。
壓測揭露的真相
理論上的平滑和實際的零丟包是兩回事。用負載測試工具(例如 Fortio)在滾動更新過程中進行壓測:
fortio load -a -c 50 -qps 500 -t 60s "http://example.com/demo"
50 個併發連線、每秒 500 個請求、持續 60 秒。結果:
Code 200 : 9933 (99.3 %)
Code 502 : 67 (0.7 %)
有 67 個請求收到了 502 錯誤。雖然比例不高,但對於金融交易、訂單提交等場景,每一個失敗的請求都可能造成實際損失。
問題根源分析
Pod 被終止時,Kubernetes 會做兩件事:
| 動作 | 細節 |
|---|---|
| 移除 Endpoint | 從 Endpoint 物件移除 Pod IP → kube-proxy 更新 iptables → 負載均衡器重新載入設定 |
| 終止 Pod | 發送 SIGTERM → 等待寬限期 → 發送 SIGKILL |
關鍵問題:這兩個動作是異步執行的,沒有順序保證。
可能發生以下情況:
- Pod 已經收到 SIGTERM 開始關閉
- 但 kube-proxy 還沒來得及更新 iptables
- 新的請求被路由到正在關閉的 Pod → 502
如果使用 Nginx Ingress Controller,情況可能更嚴重。Nginx Ingress 直接監聽 Endpoint 物件的變化,有變化時會重載 Nginx 設定,重載期間也可能造成短暫的流量中斷。
零停機部署的三個關鍵配置
1. 正確處理 SIGTERM 訊號
這是基礎前提。應用程式必須能在收到 SIGTERM 後:
- 停止接受新連線
- 等待進行中的請求處理完畢
- 釋放資源後乾淨退出
以 Spring Boot 為例:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
2. 配置 Readiness Probe
就緒探針確保 Pod 真正準備好處理流量後才被加入 Service Endpoint。理想的探針應該檢查所有需要預熱的功能(快取、資料庫連線池等):
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
3. 加入 preStop Hook
這是解決異步時序問題的核心。在 Pod 收到終止訊號前先等待一段時間,讓負載均衡器有足夠時間完成設定重載:
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 120"]
這個 sleep 的時間要夠長,確保在這段時間內 kube-proxy(或 Ingress Controller)已經完成 iptables 規則或 Nginx 設定的更新。通常 15-30 秒已足夠,但在大型叢集中可以適當拉長。
完整的 Deployment 配置
kind: Deployment
apiVersion: apps/v1
metadata:
name: demo
spec:
replicas: 3
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
terminationGracePeriodSeconds: 150
containers:
- name: demo
image: demo:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 120"]
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
注意 terminationGracePeriodSeconds 必須大於 preStop 等待時間 + 應用優雅關閉時間。
修復後的壓測結果
套用上述配置後,重新跑相同的壓測:
Code 200 : 10000 (100.0 %)
10000 個請求全部成功,真正實現了零丟包的滾動更新。
進階思考
- 多版本流量切換:在滾動更新的基礎上,可以透過部署多套 Ingress 資源來實現更精細的流量分配(如金絲雀發布)
- Helm 多版本管理:Helm 支援同一應用的多個版本並存,透過切換 Service Selector 可以實現秒級的版本切換
- PodDisruptionBudget:設定 PDB 可以確保在節點維護或驅逐場景下,也能維持最低可用副本數
總結
K8s 的滾動更新「號稱」零停機,但預設配置下確實會有少量請求失敗。問題的根源在於 Endpoint 移除和 Pod 終止的異步時序。解決方案是三管齊下:應用層正確處理 SIGTERM + preStop Hook 延遲關閉 + 充足的 terminationGracePeriodSeconds。這套配置經過壓測驗證,可以實現真正的零丟包滾動更新。

發佈留言