计海龄 发表于 4 天前

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
@endmindmap2. 临时卷

  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>> {
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:
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:
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 集群" {
as pod <<Pod>>

package "卷定义(Pod 内)" {
    as emptyDir <<Ephemeral>>
    as configMap <<Ephemeral>>
    as secret <<Ephemeral>>
    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>>

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 -d-> emptyDir
emptyDir -r-> configMap
configMap -r-> secret
secret -r-> pvcRef

pvcRef -d-> pvc
pvc -r-> sc
sc -r-> csi
csi -d-> pv
pv -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 - 痴者工良

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

寇油 发表于 3 天前

感谢发布原创作品,程序园因你更精彩
页: [1]
查看完整版本: Kubernetes存储卷:保障有状态应用的数据持久化