手動建 PV 的痛點
上一篇 PV / PVC 教完後你發現一個問題:PV 是管理員手動建的。
小規模還好。但企業環境:
- 3 個叢集
- 50 個微服務
- 每個都要 PVC
這叫靜態佈建(Static Provisioning)。K8s 還支援動態佈建(Dynamic Provisioning):開發者寫 PVC,K8s 自動建 PV。
| 靜態佈建 | 動態佈建 | |
|---|---|---|
| 流程 | 管理員建 PV → PVC 配對 | PVC 一建 → PV 自動冒出來 |
| 適合 | 學習、小規模 | 生產環境 |
| 需要 | — | StorageClass |
StorageClass:自動建 PV 的工廠
StorageClass 告訴 K8s「當有人建 PVC,用什麼方式自動建 PV」。像個工廠模板,訂單進來自動生產,不用每次重新畫圖。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path # ← 用什麼 provisioner 建 PV
reclaimPolicy: Delete
好消息:k3s 內建一個 default StorageClass:
$ kubectl get storageclass
NAME PROVISIONER DEFAULT
local-path (default) rancher.io/local-path true
(default) 表示 PVC 沒指定 storageClassName 就自動用這個。
不同雲商有不同的 provisioner:
| 環境 | Provisioner | 自動建什麼 |
|---|---|---|
| k3s | rancher.io/local-path | Node 本機目錄 |
| AWS EKS | ebs.csi.aws.com | EBS 磁碟 |
| GKE | pd.csi.storage.gke.io | Persistent Disk |
| Azure AKS | disk.csi.azure.com | Azure Disk |
動態佈建 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
spec:
storageClassName: local-path # ← 不要自己建 PV
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
$ kubectl apply -f pvc.yaml
$ kubectl get pv,pvc
NAME CAPACITY STATUS CLAIM
pv/pvc-abc12 1Gi Bound default/dynamic-pvc
NAME STATUS VOLUME CAPACITY
pvc/dynamic-pvc Bound pvc-abc12 1Gi
沒有手動建 PV,K8s 自動產出 pvc-abc12 這顆 PV。整個過程開發者完全不用碰 PV。
但是 — 資料庫不能用 Deployment
有了動態佈建,可以正式跑 MySQL 了。但用 Deployment 跑 MySQL 有四個致命問題:
問題 1:Pod 名稱隨機
Deployment 的 Pod 叫 mysql-deploy-7d8c-abc12 — random hash,每次重建名字都變。MySQL 主從架構誰是主誰是從?
問題 2:沒有啟動順序
3 個副本同時啟動。但 MySQL 主從複製要主庫先起來、拿到 binlog position、從庫再連上去同步。同時起會打架。
問題 3:所有 Pod 共用同一個 PVC
Deployment 的 volumes 區塊只能寫一個 PVC,所有 Pod 都掛到那一個。3 個 MySQL 同時寫同一塊磁碟 = 資料毀滅。
問題 4:沒有穩定的網路身份
Service 會做負載均衡,連線隨機分到後端 Pod。但寫入要送主庫、讀取送從庫怎麼區分?
→ 這就是為什麼 K8s 有 StatefulSet。
StatefulSet:給有狀態應用用的
StatefulSet 跟 Deployment 像,但給三個保證:
| 保證 | 內容 |
|---|---|
| 穩定的身份 | Pod 名固定 mysql-0 / mysql-1 / mysql-2 |
| 獨立的儲存 | 每個 Pod 自動建獨立 PVC |
| 有序的生命週期 | 啟動 0 → 1 → 2,刪除 2 → 1 → 0 |
| Deployment | StatefulSet | |
|---|---|---|
| Pod 名 | random hash | 固定序號 mysql-0 |
| 啟動 | 同時 | 有序 0 → 1 → 2 |
| 刪除 | 隨機 | 反序 2 → 1 → 0 |
| PVC | 共用 | 每個 Pod 獨立 |
| DNS | 只有 Service DNS | 每個 Pod 自己的 DNS |
Headless Service:每個 Pod 自己的 DNS
StatefulSet 必須搭 Headless Service(無頭 Service)。
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # ← 這個是 Headless 的標誌
selector:
app: mysql-sts
ports:
- port: 3306
clusterIP: None 意思是「不要做負載均衡」。
普通 Service:你連 mysql-svc → 隨機分到後面某個 Pod。
Headless Service:每個 Pod 自己有 DNS:
mysql-0.mysql-headless.default.svc.cluster.local → mysql-0 Pod
mysql-1.mysql-headless.default.svc.cluster.local → mysql-1 Pod
mysql-2.mysql-headless.default.svc.cluster.local → mysql-2 Pod
應用就能精準連到「我要連主庫 mysql-0、讀 mysql-1」。
StatefulSet YAML
apiVersion: apps/v1
kind: StatefulSet # ← 不是 Deployment
metadata:
name: mysql
spec:
serviceName: mysql-headless # ← 對應 Headless Service
replicas: 2
selector:
matchLabels:
app: mysql-sts
template:
metadata:
labels:
app: mysql-sts
spec:
containers:
- name: mysql
image: mysql:8.0
envFrom:
- secretRef:
name: mysql-secret
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
# ★ 重點:volumeClaimTemplates
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# 不寫 storageClassName 就用 default(k3s 是 local-path)
跟 Deployment 三個關鍵差別:
kind: StatefulSetserviceName 指向 Headless ServicevolumeClaimTemplates 取代 volumes — 每個 Pod 自動建獨立 PVC觀察有序啟動
$ kubectl apply -f statefulset-mysql.yaml
$ kubectl get pods -w
NAME READY STATUS AGE
mysql-0 0/1 ContainerCreating 2s
mysql-0 1/1 Running 10s
mysql-1 0/1 Pending 10s # ← mysql-0 Ready 後才開始
mysql-1 0/1 ContainerCreating 12s
mysql-1 1/1 Running 20s
mysql-0 完全 Ready,mysql-1 才開始建。Deployment 不會這樣。
自動建的 PVC
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY
mysql-data-mysql-0 Bound pvc-abc12 1Gi
mysql-data-mysql-1 Bound pvc-def34 1Gi
每個 Pod 自己一個 PVC,命名格式 。完全自動。
砍 Pod 驗證資料持久化
# 1. 建資料
$ kubectl exec mysql-0 -- mysql -prootpw -e "CREATE DATABASE testdb;"
# 2. 砍 mysql-0
$ kubectl delete pod mysql-0
# 3. StatefulSet 自動重建,名字還是 mysql-0
$ kubectl get pods
NAME STATUS
mysql-0 Running # ← 名字沒變
mysql-1 Running
# 4. 資料還在
$ kubectl exec mysql-0 -- mysql -prootpw -e "SHOW DATABASES;"
testdb # ← 還在!
關鍵:新建的 mysql-0 自動掛回 mysql-data-mysql-0 PVC。資料完全保留。
scale 行為
$ kubectl scale statefulset mysql --replicas=3
# mysql-2 才被建
$ kubectl scale statefulset mysql --replicas=2
# mysql-2 先被刪,mysql-0、mysql-1 留著
有序建立,反序刪除。確保資料庫主從關係不被破壞。
重點整理
- StorageClass 把「手動建 PV」自動化,PVC 一建 PV 自動冒出來
- k3s 內建
local-path是 default,PVC 不用指定storageClassName就 work - Deployment 跑 DB 四個問題:名字、順序、共用 PVC、沒網路身份
- StatefulSet 三保證:穩定身份 / 獨立儲存 / 有序生命週期
- 必須搭 Headless Service(
clusterIP: None) volumeClaimTemplates自動為每個 Pod 建獨立 PVC
下一步
到這邊你的目錄有 7-8 個 YAML 檔了:Secret、ConfigMap、StatefulSet、Headless Service、PVC、Ingress... 一個 MySQL 就這麼多。再加上 Redis、Elasticsearch — 整個系統幾十個檔案。
而且 dev / staging / prod 三個環境,replicas / Image tag / 密碼都不同,你要維護三套?
下一篇:Helm — K8s 的套件管理器。
📅 下一篇:Helm — K8s 的套件管理器
YAML 檔多到爆炸,Helm 把它們打包成「模板 + 變數」,dev/staging/prod 只差一個 values.yaml。
📚 完整系列總覽:K8s 系列教學首頁(共 40 課,按學習路徑順序排)