Kubernetes Storage - 數據持久化 (PV & PVC)

在 Kubernetes 中,Pod 是暫時的。當 Pod 被刪除重建,裡面的檔案系統也會重置。 如果要讓資料 (如資料庫檔案) 能夠 持久化 (Persist) 且跨 Pod 生命週期存在,我們需要使用 Kubernets 的儲存系統。

基本 Volume 類型

Kubernetes 支援多種 Volume 類型,最簡單的有:

  1. emptyDir: 臨時目錄。Pod 建立時產生,Pod 刪除時消失。適合 Cache 或暫存檔。

    • 應用情境:Redis 快取節點、Spark 任務的暫存空間、同一個 Pod 內不同 Container 共享資料 (sidecar pattern)。
    volumes:
      - name: cache-volume
        emptyDir: {}
    
  2. hostPath: 掛載 Node (宿主機) 上的目錄。適合 Minikube 單節點測試,但在多節點叢集會有問題 (Pod 飄到別台機器就讀不到了)。

    • 應用情境:Minikube 測試 PV/PVC、或是需要收集宿主機 Log (如 Fluentd) 或監控數據 (如 Prometheus Node Exporter) 的系統層級 Agent。
    volumes:
      - name: test-volume
        hostPath:
          # directory location on host
          path: /data
          # this field is optional
          type: Directory
    

進階儲存架構: PV 與 PVC

這兩個概念通常一起出現,初學者容易搞混。簡單來說,這是 Kubernetes 為了讓開發者不需煩惱底層硬碟是用哪牌的,所設計的「供需媒合」機制。

你可以這樣想像:

  • PV (PersistentVolume) 是「實際的倉儲空間」(供應方)。

    • 維運人員 (Ops) 負責準備。
    • 它對應到真實的物理儲存(如 AWS EBS 硬碟、NFS 伺服器、GCP Persistent Disk)。
    • 口語:「倉庫裡已經準備好一塊 100GB 的硬碟空間了。」
  • PVC (PersistentVolumeClaim) 是「領料申請單」(需求方)。

    • 開發者 (Dev) 定義。
    • 你在 YAML 裡只需要開出規格:「我需要 10GB、要可以讀寫」。
    • 口語:「我的 App 需要申請 10GB 的空間來存資料。」

運作流程:

  1. 開發者建立 PVC (申請單)。
  2. Kubernetes 看到申請單,自動在系統裡尋找符合規格 (例如大小足夠) 的 PV (硬碟)。
  3. 一旦配對成功,兩者就會 綁定 (Bound) 在一起。
  4. Pod 只需要掛載這個 PVC,就能使用那塊 PV 的空間,完全不用管底層是接什麼線。
PV 與 PVC 的綁定是「一對一」的 (Exclusive): 一個 PV 一旦綁定給某個 PVC,就不能再被其他 PVC 使用 (除非釋放)。 但是,一個 PVC 可以被多個 Pod 同時使用 (這取決於 AccessMode 設定,例如 ReadWriteMany)。

實戰:使用 HostPath 練習

在 Minikube 中,我們可以使用 hostPath 來模擬 PV。

定義 PV

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
  labels:
    type: local
spec:
  # 定義 StorageClass 名稱,PVC 必須匹配此名稱才能綁定
  storageClassName: manual
  # 定義 PV 的總容量
  capacity:
    storage: 10Gi
  # 定義可以被掛載的模式 (RWO, ROX, RWX)
  accessModes:
    - ReadWriteOnce
  # 定義底層儲存類型,這裡是 hostPath (測試用)
  hostPath:
    path: '/mnt/data'

定義 PVC

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  # 必須與 PV 的 storageClassName 一致才能綁定
  storageClassName: manual
  # 申請的存取模式,必須是 PV 支援的模式之一
  accessModes:
    - ReadWriteOnce
  # 申請的資源大小
  resources:
    requests:
      storage: 3Gi # 只要 PV 容量 >= 3Gi 且其他條件符合,就可以綁定

在 Pod 中使用 PVC

Pod 不需要知道 PV 的存在,只需要引用 PVC 即可。

# pod-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim:
        claimName: task-pv-claim # 引用上面的 PVC
  containers:
    - name: task-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: 'http-server'
      volumeMounts:
        - mountPath: '/usr/share/nginx/html'
          name: task-pv-storage

StorageClass (SC)

手動建立 PV 很麻煩。現代 K8s 叢集通常都有設定預設的 StorageClass (如 AWS gp2, GCP standard)。 只要定義 PVC 時指定 StorageClass (或者留空使用預設值),K8s 就會自動呼叫雲端 API 建立硬碟並掛載進來,這稱為 Dynamic Provisioning

存取模式 (Access Modes) 與 PVC 重用性

常常有人問:「一個 PVC 可以給幾個 Pod 用?」這其實取決於 AccessMode 的設定。需要注意的是,PV 與 PVC 的綁定永遠是一對一的,但一個 PVC 可以被多個 Pod 掛載

  1. ReadWriteOnce (RWO):

    • 單節點讀寫
    • 限制:該 PVC 同一時間只能被掛載到 單一個 Node
    • 情境:
      • Pod A (在 Node 1) 使用 PVC -> 成功
      • Pod B (也在 Node 1) 使用同一 PVC -> 成功
      • Pod C (在 Node 2) 想用同一 PVC -> 失敗 (因為已在 Node 1 被掛載)。
    • 適用:大多數雲端區塊儲存 (AWS EBS, GCP PD, Azure Disk)。
  2. ReadOnlyMany (ROX):

    • 多節點唯讀
    • 可以跨多個 Node 掛載,但是大家只能讀,不能寫。
  3. ReadWriteMany (RWX):

    • 多節點讀寫
    • PVC 可以同時被 多個 Node 上的多個 Pod 掛載並讀寫。
    • 適用:檔案系統級別的儲存 (NFS, AWS EFS, Google Filestore, CephFS)。

PVC 的釋放與回收 (Reclaim Policy)

當你不再需要某個儲存空間時,可以刪除 PVC 來釋放資源:

kubectl delete pvc task-pv-claim

刪除 PVC 後,原本綁定的 PV 會發生什麼事?這取決於 PV 的 Reclaim Policy (回收策略):

  1. Retain (保留):
    • PV 狀態變成 Released
    • 資料還在,但該 PV 不能被其他 PVC 直接重新綁定 (為了資料安全)。
    • 需要管理員手動介入 (備份資料、刪除 PV 物件) 才能再次使用。
  2. Delete (刪除):
    • (大多雲端 StorageClass 的預設值)
    • PV 物件會被自動刪除。
    • 背後的儲存資產也會被刪除 (例如 AWS EBS Volume 會被 Terminate)。資料會永久消失
  3. Recycle (回收):
    • (已棄用/少用)
    • 系統會執行基本的 rm -rf /thevolume/* 清除資料,然後讓 PV 變回 Available 狀態,供下一個 PVC 申請使用。

如果我有 100 個 Pod 怎麼辦? (StatefulSet)

這是一個常見的問題:如果我跑一個 MongoDB Replica Set (3 個 Pod),每個 Pod 都需要自己獨立的 10GB 空間,難道我要手動寫 mongo-pvc-1.yaml, mongo-pvc-2.yaml... 嗎?

不需要! 這時候你會使用 StatefulSet 搭配 volumeClaimTemplates

什麼是 volumeClaimTemplates?

它就像是一個「PVC 模具」。當 StatefulSet 建立 Pod 時,會根據這個模具,自動 為每一個 Pod 產生一個專屬的 PVC。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: 'nginx'
  replicas: 3 # 會產生 web-0, web-1, web-2
  template:
    # Pod 的定義 (略)
  volumeClaimTemplates: # 這裡定義 PVC 模板
    - metadata:
        name: www
      spec:
        accessModes: ['ReadWriteOnce']
        storageClassName: 'my-storage-class'
        resources:
          requests:
            storage: 1Gi

結果 (StatefulSet 自動化):

K8s 會自動幫你建立 3 個 PVC (與對應的 PV),名稱通常為 [模板名]-[Pod名]

  1. www-web-0 (給 web-0 用)
  2. www-web-1 (給 web-1 用)
  3. www-web-2 (給 web-2 用)

這樣每個 Pod 都有自己獨立的儲存空間,而且擴展皆自動化 (Scale 到 100 個副本,就會有 100 個 PVC)。

總結

資料庫等有狀態應用必須使用 PV/PVC 來確保資料安全。結合 StatefulSet,可以為每個副本自動產生專屬的 PVC,實現分散式儲存架構。