Deployment 與自我修復:reconcile loop 的實戰
· tech
📑 目錄
第一篇給了靈魂(reconcile loop),第二篇給了原子(Pod)。但實務上你幾乎不會手動去建一個 Pod —— 你宣告的是 Deployment,而它正是 reconcile loop 最實用、最常見的化身。這篇看它怎麼自我修復、又怎麼零停機換版。
為什麼不直接建 Pod
因為裸 Pod 掛了沒人救。 你手動建一個 Pod,它一旦當掉或所在的 node 壞了,就這樣沒了 —— 沒有任何東西記得「本來該有它」。你要的不是「開一個 Pod」,而是「永遠維持 N 個健康的 Pod」。這正是需要一個 controller 幫你盯著的事,而 Deployment 就是幹這個的:
分工很清楚:Deployment 管版本與更新策略;它底下的 ReplicaSet 只負責一件事——盯著實際 Pod 數,少了就補、多了就砍。 你只宣告「我要 3 份 v1」,剩下的都是那個迴圈在跑。
這份「期望」落成 YAML 長這樣
K8s 是宣告式的:你不下一步步的指令,而是寫一份描述「我要什麼」的 YAML,交給 reconcile loop 去達成——這也是為什麼大家說 K8s 有種 Infrastructure as Code 的味道。上面那整套自我修復,其實就這麼幾行宣告出來的:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3 # 期望:永遠維持 3 份
selector:
matchLabels: { app: web } # 這個 Deployment 管哪些 Pod
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 換版時最多幾個同時不可用
maxSurge: 1 # 換版時最多可額外多開幾個新的
template: # ↓ 以下是「每個 Pod 長怎樣」的樣板
metadata:
labels: { app: web } # Pod 的標籤,要被上面 selector 選中
spec:
containers:
- name: web
image: myrepo/web:1.0
resources: # 排程與 QoS 的依據
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "256Mi" }
readinessProbe: # 沒通過就不進 Service 收流量名單
httpGet: { path: /healthz, port: 8080 }
三個地方是理解 Deployment 的關鍵:
replicas: 3就是你的「期望」——ReplicaSet 整天盯著它,少了補、多了砍。改成 5、kubectl apply一下,loop 自動補到 5,你從沒下過「開兩個容器」這種指令。selector與template.labels必須對得上——這是 Deployment 認得「哪些 Pod 是我的」的方式。兩邊標籤兜不起來,apply直接被擋。template是 Pod 的樣板,也是滾動更新的觸發點——改這裡面任何一格(image、env、resources…)才會觸發換版;只改replicas不算換版,只是加減數量。(也因此,改一個被引用的 ConfigMap/Secret 不會自動 rollout——它沒動到這份 template。)
自我修復:reconcile loop 一直在跑
所謂「K8s 會自我修復」,拆開來一點都不神秘,就是 reconcile loop 在做它該做的:
- 你宣告期望 = 3 個 Pod。
- ReplicaSet controller 不斷比對實際:現在幾個健康的?
- Pod 當掉、或整台 node 壞掉 → 實際變 2 → 有落差 → 建一個新 Pod 補回 3。
你半夜不用被叫起來重啟服務,因為那個迴圈替你做了。 這也是為什麼上一篇說 Pod 短命是特性:正因為它可拋棄,壞了才能無痛換新的。
滾動更新:把「部署」變成日常
Deployment 真正值錢的另一半,是換版不中斷服務。你把 image 從 v1 改成 v2,它不會一次全砍掉重開,而是新版一個個起、舊版一個個收:
背後的機制還是同一套:更新時 Deployment 開一個新的 ReplicaSet(v2),一邊把它擴上去、一邊把舊 ReplicaSet(v1)縮下來。出包了怎麼辦?因為舊 ReplicaSet 還留著,一行 kubectl rollout undo 就秒回到 v1。
日常操作其實就這幾條,全是「改期望」:
kubectl apply -f web.yaml # 宣告期望(3 份 v1)
kubectl set image deploy/web web=web:2.0 # 換版 → 觸發滾動更新
kubectl rollout undo deploy/web # 出包 → 一鍵回滾
kubectl scale deploy/web --replicas=5 # 改份數 → loop 幫你補到 5
注意:你從頭到尾沒有下過一個「開容器」「停容器」的指令 —— 你只是不斷更新那份「期望」,reconcile loop 把現實收斂過去。
一個常見的坑:改了 ConfigMap / Secret,Pod 會自動換嗎?不會。 reconcile loop 盯的是 Deployment 的 Pod 樣板;你去
kubectl edit一份被引用的 ConfigMap/Secret,樣板本身沒變,Deployment 就不會觸發滾動更新。用 env 注入的 Pod 會繼續拿舊值,直到你手動kubectl rollout restart deploy/web(或在樣板加一個內容 hash 的 annotation,讓值一改樣板就跟著變、自動滾動)。用 volume 掛載的檔案雖然會被 kubelet 更新,但應用程式得自己重讀才會生效。一句話:滾動更新只認「樣板變了沒」,不認「樣板指到的東西變了沒」。
反思
「自我修復」不是魔法,是那個迴圈一直在跑
第一次看到 Pod 被 kill 掉、幾秒後自己又冒出來,確實很神奇。但拆開就發現一點都不玄:ReplicaSet controller 只是不停地問「實際幾個?和期望差多少?」,有差就動手。 想通這件事之後,K8s 的「韌性」對我不再是黑魔法,而是一個很樸素的迴圈 —— 這也讓我 debug 更有方向:服務沒被拉回來,那八成是這個迴圈被什麼卡住了(資源不夠排不進、健康檢查一直失敗…),而不是「玄學」。把神奇的東西還原成機制,是我學任何系統的第一步。
滾動更新讓「部署」從緊張大事變成日常
我很吃這一套。以前部署是件要挑半夜、全員待命、深怕中斷服務的大事;有了 Deployment 的滾動更新 + 一鍵回滾,部署變成低風險的日常動作 —— 新版慢慢頂替、出包秒退。這跟我在 Airflow 那篇講的「Production 作業要冪等、可重跑」是同一種安全感:讓「改變」變得可逆、可控,人就敢頻繁地小步前進,而不是攢一大包、賭一次大的。
你宣告「要什麼」,不是「怎麼做」——Deployment 是最好的示範
整個系列的主軸在這篇最具體:你給 Deployment 的永遠是目標狀態(3 份、v2),從不是步驟(先開這個、再停那個)。這份「期望」還能寫進 Git、版控、review —— 就是第一篇說的宣告式 + GitOps 的紅利。之後你會遇到的 Service、StatefulSet、HPA,全是同一個模式的不同套用。抓住「宣告期望、loop 收斂」,K8s 後面的東西就都是變形題。