前言

如果你已經熟悉 Docker,應該很習慣用 docker run 啟動容器、用映像檔部署應用程式。但 Kubernetes 的難點不在於「如何再寫一份更長的 YAML」,而在於它把一個應用程式拆成多個控制迴圈、API 物件、節點元件與網路層,讓整個叢集持續朝你宣告的狀態靠近。

這篇文章的目標,是先建立一張可以用來排錯的地圖:當你執行 kubectl apply 後,誰儲存設定、誰建立 Pod、誰選擇 Node、誰啟動容器,以及 Pod 為什麼可能一直停在 PendingReady=False。讀完後,你應該能解釋 Kubernetes 的控制平面、節點元件與資料平面各自負責什麼,而不是只背一張元件名稱表。

本文刻意不把 StatefulSetDaemonSetJob 等 Workload 類型逐一展開,也不在這裡完整拆解 Pod-to-Pod 的封包路徑。前者會在下一篇 Workload 實戰篇處理,後者會在網路篇從 Network Namespace、CNI、Service 與 CoreDNS 開始拆開。本篇先回答更基礎、也更常影響排錯方向的問題:Kubernetes 到底如何把一份宣告變成正在執行的 Pod?

一、先建立 Kubernetes 的心智模型

這一節先不急著記元件名稱,而是先理解 Kubernetes 的核心工作方式:你提交的是「想要的狀態」,叢集中的控制器則持續觀察「目前的狀態」,並採取動作讓兩者逐漸一致。

1.1 Kubernetes 是宣告式系統

在命令式操作中,你會告訴系統「現在執行這個動作」;在 Kubernetes 中,你通常描述的是「最後希望叢集長什麼樣子」。例如下面的 Deployment 不是一段依序執行的腳本,而是一筆持續存在的 API 物件:

1
2
3
4
5
6
7
spec:
replicas: 2
template:
spec:
containers:
- name: web
image: nginx:1.27-alpine

這段 spec 表達的意思是:「我希望有兩個符合這個模板的 Pod」。它沒有指定「先在哪台主機建立容器,再呼叫哪個指令啟動」;具體的執行步驟由 Kubernetes 的元件協作完成。

可以把叢集中的狀態分成三個詞:

詞彙 意義 常見來源
Desired state(期望狀態) 使用者希望系統最後達到的樣子 API 物件的 spec,例如 Deployment 的 replicas: 2
Current state(目前狀態) 叢集現在實際觀察到的樣子 Pod 的 status、Node 的條件、容器狀態
Reconciliation(調和) 把目前狀態往期望狀態推進的過程 Controller、Scheduler、Kubelet 等控制迴圈

因此,kubectl apply 成功不代表容器已經啟動。它通常只代表 API Server 已經接受並儲存這筆宣告;後續控制迴圈仍需要建立 ReplicaSet、建立 Pod、安排 Node、拉取映像檔、啟動容器,最後才會回報 status

1.2 控制迴圈像一個不斷運作的自動駕駛

Kubernetes 的 Controller 可以類比成一個不斷運作的溫控器:它讀取目標溫度與目前溫度,必要時開啟或關閉設備,然後再次觀察結果。Controller 不會假設「我下過一次指令,事情就永遠完成」,而是持續面對 Pod 被刪除、Node 故障、映像檔拉取失敗與設定變更等情況。

flowchart LR
    D["YAML / kubectl\n描述 desired state"] --> API["kube-apiserver\nAPI、驗證與授權"]
    API <--> ETCD["etcd\n儲存 API objects"]
    API --> OBS["list / watch\n觀察物件變化"]
    OBS --> CTRL["Controllers\n調和資源"]
    OBS --> SCHED["Scheduler\n選擇 Node"]
    OBS --> KUBELET["Kubelet\n執行已分派的 Pod"]
    CTRL --> API
    SCHED --> API
    KUBELET --> API
    KUBELET --> ACTUAL["Node 上的 Runtime、Pod、網路"]
    ACTUAL --> STATUS["觀察到的 status"]
    STATUS --> KUBELET
    KUBELET --> API

這張圖有兩個容易被忽略的重點:

  1. 多數控制元件透過 Kubernetes API 交換叢集狀態,而不是彼此直接呼叫私有 API。
  2. 真正的容器程序與 Pod 網路在 Node 上運作;一般 Pod-to-Pod 流量不會繞到 API Server。API Server 儲存與傳遞「應該怎麼做」的資訊,並不是所有資料流量的代理。

1.3 Kubernetes API 物件是持續存在的紀錄

Kubernetes API 物件不是一次性的命令。當你建立一個 Deployment,這個物件會保留在 API Server 中,並由控制器持續引用。metadata 說明物件是誰,spec 說明想要什麼,status 則由叢集元件回報目前結果:

1
2
3
4
5
6
7
8
9
10
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
# 這裡描述期望狀態
status:
# 這裡由 Kubernetes 回報觀察結果,不是使用者拿來指揮控制器的主要欄位
availableReplicas: 2

實際的 status 欄位會依資源種類與 Kubernetes 版本而不同,且更新是非同步的。這也是為什麼排錯時不能只看 kubectl apply 的回應,還要搭配 getdescribe、事件與容器日誌觀察每一層的進度。

二、把叢集分成 Control Plane、Node Components 與 Data Plane

Kubernetes 的元件常被畫成一張大圖,初學者容易因此誤以為每個元件都做同一件事。本節把它們依責任拆開:Control Plane 做決策與保存叢集狀態,Node Components 把被分派的 Pod 變成實際程序,Data Plane 承載應用程式與網路流量。

2.1 整體拓樸

flowchart TB
    USER["kubectl / CI/CD / 使用者"] --> API["kube-apiserver"]

    subgraph CP["Control Plane 控制平面"]
        API
        ETCD["etcd\nAPI objects 的持久化儲存"]
        CONTROLLER["kube-controller-manager\n多個 Controllers"]
        SCHEDULER["kube-scheduler\nPod 到 Node 的綁定"]
    end

    API <--> ETCD
    CONTROLLER <--> API
    SCHEDULER <--> API

    subgraph NODE["Node 節點"]
        KUBELET["kubelet"]
        RUNTIME["Container Runtime\ncontainerd / CRI-O"]
        CNI["CNI plugin\nPod 網路設定"]
        PROXY["kube-proxy 或其他 Service data path"]
        PODS["Pods / containers\n實際應用程序"]
    end

    API <--> KUBELET
    KUBELET -->|CRI gRPC| RUNTIME
    RUNTIME -->|建立 sandbox 時呼叫| CNI
    RUNTIME --> PODS
    CNI --> PODS
    PODS <-->|"Data Plane:應用程式流量"| PODS
    PROXY --> PODS

這裡的 Node ComponentsData Plane 不是互斥的分類:kubelet、Runtime、CNI 都在 Node 上,並共同支撐資料平面;分開寫是為了區分「負責管理與執行的元件」和「真正承載請求的 Pod、網路與轉送規則」。

2.2 Control Plane:做決策、保存狀態、推動調和

Control Plane 不直接在每個 Node 上執行你的 Web Server。它維護 Kubernetes API 所代表的叢集狀態,並透過控制器與 Scheduler 產生下一步應該發生的變更。

API Server:所有 Kubernetes API 操作的入口

kube-apiserver 是 Kubernetes HTTP API 的核心服務。kubectl、Controller、Scheduler、Kubelet,以及使用 Kubernetes API 的其他客戶端,通常都透過它讀取或修改 API 物件。

API Server 的責任包括:

  • 驗證請求者身分(authentication)與權限(authorization)。
  • 驗證 API schema、套用預設值,並執行 admission 相關檢查。
  • 將 API 物件讀寫到 etcd。
  • 提供 listwatch 等機制,讓其他元件觀察物件變化。
  • 對特定操作代理到 Kubelet,例如取得日誌、execattachport-forward

但 API Server 不是容器執行引擎,也不是所有網路流量的中介。例如 Pod A 連到 Pod B 時,封包由 Node 上的網路元件處理,不會因為 Kubernetes API 的存在而每一跳都經過 API Server。

etcd:API 物件的持久化資料庫

etcd 是一致性鍵值儲存,保存 Kubernetes API Server 的叢集資料,例如 Deployment、Pod、Node、ConfigMap 與 Service 等物件。通常只有 API Server 直接對外提供 Kubernetes API;其他控制元件透過 API Server 讀寫叢集狀態。

把 etcd 想成「Kubernetes 的唯一真相來源」很有用,但要注意它不是應用程式資料庫,也不會替你保存容器內的應用程式資料。正式環境還要考慮 etcd 的多副本、備份、還原與磁碟延遲。

Controller:把目前狀態推向期望狀態

Controller 是一種控制迴圈,不是單一固定元件。它會觀察特定 API 物件,並透過 API Server 建立或更新其他物件,使目前狀態更接近期望狀態。

以 Deployment 為例,可以先記住這個責任分工:

  • Deployment Controller 觀察 Deployment,管理不同版本的 ReplicaSet 與 rollout。
  • ReplicaSet Controller 觀察 ReplicaSet,確保符合模板的 Pod 數量足夠。
  • 其他 Controllers 則處理 Node、Job、EndpointSlice 等不同領域。

Controller 自己不會在 Node 上呼叫 docker run,也不會直接把應用程式程序啟動起來。它透過 API Server 建立「應該存在的 Pod 物件」,後續由 Scheduler、Kubelet 與 Runtime 接手。

Scheduler:只決定 Pod 要去哪裡

kube-scheduler 會觀察還沒有 spec.nodeName 的 Pod,從可行的 Node 中依資源需求、taint/toleration、affinity、拓樸與其他規則進行篩選與評分,最後把 Pod bind 到某個 Node。

Scheduler 的工作到「選擇與綁定 Node」為止;它不負責拉映像檔、不建立容器,也不負責判斷應用程式的 HTTP readiness。若沒有符合條件的 Node,Pod 會維持未排程狀態,kubectl describe pod 的 Events 通常比盯著一個固定的 Pending 文字更有用。

2.3 Node Components:把被分派的 Pod 實際跑起來

Node 上的元件接收已經被分派的 Pod,建立執行環境,並將實際狀態回報到 API Server。它們和 Control Plane 有 API 層的互動,但容器啟動與資料流量是 Node 本地的工作。

Kubelet:每台 Node 的 Pod 生命週期代理人

Kubelet 是 Node 上的主要代理程式。它會取得被指派到自己 Node 的 Pod 規格,持續執行本地同步迴圈,並負責:

  • 透過 Container Runtime 建立或停止 Pod sandbox 與容器。
  • 執行 startup、liveness、readiness 等 probes。
  • 將容器狀態、Pod conditions 與 Node heartbeat 回報給 API Server。
  • 掛載由 Kubernetes 指定的 Volume,並處理容器日誌等本地生命週期工作。

Kubelet 不負責替 Pod 選 Node;那是 Scheduler 的責任。它也不會把每個 Pod-to-Pod 封包送到 API Server。

CRI 與 Container Runtime:Kubelet 如何請 Runtime 工作

熟悉 Docker 的人容易把 kubelet 想成一直執行 docker run。實際上,Kubelet 透過 CRI(Container Runtime Interface) 使用 gRPC 與 Container Runtime 溝通。CRI 是介面與協定,Runtime 才是實際負責映像檔、Pod sandbox、容器建立、啟動、停止與狀態查詢的程式。

常見 Runtime 包括 containerdCRI-O。Docker 建立的 OCI 映像檔通常可以被這些 Runtime 使用,但 Kubernetes 不需要透過 Docker CLI 才能執行容器;「建立映像檔」和「在 Kubernetes Node 上執行映像檔」是兩件不同的事。

1
2
3
4
kubelet --(CRI / gRPC)--> containerd 或 CRI-O
├─ image service:拉取與管理映像檔
├─ runtime service:建立 Pod sandbox、容器與生命週期
└─ 呼叫已設定的 CNI plugin 建立 Pod 網路

CNI:替 Pod 建立網路環境

CNI(Container Network Interface)不是一個固定的 Kubernetes 控制平面服務,而是一套介面規格與 plugins。 當 Runtime 建立 Pod sandbox 時,會依叢集配置呼叫 CNI plugin,為該 Pod 的 network namespace 設定介面、IP、路由或其他網路能力。

CNI 主要回答「這個 Pod 如何接上叢集網路」;它不是用來啟動應用程式,也不是 CoreDNS。具體的 IPAM、跨 Node 傳輸、NetworkPolicy 與 eBPF 實作,會依 CNI 方案不同而改變,留到網路篇再拆解。

kube-proxy 與其他資料平面實作

許多叢集會在 Node 上運行 kube-proxy,維護 Service 相關的轉送規則;也有其他方案使用 eBPF 等資料平面實作。這些元件影響 Service 流量如何找到後端 Pod,但不是 Scheduler 或 Kubelet 的替代品。

2.4 Data Plane:真正承載應用程式請求的地方

Data Plane 指的是實際承載工作負載與流量的部分,包括 Node 上的 Pod、容器程序、network namespace、介面、路由,以及 Service 流量的轉送規則。Control Plane 可以決定「應該有哪幾個 Pod」,但 HTTP、TCP 或 UDP 封包通常在 Data Plane 中直接流動。

可以用下面的責任表快速定位問題:

區域 代表元件 主要問題
Control Plane API Server、etcd、Controllers、Scheduler 叢集應該有哪些物件?Pod 應該綁到哪個 Node?
Node Components Kubelet、CRI Runtime、CNI、kube-proxy 被指派的 Pod 是否能在這台 Node 建立與運作?
Data Plane Pod、容器程序、Pod 網路、Service 轉送規則 實際請求是否能抵達應用程式?

對應到常見的通信路徑:

通信雙方 用途 是否是一般應用程式資料流量
kubectl → API Server 建立、查詢、更新 API 物件 否,是控制操作
Controller / Scheduler → API Server 讀取狀態、建立物件、寫入 binding 或 status 否,是控制平面協作
Kubelet ↔ API Server 取得 Pod 規格、回報 Node/Pod 狀態 否,是管理與回報
Kubelet → Runtime 透過 CRI 建立 sandbox 與容器 否,是 Node 本地執行
Runtime → CNI 建立 Pod 網路介面與路由 否,是 Node 本地網路設定
Pod A → Pod B 應用程式互相呼叫 是,走 Data Plane,不經 API Server

這張表可以避免一個很常見的錯誤模型:「Kubernetes 的所有元件只和 API Server 溝通,所以應用程式流量也會經過 API Server。」 API Server 是叢集 API 的 hub,但不是應用程式網路的 router。

三、從 API 物件理解「宣告」與「回報」

理解 specstatus 與物件之間的關係,能讓你在看到一份 YAML 或一串 kubectl 輸出時知道應該問哪個控制迴圈,而不是把所有問題都歸咎於「Kubernetes 還在處理」。

3.1 spec 是我要什麼,status 是現在怎樣

以 Deployment 為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27-alpine
  • metadata 提供名稱、Namespace、labels 與 owner references 等識別資訊。
  • spec 是使用者或上層控制器宣告的期望狀態。
  • status 是 Kubernetes 元件根據實際觀察結果寫回的狀態,例如目前副本數、可用副本數與條件。

status 不一定會立即反映最新的 Node 或容器狀態,因為它必須經過 Kubelet 回報、API Server 儲存,以及上層 Controller 再次觀察的非同步流程。排錯時,status 是重要線索,但不應脫離 Events、describe 與 Logs 單獨解讀。

3.2 Controller 不會「執行 YAML」,而是產生下一個物件

可以把 Deployment 的一段調和理解成:

1
2
3
4
5
6
7
8
9
10
Deployment.spec.replicas = 2
目前沒有符合條件的 Pod

Deployment Controller 建立 ReplicaSet 物件

ReplicaSet Controller 建立 2 個 Pod 物件

Scheduler 為 Pod 填入 spec.nodeName

對應 Node 的 Kubelet 才開始要求 Runtime 啟動容器

每一個箭頭之間都可能有延遲或失敗。Deployment Controller 建立 ReplicaSet 成功,不代表 ReplicaSet 已經建立 Pod;Pod 建立成功,也不代表它已經有 Node 或容器程序。這個分層正是 kubectl get deployment,rs,pod 很有價值的原因。

3.3 Container restart、Pod replacement 與 reconciliation 不同

Part 2 會完整介紹 Workload 的生命週期;這裡只先建立排錯所需的區分:

現象 主要處理者 你應該先問什麼
同一個 Pod 的容器反覆重啟 Kubelet、Runtime 程序是否退出?liveness probe 是否失敗?看 restartCount、Logs 與 Events
Pod 物件被替換 擁有它的 Workload Controller 是誰擁有這個 Pod?新 Pod 是否被重新排程?看 ownerReferences 與 ReplicaSet
specstatus 持續靠近 對應的 Controller、Scheduler、Kubelet 哪一個控制迴圈還沒有完成工作?看下一層物件與 Events

因此,CrashLoopBackOff 通常表示同一個 Pod 內的容器反覆失敗並逐漸延長重試間隔;它不等於 Kubernetes 已經建立了很多個新 Pod。相反地,直接建立的裸 Pod 沒有 Deployment 或 ReplicaSet 負責在 Pod 物件消失後補回它。

四、完整流程:kubectl apply 一個 Deployment 後發生什麼?

這是本篇最重要的流程。下面以一個兩副本的 Nginx Deployment 為例,依照責任邊界追蹤從 API 請求到 Pod Ready 的過程;實際事件是非同步的,時間上可能交錯,但因果關係可以用這個順序理解。

4.1 使用者宣告 Deployment

先準備一個最小但包含 readiness probe 的 Deployment。這裡沒有建立 Service,因為本節只追蹤「Pod 如何被建立並啟動」;Service 與 Pod 之間的流量會在後續文章處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
name: mental-model-demo
labels:
app: mental-model-demo
spec:
replicas: 2
selector:
matchLabels:
app: mental-model-demo
template:
metadata:
labels:
app: mental-model-demo
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 2
timeoutSeconds: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi

執行:

1
2
kubectl apply -f deployment-demo.yaml
kubectl rollout status deployment/mental-model-demo

kubectl apply 的回應只代表 API 請求被接受;rollout status 才是在等待 Deployment 的 rollout 條件逐漸達成。不要把這兩個命令當成同一件事。

4.2 階段一:API Server 驗證並儲存 Deployment

  1. kubectl 讀取目前 context 的憑證與 API Server 位址,送出建立或更新 Deployment 的 HTTPS 請求。
  2. API Server 進行 authentication、authorization、schema validation、defaulting 與 admission 檢查。
  3. 通過後,API Server 將 Deployment 物件寫入 etcd,並把請求結果回傳給 kubectl

此時可以說「Deployment 這筆宣告已經進入叢集」,但還不能說「兩個 Nginx 容器已經在運行」。API Server 不會在同一個 HTTP 請求中同步等待後續所有控制迴圈完成。

1
2
3
kubectl --HTTPS--> kube-apiserver --讀寫--> etcd

└─ 其他元件之後透過 API 的 list/watch 觀察變化

4.3 階段二:Deployment Controller 建立 ReplicaSet

Deployment Controller 觀察到新的 Deployment,將它和目前已存在的 ReplicaSet 比對。因為目前沒有相同 Pod template 的 ReplicaSet,它會透過 API Server 建立一個新的 ReplicaSet,並以 owner reference 表示這個 ReplicaSet 由 Deployment 管理。

ReplicaSet 通常會帶有能識別 Pod template 版本的 label。這讓 Deployment 可以在更新映像檔時建立新 ReplicaSet,並逐步調整新舊版本;本篇先不展開 rolling update 的細節。

1
2
3
Deployment Controller
│ 觀察 Deployment.spec
└─API Server:建立 ReplicaSet

4.4 階段三:ReplicaSet Controller 建立未排程的 Pod 物件

ReplicaSet Controller 觀察到 ReplicaSet 的期望副本數是 2,但目前符合 selector 的 Pod 數量不足,因此透過 API Server 建立兩個 Pod 物件。

這兩個 Pod 物件一開始通常還沒有 spec.nodeName。它們是「希望某台 Node 執行的宣告」,不是已經在某台機器上存在的 Linux process。此時你可能看到 Pod 為 Pending,而這個階段尚未代表映像檔或應用程式本身有問題。

1
2
3
ReplicaSet Controller
│ 觀察 ReplicaSet.spec.replicas = 2
└─API Server:建立 Pod A、Pod B(尚未綁定 Node)

4.5 階段四:Scheduler 選擇 Node 並完成 binding

Scheduler 透過 API Server 觀察到未排程的 Pod,為每個 Pod 執行排程流程:

  1. Filtering:排除資源不足、taint 不相容、node affinity 不符合等 Node。
  2. Scoring:在可行 Node 中依規則評分。
  3. Binding:把選定的 Node 寫入 Pod 的 spec.nodeName

Scheduler 只負責「這個 Pod 應該去哪一台 Node」,不會直接操作 Node 上的 Runtime。若所有 Node 都不符合條件,Pod 會繼續留在未排程狀態;此時應從 kubectl describe pod 的 Events 找出原因,而不是只重複執行 kubectl apply

1
2
3
4
5
Pod A(nodeName 空白) ─┐
├─ Scheduler:filter → score → bind
Pod B(nodeName 空白) ─┘

└─ API Server:寫入各自的 spec.nodeName

4.6 階段五:Kubelet 透過 CRI 要求 Runtime 建立 Pod

被綁定的 Node 上,Kubelet 透過 API Server 的 watch 或同步流程取得這些 Pod 規格。Kubelet 接著與該 Node 的 Container Runtime 溝通,通常會經過以下工作:

  1. 建立 Pod sandbox 與對應的 network namespace。
  2. 在建立 sandbox 的過程中,讓 Runtime 依設定呼叫 CNI plugin,配置 Pod 的介面、IP 與路由。
  3. 檢查本地是否已有映像檔;沒有時從 registry 拉取。
  4. 依 Pod spec 建立並啟動容器。
  5. 監控容器、執行 probes、收集狀態與事件。

這裡可以看到 CRI、CNI 與 Kubelet 的邊界:Kubelet 負責 Pod 生命週期協調,CRI 是 Kubelet 和 Runtime 的協定,Runtime 負責容器執行,CNI 負責把 Pod 接上網路。任何一層失敗,都可能讓 Pod 卡在 ContainerCreating 或反覆重試,但原因要靠 Events 與 Node 上的日誌確認。

4.7 階段六:Kubelet 回報 status,readiness 決定是否 Ready

容器程序啟動後,Kubelet 會持續執行 readiness probe。對上面的 Nginx 而言,Kubelet 會在 Pod 的網路環境中檢查 HTTP / 是否成功;probe 成功後,Kubelet 將容器與 Pod 的狀態回報給 API Server。

這裡要區分兩個概念:

  • Running 通常表示容器已經被啟動,但不代表應用程式已經可以接收流量。
  • Ready=True 表示這個 Pod 通過目前設定的 readiness 條件,之後 Service 等流量管理元件才會把它視為可用後端。

Deployment Controller 會再觀察 Pod 的狀態,更新 Deployment 的 status,例如目前副本數、updated replicas 或 available replicas。當實際狀態符合 Deployment 的期望,kubectl rollout status 才會結束等待。

sequenceDiagram
    participant U as kubectl
    participant A as API Server
    participant E as etcd
    participant D as Deployment Controller
    participant R as ReplicaSet Controller
    participant S as Scheduler
    participant K as Kubelet
    participant RT as Runtime / CNI

    U->>A: apply Deployment
    A->>E: 儲存 Deployment
    A-->>U: 請求已接受
    D->>A: 建立 ReplicaSet
    R->>A: 建立未排程的 Pods
    S->>A: 讀取未綁定 Pods
    S->>A: 寫入 spec.nodeName
    K->>A: 取得被指派的 Pod spec
    K->>RT: CRI:建立 sandbox、容器
    RT->>RT: 呼叫 CNI、拉取映像檔、啟動容器
    K->>K: 執行 readiness probe
    K->>A: 回報 Pod status / Ready
    A->>E: 儲存 status

這個流程是「控制器逐步調和」而不是一個大型函式呼叫。各元件可能重試、並行處理或因為 API、資源、映像檔與網路問題暫停,因此實際 Events 的先後順序可能與示意圖略有不同。

五、用心智模型判讀 kubectl 輸出

當狀態不如預期時,最有效的做法是沿著物件與控制迴圈逐層縮小範圍。先判斷「哪一個物件沒有出現」或「哪個狀態沒有往前走」,再找負責該轉換的元件。

5.1 從 Deployment 一路往下查

觀察到的情況 可能卡住的邊界 第一批觀察指令
Deployment 存在,但沒有 ReplicaSet API Server 或 Deployment Controller kubectl describe deployment mental-model-demokubectl get events
ReplicaSet 存在,但沒有足夠 Pod API Server 或 ReplicaSet Controller kubectl describe rskubectl get events
Pod 存在但沒有 nodeName Scheduler 或排程條件 kubectl describe podkubectl get events
Pod 已有 Node,但長時間 ContainerCreating Kubelet、Runtime、CNI、映像檔或 Volume kubectl describe pod、Node/Kubelet/Runtime 日誌
Pod RunningReady=False readiness probe 或應用程式啟動 kubectl describe podkubectl logs、probe 設定
Pod Ready 但請求失敗 Service、DNS、網路政策或應用程式 留到網路篇按封包路徑排查

這張表的重點不是把每個錯誤都背起來,而是建立「資源層級 → 負責元件 → 觀察證據」的關聯。例如看到 Pending 時,先問它是不是根本還沒有 Node;看到 ContainerCreating 時,則已經表示 Scheduler 大致完成工作,排查焦點應往 Kubelet、Runtime、CNI 或映像檔移動。

5.2 getdescribeeventswatch 各自回答什麼

1
2
3
4
5
6
7
8
9
10
11
12
# get:快速看物件是否存在,以及狀態是否持續變化
kubectl get deployment,replicaset,pod -l app=mental-model-demo

# describe:看單一物件的 spec、conditions、owner 與 Events
kubectl describe deployment mental-model-demo
kubectl describe pod <pod-name>

# events:按時間看叢集對該物件做過哪些動作或遇到哪些失敗
kubectl get events --sort-by=.metadata.creationTimestamp

# watch:不要只看一次快照,觀察物件如何從建立走到 Ready
kubectl get pods -l app=mental-model-demo -o wide -w

kubectl get 是快照,kubectl get -w 才能看見非同步調和的過程;describe 通常提供資源與事件的上下文;Events 適合回答「最近哪個元件嘗試做什麼」。它們不能取代容器 Logs,也不能取代 Node 上的 Runtime/CNI 日誌,但能幫你先定位責任層。

六、etcd 不可用時,為什麼不是「整個叢集立即癱瘓」?

這個問題很適合用來檢查自己的架構模型。etcd 對 Kubernetes 的控制面非常重要,但「控制面無法繼續改變狀態」和「所有已經存在的程序在同一秒停止」不是同一件事。

6.1 etcd 故障會先影響哪些事情?

當 API Server 無法從 etcd 讀寫資料時,常見影響包括:

  • 新的 kubectl apply、刪除、更新與許多查詢可能失敗或無法取得最新資料。
  • Controllers 與 Scheduler 無法可靠地讀取或寫入它們需要的 API 狀態,因此新的調和、排程與 status 更新會停滯。
  • Kubelet 的 Node heartbeat 與 Pod status 可能無法回報;Control Plane 看到的狀態會變舊。
  • 已經在 Node 上運行的容器,可能暫時仍由 Kubelet 與 Runtime 依本地狀態繼續運作。
  • 已經存在的部分網路轉送規則,可能在沒有新變更時繼續處理流量,但不能把這當成所有叢集、所有 CNI 與所有故障情境的保證。

最重要的限制是:「現有程序暫時還活著」不代表叢集仍然健康。 如果容器之後崩潰、Node 重啟、Pod 需要被替換、Service 規則需要更新,控制面無法持久化與調和就可能無法恢復預期狀態。

6.2 高可用與恢復的正確理解

正式環境通常會讓 etcd 以奇數個成員形成 quorum,並配合定期快照、備份還原演練與磁碟效能監控。這是為了降低「單一 etcd 成員故障」對 API Server 的影響,不是讓 etcd 變成可以隨意遺失資料的快取。

因此,比「etcd 掛掉等於叢集立即停止」更精確的說法是:

etcd 不可用時,Kubernetes 控制面通常無法可靠地讀寫與調和叢集狀態;既有 Node 上的部分工作負載可能暫時繼續運作,但新變更、故障修復、狀態回報與控制面決策都可能停滯。實際影響取決於故障範圍、持續時間、Node 與資料平面當時的狀態。

這個區分也說明了為什麼 Kubernetes 的備份重點不只是 YAML:YAML 可以重建宣告,但不能取代 etcd 中的叢集 metadata、Secret、RBAC、Lease 與其他 API 物件,也不能取代應用程式資料的獨立備份。

七、本地實驗:觀察一個 Deployment 如何被調和

這個實驗把前面的元件放到一個可重建的 kind 多節點叢集中。它不會假裝貼出固定的 Pod 名稱、Node 名稱或事件輸出;請以你本機實際觀察到的結果為準,並用 getdescribe、Events 與 watch 對照每個階段。

7.1 建立 kind 多節點叢集

請先準備 Docker、kubectlkind。kind 的 Node 是 Docker 容器,適合做控制流程與排錯練習,但不等同於正式環境的硬體隔離或高可用架構。

1
2
3
4
5
6
7
8
9
10
11
12
cat <<'EOF' | kind create cluster --name k8s-mental-model --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

kubectl config use-context kind-k8s-mental-model
kubectl get nodes -o wide
kubectl get pods -n kube-system -o wide

kubectl get pods -n kube-system 可以讓你看到這個環境中的控制面與系統元件;名稱與版本會依 kind 與 Kubernetes 版本變化,不要把某次輸出硬編碼成判斷標準。

7.2 建立並套用 Deployment

前面 4.1 節的 YAML儲存為 deployment-demo.yaml,然後套用:

1
2
kubectl apply -f deployment-demo.yaml
kubectl rollout status deployment/mental-model-demo

如果你想完全不建立本地檔案,也可以把同一份 YAML 透過標準輸入傳給 kubectl apply -f -;本實驗後續指令假設資源名稱是 mental-model-demo

7.3 用 watch 看物件逐層出現

開啟兩到三個終端機,分別執行以下觀察。這些指令只要求 Kubernetes 回報當下狀態,不預設固定輸出:

1
2
# 終端機 A:Deployment、ReplicaSet、Pod 的物件關係與狀態變化
kubectl get deployment,replicaset,pod -l app=mental-model-demo -w
1
2
# 終端機 B:觀察建立、排程、拉取映像檔與 probe 相關事件
kubectl get events --sort-by=.metadata.creationTimestamp -w
1
2
# 終端機 C:特別注意 Pod 被分派到哪個 Node,以及 READY 欄位何時變化
kubectl get pods -l app=mental-model-demo -o wide -w

你可以把觀察到的變化對回前面的流程:Deployment 先存在,接著出現 ReplicaSet,再出現沒有 Node 或仍在等待的 Pod;Scheduler 綁定後,Pod 的 NODE 欄位才會有值;Kubelet 與 Runtime 完成工作後,容器狀態與 readiness 才會逐步更新。

7.4 用 describe 與 JSONPath 找出每一層的證據

先取得一個實際 Pod 名稱,再查看它的 owner、Node 與 Ready condition:

1
2
3
4
5
6
7
POD=$(kubectl get pod -l app=mental-model-demo -o jsonpath='{.items[0].metadata.name}')

kubectl describe pod "$POD"
kubectl get pod "$POD" -o jsonpath='{.metadata.ownerReferences[0].kind}{"\n"}'
kubectl get pod "$POD" -o jsonpath='{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get pod "$POD" -o jsonpath='{.spec.nodeName}{"\n"}'
kubectl get pod "$POD" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}{"\n"}'

請特別查看 describe 的 Events,而不是只截取 kubectl get pod 的一行狀態。你應該能從實際物件確認:Pod 的 owner 是哪個 ReplicaSet、spec.nodeName 是否已填入,以及 Ready condition 是由哪個階段之後才出現。

7.5 刪除 Pod,觀察 Controller 如何補回

這一步用來驗證「Deployment 是持續存在的期望狀態,不是一次性命令」:

1
2
3
kubectl delete pod -l app=mental-model-demo --wait=false
kubectl get pods -l app=mental-model-demo -o wide -w
kubectl get deployment,replicaset,pod -l app=mental-model-demo

刪除的是 Pod 物件,不是 Deployment。ReplicaSet Controller 會再次比較副本數,並建立新的 Pod;新 Pod 的名稱、UID 或 Node 可能和原本不同。這也正是為什麼正式服務通常由 Workload Controller 管理,而不是只建立裸 Pod。

7.6 清理實驗

完成觀察後刪除資源與 kind 叢集,避免本地環境留下不必要的容器:

1
2
kubectl delete -f deployment-demo.yaml
kind delete cluster --name k8s-mental-model

八、小結:遇到問題時,先問「誰應該做下一步?」

這一篇的重點不是記住更多元件名稱,而是用責任邊界理解 Kubernetes:API Server 接受並提供 API,etcd 持久化 API 狀態,Controllers 調和資源,Scheduler 選擇 Node,Kubelet 協調 Node 本地生命週期,CRI 讓 Kubelet 使用 Runtime,CNI 建立 Pod 網路,而 Data Plane 才承載應用程式流量。

可以把整篇濃縮成這條鏈:

1
2
3
4
5
6
7
8
宣告 desired state
→ API Server 驗證並儲存
→ Controllers 建立下一層物件
→ Scheduler 將 Pod 綁定到 Node
→ Kubelet 透過 CRI 要求 Runtime 工作
→ Runtime 呼叫 CNI、啟動容器
→ Kubelet 回報 status / readiness
→ Controllers 再次調和,直到目前狀態接近期望狀態

下次看到 PendingContainerCreatingRunningReady=False 時,先不要把它們當成單一錯誤類型;先問:哪個物件已經存在?哪個轉換尚未完成?負責這個轉換的元件是哪一個?我可以用哪個 kubectl 證據驗證? 這套問題比背命令更能把 Kubernetes 的行為串起來。

系列文章導覽

以下連結使用目前專案文章的實際 permalink,方便沿著概念、網路、流量與排錯路徑閱讀。

參考資源

本文的元件責任、控制迴圈與本地實驗以官方文件為準;實際叢集的 Runtime、CNI、雲端整合與 Kubernetes 版本可能改變細節。