在前面几个章节,介绍了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。”
这是因为如果我们有两个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经典网络文件系统,开源、轻量、广泛支持中小规模集群,自建存储CephFSCeph 提供的 POSIX 兼容文件系统大规模分布式存储,高可用GlusterFS开源分布式文件系统(Red Hat 支持)已逐渐被 Ceph 取代AWS EFSAmazon Elastic File SystemAWS 上的托管 RWX 文件存储Azure Files微软 Azure 的 SMB/NFS 文件服务Azure 云环境GCP FilestoreGoogle 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实际存储资源管理员(静态)或系统(动态)集群级,独立于 PodPVC存储申请单应用开发者命名空间级,绑定 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. 脚注
[注]
卷 | Kubernetes
[注]
5.2 secret 和 ConfigMap 卷 · Kubernetes - 痴者工良
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |