K8s · 工作負載 · 第 15 課 · · 10min read

滾動更新 vs 回滾:零停機部署 nginx 1.26 → 1.27 全流程

更新版本不用停機,K8s 一個一個 Pod 換掉。但新版本壞了怎麼辦?kubectl rollout undo 一個指令就退回上一版。這篇實測整段流程。

#Kubernetes #滾動更新 #回滾 #rollout
章節目錄 · 12

API 有新版本,怎麼把 v1 換成 v2?

上一篇學完擴縮容,這篇處理另一個現實問題:版本更新

你的 API 跑 nginx 1.26,新版本 1.27 開發完了。要把線上 v1.26 換成 v1.27。怎麼做?

最土的方法:把舊 Pod 全砍 → 用新 Image 建一批。問題是舊的砍掉、新的還沒起來那段時間,使用者看到 502 Bad Gateway。電商網站幾秒空窗就是幾萬塊損失,金融系統幾秒可能整筆交易出錯。

生產環境不允許這樣做。K8s 用一個叫「滾動更新(Rolling Update)」的策略解這個問題。

滾動更新核心:逐步替換

四個字:逐步替換。不是全砍全建,是一個一個來。

3 個 Pod 跑 v1,要更新到 v2:

初始:[v1] [v1] [v1]
  ↓ 建 1 個 v2
       [v1] [v1] [v1] [v2]
  ↓ 健康檢查通過 → 砍 1 個 v1
       [v1] [v1] [v2]
  ↓ 建 1 個 v2,砍 1 個 v1
       [v1] [v2] [v2]
  ↓ 建 1 個 v2,砍 1 個 v1
       [v2] [v2] [v2]

任何時刻都有 Pod 在服務,使用者請求不會落空。像接力賽,下一棒跑起來上一棒才放手,沒有沒人跑的空檔。

背後機制:新舊 ReplicaSet 的蹺蹺板

Deployment 三層關係:Deployment → ReplicaSet → Pod。

滾動更新的秘密在 ReplicaSet 這層:

觸發更新時:
  Deployment 建一個全新的 ReplicaSet
   ↓
  舊 ReplicaSet(管 v1)副本數從 3 → 0
  新 ReplicaSet(管 v2)副本數從 0 → 3
   ↓
  蹺蹺板,一邊下去一邊上來

kubectl get rs 會看到兩個 ReplicaSet:一個 READY 是 3(新版)、另一個 READY 是 0(舊版)。

舊的不會被刪掉,副本數歸零還在 — 因為要留著給回滾用。

觸發滾動更新

最佳實踐:改 YAML 再 apply

# 1. 取出現在的 YAML
kubectl get deployment my-nginx -o yaml > deployment.yaml

# 2. 改 image: nginx:1.26 → image: nginx:1.27(編輯器手動改)

# 3. apply 觸發更新
kubectl apply -f deployment.yaml

# 4. 看更新進度
kubectl rollout status deployment/my-nginx
# Waiting for deployment "my-nginx" rollout to finish: 1 out of 3 new replicas have been updated...
# Waiting for deployment "my-nginx" rollout to finish: 2 out of 3 new replicas have been updated...
# deployment "my-nginx" successfully rolled out

驗證:

# 看 Pod 名字(hash 變了,因為新 ReplicaSet)
kubectl get pods

# 看兩個 ReplicaSet
kubectl get rs
# my-nginx-aaa DESIRED 3 CURRENT 3 READY 3 ← 新版 1.27
# my-nginx-bbb DESIRED 0 CURRENT 0 READY 0 ← 舊版 1.26

# 看當前 Image
kubectl describe deployment my-nginx | grep Image
# Image: nginx:1.27

還有一個快捷方式:set image

kubectl set image deployment/my-nginx nginx=nginx:1.27

不用改檔案直接觸發更新。但生產環境推薦改 YAML 再 apply,因為:

  • YAML 跟 Git 版本控制一致
  • 團隊看檔案就知道現在跑什麼版本
  • set image 改了不會反映到你的 YAML 檔

回滾:一行指令救命

v2 上線後使用者回報 bug,老闆衝過來說「趕快退回去」:

kubectl rollout undo deployment/my-nginx

Deployment 把舊 ReplicaSet 重新擴回 3、新的縮到 0。因為舊 ReplicaSet 還在、舊 Image 還在 Node 快取,回滾通常 30 秒搞定 — 不需要重新 build、不需要重推 Registry

rollout history:看部署歷史

kubectl rollout history deployment/my-nginx
# REVISION  CHANGE-CAUSE
# 1         <none>
# 2         <none>
# 3         <none>

每次部署都記一個 revision。要回到特定版本:

kubectl rollout undo deployment/my-nginx --to-revision=2
💡 K8s 預設保留最近 10 個版本(由 spec.revisionHistoryLimit 控制)。

實戰:故意推一個壞版本看 K8s 怎麼保護你

# YAML 把 image 改成 nginx:99.99(不存在)
kubectl apply -f deployment.yaml

kubectl get pods
# 新 Pod: ImagePullBackOff
# 舊 Pod: 還活著!

K8s 不會把所有舊 Pod 砍光才建新的,滾動更新的安全機制讓舊 Pod 保留。所以:

  • 服務沒有完全更新成功
  • 但也沒有完全掛掉
  • 使用者依然能訪問到舊版
# 救命稻草
kubectl rollout undo deployment/my-nginx

舊 Pod 恢復、壞掉的新 Pod 砍掉,服務回正常。真實工作中超常見:開發人員打錯 tag、推了問題 Image、滾動更新卡住。別慌,rollout undo 一行搞定。

滾動更新指令速查

操作指令
觸發更新改 YAML → kubectl apply
看進度kubectl rollout status deployment/xxx
回上一版kubectl rollout undo deployment/xxx
回特定版本kubectl rollout undo deployment/xxx --to-revision=N
看歷史kubectl rollout history deployment/xxx
暫停kubectl rollout pause deployment/xxx
恢復kubectl rollout resume deployment/xxx

對照 Docker:K8s 大幅領先

功能Docker ComposeK8s
滾動更新沒有內建
健康檢查切換沒有內建
一行回滾沒有rollout undo
版本歷史沒有rollout history
Docker Compose up -d 換 Image 就是直接砍舊建新,有空窗期。Docker Swarm 有滾動更新但很多人不用。K8s 的滾動更新是生產環境的標配。

重點整理

  • 滾動更新 = 逐步替換,零停機
  • 背後是新舊 ReplicaSet 的蹺蹺板
  • 觸發方式:改 YAML → apply(生產推薦)或 set image(快捷)
  • 舊 ReplicaSet 保留著,回滾就是把它擴回來
  • rollout status 看進度、rollout history 看歷史、rollout undo 救命
  • 推壞 Image 不會把舊 Pod 砍光,滾動更新有保護機制

下一步

更新會了。但 Deployment 怎麼找到自己的 Pod?答案不是 Pod 名字,而是 labels。下一篇講Labels 與自我修復:K8s 怎麼認自己的 Pod,把 Day 4-5 的 Deployment 故事收尾。