前言:那兩秒之間發生了什麼事?

你敲下 kubectl apply -f deployment.yaml,按 Enter,終端機回你一行:

1
deployment.apps/my-app created

兩秒後,kubectl get pods 顯示三個 Pod 正在 Running

那兩秒之間發生了什麼事?

你的 YAML 去了哪裡?誰決定這三個 Pod 要跑在哪幾台機器上?又是誰真的把容器拉起來的?如果其中一個 Pod 掛掉,是誰發現的、誰把它補回來的?

大部分講 Kubernetes 元件的文章,會給你一張架構圖和七個名詞解釋。但那樣很難記住——因為你不知道它們什麼時候會用到

所以這篇文章換個方式:我們跟著這一個 kubectl apply 指令,走完它在叢集裡經過的每一站。 每個元件都會在它該出場的時候登場,你會看到它接收到什麼、做了什麼決定、然後把什麼交給下一棒。

走完這趟旅程,你不需要背,自然就記住了。

[!NOTE] 這篇文章假設你會用 kubectl,但不知道背後發生什麼事
你能 kubectl apply、看得懂 Pod 和 Service 大概在做什麼。但如果被問到「誰決定 Pod 要跑在哪台機器上」,你答不太出來——這篇就是要回答這種問題。

先看全景

在出發之前,先給你一張地圖。旅程會經過這幾站:

flowchart LR
    subgraph LOCAL["你的電腦"]
        K["kubectl"]
    end
    subgraph CP["Control Plane(大腦)"]
        API["kube-apiserver"]
        ETCD["etcd"]
        CM["controller-manager"]
        SCH["scheduler"]
    end
    subgraph NODE["Node(工人)"]
        KUBELET["kubelet"]
        PROXY["kube-proxy"]
    end

    K -->|"① REST 請求"| API
    API <-->|"② 讀寫狀態"| ETCD
    CM -->|"③ watch & 建立 Pod"| API
    SCH -->|"④ 決定去哪台"| API
    KUBELET -->|"⑤ 認領並建立容器"| API
    PROXY -->|"⑥ 設定網路規則"| API

    style API fill:#d4edda,stroke:#155724,color:#000
    style ETCD fill:#fff3cd,stroke:#856404,color:#000

有兩個規律先記著,等一下每一站都會再看到:

  1. 所有東西都要經過 kube-apiserver。沒有任何元件會跳過它直接找別人講話。
  2. 只有 kube-apiserver 能碰 etcd。其他元件想知道叢集現在長什麼樣,一律問 apiserver。

這兩條規律是整個架構的骨幹。現在出發。


第一站:kubectl —— 它其實不在叢集裡

旅程從你的筆電開始。

這裡有個很多人沒意識到的事實:kubectl 不是 Kubernetes 叢集的一部分。 它就是一支裝在你電腦上的 CLI 程式,跟 curl 沒有本質差別——它把你的 YAML 轉成 HTTP 請求,打到 apiserver 的 REST API。

不信的話,你可以把 kubectl 在做的事直接用 curl 做一遍:

1
2
3
4
5
# 讓 kubectl 在本機開一個到 apiserver 的代理
kubectl proxy --port=8080 &

# 然後用 curl 直接問 apiserver 要 Pod 清單
curl http://localhost:8080/api/v1/namespaces/default/pods

你會拿到一份 JSON,內容跟 kubectl get pods -o json 一模一樣。因為它們本來就是同一件事

想看得更清楚,加上 -v=8 讓 kubectl 把它實際發出的 HTTP 請求印出來:

1
kubectl apply -f deployment.yaml -v=8

你會看到類似這樣的輸出:

1
2
POST https://10.0.0.1:6443/apis/apps/v1/namespaces/default/deployments
Request Body: {"apiVersion":"apps/v1","kind":"Deployment",...}

你的 YAML 被轉成了 JSON,用 POST 送到 apiserver。 這就是第一站的全部。

[!TIP] 這解釋了升級時 kubectl 的規則為什麼比較寬鬆
因為 kubectl 只是個 API 客戶端,不是叢集內跑的元件,所以官方允許它比叢集新一個 minor 版本(其他元件一律不准比 apiserver 新)。細節可以看 升級 Kubernetes 前,怎麼提前知道哪裡會爆 的版本歪斜政策那一節。


第二站:kube-apiserver —— 叢集唯一的前門

你的請求抵達了 apiserver。這是整趟旅程最重要的一站,因為所有東西都要經過這裡

kube-apiserver 是叢集的「前門」:kubectl 要經過它、controller-manager 要經過它、scheduler 要經過它、連每個 node 上的 kubelet 都要經過它。它是唯一對外提供 REST API 的元件,也是唯一有資格碰資料庫的元件。

2-1. 你的請求要通過三道關卡

apiserver 收到請求後,不會直接寫進資料庫。它會依序過三關,任何一關沒過就直接退回

flowchart LR
    R["你的請求"] --> A["① Authentication
你是誰?"] A --> B["② Authorization
你有權限嗎?"] B --> C["③ Admission Control
這內容可以接受嗎?"] C --> D["寫入 etcd"] A -.->|"401"| X["拒絕"] B -.->|"403"| X C -.->|"依規則"| X style A fill:#cfe2ff,stroke:#084298,color:#000 style B fill:#fff3cd,stroke:#856404,color:#000 style C fill:#f8d7da,stroke:#721c24,color:#000

① Authentication(身分驗證)—— 你是誰?

確認請求的來源身分。可能是憑證、token、或雲端供應商的身分機制。你的 ~/.kube/config 裡存的就是這個。

沒通過 → 401 Unauthorized

② Authorization(授權)—— 你可以做這件事嗎?

知道你是誰之後,檢查你有沒有權限做這個動作。這一關通常由 RBAC(Role-Based Access Control,以角色為基礎的存取控制)負責——你的身分被綁定到某些角色,角色定義了「可以對哪些資源做哪些動作」。

沒通過 → 403 Forbidden

你可以直接問叢集你有沒有某個權限:

1
2
3
4
5
# 我可以在 default namespace 建立 deployment 嗎?
kubectl auth can-i create deployments --namespace default

# 我可以刪除節點嗎?
kubectl auth can-i delete nodes

③ Admission Control(准入控制)—— 這個內容我可以接受嗎?

這一關最有意思。前兩關管的是「」,這一關管的是「內容。即使你身分正確、權限也夠,叢集仍然可以因為內容不合規而拒絕你,或是偷偷幫你改寫內容

Admission controller 分兩種:

類型 做什麼 例子
Mutating(變更型) 修改你送進來的內容 自動注入 sidecar 容器、補上預設的 resource limits
Validating(驗證型) 檢查內容合不合規,只能通過或拒絕 禁止使用 latest tag、要求所有 Pod 都得有 label

先跑 Mutating,再跑 Validating——先改寫、後檢查,這個順序是有道理的:如果驗證跑在前面,後面的改寫就可能繞過驗證。

[!NOTE] 你可能已經在用 admission control 了
如果你的叢集裝了 Istio,Pod 裡那個你沒寫在 YAML 裡卻自己冒出來的 Envoy sidecar,就是 mutating admission controller 注入的。如果你裝了 OPA Gatekeeper,那些「不符合公司政策就拒絕部署」的規則,就是 validating admission controller。

2-2. 為什麼「所有東西都要經過它」是好設計

把所有流量都塞給同一個元件,聽起來像是瓶頸。但這個設計換來三件很重要的事:

  1. 安全檢查只需要做一次。驗證、授權、准入控制全部集中在一個地方,不用在每個元件裡各實作一遍——那樣不只重複,還很容易出現漏洞。
  2. 資料格式可以統一轉換。你用舊的 apiVersion 送進來,apiserver 會自動轉成目前的儲存格式。這就是為什麼 API 版本升級時,你既有的資源不會突然爆掉。
  3. etcd 被保護起來了。只有一個元件能碰資料庫,意味著資料庫的存取模式是可控的、可稽核的。

[!WARNING] 但這也意味著 apiserver 掛掉會很慘
apiserver 一掛,整個叢集就「失聰」了:kubectl 不能用、controller 無法修正狀態、scheduler 無法排程。

不過有個重要的例外——已經在跑的容器不會停。kubelet 會照著它最後收到的指令繼續維持容器運作,kube-proxy 的網路規則也還在。所以 apiserver 掛掉時,你的服務通常還活著,只是叢集失去了「自我修復」和「接受新指令」的能力。這也是為什麼正式環境要跑多台 apiserver。


第三站:etcd —— 叢集唯一的真相

請求通過三道關卡了。apiserver 現在要把你的 Deployment 存起來——存進 etcd

3-1. 淺層:它就是叢集的資料庫

etcd 是一個 key-value store(鍵值資料庫),而且是叢集裡唯一的資料庫。所有 API 物件的狀態——每個 Pod、Service、ConfigMap、Secret、Node——全都存在這裡。

它的資料長得就像一個檔案系統的路徑:

1
2
3
/registry/deployments/default/my-app     →  {Deployment 的完整 JSON}
/registry/pods/default/my-app-7d9f-x2k1 → {Pod 的完整 JSON}
/registry/services/default/my-app-svc → {Service 的完整 JSON}

這帶出一個很重要的觀念:

在 Kubernetes 裡,「存在」的定義就是「etcd 裡有這筆資料」。

你的 Deployment 現在被寫進 etcd 了,所以它存在了。但注意——此刻還沒有任何容器被建立,一個 Pod 都還沒有。目前為止發生的所有事情,只是「在資料庫裡記了一筆」。

這就是 Kubernetes 最核心的設計哲學:宣告式(declarative)

做法 你說的話 誰負責執行
命令式(imperative) 「去 node-3 上啟動一個容器」 你自己要盯著它成功沒
宣告式(declarative) 「我要有三個這樣的 Pod」 系統自己想辦法達成,而且持續維持

你寫的 YAML 是一份期望狀態(desired state) 的描述,不是一串指令。你只負責把「我要什麼」寫進 etcd,剩下的交給後面幾站的元件去實現。

[!TIP] 這解釋了為什麼 kubectl apply 回得那麼快
那一行 deployment.apps/my-app created 的意思是「你的願望已經登記完成」,不是「你的容器已經跑起來了」。所以 apply 成功不等於部署成功——這兩件事中間還隔著後面好幾站。

3-2. 中層:為什麼 etcd 要三台、五台?

正式環境的 etcd 從來不是一台,而且幾乎一定是奇數台:3 台、5 台、7 台。這是為什麼?

因為 etcd 要同時滿足兩個互相衝突的要求:

  • 不能掛:一台壞了,叢集不能跟著死
  • 不能分裂:多台之間的資料必須完全一致,絕不能各說各話

解法叫做 quorum(法定人數)任何一筆寫入,都必須有超過半數的節點確認,才算真正寫成功

flowchart TD
    W["寫入請求"] --> N1["節點 1 ✅"]
    W --> N2["節點 2 ✅"]
    W --> N3["節點 3 ❌ 掛了"]
    N1 & N2 --> Q["2 / 3 > 半數
✅ 寫入成功"] style Q fill:#d4edda,stroke:#155724,color:#000 style N3 fill:#f8d7da,stroke:#721c24,color:#000

算一下不同規模能容忍幾台故障:

總節點數 需要的 quorum 可容忍故障 評語
1 1 0 沒有容錯,測試用
2 2 0 ⚠️ 比一台還糟
3 2 1 ✅ 最常見的選擇
4 3 1 ⚠️ 多一台但容錯沒增加
5 3 2 ✅ 大型叢集
6 4 2 ⚠️ 同樣白費一台

看出偶數台的問題了嗎?4 台的容錯能力跟 3 台一樣,但你多付了一台的成本,還多了一台可能故障的機器。 這就是為什麼永遠選奇數。

至於為什麼要「超過半數」而不是「至少一半」——因為半數會出事。假設 4 台的叢集從中間斷成 2+2,如果只要求「一半」,兩邊都覺得自己有效,各自接受寫入,資料就永久分裂了。這種情況叫 split-brain(腦裂),要求「超過半數」可以從數學上保證不可能有兩群同時滿足條件。

[!WARNING] quorum 失守 = 叢集變成唯讀
3 台掛掉 2 台,剩下的 1 台無法達成 quorum,此時 etcd 會拒絕所有寫入。實際的症狀是:kubectl get 還能用(讀取還可以),但任何 applydeletescale 全部失敗。而且叢集也失去自我修復能力——Pod 掛了不會被重建,因為 controller 沒辦法寫入新狀態。

3-3. 深層:備份,以及那個沒人想遇到的場景

前面講的容錯有個前提:壞掉的是機器,不是資料

但如果是資料本身出問題呢——有人誤刪了整個 namespace、或是 etcd 資料損毀?這時候多幾台節點救不了你,因為每一台都忠實地同步了那個錯誤。這正是 quorum 的設計目的:讓所有節點保持一致。它不會判斷內容對不對。

所以你需要備份。

備份指令:

1
2
3
4
5
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot-$(date +%F).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key

檢查備份檔:

1
ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-snapshot-2026-08-22.db --write-out=table

還原:

1
2
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-snapshot-2026-08-22.db \
--data-dir=/var/lib/etcd-restored

還原的完整流程比一行指令複雜:要先停掉 apiserver(避免還原過程中有人寫入)、每個 etcd 節點都要還原、改設定指向新的 data dir、再把 apiserver 拉起來。請務必在測試環境演練過再說你有備份。

[!IMPORTANT] 沒演練過的備份,等於沒有備份
這是維運的老話但值得再說一次:備份的價值不在於「有檔案」,而在於「確定能還原。定期做一次還原演練,你會發現各種問題——憑證過期、版本不相容、磁碟空間不足、還原後 Pod 全部卡住。這些都是你不會想在真正出事那天才第一次遇到的。

還有一個容易被忽略的點:Secret 在 etcd 裡預設只是 base64 編碼,不是加密。

1
2
# 任何能讀 etcd 的人,都能直接看到你的密碼
ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret

base64 是編碼不是加密,base64 -d 一秒就還原了。這代表兩件事:etcd 的備份檔跟你的密碼一樣敏感,要當機密資料保管;以及正式環境應該開啟 encryption at rest,讓 apiserver 在寫入前先加密。

[!TIP] 升級時 etcd 為什麼要特別小心
因為它是唯一存放狀態的地方——其他元件都是無狀態的,掛了重啟就好,但 etcd 壞了資料就沒了。所以很多團隊會把 etcd 的版本節奏獨立管理,跟 K8s 主版本脫鉤,並且升級前一定先做一次 snapshot。這部分在 升級 Kubernetes 前,怎麼提前知道哪裡會爆 有更多脈絡。


第四站:controller-manager —— 把願望變成 Pod

你的 Deployment 躺在 etcd 裡了,但還是沒有任何 Pod

Deployment 只是一份宣告:「我要三個這樣的 Pod」。真正把這句話變成三個 Pod 物件的,是 kube-controller-manager

4-1. Reconcile Loop:整個 Kubernetes 的心跳

controller-manager 裡面跑著一大堆 controller,每一個都在做同一件事,這件事叫做 reconcile loop(調和迴圈)

flowchart LR
    A["讀取期望狀態
(你的 YAML)"] --> B["觀察目前狀態
(叢集實際情況)"] B --> C{"有差異嗎?"} C -->|"有"| D["採取行動
縮小差距"] C -->|"沒有"| E["什麼都不做"] D --> A E --> A style C fill:#fff3cd,stroke:#856404,color:#000

用白話講就是三句話:

「你想要什麼?」→ 「現在是什麼?」→ 「不一樣的話,我來想辦法。」

而且這個迴圈永遠不會停。它不是「執行一次就結束」的任務,而是持續運轉的循環。這就是 Kubernetes 「自我修復」能力的來源——你不需要告訴它「Pod 掛了要重建」,它只是不斷地發現現況跟期望不符,然後動手修正

回到我們的 Deployment:

時間點 期望狀態 目前狀態 Controller 的動作
剛 apply 完 3 個 Pod 0 個 Pod 建立 3 個 Pod 物件
正常運作時 3 個 Pod 3 個 Pod 什麼都不做
有台機器爆了 3 個 Pod 2 個 Pod 再建立 1 個 Pod
你手動刪一個 3 個 Pod 2 個 Pod 再建立 1 個(它不知道你是故意的)

最後一列常常讓新手困惑:「我明明刪掉了,怎麼又冒出來?」——因為你只是改變了現況,沒有改變期望。要真的減少 Pod,你得去改期望(kubectl scale 或改 YAML),而不是去刪結果。

4-2. 不只一個 controller

controller-manager 是一個程式,但裡面打包了幾十個 controller,各管各的:

Controller 負責什麼
Deployment controller 管理 ReplicaSet,處理滾動更新
ReplicaSet controller 確保指定數量的 Pod 存在
Node controller 監控節點健康,節點失聯時標記並驅逐 Pod
Job controller 管理一次性任務跑完就結束
Endpoint controller 維護 Service 與 Pod 之間的對應關係

以我們的 Deployment 為例,實際上是兩層接力

flowchart LR
    D["Deployment
我要 3 個 Pod"] -->|"Deployment controller"| RS["ReplicaSet
負責維持 3 個"] RS -->|"ReplicaSet controller"| P1["Pod 1"] RS --> P2["Pod 2"] RS --> P3["Pod 3"]

為什麼要多一層 ReplicaSet?因為滾動更新需要它。當你改了 image 版本,Deployment controller 會建立一個新的 ReplicaSet,然後慢慢把新的擴上去、舊的縮下來——兩個 ReplicaSet 同時存在,才能做到不中斷的切換。這也是 kubectl rollout undo 能夠回滾的原因:舊的 ReplicaSet 還留著。

1
2
3
4
5
# 看看你的 Deployment 底下有哪些 ReplicaSet
kubectl get rs

# 看滾動更新的歷史
kubectl rollout history deployment/my-app

4-3. 此刻的狀態:Pod 有了,但還沒有家

controller-manager 建立了三個 Pod 物件,寫回 etcd。

但這三個 Pod 現在處於一個特殊狀態——它們還不知道自己要跑在哪台機器上。Pod 規格裡有個欄位叫 spec.nodeName,現在是空的

這種狀態你一定看過:

1
2
3
4
5
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-7d9f4b6c5-x2k1 0/1 Pending 0 2s
my-app-7d9f4b6c5-p8m3 0/1 Pending 0 2s
my-app-7d9f4b6c5-q4n7 0/1 Pending 0 2s

Pending 的意思就是「這個 Pod 還沒有被分配到節點」。 它需要下一站的元件來決定它的歸屬。


第五站:kube-scheduler —— 決定去哪台機器

kube-scheduler 的工作只有一件事,而且它只做這一件事:看到沒有 nodeName 的 Pod,就幫它挑一台節點。

5-1. 兩個階段:先篩選,再評分

排程的決策分成兩步:

flowchart LR
    A["所有節點
node-1 ~ node-10"] --> B["① Filtering 篩選
哪些「可以」?"] B --> C["候選節點
node-2, node-5, node-7"] C --> D["② Scoring 評分
哪一個「最好」?"] D --> E["贏家
node-5"] style B fill:#cfe2ff,stroke:#084298,color:#000 style D fill:#fff3cd,stroke:#856404,color:#000 style E fill:#d4edda,stroke:#155724,color:#000

① Filtering(篩選)—— 把不可能的排除掉

這一關是硬性條件,不符合就直接出局:

  • 資源夠不夠?Pod 要求 2 CPU,但這台只剩 0.5 CPU → 淘汰
  • node selector / affinity 符合嗎?你指定要 disktype=ssd,這台沒有 → 淘汰
  • taint 擋住了嗎?節點有污點而 Pod 沒有對應的容忍設定 → 淘汰
  • port 衝突嗎?Pod 要用 hostPort 8080,這台已經被佔用 → 淘汰

② Scoring(評分)—— 從可以的裡面挑最好的

通過篩選的節點會被打分數,考量的因素包括:資源分布是否平均、image 是否已經在該節點上(省下拉取時間)、同一個 Service 的 Pod 是否過度集中等等。

分數最高的獲勝。

5-2. 關鍵:scheduler 不會自己建立容器

這是最容易誤解的地方。scheduler 選定 node-5 之後,它做的事只是把 nodeName: node-5 這個欄位寫回 apiserver

就這樣。它不會連到 node-5、不會拉 image、不會啟動容器。

sequenceDiagram
    participant S as scheduler
    participant A as apiserver
    participant E as etcd
    S->>A: 我看到一個 Pending 的 Pod
    S->>S: 篩選 + 評分,決定放 node-5
    S->>A: 幫我把 nodeName 設成 node-5
    A->>E: 寫入
    Note over S: scheduler 的工作到此結束

用一個比喻:scheduler 是婚姻仲介,不是搬家公司。 它負責配對,配好之後就退場了,真正把東西搬過去的是下一站的 kubelet。

這個「只寫欄位、不做事」的設計再次體現了宣告式哲學——scheduler 也只是在修改期望狀態,然後相信會有別的元件來實現它

[!TIP] 這解釋了 Pending 的除錯方向
如果 Pod 一直卡在 Pending,代表 scheduler 找不到適合的節點。直接問它原因:

1
kubectl describe pod <pod-name>

看 Events 區塊,通常會明說原因,例如 0/3 nodes are available: 3 Insufficient cpu(資源不足)或 node(s) had untolerated taint(被 taint 擋住)。


第六站:kubelet —— 終於有容器了

nodeName: node-5 被寫進 etcd 的那一刻,node-5 上的 kubelet 發現:有一個 Pod 被指派給我了。

kubelet 是每個 node 上的「代理人」——control plane 的決策,最終都由它在節點上執行。它是唯一真正會建立容器的元件

6-1. kubelet 拿到 Pod 之後做什麼

flowchart TD
    A["發現有 Pod 指派給我"] --> B["準備網路
呼叫 CNI 插件"] B --> C["準備儲存
掛載 Volume"] C --> D["拉取 image"] D --> E["透過 CRI 啟動容器"] E --> F["持續健康檢查
並回報狀態"] F --> F style E fill:#d4edda,stroke:#155724,color:#000

這裡出現三個 C 開頭的介面,它們是 Kubernetes 的插拔式設計——kubelet 自己不實作這些功能,而是定義介面讓別人來實作

介面 全名 負責 常見實作
CRI Container Runtime Interface 真正跑起容器 containerd、CRI-O
CNI Container Network Interface 給 Pod 網路和 IP Cilium、Calico、Flannel
CSI Container Storage Interface 掛載儲存空間 各家雲端的 volume driver

所以「kubelet 建立容器」其實是簡化的說法。精確地說:kubelet 透過 CRI 指揮 containerd 或 CRI-O 去建立容器。 kubelet 自己不碰容器的實作細節。

[!NOTE] 這解釋了為什麼升級時要查 CRI 版本
因為 kubelet 和 container runtime 是兩個獨立的專案,透過 CRI 介面溝通。介面會演進,所以版本要搭配得起來——這就是為什麼 CRI-O 的版號要跟 K8s 對齊。細節見 升級 Kubernetes 前,怎麼提前知道哪裡會爆

6-2. 它同時是「回報者」

kubelet 不只執行命令,它還持續回報:節點還活著嗎、資源用了多少、每個 Pod 的容器狀態如何。

kubectl get pods 看到的 RunningCrashLoopBackOffREADY 1/1,全都是 kubelet 回報上去、經由 apiserver 寫進 etcd 的資料。

它也負責執行你在 YAML 裡定義的健康檢查:

探針 檢查什麼 失敗時
livenessProbe 容器還活著嗎 重啟容器
readinessProbe 準備好接流量了嗎 從 Service 移除(不重啟)
startupProbe 啟動完成了嗎 保護啟動慢的應用不被誤殺

readinessProbe 失敗不會重啟容器,只會讓它暫時不接流量——這個區別在排查「服務時好時壞」的問題時很關鍵。

[!WARNING] kubelet 掛掉會怎樣?
節點上已經在跑的容器不會停——它們由 container runtime 維持,不是 kubelet。但這個節點會失去管理能力:無法建立新 Pod、無法回報狀態。

過一段時間後,node controller 發現這個節點失聯,會把它標記為 NotReady,並開始把上面的 Pod 驅逐、在別的節點重建。


第七站:kube-proxy —— 讓流量找得到 Pod

容器跑起來了,但還有最後一個問題:別人要怎麼連到它?

Pod 的 IP 是會變的——重啟一次就換一個。所以你不會直接連 Pod IP,而是透過 Service:一個固定不變的虛擬 IP,背後對應到一組隨時在變的 Pod。

把這件事實現出來的,就是 kube-proxy

7-1. 它其實不轉發流量

名字裡有 proxy,很容易誤會它像 Nginx 那樣代理流量。它不是。

kube-proxy 做的事是:在每個節點上維護網路轉發規則(iptables 或 IPVS 規則)。真正轉發封包的是 Linux 核心,不是 kube-proxy 這支程式

flowchart LR
    A["Service 或 Endpoint
有變動"] --> B["kube-proxy 發現"] B --> C["更新節點上的
iptables / IPVS 規則"] C --> D["Linux kernel
依規則轉發封包"] style C fill:#cfe2ff,stroke:#084298,color:#000 style D fill:#d4edda,stroke:#155724,color:#000

用比喻來說:kube-proxy 是交通號誌的設定人員,不是指揮交通的警察。 它設定好規則就退場,實際的流量疏導由核心負責。

這個設計有個很大的好處:流量不用經過 kube-proxy 這個程序,所以它掛掉的時候,既有的規則還留在核心裡,流量仍然會正常轉發——只是新的 Service 變更不會被套用。

7-2. 它怎麼知道要轉發給誰

這裡要串回第四站的 Endpoint controller。整個鏈路是這樣:

flowchart LR
    P["Pod 通過
readinessProbe"] --> E["Endpoint controller
把它加進 Endpoints"] E --> KP["kube-proxy 發現變動"] KP --> R["更新轉發規則"]

所以一個 Pod 要能接到流量,條件是:它的 readinessProbe 通過 → Endpoint controller 才會把它列入 → kube-proxy 才會把流量導過去

這解釋了滾動更新為什麼不會中斷服務:新 Pod 還沒 ready 之前,不會被加進轉發規則,流量繼續走舊 Pod

1
2
3
4
# 看看某個 Service 目前實際指向哪些 Pod IP
kubectl get endpoints my-app-svc

# 如果這裡是空的,代表沒有任何 Pod 通過 readinessProbe

[!TIP] 「Service 連不上」的排查順序

  1. kubectl get endpoints <svc> —— 有沒有 Pod IP?空的話問題在 readinessProbe 或 label selector
  2. Pod 的 label 跟 Service 的 selector 對得上嗎?
  3. 前兩項都正常才需要懷疑 kube-proxy 或 CNI

實際的連線測試方法,可以參考 Kubernetes 常用測試指令


全程回顧:那兩秒鐘的完整旅程

我們回到最初的問題:kubectl apply 之後的兩秒,到底發生了什麼?

sequenceDiagram
    autonumber
    participant U as 你
    participant K as kubectl
    participant A as apiserver
    participant E as etcd
    participant C as controller-manager
    participant S as scheduler
    participant KL as kubelet (node-5)
    participant P as kube-proxy

    U->>K: kubectl apply -f deployment.yaml
    K->>A: POST /apis/apps/v1/.../deployments
    A->>A: 驗證 → 授權 → 准入控制
    A->>E: 寫入 Deployment
    A-->>K: 201 Created
    K-->>U: deployment.apps/my-app created

    Note over C: 以下都是非同步發生的
    C->>A: watch 到新的 Deployment
    C->>A: 建立 ReplicaSet 與 3 個 Pod
    A->>E: 寫入(此時 Pod 為 Pending)

    S->>A: watch 到沒有 nodeName 的 Pod
    S->>S: 篩選 + 評分
    S->>A: 設定 nodeName = node-5

    KL->>A: watch 到指派給我的 Pod
    KL->>KL: CNI 網路 → CSI 儲存 → 拉 image
    KL->>KL: 透過 CRI 啟動容器
    KL->>A: 回報 Running

    P->>A: watch 到 Endpoint 變動
    P->>P: 更新 iptables / IPVS 規則

注意那一行 deployment.apps/my-app created 出現的位置——它在整個流程的很前面就回來了。後面所有的事情都是非同步發生的。

這就是為什麼「apply 成功」不等於「服務可用」。

七個元件,一句話總結

元件 位置 一句話
kubectl 你的電腦 API 客戶端,不是叢集的一部分
kube-apiserver Control Plane 唯一的前門,唯一能碰 etcd 的元件
etcd Control Plane 唯一的資料庫,叢集狀態的唯一真相
controller-manager Control Plane 不斷比對期望與現況,動手縮小差距
kube-scheduler Control Plane 只決定 Pod 去哪台,不負責建立
kubelet 每個 Node 唯一真正建立容器的元件,也負責回報狀態
kube-proxy 每個 Node 設定轉發規則,但不親自轉發流量

三個貫穿全文的設計原則

走完全程之後,回頭看會發現這七個元件遵循著同樣的三條原則:

① 所有溝通都經過 apiserver。 沒有任何兩個元件直接對話。scheduler 不會打電話給 kubelet,它只是修改 apiserver 上的資料,然後 kubelet 自己發現。這叫做 hub-and-spoke(軸輻式),好處是安全檢查集中、元件彼此解耦。

② 每個元件只管一件事,而且只修改狀態。 scheduler 只寫 nodeName、controller 只建立 Pod 物件。沒有人「命令」別人做事,大家都只是改變期望狀態,然後相信會有人來實現

③ 一切都是 reconcile loop。 沒有「執行一次就結束」的流程,只有永不停止的「比對現況與期望、然後修正」。這就是自我修復能力的來源。


用這些知識除錯:狀態對照表

知道每個元件負責什麼,最直接的用途就是看到問題時知道該查誰

你看到的狀態 卡在哪一站 先查什麼
kubectl 沒反應 / 連線逾時 apiserver apiserver 是否存活、kubeconfig 是否正確
403 Forbidden apiserver(授權關卡) kubectl auth can-i ... 檢查 RBAC
建立時被拒絕、或內容被改 apiserver(准入控制) 檢查 admission webhook(Gatekeeper、Istio)
Pod 一直 Pending scheduler 找不到節點 kubectl describe pod 看 Events:資源不足?taint?
Pod 卡在 ContainerCreating kubelet image 拉不下來?volume 掛不上?CNI 沒給到 IP?
CrashLoopBackOff 容器本身,不是元件問題 kubectl logs --previous 看應用程式錯誤
Pod Running 但沒流量 Endpoint → kube-proxy kubectl get endpoints 是否為空?readinessProbe 過了嗎?
全部指令都變唯讀 etcd quorum 失守 檢查 etcd 節點健康狀態
節點顯示 NotReady 該節點的 kubelet kubelet 服務是否運作、節點是否失聯

這張表的價值不在於背下來,而在於它示範了一種思考方式遇到問題時先問「這件事是誰負責的」,範圍立刻就縮小了


結語:從「會用」到「知道為什麼」

回到開頭那個問題:kubectl apply 之後的兩秒發生了什麼事?

答案是——你的願望被登記進了資料庫,然後一群各司其職的元件,各自發現了與自己有關的部分,接力把它變成現實。

沒有人指揮誰。controller-manager 不知道 scheduler 的存在,scheduler 也不認識 kubelet。它們只是各自盯著 apiserver,看到跟自己有關的變化就動手,做完之後把結果寫回去

理解這件事之後,很多原本要死背的規則會突然變得理所當然:

  • 為什麼刪掉的 Pod 會自己長回來?→ 因為你改的是現況,不是期望
  • 為什麼 apiserver 掛了服務還在跑?→ 因為容器由 kubelet 和 runtime 維持,不需要 control plane
  • 為什麼升級順序是 apiserver 優先?→ 因為所有人都要跟它講話
  • 為什麼 Pod 卡在 Pending?→ 因為排程是獨立的一步,還沒有人認領它

這也是為什麼理解元件職責不只是「知識」——它直接決定你除錯的速度。


延伸閱讀

官方參考來源

主題 連結
叢集架構總覽 https://kubernetes.io/docs/concepts/architecture/
kube-apiserver https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver
Admission Controllers https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
etcd 官方文件 https://etcd.io/docs/
備份與還原 etcd https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/
Secret 加密(encryption at rest) https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
kube-scheduler 排程機制 https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/
CRI / CNI / CSI 介面 https://kubernetes.io/docs/concepts/architecture/cri/