← 返回上一頁
Kubernetes

Kubernetes Pod 終止時 preStop 鉤子與 SIGTERM 的執行邏輯:實驗驗證無縫發版

本頁目錄
Kubernetes Pod 優雅關閉流程圖

前言

在 Kubernetes 環境中進行應用更新時,最怕的就是「發版的那幾秒鐘使用者剛好踩到 502」。這篇文章記錄了我透過實驗的方式,驗證 Pod 終止時 preStop 鉤子與 SIGTERM 訊號的執行邏輯,搞清楚怎麼做才能達到真正的無縫發版。

Kubernetes Pod 優雅關閉流程圖

Pod 優雅關閉流程示意(圖片來源:DevOpsCube)

實驗設計

我的整體實驗思路很簡單——先跑一個沒有 preStop 的 Deployment,再跑一個 preStop 的版本,對比兩者在 rollout restart 時的行為差異。具體步驟如下:

  1. 用死循環每秒對 Pod IP 發 HTTP 請求,記錄回應狀態碼與時間戳。
  2. 用死循環每秒取得 Pod 狀態(kubectl get pods),記錄 Pod 生命週期。
  3. 用死循環每秒取得 Endpoints 物件的位址清單,觀察切換時機。
  4. 執行 rollout restart 並記錄時間。
  5. 等部署完成後停止上述監控,分析 log。

實驗一:無 preStop 設定

建立資源

先部署一個基本的 nginx Deployment,沒有設定任何 lifecycle hook:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-nginx-v1
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels:
      zapp: nginx
  template:
    metadata:
      labels:
        zapp: nginx
    spec:
      containers:
      - image: nginx:1.21.6
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /index.html
            port: 80
            scheme: HTTP
          initialDelaySeconds: 5
          periodSeconds: 5
          successThreshold: 1
          timeoutSeconds: 3
      terminationGracePeriodSeconds: 30

搭配一個 ClusterIP Service:

apiVersion: v1
kind: Service
metadata:
  name: service-nginx
  namespace: test
spec:
  selector:
    zapp: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

監控指令

# 持續對 Pod IP 發請求
while true; do echo $(date +"%Y-%m-%d %H:%M:%S") - \
  $(curl -w "%{http_code} %{time_total}\n" -o /dev/null -s -m 1 http://<POD_IP>); sleep 1; done >> request-v1.log

# 持續取得 Pod 狀態
while true; do date && kubectl -n test get pods -l zapp=nginx -o wide; sleep 1; done >> pod-v1.log

# 持續取得 Endpoints
while true; do date +"%Y-%m-%d %H:%M:%S" && kubectl -n test get endpoints service-nginx; sleep 1; done >> endpoints-v1.log

觀察結果

時間 事件
23:39:58 執行 rollout restart
23:39:59 新 Pod 建立
23:40:08 新 Pod 就緒,舊 Pod 進入 Terminating
23:40:08 直接對舊 Pod IP 的 HTTP 請求回應碼從 200 變成 000(連線失敗)
23:40:08 Endpoints 從舊 Pod IP 切換到新 Pod IP
23:40:10 舊 Pod 完全消失

結論:沒有 preStop 的情況下,新 Pod 就緒後 kubelet 立刻對舊 Pod 容器發送 SIGTERM,nginx 主進程幾乎瞬間結束,舊 Pod IP 馬上不通。

實驗二:加上 preStop(sleep 20)

調整 Deployment

在容器 spec 中加入 lifecycle.preStop

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 20"]

觀察結果

時間 事件
23:51:57 執行 rollout restart
23:51:57 新 Pod 建立
23:52:08 新 Pod 就緒,舊 Pod 進入 Terminating
23:52:08 Endpoints 切換到新 Pod IP(Service 流量已切走)
23:52:08 ~ 23:52:28 直接對舊 Pod IP 的請求仍然正常回應 200(preStop sleep 中,nginx 未收到 SIGTERM)
23:52:28 preStop 執行完畢,SIGTERM 發出,舊 Pod IP 不通
23:52:29 舊 Pod 完全消失

關鍵發現:preStop 期間(約 20 秒),nginx 進程持續運行,已經轉發到舊 Pod 的請求能正常處理完畢。同時 Endpoints 在舊 Pod 進入 Terminating 後就立刻移除了它,所以 Service 不會再把新請求導過去。這就是「無縫」的關鍵。

Kubernetes Pod 終止生命週期時間線

Pod Termination 時間線(圖片來源:CNCF)

實驗三:preStop 超過 terminationGracePeriodSeconds

如果 preStop 設定 sleep 40,但 terminationGracePeriodSeconds 只有 30 秒,會怎樣?

觀察結果

時間 事件
00:00:35 執行 rollout restart
00:00:36 新 Pod 建立
00:00:46 新 Pod 就緒,舊 Pod 進入 Terminating
00:01:17 舊 Pod 被強制終止(持續約 31 秒)

preStop 雖然設了 40 秒,但超過 terminationGracePeriodSeconds 的 30 秒後,kubelet 直接發送 SIGKILL 強制殺掉容器。所以 preStop 的時間必須小於 terminationGracePeriodSeconds,否則就白搭了。

總結與最佳實踐

  1. preStop 鉤子先於 SIGTERM:配置了 preStop 後,kubelet 會先執行 preStop 邏輯,完成後才對容器發送 SIGTERM。
  2. Endpoints 即時移除:Pod 一進入 Terminating 狀態,Endpoints Controller 就會把它從清單中移除,Service 不會再把新流量導向舊 Pod。
  3. preStop 期間舊 Pod 仍能處理請求:已經轉發過來的 in-flight 請求可以正常完成,不會 502。
  4. terminationGracePeriodSeconds 是硬上限:preStop + 應用收到 SIGTERM 後的清理時間,加總不能超過這個值,否則會被 SIGKILL 強殺。
  5. 建議配置preStop sleep 時間設定在 5~15 秒即可(讓 iptables/IPVS 規則有時間同步),terminationGracePeriodSeconds 設定為 preStop 時間 + 應用優雅關閉時間 + 適當 buffer。

參考資料

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

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

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