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

Pod 優雅關閉流程示意(圖片來源:DevOpsCube)
實驗設計
我的整體實驗思路很簡單——先跑一個沒有 preStop 的 Deployment,再跑一個有 preStop 的版本,對比兩者在 rollout restart 時的行為差異。具體步驟如下:
- 用死循環每秒對 Pod IP 發 HTTP 請求,記錄回應狀態碼與時間戳。
- 用死循環每秒取得 Pod 狀態(
kubectl get pods),記錄 Pod 生命週期。 - 用死循環每秒取得 Endpoints 物件的位址清單,觀察切換時機。
- 執行
rollout restart並記錄時間。 - 等部署完成後停止上述監控,分析 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 不會再把新請求導過去。這就是「無縫」的關鍵。

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,否則就白搭了。
總結與最佳實踐
- preStop 鉤子先於 SIGTERM:配置了 preStop 後,kubelet 會先執行 preStop 邏輯,完成後才對容器發送 SIGTERM。
- Endpoints 即時移除:Pod 一進入 Terminating 狀態,Endpoints Controller 就會把它從清單中移除,Service 不會再把新流量導向舊 Pod。
- preStop 期間舊 Pod 仍能處理請求:已經轉發過來的 in-flight 請求可以正常完成,不會 502。
- terminationGracePeriodSeconds 是硬上限:preStop + 應用收到 SIGTERM 後的清理時間,加總不能超過這個值,否則會被 SIGKILL 強殺。
- 建議配置:
preStop sleep時間設定在 5~15 秒即可(讓 iptables/IPVS 規則有時間同步),terminationGracePeriodSeconds設定為 preStop 時間 + 應用優雅關閉時間 + 適當 buffer。
參考資料
- Kubernetes 官方文件 - Pod 的生命週期
- DevOpsCube - Kubernetes Pod Graceful Shutdown
- CNCF - Decoding the Pod Termination Lifecycle
- Google Cloud Blog - Terminating with Grace

發佈留言