文章

K8S持久化存储详解

现在很多应用都采用容器化技术进行部署,特别是无状态的应用程序。有时也会部署有状态应用或者想要保留应用数据,不至于在容器重建后,之前的数据都丢失了。对于初学者非常容易犯的一个错误:

网络上很多 quickstart 的云原生部署方案,就直接部署到企业环境中(甚至是生产环境),当程序出现异常退出、对应用进行变更调整或升级应用版本时,才发现之前的数据都丢失了。

Kubernetes 提供了可靠的存储来保存应用的持久化数据,这样容器在重建后,依然可以使用之前的数据。但是显然存储资源和 CPU 资源以及内存资源有很大不同,为了屏蔽底层的技术实现细节,让用户更加方便的使用,Kubernetes 便引入了 PVPVCStorageClass 等资源对象来实现对存储的管理。

Kubernetes Volume

Kubernetes 和 Docker 类似,也是通过 Volume 的方式提供对存储的支持。Volume 被定义在 Pod 上,可以被 Pod 里的多个容器挂载到相同或不同的路径下。Kubernetes 中 Volume 的 概念与Docker 中的 Volume 类似,但不完全相同。具体区别如下:

  • Kubernetes 中的 Volume 与 Pod 的生命周期相同,但与容器的生命周期不相关。当容器终止或重启时,Volume 中的数据也不会丢失。

  • 当 Pod 被删除时,Volume 才会被清理。并且数据是否丢失取决于 Volume 的具体类型,比如:emptyDir 类型的 Volume 数据会丢失,而 PV 类型的数据则不会丢失。

Kubernetes 支持很多类型的卷。 Pod 可以同时使用任意数目的卷类型。 临时卷类型的生命周期与 Pod 相同, 但持久卷可以比 Pod 的存活期长。 当 Pod 不再存在时,Kubernetes 也会销毁临时卷;不过 Kubernetes 不会销毁持久卷。 对于给定 Pod 中任何类型的卷,在容器重启期间数据都不会丢失。

卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据。 所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。

使用卷时, 在 .spec.volumes 字段中设置为 Pod 提供的卷,并在 .spec.containers[*].volumeMounts 字段中声明卷在容器中的挂载位置。

Kubernetes 目前支持多种 Volume 类型,最新数据可以官网查看:

https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/

注:这些 Volume 并非全部都是持久化的,比如: emptyDir 等,就会随着 Pod 的消亡而消失。

PV

持久卷(PersistentVolumes,PV) 是集群中的一块存储,可以由管理员事先制备, 或者使用存储类(Storage Class)来动态制备。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样, 也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。

PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件:

  • csi - 容器存储接口(CSI)

  • fc - Fibre Channel(FC)存储

  • hostPath - HostPath 卷 (仅供单节点测试使用;不适用于多节点集群;请尝试使用 local 卷作为替代)

  • iscsi - iSCSI(IP 上的 SCSI)存储

  • local - 节点上挂载的本地存储设备

  • nfs - 网络文件系统(NFS)存储

PV 属性详解

 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: pv0003
 spec:
   capacity:
     storage: 5Gi
   volumeMode: Filesystem
   accessModes:
     - ReadWriteOnce
   persistentVolumeReclaimPolicy: Recycle
   storageClassName: slow
   mountOptions:
     - hard
     - nfsvers=4.1
   nfs:
     path: /tmp
     server: 172.17.0.2
   nodeAffinity: 

容量(storage):

一般而言,每个 PV 卷都有确定的存储容量。 这是通过 PV 的 capacity.storage 属性设置的。

卷模式(volumeMode):

针对 PV 持久卷,Kubernetes 支持两种卷模式(volumeModes):Filesystem(文件系统)Block(块)volumeMode 是一个可选的 API 参数。 如果该参数被省略,默认的卷模式是 Filesystem

volumeMode 属性设置为 Filesystem 的卷会被 Pod 挂载(Mount) 到某个目录。 如果卷的存储来自某块设备而该设备目前为空,Kuberneretes 会在第一次挂载卷之前在设备上创建文件系统。

你可以将 volumeMode 设置为 Block,以便将卷作为原始块设备来使用。 这类卷以块设备的方式交给 Pod 使用,其上没有任何文件系统。 这种模式对于为 Pod 提供一种使用最快可能方式来访问卷而言很有帮助, Pod 和卷之间不存在文件系统层。另外,Pod 中运行的应用必须知道如何处理原始块设备

访问模式(accessModes):

访问模式有:

  • ReadWriteOnce

    卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式仍然可以在同一节点上运行的多个 Pod 访问该卷。 对于单个 Pod 的访问,请参考 ReadWriteOncePod 访问模式。

  • ReadOnlyMany

    卷可以被多个节点以只读方式挂载。

  • ReadWriteMany

    卷可以被多个节点以读写方式挂载。

  • ReadWriteOncePod 卷可以被单个 Pod 以读写方式挂载。 如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用 ReadWriteOncePod 访问模式。

重要提醒! 每个卷同一时刻只能以一种访问模式挂载,即使该卷能够支持多种访问模式。

卷插件

ReadWriteOnce

ReadOnlyMany

ReadWriteMany

ReadWriteOncePod

AzureFile

-

CephFS

-

CSI

取决于驱动

取决于驱动

取决于驱动

取决于驱动

FC

-

-

FlexVolume

取决于驱动

-

GCEPersistentDisk

-

-

Glusterfs

-

HostPath

-

-

-

iSCSI

-

-

NFS

-

RBD

-

-

VsphereVolume

-

-(Pod 运行于同一节点上时可行)

-

PortworxVolume

-

-

类(storageClassName):

每个 PV 可以属于某个类(Class),通过将其 storageClassName 属性设置为某个 StorageClass 的名称来指定。 特定类的 PV 卷只能绑定到请求该类存储卷的 PVC 申领。 未设置 storageClassName 的 PV 卷没有类设定,只能绑定到那些没有指定特定存储类的 PVC 申领。

回收策略(persistentVolumeReclaimPolicy):

目前的回收策略有:

  • Retain -- 手动回收

  • Recycle -- 简单擦除(rm -rf /thevolume/*

  • Delete -- 删除存储卷

挂载选项(mountOptions):

ubernetes 管理员可以指定持久卷被挂载到节点上时使用的附加挂载选项。

说明:并非所有持久卷类型都支持挂载选项。

节点亲和性(nodeAffinity):

每个 PV 卷可以通过设置节点亲和性来定义一些约束,进而限制从哪些节点上可以访问此卷。 使用这些卷的 Pod 只会被调度到节点亲和性规则所选择的节点上执行。 要设置节点亲和性,配置 PV 卷 .spec 中的 nodeAffinity

PV 阶段状态

每个持久卷会处于以下阶段(Phase)之一:

  • Available

    卷是一个空闲资源,尚未绑定到任何申领

  • Bound

    该卷已经绑定到某申领

  • Released

    所绑定的申领已被删除,但是关联存储资源尚未被集群回收

  • Failed

    卷的自动回收操作失败

PVC

PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户对存储资源的一种请求。PVCPod 比较类似,Pod 消耗的是节点资源,PVC 消耗的是 PV 资源。Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。

但是通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求,而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。关于 StorageClass 接下来会讲解。

PVC 属性详解

 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: myclaim
 spec:
   accessModes:
     - ReadWriteOnce
   volumeMode: Filesystem
   resources:
     requests:
       storage: 8Gi
   storageClassName: slow
   selector:
     matchLabels:
       release: "stable"
     matchExpressions:
       - {key: environment, operator: In, values: [dev]}

访问模式(accessModes): 使用与PV相同的约定

卷模式(volumeMode):使用与PV相同的约定,将卷作为文件系统还是块设备来使用。

资源(resources):Pod 一样,也可以请求特定数量的资源。在这个上下文中,请求的资源是存储。

选择算符(selector):

申领可以设置标签选择算符 来进一步过滤卷集合。只有标签与选择算符相匹配的卷能够绑定到申领上。 选择算符包含两个字段:

  • matchLabels - 卷必须包含带有此值的标签

  • matchExpressions - 通过设定键(key)、值列表和操作符(operator) 来构造的需求。合法的操作符有 In、NotIn、Exists 和 DoesNotExist。

来自 matchLabelsmatchExpressions 的所有需求都按逻辑与的方式组合在一起。 这些需求都必须被满足才被视为匹配。

类(storageClassName):

PVC可以通过为 storageClassName 属性设置 StorageClass 的名称来请求特定的存储类。 只有所请求的类的 PV 卷,即 storageClassName 值与 PVC 设置相同的 PV 卷, 才能绑定到 PVC 申领。

如果 PVC 的 storageClassName 属性值设置为 "", 则被视为要请求的是没有设置存储类的 PV 卷,因此这一 PVC 申领只能绑定到未设置存储类的 PV 卷(未设置注解或者注解值为 "" 的 PersistentVolume(PV)对象在系统中不会被删除, 因为这样做可能会引起数据丢失)。未设置 storageClassName 的 PVC 具体筛查方式取决于 DefaultStorageClass 准入控制器插件 是否被启用。

  • 如果准入控制器插件被启用,则管理员可以设置一个默认的 StorageClass。 所有未设置 storageClassName 的 PVC 都只能绑定到隶属于默认存储类的 PV 卷。 设置默认 StorageClass 的工作是通过将对应 StorageClass 对象的注解 storageclass.kubernetes.io/is-default-class 赋值为 true 来完成的。

  • 如果准入控制器插件被关闭,则不存在默认 StorageClass 的说法。 所有将 storageClassName 设为 "" 的 PVC 只能被绑定到也将 storageClassName 设为 "" 的 PV。

注意,如果PV的容量比PVC的大,是可以绑定的,并且使用的是PV申请的容量,反之不可以。

PV和PVC的状态映射关系

pv_and_pvc_status

使用注意事项

  1. 如果数据非常重要,不能删除,那么你在删除PVC的时候,需要注意不能自动回收了PV里面的数据。

  2. PVC中的访问模式设置为ReadWriteOnce时,如果有POD关联,则创建 Pod 挂载此 PVC 时会出现失败。

  3. 如果PV的persistentVolumeReclaimPolicy是Retain,那么在PVC删除后,PV的状态为Released。那么可以通过编辑该PV的yaml,将spec.claimRef字段删除,PV的状态就会变成Available,可以被新的PVC重新绑定了。

StorageClass

PV 是静态存储,要使用一个 PVC 的话就必须手动去创建一个 PV,这种方式在很多场景上并不能满足我们的需求,比如我们有一个应用需要对存储的并发度要求比较高,而另外一个应用对读写速度又要求比较高,特别是对于 StatefulSet 类型的应用简单的来使用静态的 PV 就很不合适了,这种情况下我们就需要用到动态 PV,这时可以使用 StorageClass 来满足我们的诉求。

每个 StorageClass 都包含 provisionerparametersreclaimPolicy 字段, 这些字段会在 StorageClass 需要动态制备 PersistentVolume 时会使用到。

StorageClass 对象的命名很重要,用户使用这个命名来请求生成一个特定的类。 当创建 StorageClass 对象时,管理员设置 StorageClass 对象的命名和其他参数。

StorageClass 属性详解

 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: standard
 provisioner: kubernetes.io/aws-ebs
 parameters:
   type: gp2
 reclaimPolicy: Retain
 allowVolumeExpansion: true
 mountOptions:
   - debug
 volumeBindingMode: Immediate

存储制备器(provisioner):

每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。

回收策略(reclaimPolicy):

由 StorageClass 动态创建的 PersistentVolume 会在类的 reclaimPolicy 字段中指定回收策略,可以是 Delete 或者 Retain。 如果 StorageClass 对象被创建时没有指定 reclaimPolicy,它将默认为 Delete

通过 StorageClass 手动创建并管理的 PersistentVolume 会使用它们被创建时指定的回收策略。

允许卷扩展(allowVolumeExpansion):

将此功能设置为 true 时,允许用户通过编辑相应的 PVC 对象来调整卷大小。

说明:此功能仅可用于扩容卷,不能用于缩小卷。而且需要云厂商提供的卷支持卷扩展。

挂载选项(mountOptions):

由 StorageClass 动态创建的 PersistentVolume 将使用类中 mountOptions 字段指定的挂载选项。

卷绑定模式(volumeBindingMode):

默认情况下,Immediate 模式表示一旦创建了 PersistentVolumeClaim 也就完成了卷绑定和动态分配。 对于由于拓扑限制而非集群所有节点可达的存储后端,PersistentVolume 会在不知道 Pod 调度要求的情况下绑定或者分配。

管理员可以通过指定 WaitForFirstConsumer 模式来解决此问题。 该模式将延迟 PersistentVolume 的绑定和分配,直到使用该 PersistentVolumeClaim 的 Pod 被创建。

参数(parameters):

Storage class 具有描述属于卷的参数。取决于分配器,可以接受不同的参数。

 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: example-nfs
 provisioner: example.com/external-nfs
 parameters:
   server: nfs-server.example.com
   path: /share
   readOnly: "false"
  • server:NFS 服务器的主机名或 IP 地址。

  • path:NFS 服务器导出的路径。

  • readOnly:是否将存储挂载为只读的标志(默认为 false)。

本地 StorageClass

在 Kubernetes 中,Local StorageClass 通常不支持自动创建 Persistent Volume (PV)。这是因为本地存储(Local Storage)的特点和使用方式与网络存储(如 NFS、Ceph 等)不同。网络存储可以动态地提供存储资源,而本地存储依赖于节点本地的磁盘,这使得自动化创建 PV 更为复杂。

然而,你可以通过以下步骤来手动设置本地存储并创建 PV:

  1. 创建 StorageClass:定义一个 Local StorageClass。

  2. 创建 Persistent Volume:手动创建 PV,并指定本地存储路径和相关参数。

  3. 创建 Persistent Volume Claim:应用程序通过 PVC 请求存储,Kubernetes 会根据 StorageClass 和 PVC 的请求绑定合适的 PV。

下面是一个示例,展示了如何配置和使用本地存储:

Step 1: 创建 StorageClass

 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: local-storage
 provisioner: kubernetes.io/no-provisioner
 volumeBindingMode: WaitForFirstConsumer

Step 2: 创建 Persistent Volume

 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: local-pv # changeme
 spec:
   capacity:
     storage: 10Gi
   volumeMode: Filesystem
   accessModes:
     - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: local-storage
   local:
     path: /Users/eric/Documents/data  # changeme
   nodeAffinity:
     required:
       nodeSelectorTerms:
         - matchExpressions:
             - key: kubernetes.io/hostname
               operator: In
               values:
                 - lima-rancher-desktop # changeme

Step 3: 创建 Persistent Volume Claim

 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: local-pvc
 spec:
   accessModes:
     - ReadWriteOnce
   resources:
     requests:
       storage: 100Gi
   storageClassName: local-storage

使用 Local Path Provisioner

本地自动化 PV 创建的替代方案,可以使用一些外部工具和脚本。例如:

Local Path Provisioner:Rancher 提供的 Local Path Provisioner 是一个简单的工具,可以动态地为本地存储创建 PV。

https://github.com/rancher/local-path-provisioner

安装 Local Path Provisioner

 kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

会自动安装 StorageClass

 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: local-path
 provisioner: rancher.io/local-path
 volumeBindingMode: WaitForFirstConsumer
 reclaimPolicy: Delete

如何使用

新建pvc:

 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: local-path-pvc
 spec:
   accessModes:
     - ReadWriteOnce
   resources:
     requests:
       storage: 100Gi
   storageClassName: local-path

或者 直接在Statefulset 中声明:

 volumeClaimTemplates:
   - metadata:
       name: data
     spec:
       storageClassName: "local-path"
       accessModes: [ "ReadWriteOnce" ]
       resources:
         requests:
           storage: 10Gi

修改存储地址:

 ## 若 local path 部署在kube-system namespace
 kubectl get configmap -n kube-system local-path-config -o yaml
 ​
 ## config.json 默认配置
 {
   "nodePathMap":[
     {
       "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
       "paths":["/var/lib/rancher/k3s/storage"]
     }
   ]
 }
 ​

将 config.json 修改为:

 {
   "nodePathMap":[
     {
       "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
       "paths":["/Users/eric/Documents/data"]
     }
   ]
 }

更新configmap命令:

 kubectl edit configmap -n kube-system local-path-config

License:  CC BY 4.0