前言:一次神秘的 Pod 複製事件
在一次例行的 Strapi CMS 更新部署到 AWS EKS 時,遇到了一個詭異的現象:明明 Deployment 設定檔中清楚寫著 replicas: 1,但實際運行的 Pod 卻有兩個!更奇怪的是,其中一個 Pod 持續處於 CrashLoopBackOff 狀態,而另一個則正常運行。
無論怎麼刪除多餘的 Pod,它總是會像打不死的蟑螂一樣再次出現。這種「靈異事件」讓我開始懷疑 Kubernetes 是不是有自己的想法…
本文將深入探討:
- 為什麼會出現多餘的 Pod
- CrashLoopBackOff 背後的機制
- Kubernetes Deployment 和 ReplicaSet 的運作原理
- 實戰排查步驟與解決方案
- Secret 編碼陷阱與預防措施
問題背景:Deployment 說一個,實際卻有兩個
問題現象
預期行為:
# my-strapi-prod-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-strapi-prod
spec:
replicas: 1 # 只要 1 個 Pod
selector:
matchLabels:
app: my-strapi-prod
實際情況:
$ kubectl get pods -n default | grep my-strapi-prod
NAME READY STATUS RESTARTS AGE
my-strapi-prod-7d4b5c8f9d-x2k4p 1/1 Running 0 10m
my-strapi-prod-8c9a6d7e5f-w8n2q 0/1 CrashLoopBackOff 5 5m
兩個 Pod 同時存在,卻只有一個正常運行!
問題時間線
timeline
title Kubernetes 部署問題時間線
section 初始狀態
舊版本正常運行 : 1 個 Pod 正常
section 更新部署
推送新版本 : 更新 Deployment
: 創建新 ReplicaSet
section 問題出現
新 Pod 啟動失敗 : CrashLoopBackOff
: 舊 Pod 繼續運行
section 現象
兩個 Pod 共存 : 新舊並存
: 無法自動恢復
核心概念:理解 Kubernetes Deployment 機制
在排查問題之前,先來理解 Kubernetes 是如何管理 Pod 的。
Deployment、ReplicaSet 與 Pod 的關係
graph TB
subgraph "Kubernetes 部署架構"
D[Deployment]
D -->|控制| RS1[ReplicaSet v1<br/>舊版本]
D -->|控制| RS2[ReplicaSet v2<br/>新版本]
RS1 -->|管理| P1[Pod v1<br/>Running]
RS2 -->|管理| P2[Pod v2<br/>CrashLoopBackOff]
end
style D fill:#4ade80
style RS1 fill:#60a5fa
style RS2 fill:#f97316
style P1 fill:#22c55e
style P2 fill:#ef4444
層級說明:
Deployment:部署控制器
- 負責管理應用的整個生命週期
- 創建和管理 ReplicaSet
- 支援滾動更新(Rolling Update)
ReplicaSet:副本集
- 確保指定數量的 Pod 副本在運行
- 每次 Deployment 更新都會創建新的 ReplicaSet
- 舊的 ReplicaSet 不會立即刪除(用於回滾)
Pod:最小部署單元
- 實際運行容器的地方
- 由 ReplicaSet 創建和管理
滾動更新機制
當你更新 Deployment 時,Kubernetes 會執行「滾動更新」:
sequenceDiagram
participant User as 開發者
participant Deploy as Deployment
participant RS_Old as ReplicaSet 舊版
participant RS_New as ReplicaSet 新版
participant Pod_Old as Pod 舊版
participant Pod_New as Pod 新版
User->>Deploy: 更新 Deployment (kubectl apply)
Deploy->>RS_New: 創建新的 ReplicaSet
RS_New->>Pod_New: 啟動新 Pod
Note over Pod_New: ⏳ 等待新 Pod 就緒 (Readiness Probe)
alt 新 Pod 啟動成功
Pod_New-->>RS_New: ✅ Pod Ready
RS_New->>Pod_Old: 開始終止舊 Pod
Deploy->>RS_Old: 縮減舊 ReplicaSet 副本數
else 新 Pod 啟動失敗
Pod_New-->>RS_New: ❌ CrashLoopBackOff
Note over RS_New,Pod_Old: ⚠️ 保留舊 Pod<br/>不敢刪除
Note over Deploy: 滾動更新卡住
end
關鍵機制:安全保護
Kubernetes 的滾動更新有一個重要的安全機制:
- 只有當新 Pod 通過健康檢查(Readiness Probe)後,才會刪除舊 Pod
- 如果新 Pod 一直無法啟動,舊 Pod 會繼續運行
- 這就是為什麼會出現「兩個 Pod 並存」的現象!
問題排查:一步步找出根本原因
步驟 1:確認 Pod 狀態
# 查看所有 Pod 狀態
kubectl get pods --all-namespaces | grep my-strapi-prod
輸出結果:
NAMESPACE NAME READY STATUS RESTARTS AGE
default my-strapi-prod-7d4b5c8f9d-x2k4p 1/1 Running 0 10m
default my-strapi-prod-8c9a6d7e5f-w8n2q 0/1 CrashLoopBackOff 5 5m
關鍵資訊:
- 兩個 Pod 的名稱中間部分不同(
7d4b5c8f9dvs8c9a6d7e5f) - 這些是 ReplicaSet 的 hash,代表它們來自不同的 ReplicaSet
步驟 2:檢查 ReplicaSet
# 查看所有 ReplicaSet
kubectl get rs -n default | grep my-strapi-prod
輸出結果:
NAME DESIRED CURRENT READY AGE
my-strapi-prod-7d4b5c8f9d 1 1 1 10m
my-strapi-prod-8c9a6d7e5f 1 1 0 5m
發現問題!
- 存在兩個 ReplicaSet 同時運行
- 舊 ReplicaSet(
7d4b5c8f9d):1 個 Pod Ready - 新 ReplicaSet(
8c9a6d7e5f):1 個 Pod 但 0 個 Ready

步驟 3:查看 Pod 日誌找出 Crash 原因
# 查看失敗 Pod 的日誌
kubectl logs my-strapi-prod-8c9a6d7e5f-w8n2q -n default
日誌輸出:
[2025-05-06 10:23:45.123] ERROR: Failed to connect to database
[2025-05-06 10:23:45.124] Error: password authentication failed for user "strapi"
[2025-05-06 10:23:45.125] INFO: Retrying in 5 seconds...
找到根本原因: 資料庫密碼錯誤!
步驟 4:檢查 Secret 設定
# 查看 Secret
kubectl get secret strapi-db-secret -n default -o yaml
發現問題:
apiVersion: v1
kind: Secret
metadata:
name: strapi-db-secret
data:
DB_PASSWORD: bXlwYXNzd29yZAo= # ❌ 多了換行符號 \n
解碼驗證:
$ echo "bXlwYXNzd29yZAo=" | base64 -d | xxd
00000000: 6d79 7061 7373 776f 7264 0a mypassword.
# 最後的 0a 就是 \n (換行符號)
CrashLoopBackOff 狀態深度解析
什麼是 CrashLoopBackOff?
CrashLoopBackOff 是 Kubernetes 的一種 Pod 狀態,表示:
- Pod 啟動後立即崩潰(Crash)
- Kubernetes 嘗試重啟 Pod(Loop)
- 每次重啟失敗後,等待時間會遞增(Backoff)
重啟等待時間演算法:
首次重啟:立即
第二次:10 秒
第三次:20 秒
第四次:40 秒
第五次:80 秒
最大值:5 分鐘
CrashLoopBackOff 流程圖
stateDiagram-v2
[*] --> Pending: 創建 Pod
Pending --> Running: 容器啟動
Running --> Succeeded: 正常結束
Running --> Failed: 程序崩潰
Failed --> Waiting: 等待重啟
Waiting --> Running: Backoff 後重啟
Failed --> CrashLoopBackOff: 多次失敗
CrashLoopBackOff --> Running: 繼續重試
Succeeded --> [*]
note right of CrashLoopBackOff
重啟間隔遞增:
10s → 20s → 40s → 80s → 5min
end note
常見的 CrashLoopBackOff 原因
| 原因類型 | 具體問題 | 排查指令 |
|---|---|---|
| 設定錯誤 | 環境變數、Secret、ConfigMap 錯誤 | kubectl logs <pod> |
| 資源不足 | 記憶體 OOM、CPU 限制 | kubectl describe pod <pod> |
| 映像檔問題 | 映像檔不存在、拉取失敗 | kubectl describe pod <pod> |
| 健康檢查 | Liveness Probe 設定過於嚴格 | 查看 Deployment YAML |
| 應用程式錯誤 | 程式碼 bug、依賴缺失 | kubectl logs <pod> |
| 權限問題 | 無法存取 Volume、Secret | kubectl describe pod <pod> |
解決方案:從錯誤到修復
嘗試方案 1:修正 Secret(失敗)
最直接的想法是修正資料庫密碼的 Secret。
正確的 base64 編碼方式:
# ❌ 錯誤:會包含換行符號
echo "mypassword" | base64
# 輸出:bXlwYXNzd29yZAo= (多了 \n)
# ✅ 正確:不包含換行符號
echo -n "mypassword" | base64
# 輸出:bXlwYXNzd29yZA==
# ✅ 或使用 printf
printf "mypassword" | base64
# 輸出:bXlwYXNzd29yZA==
更新 Secret:
# 方法 1:直接編輯
kubectl edit secret strapi-db-secret -n default
# 方法 2:重新建立
kubectl create secret generic strapi-db-secret \
--from-literal=DB_PASSWORD=mypassword \
--dry-run=client -o yaml | kubectl apply -f -
刪除 Pod 讓它重建:
kubectl delete pod -l app=my-strapi-prod -n default
結果: 新 Pod 啟動後還是 crash!檢查日誌後發現可能還有其他設定錯誤…
嘗試方案 2:Rollback 到上一版本(成功!)
既然新版本有問題,不如先回到穩定版本。
查看 Deployment 歷史:
kubectl rollout history deployment my-strapi-prod -n default
輸出:
REVISION CHANGE-CAUSE
1 Initial deployment
2 Update Strapi to v4.15.0
3 Fix database configuration (current, failing)
執行回滾:
# 回滾到上一個版本
kubectl rollout undo deployment my-strapi-prod -n default
# 或回滾到特定版本
kubectl rollout undo deployment my-strapi-prod --to-revision=2 -n default
查看回滾狀態:
kubectl rollout status deployment my-strapi-prod -n default
輸出:
Waiting for deployment "my-strapi-prod" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "my-strapi-prod" rollout to finish: 1 old replicas are pending termination...
deployment "my-strapi-prod" successfully rolled out
確認結果:
kubectl get pods -n default | grep my-strapi-prod
輸出:
NAME READY STATUS RESTARTS AGE
my-strapi-prod-7d4b5c8f9d-n5r8t 1/1 Running 0 2m
只剩下 1 個 Pod 了!✅

深入理解:為什麼回滾會成功?
Rollback 流程詳解
flowchart TD
Start[執行 rollout undo] --> CheckHistory[查詢部署歷史]
CheckHistory --> FindPrevious[找到上一個 Revision]
FindPrevious --> UpdateDeployment[更新 Deployment 設定<br/>使用舊版 Revision]
UpdateDeployment --> CreateOldRS[重啟舊的 ReplicaSet]
CreateOldRS --> StartOldPod[啟動舊版 Pod]
StartOldPod --> CheckReady{Pod 就緒?}
CheckReady -->|Yes| ScaleDownNew[縮減新 ReplicaSet 到 0]
CheckReady -->|No| Retry[重試]
Retry --> StartOldPod
ScaleDownNew --> TerminateNewPod[終止新版 Pod]
TerminateNewPod --> Cleanup[清理失敗的 Pod]
Cleanup --> End[✅ 回滾完成]
style Start fill:#4ade80
style End fill:#22c55e
style CheckReady fill:#f97316
關鍵點:
- Rollback 不是「恢復」,而是「前進到舊版本」
- 實際上是創建一個新的 Revision,內容等同於舊版本
- Kubernetes 會保留所有的 ReplicaSet 歷史(預設 10 個)
ReplicaSet 保留機制
# 查看所有 ReplicaSet(包含已縮減到 0 的)
kubectl get rs -n default
# 輸出範例
NAME DESIRED CURRENT READY AGE
my-strapi-prod-7d4b5c8f9d 1 1 1 20m ← 當前運行
my-strapi-prod-8c9a6d7e5f 0 0 0 15m ← 失敗的版本
my-strapi-prod-6a2b3c4d5e 0 0 0 2h ← 更早的版本
為什麼保留舊的 ReplicaSet?
- 快速回滾:不需要重新拉取映像檔或重新建立資源
- 歷史記錄:可以查看每個版本的設定
- 漸進式部署:可以手動控制流量分配(Canary Deployment)
設定保留數量:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-strapi-prod
spec:
revisionHistoryLimit: 10 # 保留 10 個歷史版本(預設值)
replicas: 1
# ...
Secret Base64 編碼陷阱與最佳實踐
為什麼 echo 會加入換行符號?
# echo 預設會加入換行符號
$ echo "password" | xxd
00000000: 7061 7373 776f 7264 0a password.
^^
這是 \n (0x0a)
# echo -n 抑制換行符號
$ echo -n "password" | xxd
00000000: 7061 7373 776f 7264 password
Secret 建立的最佳實踐
方法 1:使用 –from-literal(推薦)
kubectl create secret generic my-secret \
--from-literal=username=admin \
--from-literal=password=mypassword \
-n default
方法 2:使用 –from-file
# 將密碼寫入檔案(不含換行)
printf "mypassword" > password.txt
# 從檔案建立 Secret
kubectl create secret generic my-secret \
--from-file=password=password.txt \
-n default
# 記得刪除明文密碼檔案
rm password.txt
方法 3:使用 –from-env-file
# .env 檔案
cat <<EOF > secret.env
USERNAME=admin
PASSWORD=mypassword
DB_HOST=postgres.example.com
EOF
# 從 env 檔案建立
kubectl create secret generic my-secret \
--from-env-file=secret.env \
-n default
rm secret.env
方法 4:手動編寫 YAML(最靈活)
# 先產生 base64 值
DB_PASSWORD=$(echo -n "mypassword" | base64)
# 建立 YAML
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: default
type: Opaque
data:
DB_PASSWORD: ${DB_PASSWORD}
EOF
Secret 驗證方法
# 方法 1:解碼 Secret 內容
kubectl get secret my-secret -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# 應該輸出:mypassword (不含換行)
# 方法 2:檢查 hex dump
kubectl get secret my-secret -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d | xxd
# 確認最後沒有 0a (換行符號)
# 方法 3:在 Pod 中驗證
kubectl run debug-pod --rm -it --image=busybox --restart=Never -- sh
# 在 Pod 中
echo $DB_PASSWORD | xxd
實用的 kubectl 排查指令集
查看資源狀態
# 查看 Pod 詳細資訊(包含事件)
kubectl describe pod <pod-name> -n <namespace>
# 查看 Pod 日誌
kubectl logs <pod-name> -n <namespace>
# 查看之前崩潰的 Pod 日誌
kubectl logs <pod-name> -n <namespace> --previous
# 即時查看日誌(類似 tail -f)
kubectl logs -f <pod-name> -n <namespace>
# 查看特定容器的日誌(多容器 Pod)
kubectl logs <pod-name> -c <container-name> -n <namespace>
Deployment 管理
# 查看 Deployment 狀態
kubectl get deployment <deployment-name> -n <namespace>
# 查看 Deployment 詳情
kubectl describe deployment <deployment-name> -n <namespace>
# 查看 Rollout 歷史
kubectl rollout history deployment <deployment-name> -n <namespace>
# 查看特定 Revision 的詳情
kubectl rollout history deployment <deployment-name> --revision=2
# 暫停 Rollout(用於多次修改後一次部署)
kubectl rollout pause deployment <deployment-name> -n <namespace>
# 恢復 Rollout
kubectl rollout resume deployment <deployment-name> -n <namespace>
# 查看 Rollout 狀態
kubectl rollout status deployment <deployment-name> -n <namespace>
ReplicaSet 管理
# 查看所有 ReplicaSet
kubectl get rs -n <namespace>
# 查看特定 Deployment 的 ReplicaSet
kubectl get rs -n <namespace> -l app=<app-label>
# 手動調整 ReplicaSet(不推薦,應該透過 Deployment)
kubectl scale rs <replicaset-name> --replicas=0 -n <namespace>
強制刪除卡住的資源
# 強制刪除 Pod(當 Pod 卡在 Terminating 狀態)
kubectl delete pod <pod-name> --grace-period=0 --force -n <namespace>
# 刪除所有失敗的 Pod
kubectl delete pods --field-selector=status.phase=Failed -n <namespace>
# 刪除所有 CrashLoopBackOff 的 Pod
kubectl get pods -n <namespace> | grep CrashLoopBackOff | awk '{print $1}' | xargs kubectl delete pod -n <namespace>
預防措施與最佳實踐
1. 設定健康檢查
正確設定 Liveness 和 Readiness Probe 可以讓 Kubernetes 更準確地判斷 Pod 狀態。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-strapi-prod
spec:
template:
spec:
containers:
- name: strapi
image: my-strapi:v1.0.0
ports:
- containerPort: 1337
# Readiness Probe:Pod 是否準備好接收流量
readinessProbe:
httpGet:
path: /_health
port: 1337
initialDelaySeconds: 30 # 啟動後等待 30 秒再檢查
periodSeconds: 10 # 每 10 秒檢查一次
timeoutSeconds: 5 # 請求超時時間
successThreshold: 1 # 成功 1 次即視為就緒
failureThreshold: 3 # 失敗 3 次才視為未就緒
# Liveness Probe:Pod 是否還活著
livenessProbe:
httpGet:
path: /_health
port: 1337
initialDelaySeconds: 60 # 給足夠的啟動時間
periodSeconds: 30 # 不需要太頻繁
timeoutSeconds: 5
failureThreshold: 3 # 失敗 3 次才重啟
2. 設定資源限制
spec:
containers:
- name: strapi
resources:
requests: # 最小保證資源
memory: "512Mi"
cpu: "250m"
limits: # 最大可用資源
memory: "1Gi"
cpu: "500m"
3. 使用 Init Containers 進行預先檢查
spec:
initContainers:
- name: wait-for-db
image: busybox:1.35
command: ['sh', '-c']
args:
- |
until nc -z postgres-service 5432; do
echo "Waiting for PostgreSQL..."
sleep 2
done
echo "PostgreSQL is ready!"
4. 設定合理的 Rollout 策略
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # 確保至少有 1 個 Pod 運行
maxSurge: 1 # 最多多出 1 個 Pod
5. 建立 CI/CD 流程驗證
# .gitlab-ci.yml
deploy:
script:
# 部署後等待 rollout 完成
- kubectl apply -f deployment.yaml
- kubectl rollout status deployment/my-app --timeout=5m
# 如果失敗自動回滾
- |
if [ $? -ne 0 ]; then
echo "Deployment failed, rolling back..."
kubectl rollout undo deployment/my-app
exit 1
fi
總結與收穫
關鍵要點
技術層面:
- ✅ Kubernetes 的滾動更新是漸進式的:新 Pod 就緒後才會刪除舊 Pod
- ✅ CrashLoopBackOff 是保護機制:避免無限快速重啟消耗資源
- ✅ ReplicaSet 是中間層:Deployment 透過 ReplicaSet 管理 Pod
- ✅ rollout undo 是救命稻草:可以快速回到穩定版本
- ✅ Secret base64 編碼要小心:使用
echo -n或--from-literal
排查思路:
- 先查看 Pod 狀態 →
kubectl get pods - 檢查 ReplicaSet →
kubectl get rs - 查看 Pod 日誌 →
kubectl logs - 檢查設定 →
kubectl describe - 必要時回滾 →
kubectl rollout undo
最佳實踐
開發階段:
- 使用
--dry-run=client預覽變更 - 本地測試 Secret 的 base64 編碼
- 設定合理的健康檢查
部署階段:
- 使用
kubectl apply而非kubectl create - 部署後監控
kubectl rollout status - 保留足夠的 Revision 歷史
維運階段:
- 定期檢查
kubectl get rs清理無用的 ReplicaSet - 監控 Pod 重啟次數
- 建立告警機制捕捉 CrashLoopBackOff
延伸思考
Q: 為什麼不直接刪除失敗的 ReplicaSet? A: 因為它是由 Deployment 管理的。手動刪除後,Deployment 會立即重新創建它。要解決問題,應該修正 Deployment 設定或執行 rollback。
Q: 可以同時運行多個版本的 Pod 嗎? A: 可以!這就是 Canary Deployment 的原理。手動調整新舊 ReplicaSet 的副本數,就能控制流量分配(需要搭配 Service Mesh 或 Ingress)。
Q: rollout undo 會影響正在運行的服務嗎? A: 正常情況下不會,因為是滾動更新。但如果舊版本也有問題,或資源不足,可能會短暫中斷。
參考資源
官方文件:
實用工具:
- kubectl Cheat Sheet
- kubectx/kubens - 快速切換 context 和 namespace
- k9s - 強大的 Kubernetes CLI UI
延伸閱讀: