K8s 儲存:Volume、PV/PVC 與 StatefulSet
· tech
📑 目錄
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 儲存最核心的設計,是把「誰要儲存」和「儲存實際在哪」拆成兩個東西:
三個 CKA 常考、實務也要懂的細節都在圖裡:access modes 決定「幾個節點能同時掛、能不能寫」——最常見的 RWO(ReadWriteOnce,單節點讀寫,一般 block storage 的天性)、RWX(ReadWriteMany,多節點同時讀寫,要 NFS 這類檔案儲存才支援);reclaim policy 決定 PVC 刪掉後那塊 PV 的命運——Retain(保留資料等你手動處理)還是 Delete(連底層儲存一起刪掉)。這兩個設錯,輕則資源殘留、重則資料被自動刪光。
StatefulSet:讓每個有狀態 pod 都認得自己那塊盤
有了持久儲存,還有一個問題:一群有狀態的 pod,怎麼確保每個都掛回「自己」那塊盤? Deployment 做不到——它的 pod 是可拋的複製品、名字隨機。答案是 StatefulSet:
volumeClaimTemplates 讓每個 pod 自動有一塊專屬 PVC,重啟/重排都掛回同一塊盤)、以及有序的部署與擴縮(0→1→2)。這正是為什麼 Kafka、Redis 這些有狀態的東西,在 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-0、pod-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-0、data-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 花了好幾個版本、好幾套抽象才把儲存做穩——這也提醒我,評估任何「號稱什麼都能跑」的平台時,第一個該戳的地方,就是它的儲存與狀態管理到底夠不夠硬。