前言

上一篇文章介紹了 Kubernetes 的控制平面與節點元件。本篇把焦點移到「應用程式到底要如何在叢集中運行」:你要部署的是可替換的 Web API、需要固定身份的資料服務、每台節點都要有一份的 agent,還是跑完就結束的批次工作?

Kubernetes 的 Workload 資源不是一堆互相獨立的 YAML 名詞,而是不同的生命週期模型。讀完本篇,你應該能先選對資源,再從 Pod、Controller、節點與容器四個層次判斷「誰會重啟、誰會補 Pod,以及為什麼」。

本篇會用幾個可以直接套用到測試叢集的實驗,觀察 Deployment 的滾動更新、StatefulSet 的固定身份、DaemonSet 隨節點變化的行為,以及 Job/CronJob 的完成與重試。為了讓主題保持集中,Service 只保留必要基礎;CoreDNS、EndpointSlice、CNI、L4/L7 與 Ingress 的封包路徑留到後續網路篇深入拆解。

實驗假設你已經有可使用的 Kubernetes 叢集與 kubectl。除非特別說明,以下資源都放在目前 context 的 default Namespace;正式環境請改用專用 Namespace,並為映像檔、資源限制與安全設定做審查。

一、先建立 Workload 選型表

Workload 解決的是什麼問題?

Pod 是實際承載容器的執行單位,但大多數應用程式不應直接由裸 Pod 管理。真正的 Workload Controller 會持續比較「期望狀態」與「目前狀態」,並建立、刪除或更新 Pod,讓兩者逐漸一致。

可以先用下面這張表做選型。這裡的「副本」不只是數字,而是 Kubernetes 對每個執行個體的身份、完成條件與替換方式的承諾。

資源 適合解決的問題 身份語意 儲存語意 副本/完成語意 常見場景
裸 Pod 一個臨時、最小的執行單位 名稱與 IP 不保證長期穩定,沒有 Controller 負責補回 可用 emptyDir 或掛載 Volume;生命週期由 Pod 決定 只描述一個 Pod,不具備副本維護 除錯、一次性檢查、教學實驗
Deployment 可替換的無狀態服務 Pod 是可互換的,替換後通常是新 UID、新 IP 通常使用臨時儲存;共享資料應交給外部服務或另行設計 replicas 表示同質、可替換的 Pod 數量 Web、API、Worker、前端
StatefulSet 需要固定身份或每份獨立儲存的服務 具有穩定 ordinal,例如 stateful-demo-0、stateful-demo-1 volumeClaimTemplates 可為每個 ordinal 建立獨立 PVC replicas 表示一組有身份的成員,不等於資料自動複製 資料庫、訊息代理、需要節點身份的叢集
DaemonSet 每個符合條件的節點都執行一份 身份與「哪一台節點」相關,不是固定 ordinal 常見 hostPath、節點本地目錄或 plugin volume 沒有 replicas;符合條件的節點數決定 Pod 數 Node agent、日誌收集器、CNI/CSI node plugin
Job 有明確終點的批次工作 Pod 可替換,重點是成功完成幾次 可用臨時 Volume 或外部儲存,需考慮重試造成的重複寫入 completions、parallelism 與 backoffLimit 控制完成與重試 Migration、資料匯入、報表、一次性批次
CronJob 按時間週期建立 Job 每次排程產生新的 Job 與 Pod 由每次 Job 的模板決定 schedule 建立 Job;歷史數量與併發策略需另行設定 每日備份、定期同步、清理與報表

ReplicaSet 通常不是第一層的選型,而是 Deployment 在背後使用的 Controller:它負責維持某一版 Pod template 的數量;Deployment 則負責管理多個 ReplicaSet、滾動更新與回滾。

一個實用的選擇順序

遇到新服務時,可以按以下順序問自己:

  1. 這個程序會自然結束嗎?會的話選 Job;需要週期性執行則用 CronJob。
  2. 每一個符合條件的節點都需要它嗎?需要的話選 DaemonSet。
  3. 每個執行個體是否需要穩定名稱、順序或獨立 PVC?需要的話考慮 StatefulSet,但先確認應用程式本身的複製與故障轉移設計。
  4. 其他可互換的長駐服務,通常從 Deployment 開始。
  5. 只有短暫除錯或展示,才直接建立裸 Pod;不要因為 YAML 比較短就把正式服務放在裸 Pod 下。

二、Pod:共享網路與 Volume 的執行單位

Pod 不是「一個容器的別名」

Pod 是 Kubernetes 最小的可部署與調度單位。它可以只包含一個容器,也可以包含一組必須一起運行的容器。這些容器會被排到同一台節點,並共享部分執行環境:

graph LR
    subgraph Pod["Pod:共同調度的執行單位"]
        N["共享 Network Namespace<br/>同一個 Pod IP、localhost、Port 空間"]
        V["Pod Volume<br/>必須由各容器明確掛載"]
        C1["主容器"]
        C2["Sidecar / 輔助容器"]
    end

    C1 <--> N
    C2 <--> N
    C1 --> V
    C2 --> V

共享 Network Namespace

同一個 Pod 內的容器通常共享 Network Namespace,因此:

  • 它們共用同一個 Pod IP。
  • 一個容器在 127.0.0.1:8080 監聽時,同 Pod 其他容器可以用 localhost:8080 連線。
  • 容器的 Port 不能互相衝突;兩個容器不能同時綁定同一個 IP/Port。
  • Pod 外部的其他 Pod 不應依賴某個 Pod 的 IP 長期不變;Pod 被替換後,IP 通常會改變。

這也是為什麼 Sidecar 代理、日誌轉換器或和主程式緊密協作的輔助程式適合放在同一個 Pod。但兩個容器只要能各自獨立部署、擴展或故障處理,就不應為了「方便連線」而硬塞在同一個 Pod。

共享 Volume 不是自動發生的

Pod 可以定義 Volume,但每個容器仍要在 volumeMounts 中明確掛載它。emptyDir 會在 Pod 建立時出現,並在 Pod 被移除時消失;它不是跨 Pod 的永久儲存。若要讓資料跨越 Pod replacement 保留,應使用 PVC 或外部儲存,且還要確認應用程式的資料一致性設計。

同一個 Pod 內的容器還可能共享 Service Account、環境設定與其他 Pod-level 設定;但 Network Namespace 與 Volume 共享,不代表所有 Linux Namespace 都共享。例如 process namespace 預設不會讓一個容器直接看見另一個容器的程序,除非額外設定 shareProcessNamespace。

實驗:用兩個容器驗證 localhost 與共享檔案

下面的 YAML 讓 server 在 Pod 的 localhost:8080 啟動簡單 HTTP server,並把首頁寫入 emptyDir;client 透過 localhost 存取它,再從同一個 Volume 讀取檔案。套用前,先注意這個實驗要觀察的是「容器如何共享 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
36
37
apiVersion: v1
kind: Pod
metadata:
name: pod-shared-demo
labels:
app: pod-shared-demo
spec:
restartPolicy: Always
containers:
- name: server
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
echo "served by the server container" > /shared/index.html
exec busybox httpd -f -p 8080 -h /shared
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: shared
mountPath: /shared
- name: client
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
until wget -qO- http://127.0.0.1:8080; do sleep 1; done
echo "shared file:"
cat /shared/index.html
exec sleep 3600
volumeMounts:
- name: shared
mountPath: /shared
volumes:
- name: shared
emptyDir: {}

套用後可以從三個角度觀察:

1
2
3
4
5
6
7
8
9
10
kubectl apply -f pod-shared-demo.yaml
kubectl get pod pod-shared-demo -o wide
kubectl logs pod-shared-demo -c client

# 兩個容器使用同一個 Pod 的 hostname 與網路環境
kubectl exec pod-shared-demo -c server -- hostname
kubectl exec pod-shared-demo -c client -- hostname

# client 能從共享 Volume 讀到 server 寫入的檔案
kubectl exec pod-shared-demo -c client -- cat /shared/index.html

Pod 生命週期:三種「壞掉」不是同一件事

日常排查時,最容易混淆的是 container restart、Pod replacement 與 controller reconciliation。可以先用這張表分層:

事件 主要處理者 Pod UID/IP 你會看到什麼 典型原因
Container restart 節點上的 kubelet 與 container runtime Pod 物件通常仍相同,IP 通常仍相同;restartCount 增加 kubectl get pod 仍是同一個 Pod,可能出現 CrashLoopBackOff 程序退出、liveness probe 失敗、restartPolicy 允許重啟
Pod replacement Workload Controller、Scheduler、kubelet 協作 新 Pod 會有新 UID,通常也會拿到新 IP 舊 Pod 消失或進入終止,新 Pod 名稱/建立時間不同 Deployment 副本不足、Pod 被刪除、節點故障或驅逐
Controller reconciliation API Server 儲存狀態;各 Controller 持續比較期望與實際狀態 可能不直接改變既有 Pod,也可能建立 replacement Deployment、ReplicaSet、StatefulSet 或 DaemonSet 的 DESIRED/CURRENT/READY 逐漸靠攏 使用者修改宣告、節點加入、Pod 被刪、Probe/排程條件改變

因此,「Pod 掛掉不會自動重建」是一句不完整而且容易誤導的話,應該改成:

直接建立的裸 Pod 沒有 Controller 負責補回。容器程序失敗時,kubelet 可能依 restartPolicy 重啟同一個容器;如果整個 Pod 物件被替換,只有擁有它的 Deployment、StatefulSet、DaemonSet、Job 等 Controller 才會依自己的語意建立新 Pod。

CrashLoopBackOff 也不等於「Kubernetes 已經建立很多個新 Pod」;它通常表示同一個 Pod 內的容器反覆啟動、失敗,kubelet 逐步增加重試間隔。先看 restartCount、容器最後一次退出原因與事件,再判斷是否真的發生了 Pod replacement。

三、Deployment 與 ReplicaSet:管理可替換的長駐服務

三層關係:Deployment → ReplicaSet → Pod

對無狀態 Web/API 來說,最重要的不是某一個 Pod 的名字,而是「目前有幾個符合條件、已經 Ready 的 Pod」。Deployment 把這個期望狀態交給 ReplicaSet 維持:

graph TD
    D["Deployment<br/>workload-demo"]
    R1["ReplicaSet<br/>舊版本 template"]
    R2["ReplicaSet<br/>新版本 template"]
    P1["Pod"]
    P2["Pod"]
    P3["Pod"]

    D -->|"管理版本與 rollout"| R1
    D -->|"管理版本與 rollout"| R2
    R1 -->|"維持舊副本"| P1
    R2 -->|"維持新副本"| P2
    R2 -->|"維持新副本"| P3
  • Deployment 管理 ReplicaSet 的版本、更新策略、回滾與期望副本數。
  • ReplicaSet 透過 selector 與 owner reference 維持某一個 Pod template 的數量。
  • Pod 才是真正被排程、啟動容器與執行 probe 的物件。

更新 Deployment 的 Pod template 時,Deployment 會建立新的 ReplicaSet,逐步增加新版本、減少舊版本;舊 ReplicaSet 通常會保留在叢集中,讓 rollback 能夠回到上一個 template。不要在 Deployment 底下直接手動修改或縮放 ReplicaSet,否則 Deployment 下一次 reconciliation 可能把它改回去。

實驗:scale、刪除 Pod 與自動補回

先套用一個包含 readiness/liveness probe 的 Deployment。這裡 maxUnavailable: 0 與 minReadySeconds 方便我們觀察「新 Pod 真的 Ready 後,舊 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
apiVersion: apps/v1
kind: Deployment
metadata:
name: workload-demo
labels:
app: workload-demo
spec:
replicas: 3
minReadySeconds: 5
revisionHistoryLimit: 5
selector:
matchLabels:
app: workload-demo
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: workload-demo
spec:
terminationGracePeriodSeconds: 10
containers:
- name: web
image: nginx:1.25-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 3
failureThreshold: 2
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi

先確認 Deployment、ReplicaSet 與 Pod 的層級關係:

1
2
3
4
5
6
7
kubectl apply -f deployment-demo.yaml
kubectl get deployment,replicaset,pods -l app=workload-demo -o wide
kubectl describe deployment workload-demo

# 看目前 Pod 的 owner reference,通常會指向 ReplicaSet
POD=$(kubectl get pods -l app=workload-demo -o jsonpath='{.items[0].metadata.name}')
kubectl get pod "$POD" -o jsonpath='{range .metadata.ownerReferences[*]}{.kind}/{.name}{"\n"}{end}'

接著觀察水平擴展與 replacement。scale 會改變 Deployment 的期望副本數;刪除一個 Pod 則不會把 Deployment 的 replicas 減一,ReplicaSet 會發現實際數量不足並補一個新的 Pod。

1
2
3
4
5
6
7
8
# 由 3 個副本擴展到 4 個
kubectl scale deployment workload-demo --replicas=4
kubectl get deployment,replicaset,pods -l app=workload-demo -o wide

# 刪除一個 Pod,觀察新 Pod 的名字、UID、建立時間與 IP
POD=$(kubectl get pods -l app=workload-demo -o jsonpath='{.items[0].metadata.name}')
kubectl delete pod "$POD"
kubectl get pods -l app=workload-demo -o wide --watch

當你看到新 Pod 出現時,可以比較:

  • Deployment 的 DESIRED 仍然是 4。
  • ReplicaSet 的 CURRENT 會回到 4。
  • 被刪除的 Pod 不會原地「復活」;新 Pod 是新的物件,通常有新的 UID 與 IP。
  • Service 若選取這些 Pod,通常只會把流量交給已 Ready 的後端;Service 的 EndpointSlice 細節留到網路篇。

RollingUpdate:更新不只是換 image

用 kubectl set image 更新 Deployment 會改變 Pod template,因而產生新的 ReplicaSet。以下指令同時觀察 rollout、ReplicaSet 數量與每個 Pod 的版本:

1
2
3
4
5
kubectl set image deployment/workload-demo web=nginx:1.27-alpine
kubectl rollout status deployment/workload-demo
kubectl get replicasets -l app=workload-demo
kubectl get pods -l app=workload-demo -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready
kubectl rollout history deployment/workload-demo

maxSurge: 1 允許更新期間比期望副本多一個 Pod;maxUnavailable: 0 則要求至少維持原本的可用副本數。這不是「所有更新都零停機」的保證:如果應用程式啟動很慢、探針錯誤、資料庫 migration 不相容或所有節點都沒有資源,rollout 仍可能卡住。

Readiness 與 rollback 實驗

readinessProbe 失敗時,容器可以仍然是 Running,但 Pod 會是 0/1 Ready;這個狀態主要用來阻止流量進入尚未準備好的 Pod,不是用來重啟容器。相對地,livenessProbe 失敗才會讓 kubelet 重啟容器。

下面故意把新版本的 readiness URL 改成不存在的路徑。這會讓新 ReplicaSet 的 Pod 啟動但長期不 Ready,Deployment 因 maxUnavailable: 0 保留舊版本,rollout 因而停在中間:

1
2
3
4
5
6
kubectl patch deployment workload-demo --type='json' \
--patch='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/not-ready"}]'

kubectl rollout status deployment/workload-demo --timeout=60s
kubectl get pods -l app=workload-demo -o wide
kubectl describe pod -l app=workload-demo | rg -n "Readiness|Unhealthy|Events"

確認新 Pod 的狀態與事件後,使用 Deployment 的歷史版本回滾,而不是手動刪除新 Pod:

1
2
3
4
kubectl rollout history deployment/workload-demo
kubectl rollout undo deployment/workload-demo
kubectl rollout status deployment/workload-demo
kubectl get replicasets -l app=workload-demo

回滾會恢復前一個 Pod template,包含 image 與 readiness probe。若要回到特定版本,可先用 rollout history 找到 revision,再執行 kubectl rollout undo deployment/workload-demo –to-revision=。

四、StatefulSet:固定身份,不是資料庫高可用按鈕

它保證了哪些事?

StatefulSet 適合「每個成員都需要知道自己是誰」的服務。當 replicas 設為 3 時,常見的成員名稱是:

1
2
3
stateful-demo-0
stateful-demo-1
stateful-demo-2

StatefulSet 主要提供:

  • 穩定、可預測的 ordinal identity。
  • Pod 重新排程後仍可沿用相同的 ordinal 名稱。
  • 透過 Headless Service 建立每個成員可尋找的網路身份。
  • 透過 volumeClaimTemplates 為每個 ordinal 建立獨立 PVC,例如 data-stateful-demo-0。
  • 預設依序建立與更新成員,避免所有成員同時變動。

它 不 自動提供資料複製、選主、共識、備份或故障轉移。資料庫是否具備高可用,仍取決於資料庫本身的 replication/consensus 協定、儲存系統、備份與 Operator。StatefulSet 只是把「成員身份與儲存掛載」這些基礎條件穩定地交給應用程式。

實驗:Headless Service、ordinal 與每 Pod PVC

這份範例刻意使用簡單的 HTTP server,讓我們可以觀察每個 Pod 的 hostname 與獨立資料目錄。套用前先確認叢集有可用的 StorageClass;若沒有,PVC 會停在 Pending,這本身就是後續儲存篇會排查的實務狀況。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: v1
kind: Service
metadata:
name: stateful-demo
spec:
clusterIP: None
selector:
app: stateful-demo
ports:
- name: http
port: 8080
targetPort: http
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: stateful-demo
labels:
app: stateful-demo
spec:
serviceName: stateful-demo
replicas: 3
selector:
matchLabels:
app: stateful-demo
template:
metadata:
labels:
app: stateful-demo
spec:
terminationGracePeriodSeconds: 10
containers:
- name: app
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
echo "pod=$(hostname)" > /data/identity
exec busybox httpd -f -p 8080 -h /data
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
labels:
app: stateful-demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

觀察 StatefulSet 建立順序、Pod 名稱與 PVC 名稱:

1
2
3
4
5
6
7
8
kubectl apply -f statefulset-demo.yaml
kubectl get statefulset,pods,pvc -l app=stateful-demo -o wide

# 每個 ordinal 都有自己的 PVC
kubectl get pvc -l app=stateful-demo

# Pod 0 的 identity 寫在自己的 Volume 裡
kubectl exec stateful-demo-0 -- cat /data/identity

刪除中間的 Pod,再觀察它是否以相同 ordinal 回來,以及原本的 PVC 是否仍被使用:

1
2
3
kubectl delete pod stateful-demo-1
kubectl get pods,pvc -l app=stateful-demo -o wide --watch
kubectl exec stateful-demo-1 -- cat /data/identity

Headless Service 的重點是「不配置 ClusterIP」,讓每個成員可以有自己的 DNS 名稱。以下只用來驗證固定名稱;DNS 查詢如何進入 CoreDNS、記錄如何由 EndpointSlice 更新,會在網路篇完整拆解。

1
2
3
4
5
kubectl run dns-client \
--image=busybox:1.36.1 \
--restart=Never \
--rm -it -- \
nslookup stateful-demo-0.stateful-demo.default.svc.cluster.local

若要刪除測試環境,請先確認 PVC 內沒有需要保留的資料;刪除 StatefulSet 不代表所有 PVC 都會自動消失,實務上更不能把 StatefulSet deletion 當成資料清理策略。

五、DaemonSet:每個符合條件的節點各一份

它和 Deployment 的差別

Deployment 的副本數由 spec.replicas 決定;DaemonSet 則根據「目前有哪些節點符合條件」決定 Pod 數量。節點加入、移除、被標記或被 taint 時,DaemonSet Controller 會重新計算應該存在的 Pod。

DaemonSet 常見於:

  • Node exporter、日誌收集器、監控或安全 agent。
  • CNI plugin 的 node component。
  • CSI driver 的 node plugin。
  • 每台節點都要執行的清理、硬體探測或本地代理。

真正的 CNI/CSI DaemonSet 通常還需要 hostPath、hostNetwork、裝置掛載、RBAC、特定 toleration,甚至較高的 Linux capability。下面只用一個不需要主機權限的 busybox loop 做行為實驗,避免把有風險的系統權限 YAML 當成通用範本。

實驗:標記節點後才建立 Pod

這份 YAML 使用 nodeSelector,所以剛套用時如果沒有任何節點帶有 workload-agent=true,DaemonSet 的 desired Pod 數量會是 0。若要改成所有 Linux 節點各一份,可以移除 nodeSelector,或改成叢集一致的節點條件;正式環境還要一併檢查 taints/tolerations。

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
36
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-agent
labels:
app: node-agent
spec:
selector:
matchLabels:
app: node-agent
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app: node-agent
spec:
nodeSelector:
workload-agent: "true"
containers:
- name: agent
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
echo "running on $(hostname)"
exec sleep 3600
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi

先套用並確認沒有符合條件的節點,接著逐一標記節點:

1
2
3
4
5
6
7
8
9
10
11
12
13
kubectl apply -f daemonset-selected-node.yaml
kubectl get daemonset node-agent
kubectl get pods -l app=node-agent -o wide
kubectl get nodes --show-labels

# 將一台節點納入 DaemonSet
kubectl label node <node-a> workload-agent=true --overwrite
kubectl get daemonset node-agent -w
kubectl get pods -l app=node-agent -o wide

# 再納入第二台節點
kubectl label node <node-b> workload-agent=true --overwrite
kubectl get pods -l app=node-agent -o wide

再做一次「新增節點」實驗。節點必須先由雲端節點池、kubeadm、kind 或其他叢集工具加入並進入 Ready,Kubernetes 本身不會因為 DaemonSet 自動替你建立一台虛擬機器:

1
2
3
4
5
6
# 節點加入流程完成後,等待它出現在 API Server
kubectl get nodes -w

# 如果新節點沒有在加入時帶標籤,手動標記它
kubectl label node <new-node> workload-agent=true --overwrite
kubectl get pods -l app=node-agent -o wide

理想結果是:每多一台符合 selector 的節點,就多一個 node-agent Pod;移除標籤後,該節點上的 Pod 會被 DaemonSet Controller 移除或不再視為應有副本:

1
2
3
kubectl label node <node-a> workload-agent-
kubectl get daemonset node-agent
kubectl get pods -l app=node-agent -o wide

如果新節點已經 Ready 卻沒有 Pod,依序檢查 nodeSelector、節點 taint、Pod 的 Events、映像檔拉取與資源容量。DaemonSet 的 DESIRED、CURRENT、READY 很適合用來判斷問題是在「節點是否符合條件」、Pod 是否建立,還是容器是否真正 Ready。

六、Job 與 CronJob:完成就停止的工作

Job 的生命週期

長駐服務用「一直維持幾個 Pod」描述;Job 則用「成功完成幾次」描述。Job Controller 會建立 Pod 執行工作,根據退出碼判斷成功或失敗,並依 backoffLimit 重試。restartPolicy: Never 通常會在失敗後建立新的 Pod;OnFailure 則允許 kubelet 在同一個 Pod 內重啟容器,兩者要分清楚。

graph LR
    J["Job<br/>batch-demo"] --> P1["Pod attempt 1"]
    P1 -->|"失敗,未超過 backoffLimit"| P2["Pod attempt 2"]
    P2 -->|"exit 0"| C["Job Complete"]

實驗:一次性 Job 與重試

下面的 Job 會成功執行一次,完成後不再保持一個長駐服務;ttlSecondsAfterFinished 讓測試環境在完成一段時間後清理 Job 及其 Pod。套用前要觀察的是 completions、backoffLimit 與 Complete,不是 Deployment 式的 Ready。

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
apiVersion: batch/v1
kind: Job
metadata:
name: batch-demo
labels:
app: batch-demo
spec:
completions: 1
parallelism: 1
backoffLimit: 2
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: batch-demo
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
echo "job started"
sleep 5
echo "job completed"

觀察 Job、Pod 與完成狀態:

1
2
3
4
kubectl apply -f job-demo.yaml
kubectl get job,pods -l app=batch-demo -w
kubectl describe job batch-demo
kubectl logs job/batch-demo

要看重試行為,可以建立一個故意失敗的 Job。它的 exit 1 會讓 Job 持續建立失敗嘗試,直到達到 backoffLimit:

1
2
3
4
5
6
kubectl create job retry-demo \
--image=busybox:1.36.1 -- \
/bin/sh -c 'echo "intentional failure"; exit 1'
kubectl get job retry-demo
kubectl get pods -l job-name=retry-demo -w
kubectl describe job retry-demo

批次工作必須考慮重試造成的副作用。例如「寫入一筆付款」不能只靠 Job 保證只執行一次;工作內容應設計成 idempotent,或以工作 ID、唯一鍵與交易狀態防止重複處理。

CronJob:按時間建立 Job

CronJob 不是直接執行容器,而是依 schedule 建立 Job;Job 再建立 Pod。下面每五分鐘建立一個短工作,並用 Forbid 避免同一個 CronJob 的前一個 Job 尚未結束時重疊執行。

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
apiVersion: batch/v1
kind: CronJob
metadata:
name: cron-demo
labels:
app: cron-demo
spec:
schedule: "*/5 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 2
jobTemplate:
metadata:
labels:
app: cron-demo
spec:
backoffLimit: 1
template:
metadata:
labels:
app: cron-demo
spec:
restartPolicy: OnFailure
containers:
- name: task
image: busybox:1.36.1
command: ["/bin/sh", "-c"]
args:
- |
echo "run at $(date -Iseconds)"
sleep 10

套用後不必等五分鐘,可以從 CronJob 立即複製一個 Job 做觀察:

1
2
3
4
kubectl apply -f cronjob-demo.yaml
kubectl get cronjob
kubectl create job cron-demo-now --from=cronjob/cron-demo
kubectl get cronjob,job,pods -l app=cron-demo -w

在正式環境還要思考工作執行時間、時區、錯過排程時的處理、併發策略、歷史清理與外部系統的冪等性。Forbid 只能限制同一個 CronJob 的重疊,不會替你的業務邏輯提供分散式鎖。

七、Service 只先掌握必要基礎

Workload Controller 負責「Pod 應該存在幾個、如何更新」;Service 負責「其他客戶端如何找到一組會變動的 Pod」。Service 不會建立 Pod,也不是另一種 Workload Controller。

最小的 ClusterIP Service 如下。套用前請先注意 selector 必須與 Deployment template 的 label 完全匹配;selector 寫錯時,Service 物件仍可能是 Active,但沒有可用後端。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: workload-demo
spec:
selector:
app: workload-demo
ports:
- name: http
port: 80
targetPort: http

必要概念先記住三件事:

  • ClusterIP 是叢集內穩定的虛擬端點,Pod 被替換後,客戶端不需要知道新的 Pod IP。
  • NodePort 讓節點 IP 加上固定高位 Port 成為入口,常用於簡單測試或作為其他負載平衡器的後端。
  • LoadBalancer 會請雲端或叢集整合元件配置外部負載平衡器;是否真的拿到外部 IP、流量如何進叢集,取決於環境。

Service 不是固定的「Round Robin 設定」,也不是 L7 HTTP Router。Service 的 selector、Ready 狀態、EndpointSlice、kube-proxy/eBPF 與外部 Load Balancer 會共同影響實際流量路徑。Part 3 先拆解 Pod-to-Pod 與 Service VIP 的封包路徑,Part 7 再深入 CoreDNS/Service Discovery,Part 8 則比較 L4/L7、Ingress 與 Gateway API。

八、用同一套觀察方式排查 Workload

遇到問題時,先不要只看 kubectl get pods 的一行狀態;把「物件、Controller、Pod、容器、事件」一起看:

1
2
3
4
5
6
7
# 先看 Workload 與 Pod 是否達到期望狀態
kubectl get deployment,replicaset,statefulset,daemonset,job,cronjob,pods -o wide

# 再看某個 Pod 為什麼沒有被排程、沒有 Ready 或不斷重啟
kubectl describe pod <pod-name>
kubectl logs <pod-name> --all-containers
kubectl get events --sort-by=.lastTimestamp

可以用下表快速定位:

現象 優先確認 常見誤判
CrashLoopBackOff restartCount、容器退出碼、liveness probe、kubectl logs –previous 以為 Kubernetes 建立了很多個新 Pod
Deployment rollout 卡住 新 ReplicaSet、readiness probe、映像檔、資源與事件 只刪掉不 Ready 的 Pod,結果 Controller 又建立同樣的 Pod
StatefulSet Pod Pending 對應 PVC、StorageClass、Volume attach/mount 事件 以為 StatefulSet 會自動提供資料複製
DaemonSet 少一份 Node selector、taint/toleration、節點 Ready 與資源 只看 replicas,但 DaemonSet 沒有固定 replicas
Job 一直失敗 Pod exit code、backoffLimit、輸入資料與重試副作用 把 Job 當成 Deployment,期待它永遠保持一個 Running Pod
Service 存在但連不到 selector、Pod Ready 狀態與後端 Port 先去改 CoreDNS,卻沒有先確認 Service 是否有後端

這些排查方式的共同點是:先確認是哪一層的狀態不一致,再決定要修改 workload template、節點條件、容器程式,或只是等待 controller 完成 reconciliation。

本篇重點回顧

  1. Pod 是共享 Network Namespace 與明確掛載 Volume 的執行單位,不是永久伺服器。
  2. Container restart 通常保留同一個 Pod;Pod replacement 會建立新的 Pod 物件;Controller reconciliation 則是讓期望狀態與實際狀態重新一致。
  3. 無狀態長駐服務優先使用 Deployment;ReplicaSet 是 Deployment 管理版本時的中介 Controller。
  4. StatefulSet 提供穩定 ordinal、Headless Service 身份與每 Pod PVC,但不會自動把資料庫變成高可用。
  5. DaemonSet 的副本數由符合條件的節點數決定,適合 node agent、日誌收集器與 CNI/CSI node plugin。
  6. Job 以完成次數為中心,CronJob 以排程建立 Job;兩者都必須設計好重試與冪等性。
  7. Service 只先理解為穩定地指向一組 Pod 的入口;Pod 網路見 Part 3,DNS/EndpointSlice 見 Part 7,L4/L7 見 Part 8。

下一篇預告

下一篇 Part 3 將從「Pod A 呼叫 Pod B」開始,逐跳拆解 Pod IP、CNI、Service 與 kube-proxy;CoreDNS/EndpointSlice 與 L4/L7 會在 Part 7、Part 8 延伸,讓 curl http://service-name 不再只是看起來會工作的魔法。

系列文章導覽

參考資源