一個 kubectl apply 的旅程:Kubernetes 七個元件在做什麼
前言:那兩秒之間發生了什麼事?
你敲下 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
有兩個規律先記著,等一下每一站都會再看到:
- 所有東西都要經過 kube-apiserver。沒有任何元件會跳過它直接找別人講話。
- 只有 kube-apiserver 能碰 etcd。其他元件想知道叢集現在長什麼樣,一律問 apiserver。
這兩條規律是整個架構的骨幹。現在出發。
第一站:kubectl —— 它其實不在叢集裡
旅程從你的筆電開始。
這裡有個很多人沒意識到的事實:kubectl 不是 Kubernetes 叢集的一部分。 它就是一支裝在你電腦上的 CLI 程式,跟 curl 沒有本質差別——它把你的 YAML 轉成 HTTP 請求,打到 apiserver 的 REST API。
不信的話,你可以把 kubectl 在做的事直接用 curl 做一遍:
1 | # 讓 kubectl 在本機開一個到 apiserver 的代理 |
你會拿到一份 JSON,內容跟 kubectl get pods -o json 一模一樣。因為它們本來就是同一件事。
想看得更清楚,加上 -v=8 讓 kubectl 把它實際發出的 HTTP 請求印出來:
1 | kubectl apply -f deployment.yaml -v=8 |
你會看到類似這樣的輸出:
1 | POST https://10.0.0.1:6443/apis/apps/v1/namespaces/default/deployments |
你的 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 | # 我可以在 default namespace 建立 deployment 嗎? |
③ 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. 為什麼「所有東西都要經過它」是好設計
把所有流量都塞給同一個元件,聽起來像是瓶頸。但這個設計換來三件很重要的事:
- 安全檢查只需要做一次。驗證、授權、准入控制全部集中在一個地方,不用在每個元件裡各實作一遍——那樣不只重複,還很容易出現漏洞。
- 資料格式可以統一轉換。你用舊的
apiVersion送進來,apiserver 會自動轉成目前的儲存格式。這就是為什麼 API 版本升級時,你既有的資源不會突然爆掉。 - 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 | /registry/deployments/default/my-app → {Deployment 的完整 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還能用(讀取還可以),但任何apply、delete、scale全部失敗。而且叢集也失去自我修復能力——Pod 掛了不會被重建,因為 controller 沒辦法寫入新狀態。
3-3. 深層:備份,以及那個沒人想遇到的場景
前面講的容錯有個前提:壞掉的是機器,不是資料。
但如果是資料本身出問題呢——有人誤刪了整個 namespace、或是 etcd 資料損毀?這時候多幾台節點救不了你,因為每一台都忠實地同步了那個錯誤。這正是 quorum 的設計目的:讓所有節點保持一致。它不會判斷內容對不對。
所以你需要備份。
備份指令:
1 | ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot-$(date +%F).db \ |
檢查備份檔:
1 | ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-snapshot-2026-08-22.db --write-out=table |
還原:
1 | ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-snapshot-2026-08-22.db \ |
還原的完整流程比一行指令複雜:要先停掉 apiserver(避免還原過程中有人寫入)、每個 etcd 節點都要還原、改設定指向新的 data dir、再把 apiserver 拉起來。請務必在測試環境演練過再說你有備份。
[!IMPORTANT] 沒演練過的備份,等於沒有備份
這是維運的老話但值得再說一次:備份的價值不在於「有檔案」,而在於「確定能還原」。定期做一次還原演練,你會發現各種問題——憑證過期、版本不相容、磁碟空間不足、還原後 Pod 全部卡住。這些都是你不會想在真正出事那天才第一次遇到的。
還有一個容易被忽略的點:Secret 在 etcd 裡預設只是 base64 編碼,不是加密。
1 | # 任何能讀 etcd 的人,都能直接看到你的密碼 |
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 | # 看看你的 Deployment 底下有哪些 ReplicaSet |
4-3. 此刻的狀態:Pod 有了,但還沒有家
controller-manager 建立了三個 Pod 物件,寫回 etcd。
但這三個 Pod 現在處於一個特殊狀態——它們還不知道自己要跑在哪台機器上。Pod 規格裡有個欄位叫 spec.nodeName,現在是空的。
這種狀態你一定看過:
1 | $ kubectl get pods |
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 看到的 Running、CrashLoopBackOff、READY 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 | # 看看某個 Service 目前實際指向哪些 Pod IP |
[!TIP] 「Service 連不上」的排查順序
kubectl get endpoints <svc>—— 有沒有 Pod IP?空的話問題在 readinessProbe 或 label selector- Pod 的 label 跟 Service 的 selector 對得上嗎?
- 前兩項都正常才需要懷疑 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?→ 因為排程是獨立的一步,還沒有人認領它
這也是為什麼理解元件職責不只是「知識」——它直接決定你除錯的速度。
延伸閱讀
- 升級 Kubernetes 前,怎麼提前知道哪裡會爆 —— 這篇講的是「元件在做什麼」,那篇講的是「升級時這些元件的版本怎麼搭配」,包含版本歪斜政策為什麼以 apiserver 為基準
- Kubernetes 常用測試指令 —— 除錯對照表裡提到的連線測試,實際指令在這裡
- 在 k3s 上手把手打造 Prometheus + Grafana 監控系統 —— 想觀察這些元件的實際運作指標,可以從監控系統開始
官方參考來源
| 主題 | 連結 |
|---|---|
| 叢集架構總覽 | 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/ |