iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Kubernetes

從零到 CKA:30 天掌握 Kubernetes 核心觀念與實作系列 第 11

Day 11|StatefulSet — 讓有狀態應用穩定運行

  • 分享至 

  • xImage
  •  

前言

昨天我們學完了 Kubernetes 儲存三兄弟 —— PV、PVC、StorageClass,讓儲存資源可以自動建立並完成綁定。

但到目前為止,我們使用的多半都是「無狀態」應用,例如 Nginx。

這類應用通常不依賴特定 Pod 的身份,即使 Pod 被刪除後重新建立,只要 Service 能把流量導向新的 Pod,應用通常還是可以正常運作。

可是真實世界中,有很多應用其實是有狀態的

  • 資料庫(MySQL、PostgreSQL):需要持久保存資料,不同副本可能扮演不同角色,不能把所有 Pod 都當成完全相同的個體
  • 分散式系統(Kafka、ZooKeeper、Elasticsearch):每個節點通常有自己的身份與資料,需要穩定地辨識彼此
  • 主從/主備架構:不同節點可能有不同角色,例如 Primary、Replica,不能在重建後隨意互換身份

如果你用 Deployment 來跑這些應用,會發生什麼事?

  • Pod 名稱通常是由 Deployment 自動產生的,例如 app-7f8b9c6d4-xk2zp,Pod 被重建後名稱可能改變
  • Pod 沒有固定的網路身份,很難直接用固定名稱找到某一個特定副本
  • Deployment 不會自動替每個 Pod 建立並維持各自專屬的 PVC;如果有每個副本獨立儲存的需求,就必須另外設計

Kubernetes 的解法就是 StatefulSet —— 專門用來管理有狀態應用的控制器。

它可以讓每個 Pod 擁有穩定的名稱、穩定的網路身份,並搭配 volumeClaimTemplates 為每個 Pod 建立各自獨立的持久化儲存

今天會學:

  1. Deployment vs StatefulSet — 兩者有什麼差別?什麼情況該用哪一個?
  2. StatefulSet 的三大核心特性 — 穩定命名、穩定網路身份、專屬儲存
  3. Headless Service — 為 StatefulSet 提供穩定的網路識別
  4. 實作 StatefulSet — 搭配 Day 10 的 StorageClass 完整操作一次
  5. 常見問題與排錯 — 刪除行為、啟停順序與常見錯誤

以下操作皆在 master 節點 執行。


一、Deployment vs StatefulSet — 差在哪?

用一個比喻來理解:

  • Deployment自由入座的餐廳 —— 客人(Pod)來了之後隨便找位置坐,離開後座位就釋放。下次再來時,不一定還是同一個座位,也沒有固定屬於自己的餐具。
  • StatefulSet固定劃位的餐廳 —— 每個客人都有自己的座位編號,例如 seat-0seat-1seat-2,也有專屬的餐具(持久化儲存)。即使客人暫時離開再回來,仍然會回到原本的座位,繼續使用自己的那套餐具。
比較項目 Deployment StatefulSet
Pod 命名 名稱包含隨機產生的字串,例如 app-7f8b9c-xk2zp 有固定序號,例如 app-0app-1app-2
啟動順序 沒有固定的啟動順序保證 預設依序啟動(0 → 1 → 2)
刪除順序 沒有固定的刪除順序保證 預設反序終止(2 → 1 → 0)
網路身份 Pod 本身沒有固定的網路身份,重建後 Pod IP 可能改變 可搭配 Headless Service,讓每個 Pod 擁有穩定的 DNS 名稱
儲存 不會自動為每個 Pod 建立並維持專屬 PVC 可透過 volumeClaimTemplates 為每個 Pod 建立專屬 PVC
適用場景 無狀態應用,例如 Web Server、API 有狀態應用,例如資料庫、MQ、分散式系統

💡 什麼時候該用 StatefulSet?

可以先問自己三個問題:

  1. 每個 Pod 是否需要各自獨立的持久化儲存?
  2. Pod 之間是否需要透過固定名稱或穩定的網路身份互相溝通?
  3. 啟動、更新或終止時,是否有順序上的要求?

如果其中任何一個需求很重要,就可以考慮使用 StatefulSet。


二、StatefulSet 的三大核心特性

特性 1:穩定的 Pod 名稱

Deployment 建立的 Pod 名稱通常像這樣:

nginx-deployment-7f8b9c6d4-xk2zp
nginx-deployment-7f8b9c6d4-m3n7p

StatefulSet 建立的 Pod 名稱則會有固定序號:

web-0
web-1
web-2

名稱格式為 <StatefulSet 名稱>-<序號>,序號會從 0 開始遞增。

即使某個 Pod 被刪除後重新建立,它仍然會保留原本的名稱。例如 web-0 被重建後,名稱依然是 web-0

這種穩定的命名方式,讓每個 Pod 都能擁有固定且可辨識的身份。

特性 2:穩定的網路身份

搭配 Headless Service,每個 Pod 會有一個固定的 DNS 名稱:

<pod-name>.<service-name>.<namespace>.svc.cluster.local

例如:

web-0.nginx-headless.default.svc.cluster.local
web-1.nginx-headless.default.svc.cluster.local
web-2.nginx-headless.default.svc.cluster.local

這表示其他 Pod 可以透過固定的 DNS 名稱找到特定的 Pod。

即使 Pod 被刪除後重新建立、Pod IP 發生變化,只要 StatefulSet 中的身份沒有改變,對應的 DNS 名稱也會維持不變。

特性 3:專屬的持久化儲存

StatefulSet 可以透過 volumeClaimTemplates,為每個 Pod 自動建立各自專屬的 PVC:

data-web-0    →  自動建立的 PV-A
data-web-1    →  自動建立的 PV-B
data-web-2    →  自動建立的 PV-C

每個 Pod 都會使用自己的持久化儲存,彼此之間不會共用同一份資料。

即使 web-0 被刪除後重新建立,它仍然會重新掛載原本的 data-web-0,因此原本儲存在該 PVC 中的資料可以繼續使用。

volumeClaimTemplates 就是 Day 10 的延伸

還記得 Day 10 我們手動建立 PVC,並透過 storageClassName 指定 StorageClass 嗎?

volumeClaimTemplates 做的事情很類似,只是它會根據範本,自動為 StatefulSet 中的每個 Pod 建立一份專屬 PVC

PVC 的命名規則通常是:

<template-name>-<pod-name>

例如:

data-web-0
data-web-1
data-web-2

三、Headless Service — StatefulSet 的好搭檔

在繼續實作 StatefulSet 之前,我們需要先理解 Headless Service

一般的 Service(ClusterIP)會取得一個 ClusterIP,Client 存取這個 Service 時,流量會根據 kube-proxy 建立的轉送規則導向其中一個後端 Pod:

Client → Service IP(10.96.0.100)→ 隨機分配到 Pod-A 或 Pod-B

但有狀態應用有時需要的不是「任意一個 Pod」,而是要能夠直接找到某個特定的 Pod

這時就可以使用 Headless Service。

Headless Service 不會建立 ClusterIP,而是透過 DNS 提供後端 Pod 的位址:

Client → DNS 查詢 → 直接拿到每個 Pod 的 IP

搭配 StatefulSet 後,每個 Pod 還可以擁有固定的 DNS 名稱,例如:

web-0.nginx-headless.default.svc.cluster.local
web-1.nginx-headless.default.svc.cluster.local

建立 Headless Service 的關鍵,就是將 clusterIP 設為 None

apiVersion: v1
kind: Service
metadata:
  name: nginx-headless
spec:
  clusterIP: None       # 這就是 Headless 的關鍵
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
比較項目 一般 Service(ClusterIP) Headless Service
clusterIP 有(系統分配) None
DNS 回傳 Service 的 ClusterIP 後端 Pod 的 IP
流量轉送 透過 kube-proxy 建立的規則導向後端 Pod 不經過 Service ClusterIP,由 Client 直接連線到 Pod
用途 提供統一入口,將流量導向後端 Pod 讓 Client 能直接找到特定 Pod,常搭配有狀態應用

💡 為什麼 StatefulSet 通常要搭配 Headless Service?

StatefulSet 的穩定網路身份,是透過 固定的 Pod 名稱 + Headless Service 提供的 DNS 記錄來實現。

搭配 Headless Service 後,每個 Pod 都能擁有固定且可預期的 DNS 名稱,例如:

web-0.nginx-headless.default.svc.cluster.local

即使 Pod 被刪除後重新建立、IP 發生變化,其他 Pod 仍然可以透過相同的 DNS 名稱找到它。


四、實作 StatefulSet

現在來完整實作一個 StatefulSet,並搭配 Day 10 已經安裝好的 local-path StorageClass:

Step 1:建立 Headless Service

vim headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-headless
spec:
  clusterIP: None
  selector:
    app: nginx-sts
  ports:
  - port: 80
    targetPort: 80
kubectl apply -f headless-svc.yaml

Step 2:建立 StatefulSet

vim statefulset-demo.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: nginx-headless    # 必須指定對應的 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: nginx-sts
  template:
    metadata:
      labels:
        app: nginx-sts
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:          # 每個 Pod 自動建立專屬 PVC
  - metadata:
      name: data
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: local-path
      resources:
        requests:
          storage: 1Gi
kubectl apply -f statefulset-demo.yaml

⚠️注意 serviceName 欄位

serviceName 是 StatefulSet 用來指定「負責提供網路身份的 Service」的欄位,通常會填入前面建立的 Headless Service 名稱。

Kubernetes 會根據這個 Service 名稱,搭配 Pod 名稱產生穩定的 DNS 身份。

例如:

web-0.nginx-headless.default.svc.cluster.local

因此 serviceName 必須和對應的 Headless Service 名稱一致,否則 Pod 的 DNS 名稱就無法按照預期運作。

Step 3:觀察 Pod 依序啟動

kubectl get pods -w -l app=nginx-sts

會看到 Pod 依序啟動

https://ithelp.ithome.com.tw/upload/images/20260813/20181928sIfiSvi9Re.png

web-0 進入 Ready 狀態後,才會開始建立 web-1web-1 Ready 後,才會建立 web-2

這就是 StatefulSet 預設的有序部署行為。

(按 Ctrl+C 結束觀察)

Step 4:驗證專屬儲存

每個 Pod 都有自己的 PVC,查看一下:

kubectl get pvc

會看到三個自動建立的 PVC:

https://ithelp.ithome.com.tw/upload/images/20260813/20181928JeWX9Jwd7i.png

命名規則:<volumeClaimTemplate名稱>-<Pod名稱>

現在讓每個 Pod 寫入不同的資料:

kubectl exec web-0 -- sh -c 'echo "我是 web-0 的資料" > /usr/share/nginx/html/index.html'
kubectl exec web-1 -- sh -c 'echo "我是 web-1 的資料" > /usr/share/nginx/html/index.html'
kubectl exec web-2 -- sh -c 'echo "我是 web-2 的資料" > /usr/share/nginx/html/index.html'

驗證各自的資料:

kubectl exec web-0 -- cat /usr/share/nginx/html/index.html
kubectl exec web-1 -- cat /usr/share/nginx/html/index.html
kubectl exec web-2 -- cat /usr/share/nginx/html/index.html

每個 Pod 讀到的是自己的資料,互不干擾。

Step 5:驗證穩定的網路身份

用一個臨時 Pod 來測試 DNS:

kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- nslookup nginx-headless

會看到 DNS 回傳所有 Pod 的 IP(而不是一個虛擬 IP)。

https://ithelp.ithome.com.tw/upload/images/20260813/20181928UXLg73eCYw.png

測試單一 Pod 的 DNS:

kubectl run dns-test2 --image=busybox:1.36 --rm -it --restart=Never -- nslookup web-0.nginx-headless.default.svc.cluster.local

會看到 web-0.nginx-headless.default.svc.cluster.local 解析到 web-0 的 Pod IP。

https://ithelp.ithome.com.tw/upload/images/20260813/201819280QJpPFLRcj.png

⚠️ 注意:DNS 查詢失敗時,可以改用 FQDN 測試

在 Kubernetes Pod 中,短名稱通常會搭配 /etc/resolv.conf 裡的 search domain 進行解析。

如果使用 BusyBox 的 nslookup 查詢:

web-0.nginx-headless

出現 NXDOMAIN 或查詢失敗,可以先改用完整的 FQDN:

web-0.nginx-headless.default.svc.cluster.local

如果完整名稱可以正常解析,再進一步檢查 Pod 內的 /etc/resolv.conf、DNS Policy 與 search domain 設定。

Step 6:驗證刪除後身份與資料不變

刪掉 web-1,看看重建後會怎樣:

kubectl delete pod web-1
kubectl get pods -w -l app=nginx-sts

https://ithelp.ithome.com.tw/upload/images/20260813/20181928X2v0saQGyK.png

會看到新的 web-1 被自動重建(名稱一樣),驗證資料:

kubectl exec web-1 -- cat /usr/share/nginx/html/index.html

輸出依然是「我是 web-1 的資料」— 就代表 web-1 重建後,仍然重新掛載了原本對應的 PVC。

https://ithelp.ithome.com.tw/upload/images/20260813/201819284IlyjTbRhm.png

名稱保持不變、儲存也能接回原本的資料,這就是 StatefulSet 很重要的核心特性。


五、常見錯誤與注意事項

⚠️ 錯誤 1:刪除 StatefulSet 不會自動刪除 PVC

這是 StatefulSet 用來保護資料的重要設計之一。

即使刪除整個 StatefulSet:

kubectl delete statefulset web

volumeClaimTemplates 建立的 PVC 預設仍然會保留下來。

如果之後重新建立相同名稱與相同 Volume Claim Template 的 StatefulSet,對應的 Pod 通常會重新使用原本的 PVC,因此原本的資料可以繼續保留。

如果確定這些資料已經不再需要,就必須另外手動刪除 PVC:

kubectl delete pvc data-web-0 data-web-1 data-web-2

刪除 PVC 前要特別小心,後續是否連底層 PV 與資料一起刪除,還會受到 StorageClass 與 reclaimPolicy 的影響。

⚠️ 錯誤 2:忘記建立 Headless Service

如果沒有建立 StatefulSet serviceName 所對應的 Headless Service,Pod 本身仍然可能正常啟動,但預期的穩定 DNS 名稱就無法正常使用。

例如其他 Pod 可能無法透過:

web-0.nginx-headless

找到特定的 StatefulSet Pod。

可以先檢查 Service 是否存在:

kubectl get svc nginx-headless

並確認 CLUSTER-IP 欄位顯示:

None

這表示它是一個 Headless Service。

進階補充

podManagementPolicy:控制 Pod 的建立與終止順序

StatefulSet 可以透過 podManagementPolicy 控制 Pod 的管理方式:

  • OrderedReady(預設):依照序號建立 Pod(0 → 1 → 2),前一個 Pod 進入 Ready 狀態後,才會繼續建立下一個
  • Parallel:建立或終止 Pod 時,不等待前一個 Pod 完成,也不要求依照序號順序處理

如果應用程式不需要嚴格的啟動順序,只需要穩定的 Pod 身份與持久化儲存,可以使用 Parallel 加快 Pod 的建立速度:

spec:
  podManagementPolicy: Parallel

更新策略:RollingUpdatepartition

StatefulSet 預設使用 RollingUpdate 更新策略。

更新 Pod Template 後,StatefulSet 會依照序號由大到小逐一更新,例如:

web-2 → web-1 → web-0

也可以搭配 partition 控制只更新部分 Pod:

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2

partition: 2 表示只有序號 大於或等於 2 的 Pod 會套用新的 Pod Template。

以三個副本為例:

web-0    保持原版本
web-1    保持原版本
web-2    更新成新版本

這種方式很適合先更新部分 Pod 進行驗證,再逐步降低 partition,完成分階段更新。


小結

今天我們學會了 StatefulSet —— Kubernetes 為有狀態應用設計的控制器:

核心特性 說明 對比 Deployment
穩定的 Pod 名稱 web-0web-1web-2,Pod 被重建後名稱仍維持不變 Pod 名稱通常包含隨機字串,重建後可能改變
穩定的網路身份 搭配 Headless Service,可為每個 Pod 提供固定的 DNS 名稱 Pod 本身沒有固定的網路身份,通常透過 Service 統一存取
專屬持久化儲存 可透過 volumeClaimTemplates 為每個 Pod 建立專屬 PVC 不會自動為每個 Pod 建立並維持專屬 PVC
有序部署與終止 預設依序建立(0 → 1 → 2),反序終止(2 → 1 → 0) 沒有固定的建立與終止順序保證

回顧一下我們的學習路線:

  • Day 9:PV + PVC —— 讓 Pod 的資料持久化
  • Day 10:StorageClass —— 讓 PV 的建立自動化
  • Day 11:StatefulSet —— 讓有狀態應用擁有穩定的身份與專屬儲存

到這裡,我們已經能處理有狀態應用的基本需求了。

但你有沒有想過:當 Kubernetes 建立這些 Pod 時,是怎麼決定它們要跑在哪個 Node 上的?如果 Node 的 CPU 或記憶體資源不夠,又會發生什麼事?

明天我們來學 Kubernetes 的排程與資源管理,看看 Scheduler 如何替 Pod 挑選合適的 Node,以及 Kubernetes 如何管理 CPU 與 Memory 資源!


參考資源


上一篇
Day 10|StorageClass — Kubernetes 如何自動建立儲存?
系列文
從零到 CKA:30 天掌握 Kubernetes 核心觀念與實作11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言