K8s 儲存:Volume、PV/PVC 與 StatefulSet

· tech

#kubernetes#storage

📑 目錄

Pod 是短命、可拋的——自我修復隨時把它換一個。但有些東西死了不能跟著消失:資料。而容器的檔案系統天生是暫時的(ephemeral):pod 一被重排、容器一重啟,寫在裡面的檔案就沒了。所以 K8s 得把「儲存」跟「pod 的生命週期」解耦。這篇講它的儲存三層:Volume、PV/PVC、StatefulSet。

Volume:先把一塊盤掛進 pod

最基本的一層是 Volume——把一塊儲存掛進 pod 的某個路徑。有幾種類型:emptyDir(pod 生命期內的暫存,pod 一消失就沒了,適合容器間共享暫存)、hostPath(掛主機上的路徑,少用、綁死節點),以及真正重要的——透過 PVC 掛一塊「活得比 pod 久」的持久儲存。前兩種都不持久;要資料活過 pod 的死亡,就得往下看 PV/PVC。

PV / PVC:把「供給」和「需求」分開

K8s 儲存最核心的設計,是把「誰要儲存」和「儲存實際在哪」拆成兩個東西:

PV / PVC:把「需求」和「供給」分開 StorageClass:動態供應——PVC 一提出就自動生 PV Pod要掛一塊盤 PVC(需求)「我要 10Gi · RWO」app 開發者只喊這個 bound PV(供給)實際那塊 10Gi 儲存底層:EBS / NFS / Ceph… 供需分離:app 只喊「我要多大、什麼存取模式」,不用管底層是什麼硬體 access modes:RWO(單節點)· ROX(多節點唯讀)· RWX(多節點讀寫,需 NFS 之類) reclaim policy:PVC 刪掉後,PV 要 Retain(保留)還是 Delete(連底層一起刪)
PVC(PersistentVolumeClaim)是「需求」——app 只說「我要一塊 10Gi、ReadWriteOnce 的盤」;PV(PersistentVolume)是「供給」——實際那塊儲存,底層是 EBS 還是 NFS 都行。K8s 負責把兩者綁定(bound)。而 StorageClass 讓這件事自動化:PVC 一提出,就依 provisioner 動態供應一塊新 PV,不必管理員手動預先建。這個「需求/供給分離」是很漂亮的抽象——寫 app 的人完全不用知道底層是哪家雲的哪種盤

三個 CKA 常考、實務也要懂的細節都在圖裡:access modes 決定「幾個節點能同時掛、能不能寫」——最常見的 RWO(ReadWriteOnce,單節點讀寫,一般 block storage 的天性)、RWX(ReadWriteMany,多節點同時讀寫,要 NFS 這類檔案儲存才支援);reclaim policy 決定 PVC 刪掉後那塊 PV 的命運——Retain(保留資料等你手動處理)還是 Delete(連底層儲存一起刪掉)。這兩個設錯,輕則資源殘留、重則資料被自動刪光。

StatefulSet:讓每個有狀態 pod 都認得自己那塊盤

有了持久儲存,還有一個問題:一群有狀態的 pod,怎麼確保每個都掛回「自己」那塊盤? Deployment 做不到——它的 pod 是可拋的複製品、名字隨機。答案是 StatefulSet:

Deployment(無狀態) app-7f9c2 · app-2k1x8(名字隨機) pod 可拋、掛了換一個 → 名字就變 不綁定特定的盤 StatefulSet(有狀態) pod-0 pod-1 pod-2 PVC-0 PVC-1 PVC-2 每個 pod 綁自己的盤(volumeClaimTemplates) 穩定身分 + 重啟認得自己那塊盤 + 有序擴縮 有狀態的(Kafka / Redis / DB)→ StatefulSet;無狀態的 → Deployment
StatefulSet 給每個 pod 三樣 Deployment 沒有的東西:穩定的身分(`pod-0`/`pod-1`,重啟名字不變)、各自綁定的儲存(靠 volumeClaimTemplates 讓每個 pod 自動有一塊專屬 PVC,重啟/重排都掛回同一塊盤)、以及有序的部署與擴縮(0→1→2)。這正是為什麼 KafkaRedis 這些有狀態的東西,在 k8s 上一律跑 StatefulSet + PV——它們的資料綁在特定身分與磁碟上,不能像無狀態 pod 那樣隨便換

落成 YAML:一個 PVC、一個 StatefulSet

先看「需求」那一半——一份 PVC 就是 app 開發者要寫的全部,它完全不提底層是哪種盤,只喊要多大、什麼存取模式、走哪個 StorageClass:

apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: data }
spec:
  accessModes: [ "ReadWriteOnce" ]     # RWO:單一節點讀寫
  storageClassName: fast-ssd           # 交給這個 class 動態供應一塊 PV
  resources:
    requests: { storage: 10Gi }        # 我要 10Gi

而有狀態服務不會自己手寫 PVC,而是用 StatefulSet 的 volumeClaimTemplates——它像一個「PVC 模子」,幫每個 Pod(pod-0pod-1…)各生一塊專屬的盤,重排也掛回同一塊:

apiVersion: apps/v1
kind: StatefulSet
metadata: { name: db }
spec:
  serviceName: db                      # 搭一個 headless Service 給每個 Pod 穩定的 DNS 名
  replicas: 3
  selector: { matchLabels: { app: db } }
  template:
    metadata: { labels: { app: db } }
    spec:
      containers:
        - name: db
          image: postgres:16
          volumeMounts:
            - { name: data, mountPath: /var/lib/postgresql/data }
  volumeClaimTemplates:                # ← 關鍵:每個 Pod 自動獲得一塊自己的 PVC
    - metadata: { name: data }
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: fast-ssd
        resources: { requests: { storage: 10Gi } }

兩個細節值得記:volumeClaimTemplates 生出的 PVC 會叫 data-db-0data-db-1……名字綁著 Pod 的序號,這就是「pod-0 永遠掛回自己那塊盤」的實作;而且縮容時這些 PVC 預設不會被刪——K8s 寧可留著資料等你手動確認,也不敢自作主張刪掉有狀態的盤。這跟一般 Deployment「Pod 走了什麼都不留」是完全相反的預設,正是「有狀態」該有的謹慎。

反思

PV/PVC 的「供需分離」,是我很欣賞的一個抽象

第一次搞懂 PV/PVC,我覺得這設計真漂亮。它把一個混在一起的東西,乾淨地切成兩半:寫 app 的人只需要說「我要一塊 10Gi、可讀寫的盤」(PVC),完全不用知道底層是 AWS EBS、GCP PD、還是機房裡的一台 NFS。 那些底層細節,交給管理員的 PV / StorageClass 去處理。這種「宣告需求、隱藏實作」的分離,其實就是好介面的本質——就像你叫 Uber 只說「我要從 A 到 B」,不用管司機開什麼車、走哪條路。我後來設計任何系統的邊界,都會想起 PVC:讓使用方用「需求的語言」表達,而不是被逼著懂供給端的細節,是降低耦合最有效的一招。

StatefulSet 讓「短命的 pod」也能安全地擁有「不短命的資料」

K8s 一開始給人的印象,是「一切都可拋、一切都無狀態」——pod 死了換一個就好。但真實世界有資料,而資料不能可拋。StatefulSet 巧妙地調和了這個矛盾:pod 本身依然可以死、可以被換,但它的『身分』和『那塊盤』是穩定的——換上來的 pod-0,還是掛回原本 pod-0 那塊 PVC。這讓我想通一件事:「可拋」和「有狀態」不是非黑即白的對立。你可以讓執行的軀殼可拋(pod)、同時讓它守護的資料持久(PV)——把「會死的」和「不能死的」用一層抽象隔開,各自用最適合的方式對待。這也呼應了整個 infra 系列的主軸:有狀態的東西難就難在那塊盤,而 StatefulSet 就是 k8s 給這個難題的標準答案。

儲存,是 K8s 從「跑容器」長成「跑真實系統」的關鍵一步

早期很多人說「有狀態的東西別放 K8s」,原因就是儲存這塊當年還不成熟。而 PV/PVC/StorageClass/StatefulSet 這一整套的出現,正是 K8s 從「只適合跑無狀態 web 服務」跨進「能跑資料庫、訊息佇列這些真實系統」的分水嶺。這給我的體會是:一個平台成不成熟,往往看它怎麼處理『狀態』這塊最硬的骨頭。 無狀態的東西人人會調度,真正的難題永遠在「資料怎麼安全地跟著走」。K8s 花了好幾個版本、好幾套抽象才把儲存做穩——這也提醒我,評估任何「號稱什麼都能跑」的平台時,第一個該戳的地方,就是它的儲存與狀態管理到底夠不夠硬。