Deployment 與自我修復:reconcile loop 的實戰

· tech

#kubernetes#concept

📑 目錄

第一篇給了靈魂(reconcile loop),第二篇給了原子(Pod)。但實務上你幾乎不會手動去建一個 Pod —— 你宣告的是 Deployment,而它正是 reconcile loop 最實用、最常見的化身。這篇看它怎麼自我修復、又怎麼零停機換版

為什麼不直接建 Pod

因為裸 Pod 掛了沒人救。 你手動建一個 Pod,它一旦當掉或所在的 node 壞了,就這樣沒了 —— 沒有任何東西記得「本來該有它」。你要的不是「開一個 Pod」,而是「永遠維持 N 個健康的 Pod」。這正是需要一個 controller 幫你盯著的事,而 Deployment 就是幹這個的:

Deployment 期望:replicas = 3 · image v1 ReplicaSet 確保永遠有 3 個 Pod Pod ✓ 健康 Pod ✗ 掛了 Pod ✓ 健康 少一個 → ReplicaSet 觀察到落差 → 立刻補一個新的回到 3 個
你宣告 Deployment,它透過 ReplicaSet 維持「永遠有 3 個 Pod」;掛了一個就自動補一個 —— 這就是 reconcile loop 的自我修復

分工很清楚: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,你從沒下過「開兩個容器」這種指令。
  • selectortemplate.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,它不會一次全砍掉重開,而是新版一個個起、舊版一個個收:

開始 更新中 完成 v1 v1 v1 v1 v1 v2 v2 v2 v2 ■ 舊版 v1 ■ 新版 v2 新版起一個、舊版收一個 → 全程有服務;出包 kubectl rollout undo 一鍵回滾
滾動更新:一次換一點,新舊版交棒,服務不中斷;背後是 Deployment 開一個新 ReplicaSet(v2)、慢慢把舊的(v1)縮到 0

背後的機制還是同一套:更新時 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 後面的東西就都是變形題。