← 返回上一頁
Kubernetes

K8s Pod 流量無損切換實戰:從壓測驗證到零停機滾動更新

本頁目錄

前言

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

關鍵問題:這兩個動作是異步執行的,沒有順序保證

可能發生以下情況:

  1. Pod 已經收到 SIGTERM 開始關閉
  2. 但 kube-proxy 還沒來得及更新 iptables
  3. 新的請求被路由到正在關閉的 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。這套配置經過壓測驗證,可以實現真正的零丟包滾動更新。

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料