Pod被销毁时,其内部容器的数据也无法持久存在。与Docker类似,K8S也支持配置容器使用存储卷将数据持久存储于容器自身文件系统之外的存储空间中,它们可以是节点文件系统或网络文件系统之上的存储空间。但K8S的存储卷是与Pod资源绑定而非容器,存储卷定义在Pod资源之上、可被其内部的所有容器挂载的共享目录,而数据是否具有持久能力则取决于存储卷自身是否支持持久机制。

Kubernetes支持多种存储卷类型,包括本地存储(节点)和网络存储系统中的诸多存储机制,甚至还支持Secret和ConfigMap这样的特殊存储资源。

存储卷的使用方式

在Pod中定义使用存储卷之前,首先要通过spec.volumes字段定义在Pod之上的存储卷列表,其支持使用多种不同类型的存储卷且配置参数差别很大;然后通过spec.containers.volumeMounts字段在容器上定义存储卷挂载列表,它只能挂载当前Pod资源中定义的具体存储卷。 spec.volumes举例:

spec:
  volumes:
  - name: logdata
    emptyDir: {}
  - name: example
    gitRepo:
      repository: ...
      revision: master
      directory: .

上面的资源清单片段定义了由两个存储卷组成的卷列表,一个是emptyDir类型,一个是gitRepo类型,定义好的存储卷可由当前Pod资源内的各容器进行挂载。当Pod中存在多个容器且它们挂载同一个存储卷时,卷可以用于容器间数据共享,当Pod中只有一个容器时,使用存储卷的目的通常在于数据持久化。 spec.containers.volumeMounts举例:

spec
  containers:
  - name: myapp
    volumeMounts:
    - name:
      mountPath:
      readOnly:
      subPath:

其中,name、mountPath为必填字段。

临时存储卷emptyDir

emptyDir存储卷的生命周期与其所属的Pod对象相同,它无法脱离Pod对象的生命周期提供数据存储功能,因此emptyDir通常仅用于一些特殊场景中,比如同一Pod内的多个容器间文件的共享,或者作为容器数据的临时存储目录用于数据缓存系统等。 emptyDir存储卷则定义于spec.volumes.emptyDir嵌套字段中,可用字段主要包含:

  • medium:指定此目录所在的存储介质的类型,可取值为“default”或“Memory”,默认为default,表示使用节点的默认存储介质;“Memory”表示使用基于RAM的临时文件系统tmpfs,空间受限于内存,但性能非常好,通常用于为容器中的应用提供缓存空间。‰
  • sizeLimit:当前存储卷的空间限额,默认值为nil,表示不限制;不过,在medium字段值为“Memory”时建议务必定义此限额。 下面是emptyDir存储卷的简单示例:
apiVersion: v1
kind: Pod
metadata:
  name: vol-emptydir-pod
spec:
  volumes:
  - name: html
    emptyDir: {}
  containers:
  - name: nginx
    image: nginx:1.12-alpine
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
  - name: pagegen
    image: alpine
    volumeMounts:
    - name: html
      mountPath: /html
    command: ["bin/sh", "-c"]
    args: ["-c", "while true; do echo $(hostname) $(date) >> /html/index.html;sleep 10; done"]

作为边车的容器paggen,其每隔10秒生成一行信息追加到存储卷上的index.html文件中,然后通过主容器nginx的应用访问到的内容会不断变化。

apply上述配置清单后,启动一个运行CirrOS容器的Pod来测试:

kubectl run cirros-$RANDOM --rm -it --image=cirros -- sh

测试curl Pod IP

/# curl 10.1.1.109
vol-emptydir-pod Mon May 31 14:07:14 UTC 2021
vol-emptydir-pod Mon May 31 14:07:24 UTC 2021
vol-emptydir-pod Mon May 31 14:07:34 UTC 2021

节点存储卷hostPath

hostPath类型的存储卷是将工作节点上某文件系统的目录或文件挂载于Pod中的一种存储卷,它可独立于Pod资源的生命周期,具有持久性;但一般仅受控于daemonset,因为一旦Pod资源被重新调度至其他节点,别的节点并不一定保证存在挂载所指定的文件或目录,因此,这种类型的存储卷只适用于Daemonset下的特定场景,比如运行有管理任务的系统级Pod资源需要访问节点上的文件时。 定义方式如:

spec:
  volumes:
  - name: html
    hostPath:
      path: /var/log
hostPath嵌套字段共有两个,path用于指定工作节点上的目录路径,属于必选字段,另外还是可选的type,用于指定存储卷类型,包含以下几种:
DirectoryOrCreate:指定的路径不存时自动将其创建为权限是0755的空目录,属主和属组同是kubelet。
Directory:必须存在的目录路径。‰
FileOrCreate:指定的路径不存时自动将其创建为权限是0644的空文件,属主和属组同是kubelet。
File:必须存在的文件路径。‰
Socket:必须存在的Socket文件路径。‰
CharDevice:必须存在的字符设备文件路径。‰
BlockDevice:必须存在的块设备文件路径。

网络存储卷

Kubernetes拥有众多类型的用于适配专用存储系统的网络存储卷。这类存储卷包括传统的NAS或SAN设备(如NFS、iSCSI、fc)、分布式存储(如GlusterFS、RBD)、云端存储(如gcePersistentDisk、azureDisk、cinder和awsElasticBlockStore)以及建构在各类存储系统之上的抽象管理层(如flocker、portworxVolume和vsphereVolume)等。

NFS存储卷

NFS即网络文件系统(Network File System),它是一种分布式文件系统协议,允许客户端主机可以像访问本地存储一样通过网络访问服务器端文件。 Kubernetes的NFS存储卷用于将某事先存在的NFS服务器上导出的存储空间挂载到Pod中以供容器使用。NFS存储卷在Pod对象终止后仅是被卸载而非删除,此外NFS是文件系统级共享服务,它支持同时存在的多路挂载请求。 下面示例定义了用于redis持久化的nfs:

spec:
  volumes:
  - name: redisdata
    nfs:
      server: nfs.ilinux.io
      path: /var/log
      readOnly: false

前提要存在名为nfs.ilinux.io的NFS服务器,其输出了/data/redis目录,并授权给了Kubernetes集群中的节点访问。

RBD存储卷

要使用RBD存储卷需要先准备Ceph RDB存储集群,Ceph是一个专注于分布式的、弹性可扩展的、高可靠的、性能优异的存储系统平台,同时支持提供块设备、文件系统和REST三种存储接口。 使用示例:

spec:
  volumes:
  - name: redis-rdb-vol
    rdb:
      monitors:
      - '172.16.0.45:6379'
      - '172.16.0.46:6379'
      pool: kube
      image: redis
      fsType: ext4
      readOnly: false
      user: admin
      secretRef:
        name: ceph-secret

配置RBD类型的存储卷时,需要指定的字段有:

monitors <[]string>:Ceph存储监视器,逗号分隔的字符串列表;必选字段。‰
image <string>:rados image的名称,必选字段。‰
pool <string>:rados存储池名称,默认为RBD。‰
user <string>:rados用户名,默认为admin。‰
keyring <string>:RBD用户认证时使用的keyring文件路径,默认为/etc/ceph/keyring。‰
secretRef <Object>:RBD用户认证时使用的保存有相应认证信息的Secret对象,会覆盖由keyring字段提供的密钥信息。
readOnly <boolean>:是否以只读的方式进行访问。

GlusterFS存储卷

GlusterFS(Gluster File System)是一个开源的分布式文件系统,是水平扩展存储解决方案Gluster的核心,具有强大的横向扩展能力,GlusterFS通过扩展能够支持数PB存储容量和处理数千客户端。GlusterFS借助TCP/IP或InfiniBand RDMA网络将物理分布的存储资源聚集在一起,使用单一全局命名空间来管理数据。另外,GlusterFS基于可堆叠的用户空间设计,可为各种不同的数据负载提供优异的性能,是另一种流行的分布式存储解决方案。 使用示例:

spec:
  volumes:
  - name: redis-glusterfs-vol
    glusterfs:
      endpoints: glusterfs-endpoints
      path: kube-redis
      readOnly: false

常用的字段有:

endpoints <string>:Endpoints资源的名称,此资源需要事先存在,必选字段。
path <string>:用到的GlusterFS集群的卷路径,必选字段。
readOnly <boolean>:是否为只读卷。

其中的Endpoints资源需要提前创建,glusterfs-endpoints资源定义了GlusterFS集群的节点信息:

apiVersion: v1
kind: Endpoints
metadata:
  name: glusterfs-endpoints
spec:
  - addresses:
    - ip: gfs01.test.io
    ports:
    - port: 24007
      name: glustered

Cinder存储卷

Cinder是OpenStack Block Storage的项目名称,用来为虚拟机(VM)实例提供持久块存储。将Kubernetes集群部署于OpenStack构建的IaaS环境中时,Cinder的块存储功能可为Pod资源提供外部持久存储的有效方式。 使用示例:

spec:
  volumes:
  - name: redis-cinder-vol
    cinder:
      volumeID: ***
      fsType: ext4

可定义的的字段有:

volumeID <string>:用于标识Cinder中的存储卷的卷标识符,必选字段。‰
readOnly <boolean>:是否以只读方式访问。‰
fsType:要挂载的存储卷的文件系统类型,至少应该是节点操作系统支持的文件系统,如ext4、xfs、ntfs等,默认为“ext4”。

PersistentVolumeClaim

从前面对持久存储卷的使用可知,Kubernetes用户必须要清晰了解所用到的网络存储系统的访问细节才能完成存储卷相关的配置任务,为了能进一步地隐藏底层架构的细节,使得对存储资源的使用最好也能像使用计算资源一样,用户和开发人员无须了解Pod资源究竟运行于哪个节点,也无须了解存储系统是什么设备以及位于何处,Kubernetes的PersistentVolume子系统在用户与管理员之间添加了一个抽象层,从而使得存储系统的使用和管理职能互相解耦。 PersistentVolume(PV)是指由集群管理员配置提供的某存储系统上的一段存储空间,它是对底层共享存储的抽象。通过存储插件机制,PV支持使用存储系统,例如NFS、RBD和Cinder等。 PV是集群级别的资源,不属于任何名称空间,用户对PV资源的使用需要通过PersistentVolumeClaim(PVC)来完成绑定,PVC是PV资源的消费者,它向PV申请特定大小的空间及访问模式(如rw或ro),从而创建出PVC存储卷,Pod关联使用的是PVC存储卷。

创建PV

创建PV时需要定义PV的容量、访问模式和回收策略等:

  • Capacity,PV的容量
  • 访问模式,PV依赖的存储设备支持及启用的功能特性不尽相同,所以PV的模式要与存储设备支持的功能保持一致,比如
    • ReadWriteOnce:仅可被单个节点读写挂载;
    • ReadOnlyMany:可被多个节点同时只读挂载;
    • ReadWriteMany:可被多个节点同时读写挂载;
  • persistentVolumeReclaimPolicy,PV空间被释放时的处理机制
    • Retain:保持不动,由管理员随后手动回收,这是默认选项
    • Recycle:删除存储卷目录下的所有文件(包括子目录和隐藏文件),目前仅NFS和hostPath支持此操作。
    • Delete:删除存储卷,仅部分云端存储系统支持,如AWS EBS、GCE PD、Azure Disk和Cinder。
  • volumeMode:卷模型,用于指定此卷可被用作文件系统还是裸格式的块设备;默认为Filesystem。
  • storageClassName:当前PV所属的StorageClass的名称;默认为空值,即不属于任何StorageClass。
  • mountOptions:挂载选项组成的列表,如ro、soft和hard等。

下面的配置清单定义了一个使用NFS存储后端的PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-1
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: "/webdata"
    server: nfs.ilinux.io

通过kubectl get pv可以看到,虽然指定的nfs server不可用,也可以apply成功。

创建PVC

创建PersistentVolumeClaim(PVC),即申请占用某个PersistentVolume,它与PV是一对一的关系。申请时,需要定义的字段有:

  • accessMode:当前PVC的访问模式,其可用模式与PV相同。‰
  • resources:当前PVC存储卷需要占用的资源量最小值;目前,PVC的资源限定仅指其空间大小。
  • selector:绑定时对PV应用的标签选择器(matchLabels)或匹配条件表达式(matchEx-pressions),用于挑选要绑定的PV;如果同时指定了两种挑选机制,则必须同时满足。
  • storageClassName:所依赖的存储类的名称。‰
  • volumeMode:卷模型,用于指定此卷可被用作文件系统还是裸格式的块设备;默认为“Filesystem”。
  • volumeName:用于直接指定要绑定的PV的卷名。

下面的PVC申请使用了前面创建的pv-nfs-1:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-nfs-1
spec:
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: slow
  volumeName: pv-nfs-1

在Pod中使用PVC

在Pod资源中调用PVC资源,只需要在定义volumes时使用persistentVolumeClaims字段嵌套指定两个字段即可:

  • claimName:要调用的PVC存储卷的名称,PVC卷要与Pod在同一名称空间中。‰
  • readOnly:是否将存储卷强制挂载为只读模式,默认为false。

downwardAPI存储卷

downwardAPI用于提供所在环境信息的元数据,要使用downwardAPI可以通过环境变量和downwardAPI存储卷两种方式。

环境变量式元数据注入

只有常量类的属性才能够通过环境变量注入到容器中,因为在进程启动完成后将无法再向其告知变量值的变动,环境变量也不支持中途的更新操作。 可以在valueFrom字段中嵌套fieldRef或resourceFieldRef字段来引用相应的数据源,通过fieldRef字段可引用的信息有:

spec.nodeName‰
status.hostIP
metadata.name
metadata.namespace
status.podIP
spec.serviceAccountName:Pod对象使用的ServiceAccount资源的名称
metadata.uid
metadata.labels['<KEY>']:Pod对象标签中的指定键的值, 只有1.9及之后的版本支持
metadata.annotations['<KEY>']:Pod对象注解信息中的指定键的值,仅1.9及之后的版本支持

通过resourceFieldRef字段可引用的信息是指当前容器的资源请求及资源限额的定义,包括requests.cpu、limits.cpu、requests.memory和limits.memory四项。 下面的示例中为容器注入了元数据信息,应用会打印出所有的环境变量:

apiVersion: v1
kind: Pod
metadata:
  name: env-test-pod
  labels:
    app: label-env-test-pod
spec:
  containers:
  - name: env-test-container
    image: busybox
    command: ["/bin/sh", "-c", "env"]
    env:
    - name: MY_POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: MY_APP_LABEL
      valueFrom:
        fieldRef:
          fieldPath: metadata.labels['app']

apply后运行logs命令就可以看到成功注入的元数据信息:

✗ kubectl logs env-test-pod | grep "^MY_"
MY_APP_LABEL=label-env-test-pod
MY_POD_NAME=env-test-pod

另外,在定义资源请求或资源限制时还可额外指定“divisor”字段,用于为引用的值指定一个除数以实现所引用的相关值的单位换算。

存储卷式元数据注入

向容器注入元数据信息的另一种方式是使用downwardAPI存储卷,它将配置的字段数据映射为文件并可通过容器中的挂载点进行访问。 使用这种方式非常方便的一点是可通过metadata.labels或metadata.annotations获取Pod对象的所有标签信息或注解信息,每行一个。

apiVersion: v1
kind: Pod
metadata:
  name: vol-test-pod
  labels:
    app: label-vol-test-pod
    zone: us-east-2
spec:
  containers:
  - name: vol-test-container
    image: busybox
    command: ["/bin/sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: podinfo
      mountPath: /etc/podinfo
      readOnly: false
  volumes:
  - name: podinfo
    downwardAPI:
      defaultMode: 420
      items:
      - fieldRef:
          fieldPath: metadata.name
        path: pod_name
      - fieldRef:
          fieldPath: metadata.labels
        path: pod_labels

apply后,可以在pod_labels查看所有的label

✗ kubectl exec vol-test-pod -- cat /etc/podinfo/pod_labels
app="label-vol-test-pod"
zone="us-east-2"%

而且对label的修改可以实时反映到volume中,比如执行kubectl label pods vol-test-pod env="test"后,再查看pod_labels文件,已经包含了新增的label。

学习资料

《Kubernetes实战进阶》 马永亮著

06-14 20:46