前面我們已經把 PostgreSQL 放進 Kubernetes,大概長這樣:
Deployment
↓
PostgreSQL Pod
↓
PVC
在正式開始之前,我們先把今天最重要的新名詞講清楚:
StatefulSet
Stateful 可以翻成:
有狀態的。
所謂「狀態」,可以簡單理解成:
這個程式有一些不能隨便消失、不能隨便換掉的資訊。
例如 PostgreSQL 裡面存了:
使用者帳號
訂單資料
交易紀錄
文章內容
這些東西就是 Database 的 State。
假設 PostgreSQL Pod 被刪掉:
postgres Pod
X
我們不能接受:
Pod 沒了
↓
資料也全部沒了
所以 Database 就屬於典型的:
Stateful Application
也就是:
有需要被保存的狀態的應用程式。
相反的概念叫:
Stateless
也就是:
程式本身不依賴某一個特定 Pod 裡面的資料或身份。
例如我們前面做的 FastAPI:
FastAPI Pod A
FastAPI Pod B
FastAPI Pod C
理想情況下三顆都一樣。
如果:
Pod A
死掉,Deployment 重新建立一顆:
Pod D
通常沒差。
因為真正的資料可能放在:
PostgreSQL
Redis
外部 Storage
而不是 FastAPI Pod 本身。
所以 FastAPI 這種應用通常比較接近:
Stateless Application
現在才來看今天主角。
StatefulSet 是 Kubernetes 裡的一種 Workload Controller。
這裡又出現新名詞:
Workload
Workload 可以理解成:
你希望 Kubernetes 幫你執行與管理的應用程式。
例如我們之前學過:
Deployment
Job
CronJob
DaemonSet
它們都是不同類型的 Workload。
而 Controller 可以理解成:
負責持續觀察並維持你指定狀態的 Kubernetes 管理器。
例如你告訴 Deployment:
我要 3 顆 Pod
Deployment 就會一直想辦法維持:
Pod = 3
死掉一顆,就再建立一顆。
所以:
StatefulSet 是 Kubernetes 專門用來管理「需要固定身份、固定 Storage、可預期建立順序」這類 Stateful Application 的 Workload Controller。
例如:
PostgreSQL
MySQL
Kafka
ZooKeeper
某些 Redis Cluster
都可能使用 StatefulSet。
但這不代表:
Database 一定只能使用 StatefulSet
而是它提供的特性通常更符合 Stateful Application 的需求。
Deployment 建立的 Pod 通常長這樣:
api-7dc84bfc75-abc12
api-7dc84bfc75-def34
名字後面帶有產生出來的字串。
如果第一顆 Pod 壞掉:
api-7dc84bfc75-abc12
新的 Pod 可能變成:
api-7dc84bfc75-x82jd
這其實是刻意設計的。
因為 Deployment 的概念是:
我不在乎你是哪一顆 Pod,只要有足夠數量的 Pod 可以提供服務就好。
所以這些 Pod 可以被視為:
Disposable
Disposable 就是:
可丟棄、可替換的。
例如 FastAPI Pod:
Pod A 壞掉
↓
換 Pod B
沒什麼關係。
假設我們有三個 Database Node:
database-0
database-1
database-2
這三個可能不是完全一樣的角色。
例如:
database-0
可能是 Primary
database-1
可能是 Replica
database-2
可能是另一個 Replica
這裡的 Replica 是:
另一份用來同步資料的 Database 節點。
所以有些系統會需要知道:
我是 database-0
你是 database-1
這就是所謂的:
Stable Identity
也就是:
穩定、不會因為 Pod 重建就改變的身份。
StatefulSet 建立出來的 Pod 名稱不是亂數,而是:
postgres-0
postgres-1
postgres-2
假設:
postgres-0
被刪掉。
Kubernetes 建立新的 Pod 時,新 Pod 還是:
postgres-0
而不是:
postgres-x82jd
所以我們可以理解成:
Deployment
↓
我只在乎「有幾顆」
StatefulSet
↓
除了有幾顆,我還在乎「這一顆是誰」
對 Database 來說,只有名字固定還不夠。
我們還希望:
postgres-0
一直使用自己的硬碟。
例如:
postgres-0
↓
自己的資料空間
postgres-1
↓
另一份資料空間
在 Kubernetes 裡,通常會變成:
postgres-0
↓
data-postgres-0
postgres-1
↓
data-postgres-1
其中:
data-postgres-0
就是 PVC。
PVC 全名:
PersistentVolumeClaim
可以理解成:
Pod 向 Kubernetes 提出的 Storage 使用申請。
例如:
resources:
requests:
storage: 1Gi
意思就是:
我要一個至少 1Gi 的 Storage。
PVC 本身不是硬碟。
它比較像:
Storage 申請單
PV 全名:
PersistentVolume
它代表 Kubernetes 可以使用的一份實際儲存資源。
所以可以先這樣記:
PVC
= 我要 Storage
PV
= Kubernetes 給你的 Storage
最後可能變成:
Pod
↓
PVC
↓
PV
↓
真正的 Disk
StatefulSet 有一個非常重要的功能:
volumeClaimTemplates
Template 就是:
模板。
所以 volumeClaimTemplates 可以理解成:
建立 PVC 的模板。
例如:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
這不是只建立一個 PVC。
而是在告訴 StatefulSet:
每建立一個 Pod,都按照這個模板替它建立自己的 PVC。
所以:
postgres-0
↓
data-postgres-0
如果有第二顆:
postgres-1
↓
data-postgres-1
第三顆:
postgres-2
↓
data-postgres-2
這裡就是 StatefulSet 最重要的地方。
假設:
postgres-0
↓
data-postgres-0
今天 postgres-0 掛掉了。
StatefulSet 建立新的:
postgres-0
新的 postgres-0 還會重新掛回:
data-postgres-0
因此:
Pod 可以重建
但 PVC 還在
這也是為什麼 Database 資料不應該直接存在 Pod 本身。
(它是一份「規格範本」)
StorageClass 可以理解成:
Kubernetes 裡「怎麼建立 Storage」的一組規則。
例如一間公司可能同時有:
高速 SSD
普通 Disk
NFS
Cloud Disk
不同 StorageClass 可以代表不同種類的 Storage。
查看目前 Cluster:
kubectl get storageclass
或縮寫:
kubectl get sc
看到:

其中:
(default)
代表:
如果 PVC 沒特別指定 StorageClass,就預設使用它。
在 Kubernetes 的權責劃分中:
StorageClass(簡稱 SC)就是維運人員開出來的「儲存型態範本(Profile)」。
看一個真實的 StorageClass YAML 結構:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd # StorageClass 名稱
provisioner: ebs.csi.aws.com # 指定由哪一個外掛負責動工
parameters:
type: gp3 # 傳給雲端硬碟的具體參數(如 AWS EBS gp3)
iops: "3000"
reclaimPolicy: Delete # PVC 刪除後,這顆實體硬碟是要保留還是刪除
volumeBindingMode: WaitForFirstConsumer # 等 Pod 被排程到特定節點後才建立硬碟
provisioner:告訴 K8s「該叫哪一個驅動程式去建硬碟」。parameters:這是給 Provisioner 看的參數(例如要 gp3 還是 io2、檔案系統是 ext4 還是 xfs),K8s 本身不干涉這些細節。volumeBindingMode:決定「何時」建硬碟。如果是 WaitForFirstConsumer,它會等到 Pod 確定落在哪個 Availability Zone(可用區)後才建硬碟,避免硬碟建在 Zone A 但 Pod 跑在 Zone B 掛載不到的悲劇。(default) 標記:當 SC 的 metadata 加上 storageclass.kubernetes.io/is-default-class: "true",開發者寫 PVC 時就算完全不寫 storageClassName,K8s 也會自動套用它。(它是真正的「驅動外掛」)
StorageClass 背後通常會指定:
Provisioner
Provisioner 可以理解成:
真正負責幫 Kubernetes 建立 Storage 的元件。
所以完整流程其實是:
PVC
│
│ 我要 1Gi
▼
StorageClass
│
│ 告訴 Kubernetes 要用哪種 Storage
▼
Provisioner
│
│ 實際建立 Storage
▼
PV
│
▼
Real Storage
例如在 Cloud 環境裡,Provisioner 可能會真的幫你建立一顆 Cloud Disk。
Kubernetes 本身是個容器調度引擎,它天生不會操作 AWS、Google Cloud 或實體機房的儲存設備。
ebs.csi.aws.com 或 nfs.csi.k8s.io)。vol-0a1b2c3d)。這裡又會看到一個很常見的名詞:
Dynamic Provisioning
意思其實很簡單:
PVC 建立之後,Kubernetes 自動替你建立對應的 Storage。
以前可能需要 Administrator:
先手動建立 PV
↓
Developer 建 PVC
↓
找到適合的 PV
↓
Binding
其中 Binding 就是:
PVC 和 PV 配對成功。
現在有 StorageClass 後,可以:
Developer 建 PVC
↓
StorageClass
↓
Provisioner
↓
自動建立 PV
這就是:
Dynamic Provisioning
也就是:
動態、自動產生 Storage。
前面的示意圖如果改用時間軸(Sequence)來看,就不會產生「多此一舉」的疑惑:
[ Developer ] [ K8s API ] [ Provisioner (CSI) ] [ Cloud Provider (如 AWS) ]
│ │ │ │
1. │ 建立 PVC (指定 sc: ssd) │ │ │
├────────────────────────>│ │ │
│ │ 2. 廣播事件:「有新 PVC!」│ │
│ ├─────────────────────────>│ │
│ │ │ 3. 呼叫雲端 API 建立硬碟 │
│ │ ├──────────────────────────────>│
│ │ │ │ (硬碟建立中...)
│ │ │ 4. 回傳實體硬碟 ID (vol-xxxx) │
│ │ │<──────────────────────────────┤
│ │ 5. 自動建立 PV (含硬碟ID) │ │
│ │<─────────────────────────┤ │
│ │ │ │
│ │ 6. 自動綁定 (Binding): │ │
│ │ PVC <───> PV │ │
│ │ │ │
7. │ PVC 狀態變成 Bound! │ │ │
│<────────────────────────┤ │ │
| 元件 | 它是什麼? | 比喻 | 負責人 |
|---|---|---|---|
| PVC | 儲存需求單(要多大、什麼存取模式) | 點餐單 | 開發者(Developer) |
| StorageClass | 儲存規格定義(哪種等級、用哪台機器) | 菜單項目 / 配方 | 維運工程師(Admin) |
| Provisioner | 真正執行建立/刪除實體硬碟的背景程式 | 外掛機器人 / 廚師 | 雲端廠商 / CSI 插件 |
| PV | 一筆指針紀錄(指向已建好的實體硬碟) | 取餐號碼牌 | Provisioner 自動產出 |
動態配置(Dynamic Provisioning)的核心價值在於解耦:開發者只需要專注要多少容量(PVC),維運只要預先制定好規則(StorageClass),中間調用實體硬碟與登錄 PV 的髒活全交給 Provisioner 自動完成。
PVC 裡常看到:
accessModes:
- ReadWriteOnce
AccessMode 指的是:
這個 Volume 可以用什麼方式被掛載與使用。
常見有:
ReadWriteOnce
ReadOnlyMany
ReadWriteMany
ReadWriteOncePod
例如:
ReadWrite
代表:
可以讀,也可以寫
ReadOnly:
只能讀
但不要直接把:
ReadWriteOnce
背成:
只能一個 Pod
更精準的理解是:
它描述 Storage 能以什麼方式被 Node / Pod 掛載,而真正的行為也和 Storage Driver 有關。
Storage Driver 則是:
Kubernetes 與實際儲存系統溝通的驅動程式。
接下來介紹一個非常重要的 Storage 名詞:
Reclaim Policy
它是在決定:
PVC 不用了之後,背後 Storage 要怎麼處理。
先看 PV:
kubectl get pv
再:
kubectl get pv <PV_NAME> -o yaml
裡面可能看到:
persistentVolumeReclaimPolicy: Delete
常見有:
Delete
Retain
Delete:
PVC 被刪
↓
PV 被處理掉
↓
底層 Storage 可能一起刪掉
Retain:
PVC 被刪
↓
底層資料保留
↓
之後由 Administrator 人工處理
對 Database 來說非常重要。
因為:
刪掉 Kubernetes Object
跟:
刪掉真正的資料
完全不是同一件事情。
現在概念都介紹完了,再來做實作。
建立 Namespace:
kubectl create namespace cka-lab \
--dry-run=client \
-o yaml | kubectl apply -f -
先建立 PostgreSQL Password:
kubectl create secret generic postgres-secret \
-n cka-lab \
--from-literal=POSTGRES_PASSWORD='cka-lab-password'
這裡的 Secret 是 Kubernetes 專門拿來存放:
密碼
Token
Credential
這類敏感設定的 Object。
建立:
vim postgres-statefulset.yaml
內容:
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: cka-lab
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: cka-lab
spec:
serviceName: postgres-headless
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
套用:
kubectl apply -f postgres-statefulset.yaml
剛剛 YAML 裡有一個:
clusterIP: None
這種 Service 叫:
Headless Service
一般 Service 通常會有一個固定 IP:
Client
↓
Service IP
↓
某一顆 Pod
Client 不需要知道後面是哪一顆 Pod。
但 StatefulSet 有時候反而希望:
我可以直接找到某一顆特定 Pod。
例如:
postgres-0
postgres-1
所以 Headless Service 不幫你提供一般的單一 Virtual IP,而是協助提供每顆 StatefulSet Pod 的 DNS Identity。
例如:
postgres-0.postgres-headless
因此 StatefulSet 不只是:
名字固定
還可以擁有:
穩定的網路身份
查看:
kubectl get statefulset -n cka-lab
也可以:
kubectl get sts -n cka-lab
你應該看到:
postgres
<>
再:
kubectl get pod -n cka-lab
你應該看到:
postgres-0

注意:
不是亂數名稱
而是固定的:
postgres-0
再看 PVC:
kubectl get pvc -n cka-lab
會看到類似:
data-postgres-0

所以關係是:
StatefulSet
↓
postgres-0
↓
data-postgres-0 PVC
↓
PV
↓
Real Storage
先進 PostgreSQL:
kubectl exec -it \
-n cka-lab \
postgres-0 \
-- psql -U postgres
建立資料:
CREATE TABLE demo (
id SERIAL PRIMARY KEY,
name TEXT
);
INSERT INTO demo(name)
VALUES ('StatefulSet Test');
SELECT * FROM demo;
離開:
\q
現在故意刪掉 Pod:
kubectl delete pod postgres-0 -n cka-lab
觀察:
kubectl get pod -n cka-lab -w
你會發現:
postgres-0
會重新出現。
等它 Ready 後再次進入:
kubectl exec -it \
-n cka-lab \
postgres-0 \
-- psql -U postgres
再:
SELECT * FROM demo;
應該仍然可以看到:
StatefulSet Test
<>
為什麼?
因為:
舊 postgres-0 Pod
↓
被刪除
新 postgres-0 Pod
↓
重新掛載
data-postgres-0 PVC
↓
資料仍然存在
最後一定要理解:
StatefulSet
≠
Database HA
這裡的 HA 是:
High Availability
也就是:
高可用性,某個節點故障時,服務仍然能繼續運作。
假設你把:
replicas: 1
改成:
replicas: 3
Kubernetes 會建立:
postgres-0
postgres-1
postgres-2
但是它不知道:
哪一台是 Primary
哪一台是 Replica
資料怎麼同步
Primary 掛了誰接手
其中資料同步叫:
Replication
Primary 掛掉之後由其他節點接手叫:
Failover
這些都是 PostgreSQL 本身需要處理的事情。
StatefulSet 只是在 Kubernetes 層幫你提供:
固定 Pod Identity
固定 Storage
固定 Network Identity
可預期的 Pod 建立 / 終止方式
因此實際 Production 裡常常還會看到:
Operator
Operator 可以先簡單理解成:
把某個複雜軟體的維運知識寫進 Kubernetes Controller 裡。
例如 PostgreSQL Operator 可能知道:
怎麼建立 Primary
怎麼建立 Replica
怎麼做 Failover
怎麼 Backup
怎麼升級
所以:
StatefulSet
只是底層 Primitive。
Primitive 可以理解成:
Kubernetes 提供的基礎積木。
真正完整的 Database HA,通常還需要更多工具與 Database 本身的能力。
假設今天 Production 是:
Deployment
↓
PostgreSQL
↓
PVC
↓
重要資料
不要因為學到 StatefulSet 就直接:
kubectl delete deployment postgres
kubectl apply -f postgres-statefulset.yaml
因為這涉及:
Data Migration
也就是:
把現有資料安全搬到新的架構。
正式搬遷通常還需要考慮:
Backup
資料一致性
Downtime
Service 切換
Rollback
其中 Rollback 就是:
新的版本出問題時,可以退回原本架構。
所以:
StatefulSet Migration 不只是換一份 YAML,而是一個真正的資料遷移工程。
徹底搞懂 StatefulSet 與 Deployment 的本質
在 Kubernetes 中,挑選 Workload 控制器前,必須先看清楚應用的本質:
Stateless(無狀態) ──> Pod 換掉通常沒關係 ──> 適合 Deployment
Stateful(有狀態) ──> 資料、身分或狀態需保存 ──> 適合 StatefulSet
很多人誤以為「有存資料就一定要用 StatefulSet」,這是不精確的。兩者的核心界限在於:Pod 之間到底有沒有「獨立性」與「身分認同(Identity)」的差別。
api-pod-A 處理或由 api-pod-B 處理,得到的結果完全一樣。api-pod-A 崩潰,控制器直接將它銷毀,隨機生出一個 api-pod-C 頂替,用戶端完全無感。Deployment 掛載 PVC(吃大鍋飯):
若開了 3 個副本且指定同一個 PVC,這 3 個 Pod 會嘗試搶著掛載同一個儲存資源。在 AWS EBS、GCP PD 等僅支援單節點讀寫(ReadWriteOnce)的硬碟架構下,第二個 Pod 會因磁區鎖定衝突而直接報錯無法啟動;即使底層是 NFS 等支援多點讀寫(ReadWriteMany)的儲存,所有 Pod 看到的也是同一份資料,無法實現分散式節點的獨立數據隔離。
StatefulSet 掛載 PVC(一人一盤):
StatefulSet 具備 volumeClaimTemplates 機制。若宣告 3 個副本,Kubernetes 會依序自動建立 3 個專屬的獨立 PVC:
postgres-0 綁定 data-postgres-0
postgres-1 綁定 data-postgres-1
postgres-2 綁定 data-postgres-2
即使 postgres-1 故障漂移到其他節點重啟,它只會精確掛載回原有的 data-postgres-1,資料絕不搞混。
分散式系統高度依賴節點身分:
| 比較維度 | Deployment(無差別代工) | StatefulSet(各司其職的團隊) |
|---|---|---|
| Pod 命名 | 隨機雜湊碼(app-7df4f8-abcde) |
固定序號(db-0、db-1、db-2) |
| 重啟後的身分 | 舊的銷毀,生成全新隨機名稱 | 舊的銷毀,新生的 Pod 依舊沿用 db-0 |
| 網路身分 | 共用單一 Service IP,無法指定訪問特定節點 | 搭配 Headless Service,擁有專屬 DNS 紀錄(如 db-0.db-service) |
| 啟動與終止順序 | 副本同時平行開關,無順序要求 | 依序啟動(0→1→2),逆序安全退場(2→1→0) |
為了滿足「保存身分與資料」的要求,StatefulSet 設計了一條環環相扣的控制鏈:
Plaintext
StatefulSet (控制器)
│
▼
Stable Pod Identity (固定身分:postgres-0)
│
▼
volumeClaimTemplates (專屬宣告範本)
│
▼
PVC (專屬帳單:data-postgres-0)
│
▼
StorageClass (儲存規格策略)
│
▼
Provisioner (CSI 驅動外掛)
│
▼
PV (K8s 儲存資源實體映射)
│
▼
Real Storage (底層實體硬碟:AWS EBS / 機房 SAN)
0 → 1 → 2)依次啟動、更新與逆序關閉,並確保每個 Pod 都有獨一無二的固定序號。postgres-0)
postgres-0。搭配 Headless Service(無頭服務)時,叢集內部會為它分配專屬的內部網域名稱(postgres-0.db-service),即使 Pod 內部 IP 每次漂移更換,這個身分網址永久有效。postgres-0 的當下,自動以 <範本名稱>-<Pod名稱> 的格式推導出這個身分的專屬儲存需求。data-postgres-0)
postgres-0 獨立簽發一張名為 data-postgres-0 的需求清單。這個 PVC 在 Pod 刪除或重啟時絕對不會被刪除。當新的 postgres-0 再次啟動時,它只會指名掛載回同一張 data-postgres-0。data-postgres-0 的要求與 StorageClass 的規格後,呼叫底層 API(如 AWS、GCP 或機房儲存設備)實際割出一顆符合規格的實體硬碟,並回報實體硬碟 ID。data-postgres-0」。postgres-0 容器內的路徑。Deployment 的核心哲學是「無名氏可替換」;而 StatefulSet 的核心哲學是「精確歸位」——透過固定名稱(
postgres-0)鎖定專屬 PVC(data-postgres-0),進而鎖定底層實體硬碟,達成資料、身分與狀態的完整保存。