K8s · 設定與儲存 · 第 28 課 · · 11min read

StorageClass + StatefulSet:在 K8s 上跑 MySQL 的正確做法

手動建 PV 太煩,StorageClass 自動佈建。Deployment 不適合跑 DB(Pod 名字會變),StatefulSet 給每個 Pod 固定身份。這篇實作 StatefulSet MySQL。

#Kubernetes #StorageClass #StatefulSet #MySQL #DB
章節目錄 · 12

手動建 PV 的痛點

上一篇 PV / PVC 教完後你發現一個問題:PV 是管理員手動建的

小規模還好。但企業環境:

  • 3 個叢集
  • 50 個微服務
  • 每個都要 PVC
管理員每天工作就是建 PV、刪 PV、改 PV。建大了浪費、建小了不夠用。

這叫靜態佈建(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自動建什麼
k3srancher.io/local-pathNode 本機目錄
AWS EKSebs.csi.aws.comEBS 磁碟
GKEpd.csi.storage.gke.ioPersistent Disk
Azure AKSdisk.csi.azure.comAzure Disk
你 PVC 寫一樣的 YAML,換到不同雲端,PV 自動換對應的儲存。

動態佈建 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
對照表:
DeploymentStatefulSet
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: StatefulSet

  • serviceName 指向 Headless Service

  • volumeClaimTemplates 取代 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 課,按學習路徑順序排)