Kubernetes存儲卷:保障有狀態應用的數據持久化

在前面幾個章節,介紹了k8s的pod以及網絡相關的知識,在這一章,則開始對k8s的存儲進行介紹。k8s卷(volumes)為container提供了文件系統訪問以及數據共享的能力。
1. 引入
首先先引入一個問題,為什么k8s要提供文件系統訪問以及數據共享的能力?
如果大家使用docker部署過數據庫,就會發現,一般來說,我們都會將容器數據庫中的data,log目錄掛載到host的目錄下,這樣即使容器G了,至少我們的數據不會丟失。因此在k8s中,我們也需要某個存儲方式,能夠讓數據脫離容器的生命周期而存在,然后也能夠讓數據在不同的pod、容器內進行共享,存儲。例如:
- 數據庫pod:將數據庫的數據進行持久化保存,就需要掛載持久化存儲。
- 日志文件:例如一個pod中多個容器,進行共享日志文件。比如說一個pod中的應用容器產生日志,而sidecar容器收集日志,上傳到統一的日志中心。
- 掛載配置文件(configmap):一般來說,我們的應用在啟動的過程中,可能需要讀取一些配置文件。我們當然可以在打包鏡像的時候,將配置文件放進去,但是這樣的話,就失去了靈活(配置進行更改的時候,還需要重新打包鏡像,上傳,部署)。如果我們將配置寫入到某個統一的地方,然后掛載到應用容器中去,那么應用便可以實時讀取配置(因為修改配置之后,掛載的文件也會自動更新)。
- 掛載敏感信息(Secret):比如說,我的應用需要使用數據庫,我們可以將數據庫賬號密碼設置為環境變量,但是環境變量存在泄露的風險(
ps -ef打印環境變量),我們也可以將賬號密碼放在configmap里面,但是configmap誰都能看到,不符合最小化權限設計原則。因此,我們可以將相關敏感信息放在某中類型的存儲卷(Secret)中,然后對其進行權限控制,甚至使用密鑰進行加密。 - 使用云存儲:這個就是云廠商的存儲卷直接掛載到pod中,方便使用。
加下來將詳細的對各個類型的卷進行介紹。
@startmindmap
* Kubernetes 存儲 (Storage)
** 臨時卷 (Ephemeral Volumes) <<Pod 內置>>
*** emptyDir
*** configMap
*** secret
*** 通用臨時卷
** 持久卷 (Persistent Volumes) <<集群級資源>>
*** 持久卷聲明 (PVC) <<用戶接口>>
**** 通過 StorageClass 動態供應
**** 綁定靜態 PV
*** 持久卷 (PV) <<管理員/系統創建>>
**** 本地存儲 (Local Storage)
***** hostPath <<僅單節點>>
***** local (Local Persistent Volume) <<多節點需調度約束>>
**** 網絡/云存儲 (Network / Cloud Storage)
***** 文件存儲 (File Storage) <<支持 RWX>>
****** nfs
****** cephfs
****** azureFile
****** glusterfs
****** AWS EFS / GCP Filestore
***** 塊存儲 (Block Storage) <<通常 RWO>>
****** awsElasticBlockStore (EBS)
****** gcePersistentDisk (GCE PD)
****** azureDisk
****** cinder (OpenStack)
****** rbd (Ceph RBD)
****** iscsi
****** fc (Fibre Channel)
***** 分布式/企業存儲
****** portworxVolume
****** storageos
****** scaleIO
****** quobyte
****** vsphereVolume
****** photonPersistentDisk
@endmindmap
2. 臨時卷
K8s中的臨時卷(Ephemeral Volumes) 是一種生命周期與 Pod 綁定的存儲卷,它在 Pod 創建時動態創建,在 Pod 刪除時自動清理。臨時卷主要用于提供臨時、高性能或特定用途的本地存儲,不適用于需要持久化數據的場景。常用的可以分為如下幾種:
-
emptyDir:Pod 啟動時為空,存儲介質可以是磁盤或內存(Node中的磁盤或內存),pod中所有容器共享該卷。在生產中,我們常用emptyDir來收集日志。例如,在一個pod中多個容器,應用容器生成日志在臨時卷中,sidecar容器(日志收集容器)將臨時卷中的日志上傳到ELK。又或者說,CI/CD構建過程中,源碼pull在emptyDir中,編譯后的產物通過sidecar上傳到制品庫。
@startuml ' 啟用中文支持(確保環境支持 UTF-8) skinparam defaultTextAlignment center skinparam wrapWidth 200 skinparam backgroundColor #FFFFFF ' 自定義顏色 skinparam component { BackgroundColor #E6F3FF BorderColor #1E88E5 FontColor #0D47A1 } skinparam package { BackgroundColor #F0F8E0 BorderColor #7CB342 FontColor #33691E } skinparam folder { BackgroundColor #FFF3E0 BorderColor #FB8C00 FontColor #E65100 } package "Kubernetes 節點" <<Node>> { [Pod\n(my-app)] as pod #BBDEFB package "容器 1\n(主應用)" <<Container>> { [掛載點: /cache] as m1 } package "容器 2\n(日志收集器)" <<Container>> { [掛載點: /shared] as m2 } [臨時卷 emptyDir\n(名稱: temp-storage)] as emptydir #FFECB3 pod --> m1 pod --> m2 m1 --> emptydir : 掛載\nmountPath: /cache m2 --> emptydir : 掛載\nmountPath: /shared } node "節點本地存儲" <<Storage>> { folder "/var/lib/kubelet/pods/...\n/temp-storage" as nodeDir } emptydir --> nodeDir : 存儲位置\n? 默認:節點磁盤\n? 可選:內存 (tmpfs) @enduml -
configMap、secret等一類將資源文件掛載為卷:正如我們前面所提到,我們需要將某些配置文件或者私密文件作為pod容器啟動或者運行參數配置,而這些配置對于每個容器都是統一的,但是在生產的過程中有可能發生變更(例如nginx的config)。這時候我們就可以定義configMap,或者secret?[注](本質上,這兩者都是資源文件,當我們定義它們的時候,相關的配置文件會保存到k8s的etcd中),然后在pod運行的時候,將configMap或者secret文件掛載到容器中(一般來說,都是ready only的)。具體的使用,可以參考5.2 secret 和 ConfigMap 卷 · Kubernetes - 癡者工良?[注]。舉個例子:
# nginx-configmap.yaml 定義一個configMap資源 apiVersion: v1 kind: ConfigMap metadata: name: nginx-config data: nginx.conf: | events {} http { server { listen 80; location / { return 200 "Hello from ConfigMap!\n"; add_header Content-Type text/plain; } } } #----------另外一個pod定義文件--------------- # nginx-pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx-with-config spec: containers: - name: nginx image: nginx:alpine volumeMounts: # 表示這個掛載點引用的是下面 volumes 中定義的名為 config-volume 的卷 - name: config-volume # 表示要把卷掛載到容器內的 這個具體路徑 mountPath: /etc/nginx/nginx.conf # 只掛載卷中的 nginx.conf 這一個文件,而不是整個卷目錄。 subPath: nginx.conf volumes: # 定義一個名為 config-volume 的卷,供上面的 volumeMounts 引用。 - name: config-volume configMap: # 數據來源是一個叫 nginx-config 的 ConfigMap。 name: nginx-config -
通用臨時卷:Generic Ephemeral Volume(通用臨時卷)的作用基本上和emptyDir很類似,都是k8s為pod提供的臨時存儲方案,數據的生命周期與pod進行綁定。但是通用臨時卷相比于emptyDir,容量可控、性能更強。通用臨時卷依靠CSI驅動(Container Storage Interface,容器存儲接口, 將外部存儲系統翻譯為k8s存儲系統的插件),可以將外部存儲(云盤、本地SSD、網絡存儲)掛載為pod的臨時卷,并支持指定容量大小,以及高級的存儲特性(例如加密,快照)。?
emptyDir?? 是“輕量級臨時盤”,通用臨時卷是“帶容量和性能保障的臨時云盤”。簡單場景用emptyDir,高性能/大容量/需管控的場景用通用臨時卷。 如果用一個形象的例子來理解,就是emptyDir是個人電腦上的臨時文件夾,而通用臨時卷就是NAS上面的臨時文件夾(容量大,有快照,可配置容量限制……)。
3. 持久卷
3.1 PV & PVC
在生產中,我們當然不僅僅是使用臨時卷,還需要使用持久卷(Persistent Volume,PV),以實現數據的持久化。這樣及時pod被刪除、重建也能夠實現數據的保留以支持有狀態應用(StatefulSets,例如數據庫,redis,kafka、對象存儲),或者實現跨節點共享數據。
- PV 是集群中由管理員預先配置或由存儲類(StorageClass,本質上是一個 “存儲模板”,告訴k8s如何動態創建持久卷)動態創建的一塊存儲資源(如 NFS、iSCSI、云盤等)。它是集群級別的資源,生命周期獨立于使用它的 Pod。
- Persistent Volume Claim(PVC) ,PVC 是用戶對存儲資源的“申請”,類似于 Pod 對 CPU/內存的請求,定義了用戶希望使用的存儲大小、訪問模式(如只讀、讀寫、單節點或多節點訪問)。k8s會根據 PVC 的要求,自動綁定一個合適的PV。
# pv配置文件 pv.yaml
apiVersion: v1
# 資源類型:PersistentVolume(持久卷)
kind: PersistentVolume
metadata:
# PV 的名稱,在整個集群中必須唯一
name: my-pv
spec:
# 定義該 PV 的存儲容量
capacity:
# 請求的存儲大小,單位可以是 Gi(Gibibyte)、Mi 等
storage: 5Gi
# 訪問模式:定義該卷如何被掛載
# - ReadWriteOnce (RWO):只能被單個節點以讀寫方式掛載
# - ReadOnlyMany (ROX):可被多個節點以只讀方式掛載
# - ReadWriteMany (RWX):可被多個節點以讀寫方式掛載
accessModes:
- ReadWriteOnce
# 回收策略:當 PVC 被刪除后,PV 如何處理
# - Retain(保留):手動回收,數據不會被刪除(適合重要數據)
# - Delete(刪除):自動刪除底層存儲(如云盤),僅適用于動態供應
# - Recycle(已廢棄):舊版本的自動清理方式,不推薦使用
persistentVolumeReclaimPolicy: Retain
# hostPath 是一種 將宿主機(Node)上的文件或目錄掛載到 Pod 中 的方式。
hostPath:
# 宿主機上的實際路徑,PV 的數據將存儲在此目錄
path: /mnt/data
# -------pvc配置文件------ pvc.yaml
apiVersion: v1
# 資源類型:PersistentVolumeClaim(持久卷聲明)
kind: PersistentVolumeClaim
metadata:
# PVC 的名稱,在命名空間內唯一 Pod 將通過此名稱引用該 PVC
name: my-pvc
# namespace: my-namespace
# PVC 的規格
spec:
# 期望的訪問模式,必須與 PV 的 accessModes 兼容
accessModes:
- ReadWriteOnce
resources:
requests:
# k8s會尋找 capacity.storage >= 3Gi 且未被綁定的 PV
storage: 3Gi
# ----------pod.yaml 文件
apiVersion: v1
kind: Pod
metadata:
# Pod 的名稱
name: nginx-pod
# Pod 的規格
spec:
containers:
- name: nginx # 容器名稱
image: nginx:alpine # 使用的鏡像
volumeMounts: # 掛載卷到容器內
- name: web-content # 與下方 volumes.name 對應
mountPath: "/usr/share/nginx/html" # 容器內的掛載路徑
# 定義 Pod 使用的卷(volumes)
volumes:
- name: web-content # 卷名稱,需與 volumeMounts.name 一致
persistentVolumeClaim: # 表示該卷使用 PVC 提供的存儲
claimName: my-pvc # 引用前面創建的 PVC 名稱 必須在同一命名空間下
上面的代碼,是我們手動創建了一個pv,pvc,然后pod去使用pvc。在k8s中,創建pv有兩種方式,一種是上面的這種,用戶手動創建一個pv,指定pv相關的配置,然后讓pvc消費,這種稱之為靜態制備。還有一種,是pvc進行制備,比如說pvc指定了一個不存在pv,則就根據pvc里面StorageClass的配置,制作出一塊pv出來,稱之為動態制備。
| 方式 | 是否涉及StorageClass | |
|---|---|---|
| 靜態制備 | 不涉及 | 管理員手動提前創建好 PV(比如用hostPath?、NFS 等),PVC 去匹配它。PV 里沒有 ?storageClassName?? 字段。 |
| 動態制備 | 涉及 | PVC 指定 StorageClass → 系統自動創建 PV →這個 PV 會自動帶上 ?.spec.storageClassName?? 字段,值等于 PVC 請求的 StorageClass 名稱。 |
3.2 本地存儲
本地存儲分為hostPath和local PV:
-
hostPath:
hostPath卷能將Node工作節點文件系統上的文件或目錄掛載到你的 Pod 中。也就是說,如果pod部署在A節點上,就會使用A節點的某個目錄,如果pod被刪除重新部署到B節點上,那么就會使用B節點的某個目錄,之前的數據就丟失了(因為數據在A節點上)。因此多副本 Pod 無法共享數據(每個節點數據獨立),只適合單節點應用測試,不適宜生產環境。- 沒有 PVC,沒有 PV。
- Pod 被調度到哪個節點,就用哪個節點的
/mnt/data。
# pod.yaml apiVersion: v1 kind: Pod spec: containers: - name: app volumeMounts: - name: data mountPath: /data volumes: - name: data hostPath: path: /mnt/data # ← 直接指定節點路徑! -
local PV:在hostPath中,我們無法控制pod部署在哪個節點(即使我們通過
nodeSelector?來進行控制,如果未來pod需要重新部署在其他節點,那么我們所有的pod配置都需要修改,也就是說pod和node進行了一個強耦合。)而localPV就是為了解決這個問題,localPV只支持靜態制備。管理員預先在特定節點上準備磁盤或目錄,然后創建local PV,顯式的聲明該存儲位于哪個節點,然后pod使用pvc去掛載目錄。在這種情況下,pod并沒有與節點node形成一個強依賴,pod只是依賴于pvc。在下面的依賴配置中,pv-local指定為node-1節點,也就是說pv部署在node-1中。而pvc通過storageClassName: local-storage?,可以將pvc-local?與pv-local?進行綁定。而pod通過使用pvc-local就會將pod調度到node-1中。默認情況下,k8s在 PVC 創建后立即嘗試綁定一個 PV(稱為 Immediate Binding)。而
volumeBindingMode: WaitForFirstConsumer表示:“不要急著綁定 PV!等第一個使用這個 PVC 的 Pod 被調度時,再根據 Pod 的調度結果來綁定合適的 PV?!?/p>這是因為如果我們有兩個pv,pv-1綁定在node1中,pv-2綁定在node2中。如果創建pvc的時候,立即綁定pv(比如說隨機選到了pv-1,綁定到了node1),但是創建pod的時候,node1對應的cpu或者內存資源又不足,調度器想把pod調度到node2中,那么肯定會調度失敗,因為Pod 要去
node-2?,但存儲在node-1,因此會調度失敗。因此我們需要進行延遲綁定。# storageclass-local.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass meta name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer # pv-local.yaml apiVersion: v1 # 資源類型:PersistentVolume(持久卷) kind: PersistentVolume metadata: name: pv-local spec: capacity: # 請求的存儲大小,這只是聲明值,Kubernetes 不會驗證底層實際大小 storage: 100Gi # 卷的模式:指定存儲是作為文件系統還是原始塊設備使用 # - Filesystem(默認):掛載為目錄,Pod 通過文件讀寫(絕大多數場景) # - Block:作為原始塊設備暴露給容器(需容器內格式化,高級用法) volumeMode: Filesystem # 訪問模式:定義該卷如何被節點掛載 accessModes: - ReadWriteOnce # 回收策略:當綁定的 PVC 被刪除后,PV 如何處理 persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: # 節點上實際的目錄或掛載點路徑 path: /mnt/disks/ssd1 # 節點親和性:強制指定該 PV 只能被調度到特定節點 nodeAffinity: required: nodeSelectorTerms: - matchExpressions: # 匹配節點的標簽 - key: kubernetes.io/hostname # In 表示節點 hostname 必須在 values 列表中 operator: In # 允許使用該 PV 的節點主機名列表 # 通常只寫一個節點(因為本地存儲不共享) values: ["node-1"] # ← 明確綁定到 node-1 --- # pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-local spec: storageClassName: local-storage accessModes: [ReadWriteOnce] resources: requests: storage: 100Gi --- # pod.yaml spec: volumes: - name: data persistentVolumeClaim: claimName: pvc-local # ← 通過 PVC 間接使用
3.3 網絡存儲
前面我們介紹的local pv,hostPath,都存在一個問題,那就是pv是跟node節點進行了一個強綁定,多節點多pod沒法使用同一個pv。因此“網絡存儲”出來了,網絡存儲的數據不綁定在某一臺物理節點上,因此更適合多節點集群中的持久化需求,還能夠實現快照,副本等等高級存儲特性,聽起來是不是跟通用臨時卷很像。在 Kubernetes(k8s)中,持久卷(PersistentVolume, PV)的“網絡存儲” 是指通過網絡協議訪問的、可跨節點共享或掛載的存儲系統。
-
文件存儲(File Storage) ?:多個 Pod(跨節點)可同時讀寫同一份數據,適合共享配置、上傳目錄等場景。
存儲類型 說明 適應場景 NFS 經典網絡文件系統,開源、輕量、廣泛支持 中小規模集群,自建存儲 CephFS Ceph 提供的 POSIX 兼容文件系統 大規模分布式存儲,高可用 GlusterFS 開源分布式文件系統(Red Hat 支持) 已逐漸被 Ceph 取代 AWS EFS Amazon Elastic File System AWS 上的托管 RWX 文件存儲 Azure Files 微軟 Azure 的 SMB/NFS 文件服務 Azure 云環境 GCP Filestore Google Cloud 的托管 NFS 服務 GCP 云環境 例如,定義一個NFS PV:
apiVersion: v1 kind: PersistentVolume spec: capacity: storage: 100Gi accessModes: [ReadWriteMany] nfs: server: nfs.example.com path: "/shared/data" -
塊存儲(Block Storage): 將遠程塊設備(如云硬盤)掛載到單個節點,不能跨節點共享,但性能高。需要 Pod 自己格式化和管理文件系統
# ebs-sc.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ebs-sc provisioner: ebs.csi.aws.com volumeBindingMode: WaitForFirstConsumer
4. 4. 總結
卷(Volume)是 Kubernetes 中用于解決容器臨時性文件系統問題的機制,它允許:
- 容器重啟后數據不丟失(持久化)
- 同一 Pod 內多個容器共享數據
- 應用與存儲解耦,實現可移植
@startuml
' 設置樣式
skinparam defaultTextAlignment center
skinparam wrapWidth 200
skinparam shadowing false
skinparam component {
backgroundColor<<Pod>> LightBlue
borderColor<<Pod>> #336699
backgroundColor<<Ephemeral>> LightGreen
borderColor<<Ephemeral>> #2E8B57
backgroundColor<<PVC>> LightYellow
borderColor<<PVC>> #DAA520
backgroundColor<<Storage>> LightPink
borderColor<<Storage>> #FF6347
backgroundColor<<CSI>> LightGray
borderColor<<CSI>> #696969
backgroundColor<<Backend>> Wheat
borderColor<<Backend>> #8B4513
}
package "Kubernetes 集群" {
[Pod\n(應用容器)] as pod <<Pod>>
package "卷定義(Pod 內)" {
[emptyDir\n(臨時卷)] as emptyDir <<Ephemeral>>
[configMap\n(配置注入)] as configMap <<Ephemeral>>
[secret\n(密鑰注入)] as secret <<Ephemeral>>
[persistentVolumeClaim\n(持久卷聲明引用)] as pvcRef <<PVC>>
}
[持久卷聲明(PVC)\n? 存儲大小:10Gi\n? 訪問模式:RWO\n? 存儲類:fast-ssd] as pvc <<PVC>>
[存儲類(StorageClass)\n? 名稱:fast-ssd\n? 供應器:ebs.csi.aws.com\n? 參數:類型、加密等] as sc <<Storage>>
[持久卷(PV)\n? 容量:10Gi\n? 后端:AWS EBS / NFS / 本地磁盤\n? 節點親和性] as pv <<Storage>>
[CSI 存儲驅動\n? Controller 服務\n? Node 服務\n? Sidecar 容器:\n - external-provisioner\n - node-driver-registrar] as csi <<CSI>>
}
[底層存儲系統\n(AWS EBS / NFS / Ceph / 本地 SSD)] as storage <<Backend>>
' 連接關系
pod --> pvcRef : 掛載卷
pod --> emptyDir : 掛載卷
pod --> configMap : 掛載卷
pod --> secret : 掛載卷
pvcRef --> pvc : 引用
pvc --> sc : 使用存儲類
sc --> csi : 觸發動態供應
csi --> pv : 創建 PV 和底層存儲
pvc --> pv : 綁定關系
pv --> storage : 由...提供支持
' 布局優化(隱藏連線調整位置)
pod -[hidden]d-> emptyDir
emptyDir -[hidden]r-> configMap
configMap -[hidden]r-> secret
secret -[hidden]r-> pvcRef
pvcRef -[hidden]d-> pvc
pvc -[hidden]r-> sc
sc -[hidden]r-> csi
csi -[hidden]d-> pv
pv -[hidden]d-> storage
@enduml
StorageClass、PV、PVC關系如下:
PVC →(引用)→ StorageClass →(觸發創建)→ PV
| 資源 | 角色 | 創建者 | 生命周期 |
|---|---|---|---|
| StorageClass | 存儲模板 | 集群管理員 | 集群級,長期存在 |
| PV | 實際存儲資源 | 管理員(靜態)或系統(動態) | 集群級,獨立于 Pod |
| PVC | 存儲申請單 | 應用開發者 | 命名空間級,綁定 PV 后長期存在 |
前面我們在很多地方都定義了accessModes,以下是對Access Modes做的一個總結表格:
| 模式 | 含義 | 支持的存儲 | 關鍵說明 |
|---|---|---|---|
??ReadWriteOnce? |
卷可以被單個節點以讀寫模式掛載 | 絕大多數存儲(包括本地存儲、塊存儲如 AWS EBS、GCP PD) | ?這是最常用的模式。一個節點上可以運行多個 Pod 并同時訪問該卷。 |
??ReadOnlyMany? |
卷可以被多個節點以只讀模式掛載 | NFS、CephFS 等?文件存儲/共享存儲 | 常用于需要跨多個 Pod 分發只讀配置、數據或代碼的場景。 |
??ReadWriteMany? |
卷可以被多個節點以讀寫模式掛載 | ?主要限于文件存儲(如 NFS, CephFS, Azure Files) | 需要多個 Pod 同時寫入同一存儲的場景(如內容管理系統)。 |
??ReadWriteOncePod? |
卷可以被單個 Pod 以讀寫模式掛載 | ?僅支持 CSI 卷,且需要 Kubernetes v1.22+ | ?確保卷的獨占性。這是有狀態工作負載的理想選擇,可防止其他 Pod 誤掛載。 |
5. 腳注
作者: 渣渣輝啊
出處:http://www.rzrgm.cn/xiaohuiduan/p/19133679/kubernetes-storage-volume

浙公網安備 33010602011771號