目录

K8S的PV和PVC介绍

介绍K8S的PV和PVC概念和设计原理。

k8s的PV和PVC概念

Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用 程序带来一些问题。

  1. 问题之一是当容器崩溃时文件丢失。kubelet 会重新启动容器, 但容器会以干净的状态重启。
  2. 第二个问题会在同一 Pod 中运行多个容器并共享文件时出现。

Kubernetes 卷(Volume) 这一抽象概念能够解决这两个问题。

Kubernetes 支持很多类型的卷。 Pod 可以同时使用任意数目的卷类型。 临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长。 因此,卷的存在时间会超出 Pod 中运行的所有容器,并且在容器重新启动时数据也会得到保留。 当 Pod 不再存在时,卷也将不再存在。

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

configMap

configMap 卷 提供了向 Pod 注入配置数据的方法。 ConfigMap 对象中存储的数据可以被 configMap 类型的卷引用,然后被 Pod 中运行的 容器化应用使用。

引用 configMap 对象时,你可以在 volume 中通过它的名称来引用。 你可以自定义 ConfigMap 中特定条目所要使用的路径。 下面的配置显示了如何将名为 log-config 的 ConfigMap 挂载到名为 configmap-pod 的 Pod 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
  name: configmap-pod
spec:
  containers:
    - name: test
      image: busybox
      volumeMounts:
        - name: config-vol
          mountPath: /etc/config
  volumes:
    - name: config-vol
      configMap:
        name: log-config
        items:
          - key: log_level
            path: log_level

log-config ConfigMap 以卷的形式挂载,并且存储在 log_level 条目中的所有内容 都被挂载到 Pod 的 /etc/config/log_level 路径下。 请注意,这个路径来源于卷的 mountPath 和 log_level 键对应的 path。

说明:

  • 在使用 ConfigMap 之前你首先要创建它。
  • 容器以 subPath 卷挂载方式使用 ConfigMap 时,将无法接收 ConfigMap 的更新。
  • 文本数据挂载成文件时采用 UTF-8 字符编码。如果使用其他字符编码形式,可使用 binaryData 字段。

emptyDir

当 Pod 分派到某个 Node 上时,emptyDir 卷会被创建,并且在 Pod 在该节点上运行期间,卷一直存在。 就像其名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,这些容器都可以读写 emptyDir 卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。

说明: 容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。 emptyDir 的一些用途:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

hostPath

hostPath 卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。 虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的逃生舱。

例如,hostPath 的一些用法有:

运行一个需要访问 Docker 内部机制的容器;可使用 hostPath 挂载 /var/lib/docker 路径。 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys。 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。 除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type。

Secret

secret 卷用来给 Pod 传递敏感信息,例如密码。你可以将 Secret 存储在 Kubernetes API 服务器上,然后以文件的形式挂在到 Pod 中,无需直接与 Kubernetes 耦合。 secret 卷由 tmpfs(基于 RAM 的文件系统)提供存储,因此它们永远不会被写入非易失性 (持久化的)存储器。

说明: 使用前你必须在 Kubernetes API 中创建 secret。 说明: 容器以 subPath 卷挂载方式挂载 Secret 时,将感知不到 Secret 的更新。

secret说明文档:https://kubernetes.io/zh/docs/concepts/configuration/secret/

nfs

nfs 卷能将 NFS (网络文件系统) 挂载到你的 Pod 中。 不像 emptyDir 那样会在删除 Pod 的同时也会被删除,nfs 卷的内容在删除 Pod 时会被保存,卷只是被卸载。 这意味着 nfs 卷可以被预先填充数据,并且这些数据可以在 Pod 之间共享。

注意: 在使用 NFS 卷之前,你必须运行自己的 NFS 服务,并将目标 share 导出备用。

nfs示例

persistentVolumeClaim persistentVolumeClaim 卷用来将持久卷(PersistentVolume) 挂载到 Pod 中。 持久卷声明(PersistentVolumeClaim)是用户在不知道特定云环境细节的情况下"声明"持久存储 (例如 GCE PersistentDisk 或者 iSCSI 卷)的一种方法。

更多详情请参考持久卷示例

持久卷

persistent-volumes概念

存储的管理是一个与计算实例的管理完全不同的问题。PersistentVolume 子系统为用户 和管理员提供了一组 API,将存储如何供应的细节从其如何被使用中抽象出来。 为了实现这点,我们引入了两个新的 API 资源:PersistentVolume 和 PersistentVolumeClaim。

持久卷(PersistentVolume,PV)是集群中的一块存储,可以由管理员事先供应,或者 使用存储类(Storage Class)来动态供应。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样,也是使用 卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。

持久卷声明(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 声明会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存);同样 PVC 声明也可以请求特定的大小和访问模式 (例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载,参见访问模式)。

尽管 PersistentVolumeClaim 允许用户消耗抽象的存储资源,常见的情况是针对不同的 问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。 集群管理员需要能够提供不同性质的 PersistentVolume,并且这些 PV 卷之间的差别不 仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。 为了满足这类需求,就有了 存储类(StorageClass) 资源。

PV不属于任何命名空间, 它跟节点(node)一样是集群层面的资源,区别于pod和PVC。由系统管理员创建管理。

当集群用户需要在其pod中使用持久化存储时,他们首先创建PVC清单,指定所需要的最低容量要求和访问模式,然后用户将待久卷声明清单提交给Kubernetes API服务器,Kubernetes将找到可匹配的PV并将其绑定到PVC。PVC可以当作pod中的一个卷来使用,其他用户不能使用相同的PV,除非先通过删除PVC绑定来释放。

供应

PV 卷的供应有两种方式:静态供应或动态供应。

静态供应

集群管理员创建若干 PV 卷。这些卷对象带有真实存储的细节信息,并且对集群 用户可用(可见)。PV 卷对象存在于 Kubernetes API 中,可供用户消费(使用)。

动态供应

如果管理员所创建的所有静态 PV 卷都无法与用户的 PersistentVolumeClaim 匹配, 集群可以尝试为该 PVC 申领动态供应一个存储卷。 这一供应操作是基于 StorageClass 来实现的:PVC 申领必须请求某个 存储类,同时集群管理员必须 已经创建并配置了该类,这样动态供应卷的动作才会发生。 如果 PVC 申领指定存储类为 “",则相当于为自身禁止使用动态供应的卷。

为了基于存储类完成动态的存储供应,集群管理员需要在 API 服务器上启用 DefaultStorageClass 准入控制器。 举例而言,可以通过保证 DefaultStorageClass 出现在 API 服务器组件的 –enable-admission-plugins 标志值中实现这点;该标志的值可以是逗号 分隔的有序列表。关于 API 服务器标志的更多信息,可以参考 kube-apiserver 文档。

绑定

用户创建一个带有特定存储容量和特定访问模式需求的 PersistentVolumeClaim 对象; 在动态供应场景下,这个 PVC 对象可能已经创建完毕。 主控节点中的控制回路监测新的 PVC 对象,寻找与之匹配的 PV 卷(如果可能的话), 并将二者绑定到一起。 如果为了新的 PVC 申领动态供应了 PV 卷,则控制回路总是将该 PV 卷绑定到这一 PVC 申领。 否则,用户总是能够获得他们所请求的资源,只是所获得的 PV 卷可能会超出所请求的配置。 一旦绑定关系建立,则 PersistentVolumeClaim 绑定就是排他性的,无论该 PVC 申领是 如何与 PV 卷建立的绑定关系。 PVC 申领与 PV 卷之间的绑定是一种一对一的映射,实现上使用 ClaimRef 来记述 PV 卷 与 PVC 申领间的双向绑定关系

如果找不到匹配的 PV 卷,PVC 申领会无限期地处于未绑定状态(即pvc处于pending状态)。 当与之匹配的 PV 卷可用时,PVC 申领会被绑定。 例如,即使某集群上供应了很多 50 Gi 大小的 PV 卷,也无法与请求 100 Gi 大小的存储的 PVC 匹配。当新的 100 Gi PV 卷被加入到集群时,该 PVC 才有可能被绑定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@node131 k8s_pv_pvc]# kubectl get pvc -A
NAMESPACE   NAME                       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
default     nfs-pv-provisioning-demo   Pending                                                     7s
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]# kubectl describe  pvc nfs-pv-provisioning-demo
Name:          nfs-pv-provisioning-demo
Namespace:     default
StorageClass:
Status:        Pending
Volume:
Labels:        demo=nfs-pv-provisioning
Annotations:   <none>
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode:    Filesystem
Used By:       <none>
Events:
  Type    Reason         Age               From                         Message
  ----    ------         ----              ----                         -------
  Normal  FailedBinding  5s (x3 over 27s)  persistentvolume-controller  no persistent volumes available for this claim and no storage class is set
[root@node131 k8s_pv_pvc]#

使用

Pod 将 PVC 申领当做存储卷来使用。集群会检视 PVC 申领,找到所绑定的卷,并 为 Pod 挂载该卷。对于支持多种访问模式的卷,用户要在 Pod 中以卷的形式使用申领 时指定期望的访问模式。

一旦用户有了申领对象并且该申领已经被绑定,则所绑定的 PV 卷在用户仍然需要它期间 一直属于该用户。用户通过在 Pod 的 volumes 块中包含 persistentVolumeClaim 节区来调度 Pod,访问所申领的 PV 卷。 相关细节可参阅使用申领作为卷。

保护使用中的存储对象

保护使用中的存储对象(Storage Object in Use Protection)这一功能特性的目的 是确保仍被 Pod 使用的 PersistentVolumeClaim(PVC)对象及其所绑定的 PersistentVolume(PV)对象在系统中不会被删除,因为这样做可能会引起数据丢失。

说明: 当使用某 PVC 的 Pod 对象仍然存在时,认为该 PVC 仍被此 Pod 使用。 如果用户删除被某 Pod 使用的 PVC 对象,该 PVC 申领不会被立即移除。 PVC 对象的移除会被推迟,直至其不再被任何 Pod 使用。 此外,如果管理员删除已绑定到某 PVC 申领的 PV 卷,该 PV 卷也不会被立即移除。 PV 对象的移除也要推迟到该 PV 不再绑定到 PVC。

你可以看到当 PVC 的状态为 Terminating 且其 Finalizers 列表中包含 kubernetes.io/pvc-protection 时,PVC 对象是处于被保护状态的。

每个 PV 对象都包含 spec 部分和 status 部分,分别对应卷的规约和状态。 PersistentVolume 对象的名称必须是合法的 DNS 子域名.

PV说明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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: 192.168.182.131

说明: 在集群中使用持久卷存储通常需要一些特定于具体卷类型的辅助程序。 在这个例子中,PersistentVolume 是 NFS 类型的,因此需要辅助程序 /sbin/mount.nfs 来支持挂载 NFS 文件系统。

容量

一般而言,每个 PV 卷都有确定的存储容量。 容量属性是使用 PV 对象的 capacity 属性来设置的。 参考 Kubernetes 资源模型(Resource Model) 设计提案,了解 capacity 字段可以接受的单位。

目前,存储大小是可以设置和请求的唯一资源。 未来可能会包含 IOPS、吞吐量等属性。

卷模式

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

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

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

访问模式

PersistentVolume 卷可以用资源提供者所支持的任何方式挂载到宿主系统上。 如下表所示,提供者(驱动)的能力不同,每个 PV 卷的访问模式都会设置为 对应卷所支持的模式值。 例如,NFS 可以支持多个读写客户,但是某个特定的 NFS PV 卷可能在服务器 上以只读的方式导出。每个 PV 卷都会获得自身的访问模式集合,描述的是 特定 PV 卷的能力。

访问模式有:

  • ReadWriteOnce – 卷可以被一个节点以读写方式挂载;
  • ReadOnlyMany – 卷可以被多个节点以只读方式挂载;
  • ReadWriteMany – 卷可以被多个节点以读写方式挂载。

在命令行接口(CLI)中,访问模式也使用以下缩写形式:

  • RWO - ReadWriteOnce
  • ROX - ReadOnlyMany
  • RWX - ReadWriteMany

重要提醒! 每个卷只能同一时刻只能以一种访问模式挂载,即使该卷能够支持 多种访问模式。例如,一个 GCEPersistentDisk 卷可以被某节点以 ReadWriteOnce 模式挂载,或者被多个节点以 ReadOnlyMany 模式挂载,但不可以同时以两种模式 挂载。

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

早前,Kubernetes 使用注解 volume.beta.kubernetes.io/storage-class 而不是 storageClassName 属性。这一注解目前仍然起作用,不过在将来的 Kubernetes 发布版本中该注解会被彻底废弃。

回收策略

目前的回收策略有:

  • Retain – 手动回收
  • Recycle – 基本擦除 (rm -rf /thevolume/*)
  • Delete – 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除

目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

回收策略 Retain 使得用户可以手动回收资源。当 PersistentVolumeClaim 对象 被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为"已释放(released)"。 由于卷上仍然存在这前一申领人的数据,该卷还不能用于其他申领。

测试Retain模式如下: 说明:Retain模式下,删除以前绑定pv的pvc后,再创建使用该pv的pvc,会导致该pvc一直pending。因为pv的Claim: default/nfs字段导致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[root@node131 ~]#
[root@node131 ~]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM         STORAGECLASS   REASON   AGE
nfs    1Mi        RWX            Retain           Bound    default/nfs                           5h58m
[root@node131 ~]
[root@node131 k8s_pv_pvc]# kubectl delete -f nfs/nfs-pvc.yaml
persistentvolumeclaim "nfs" deleted
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]#
[root@node131 ~]#
[root@node131 ~]#
[root@node131 ~]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM         STORAGECLASS   REASON   AGE
nfs    1Mi        RWX            Retain           Released   default/nfs                           6h1m
[root@node131 ~]#
[root@node131 k8s_pv_pvc]# kubectl create -f nfs/custom-nfs-pvc.yaml
persistentvolumeclaim/nfs created
[root@node131 k8s_pv_pvc]#
[root@node131 ~]#
[root@node131 ~]# kubectl get pvc
NAME   STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs    Pending                                                     8s
[root@node131 ~]#
[root@node131 ~]#
[root@node131 ~]# kubectl describe pv
Name:            nfs
Labels:          <none>
Annotations:     pv.kubernetes.io/bound-by-controller: yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Released
Claim:           default/nfs
Reclaim Policy:  Retain
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        1Mi
Node Affinity:   <none>
Message:
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    10.233.16.102
    Path:      /exports
    ReadOnly:  false
Events:        <none>


节点亲和性

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

说明: 对大多数类型的卷而言,你不需要设置节点亲和性字段。 AWS EBS、 GCE PD 和 Azure Disk 卷类型都能 自动设置相关字段。 你需要为 local 卷显式地设置 此属性

PV的阶段

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

  • Available(可用)– 卷是一个空闲资源,尚未绑定到任何申领;
  • Bound(已绑定)– 该卷已经绑定到某申领;
  • Released(已释放)– 所绑定的申领已被删除,但是资源尚未被集群回收;
  • Failed(失败)– 卷的自动回收操作失败。 命令行接口能够显示绑定到某 PV 卷的 PVC 对象

PVC说明

每个 PVC 对象都有 spec 和 status 部分,分别对应申领的规约和状态。 PersistentVolumeClaim 对象的名称必须是合法的 DNS 子域名.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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]}
访问模式

申领在请求具有特定访问模式的存储时,使用与卷相同的访问模式约定。

卷模式

申领使用与卷相同的约定来表明是将卷作为文件系统还是块设备来使用。

资源

申领和 Pod 一样,也可以请求特定数量的资源。在这个上下文中,请求的资源是存储。 卷和申领都使用相同的 资源模型。

选择算符

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

matchLabels - 卷必须包含带有此值的标签 matchExpressions - 通过设定键(key)、值列表和操作符(operator) 来构造的需求。合法的操作符有 In、NotIn、Exists 和 DoesNotExist。 来自 matchLabels 和 matchExpressions 的所有需求都按逻辑与的方式组合在一起。 这些需求都必须被满足才被视为匹配。

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

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

  • 如果准入控制器插件被启用,则管理员可以设置一个默认的 StorageClass。 所有未设置 storageClassName 的 PVC 都只能绑定到隶属于默认存储类的 PV 卷。 设置默认 StorageClass 的工作是通过将对应 StorageClass 对象的注解 storageclass.kubernetes.io/is-default-class 赋值为 true 来完成的。 如果管理员未设置默认存储类,集群对 PVC 创建的处理方式与未启用准入控制器插件 时相同。如果设定的默认存储类不止一个,准入控制插件会禁止所有创建 PVC 操作。
  • 如果准入控制器插件被关闭,则不存在默认 StorageClass 的说法。 所有未设置 storageClassName 的 PVC 都只能绑定到未设置存储类的 PV 卷。 在这种情况下,未设置 storageClassName 的 PVC 与 storageClassName 设置未 "” 的 PVC 的处理方式相同。

取决于安装方法,默认的 StorageClass 可能在集群安装期间由插件管理器(Addon Manager)部署到集群中。

当某 PVC 除了请求 StorageClass 之外还设置了 selector,则这两种需求会按 逻辑与关系处理:只有隶属于所请求类且带有所请求标签的 PV 才能绑定到 PVC。

说明: 目前,设置了非空 selector 的 PVC 对象无法让集群为其动态供应 PV 卷。

早前,Kubernetes 使用注解 volume.beta.kubernetes.io/storage-class 而不是 storageClassName 属性。这一注解目前仍然起作用,不过在将来的 Kubernetes 发布版本中该注解会被彻底废弃。

使用申领作为卷

Pod 将申领作为卷来使用,并藉此访问存储资源。 申领必须位于使用它的 Pod 所在的同一名字空间内。 集群在 Pod 的名字空间中查找申领,并使用它来获得申领所使用的 PV 卷。 之后,卷会被挂载到宿主上并挂载到 Pod 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim

如上,业务pod使用了 pvc name为myclaim的pvc,做为卷存储,挂载到容器的/var/www/html

PV和PVC设计目标

Kubernetes makes no guarantees at runtime that the underlying storage exists or is available. High availability is left to the storage provider.

Goals

  • Allow administrators to describe available storage.(通过pv来定义存储资源)
  • Allow pod authors to discover and request persistent volumes to use with pods.(允许pod使用像使用pod的request资源一样使用存储pv)
  • Enforce security through access control lists and securing storage to the same namespace as the pod volume.(通过访问控制列表机制来保证存储使用安全)
  • Enforce quotas through admission control.(通过准入机制实现存储配额)
  • Enforce scheduler rules by resource counting.(基于资源数量调度,调度pvc->pv)
  • Ensure developers can rely on storage being available without being closely bound to a particular disk, server, network, or storage device.(通过抽象层设计,pod与具体的存储资源隔离)

PersistentVolumeController分析

实例化

volume manager controller 来管理persistentvolume kubernetes\pkg\controller\volume\persistentvolume\

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// NewController creates a new PersistentVolume controller
controller := &PersistentVolumeController{
        volumes:                       newPersistentVolumeOrderedIndex(),
        claims:                        cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc),
        kubeClient:                    p.KubeClient,
        eventRecorder:                 eventRecorder,
        runningOperations:             goroutinemap.NewGoRoutineMap(true /* exponentialBackOffOnError */),
        cloud:                         p.Cloud,
        enableDynamicProvisioning:     p.EnableDynamicProvisioning,
        clusterName:                   p.ClusterName,
        createProvisionedPVRetryCount: createProvisionedPVRetryCount,
        createProvisionedPVInterval:   createProvisionedPVInterval,
        claimQueue:                    workqueue.NewNamed("claims"),
        volumeQueue:                   workqueue.NewNamed("volumes"),
        resyncPeriod:                  p.SyncPeriod,
        operationTimestamps:           metrics.NewOperationStartTimeCache(),
    }

pv cache 构造

newPersistentVolumeOrderedIndex -> persistentVolumeOrderedIndex pv在cache中按AccessModes索引,并按存储量大小排序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

// persistentVolumeOrderedIndex is a cache.Store that keeps persistent volumes
// indexed by AccessModes and ordered by storage capacity.
type persistentVolumeOrderedIndex struct {
    store cache.Indexer
}

func newPersistentVolumeOrderedIndex() persistentVolumeOrderedIndex {
    return persistentVolumeOrderedIndex{cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"accessmodes": accessModesIndexFunc})}
}

进行pv的同步操作

  • 如果 volume.Spec.ClaimRef == nil 说明pv没有被pvc绑定使用,直接更新pv:ctrl.updateVolumePhase(volume, v1.VolumeAvailable, “")
  • volume.Spec.ClaimRef != nil 说明pv被pvc绑定使用,需要进行相应逻辑处理

说明:pvc和 pv 是通过UID来进行关联标识:claim.UID != volume.Spec.ClaimRef.UID ?

1
2
3
4
5
6
7
// syncVolume is the main controller method to decide what to do with a volume.
// It's invoked by appropriate cache.Controller callbacks when a volume is
// created, updated or periodically synced. We do not differentiate between
// these events.
func (ctrl *PersistentVolumeController) syncVolume(volume *v1.PersistentVolume) error {
}

为pvc匹配查找最佳pv

根据声明的pvc,在pv列表中匹配查找

  1. 先按pvc要求的AccessModes,过滤出符合要求的pv候选列表
  2. 在pv候选列表中优选出最佳的pv
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// findBestMatchForClaim is a convenience method that finds a volume by the claim's AccessModes and requests for Storage
func (pvIndex *persistentVolumeOrderedIndex) findBestMatchForClaim(claim *v1.PersistentVolumeClaim, delayBinding bool) (*v1.PersistentVolume, error) {
    return pvIndex.findByClaim(claim, delayBinding)
}



// find returns the nearest PV from the ordered list or nil if a match is not found
func (pvIndex *persistentVolumeOrderedIndex) findByClaim(claim *v1.PersistentVolumeClaim, delayBinding bool) (*v1.PersistentVolume, error) {
    // PVs are indexed by their access modes to allow easier searching.  Each
    // index is the string representation of a set of access modes. There is a
    // finite number of possible sets and PVs will only be indexed in one of
    // them (whichever index matches the PV's modes).
    //
    // A request for resources will always specify its desired access modes.
    // Any matching PV must have at least that number of access modes, but it
    // can have more.  For example, a user asks for ReadWriteOnce but a GCEPD
    // is available, which is ReadWriteOnce+ReadOnlyMany.
    //
    // Searches are performed against a set of access modes, so we can attempt
    // not only the exact matching modes but also potential matches (the GCEPD
    // example above).
    allPossibleModes := pvIndex.allPossibleMatchingAccessModes(claim.Spec.AccessModes)

    for _, modes := range allPossibleModes {
        volumes, err := pvIndex.listByAccessModes(modes)
        if err != nil {
            return nil, err
        }

        bestVol, err := pvutil.FindMatchingVolume(claim, volumes, nil /* node for topology binding*/, nil /* exclusion map */, delayBinding)
        if err != nil {
            return nil, err
        }

        if bestVol != nil {
            return bestVol, nil
        }
    }
    return nil, nil
}

优选算法函数如下,该函数会被PV controller 和 scheduler 使用

  • 参数delayBinding 只在PV controller流程为true
  • 参数node和excludedVolumes 只在scheduler流程设置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// FindMatchingVolume goes through the list of volumes to find the best matching volume
// for the claim.
//
// This function is used by both the PV controller and scheduler.
//
// delayBinding is true only in the PV controller path.  When set, prebound PVs are still returned
// as a match for the claim, but unbound PVs are skipped.
//
// node is set only in the scheduler path. When set, the PV node affinity is checked against
// the node's labels.
//
// excludedVolumes is only used in the scheduler path, and is needed for evaluating multiple
// unbound PVCs for a single Pod at one time.  As each PVC finds a matching PV, the chosen
// PV needs to be excluded from future matching.
func FindMatchingVolume(
    claim *v1.PersistentVolumeClaim,
    volumes []*v1.PersistentVolume,
    node *v1.Node,
    excludedVolumes map[string]*v1.PersistentVolume,
    delayBinding bool) (*v1.PersistentVolume, error) {

    }

Matching and binding PVC->PV

PersistentVolumeClaimBinder尝试查找与用户请求最接近的可用卷。如果存在,则通过将pv上的引用绑定到pvc。如果找不到合适的匹配,请求可能无法满足。

claim(PVC)必须请求访问模式和存储容量。这是因为内部PV是按其AccessModes索引的,目标PV在某种程度上是按其容量排序的。pvc声明可以请求以下多个属性中的一个来更好地匹配PV:卷名称、选择器和卷类(当前实现为注释)。

PV可以定义一个ClaimRef,它会对PVC的匹配产生很大的影响(但不是绝对的保证)。PV还可以定义标签、注释和卷类(当前作为注释实现)以更好地针对目标PVC。

PVC->PV匹配算法说明:

As of Kubernetes version 1.4, the following algorithm describes in more details how a claim is matched to a PV:

  1. Only PVs with accessModes equal to or greater than the claim’s requested accessModes are considered. “Greater” here means that the PV has defined more modes than needed by the claim, but it also defines the mode requested by the claim.

  2. The potential PVs above are considered in order of the closest access mode match, with the best case being an exact match, and a worse case being more modes than requested by the claim.

  3. Each PV above is processed. If the PV has a claimRef matching the claim, and the PV’s capacity is not less than the storage being requested by the claim then this PV will bind to the claim. Done.

  4. Otherwise, if the PV has the “volume.alpha.kubernetes.io/storage-class” annotation defined then it is skipped and will be handled by Dynamic Provisioning.

  5. Otherwise, if the PV has a claimRef defined, which can specify a different claim or simply be a placeholder, then the PV is skipped. 这点说明了 PV和PVC之间的关系是1对1

  6. Otherwise, if the claim is using a selector but it does not match the PV’s labels (if any) then the PV is skipped. But, even if a claim has selectors which match a PV that does not guarantee a match since capacities may differ.

  7. Otherwise, if the PV’s “volume.beta.kubernetes.io/storage-class” annotation (which is a placeholder for a volume class) does not match the claim’s annotation (same placeholder) then the PV is skipped. If the annotations for the PV and PVC are empty they are treated as being equal.

  8. Otherwise, what remains is a list of PVs that may match the claim. Within this list of remaining PVs, the PV with the smallest capacity that is also equal to or greater than the claim’s requested storage is the matching PV and will be bound to the claim. Done. In the case of two or more PVCs matching all of the above criteria, the first PV (remember the PV order is based on accessModes) is the winner. 候选PV列表中最小资源满足的PV为最佳优选结果

Note: if no PV matches the claim and the claim defines a StorageClass (or a default StorageClass has been defined) then a volume will be dynamically provisioned.

测试NFS-PV-PVC

下面的示例演示如何从单个nfs server的POD RC控制器导出NFS共享,并将其导入web的两个RC控制器。

nfs server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Copyright 2016 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM centos
RUN yum -y install /usr/bin/ps nfs-utils && yum clean all
RUN mkdir -p /exports
ADD run_nfs.sh /usr/local/bin/
ADD index.html /tmp/index.html
RUN chmod 644 /tmp/index.html

# expose mountd 20048/tcp and nfsd 2049/tcp and rpcbind 111/tcp
EXPOSE 2049/tcp 20048/tcp 111/tcp 111/udp

ENTRYPOINT ["/usr/local/bin/run_nfs.sh", "/exports"]


1
2
3
4
# 制作镜像
docker build -t k8s.gcr.io/volume-nfs:0.8 .

docker save k8s.gcr.io/volume-nfs:0.8 -o volume-nfs-img.tar
1
2
# 在测试环境中导出镜像
docker load -i volume-nfs-img.tar

NFS server part

  • 把nfs测试配置脚本文件examples/staging/volumes/nfs拷贝到测试环境中
  • 修改下测试镜像名称:如busybox和nginx
  • 按需要, 创建 gce-pv
1
2
3
# If you are on GCE, create a GCE PD-based PVC:
# kubectl create -f examples/staging/volumes/nfs/provisioner/nfs-server-gce-pv.yaml
# kubectl create -f nfs/provisioner/nfs-server-gce-pv.yaml

test pv和 pvc

1
2
3
4
5
# test pv
kubectl create -f nfs/test/pv0001.yaml

# test pvc
kubectl create -f nfs/test/pvc-pv0001.yaml
  • 创建 NFS server and service
1
2
3
4
#kubectl create -f examples/staging/volumes/nfs/nfs-server-rc.yaml
#kubectl create -f examples/staging/volumes/nfs/nfs-server-service.yaml
kubectl create -f nfs/custom-nfs-server-rc.yaml
kubectl create -f nfs/nfs-server-service.yaml

说明创建pod时,其使用的pvc必须为bound状态才能进行调度

1
2
3
4
5
6
7
8
Tolerations:     node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                 node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  29s   default-scheduler  0/1 nodes are available: 1 persistentvolumeclaim "nfs-pv-provisioning-demo" not found.
  Warning  FailedScheduling  29s   default-scheduler  0/1 nodes are available: 1 persistentvolumeclaim "nfs-pv-provisioning-demo" not found.

  • 创建基于NFS的pv和pvc 检查下nfs-server
1
2
3

kubectl describe services nfs-server

nfs的服务端口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@node131 k8s_pv_pvc]# kubectl describe services nfs-server
Name:              nfs-server
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          role=nfs-server
Type:              ClusterIP
IP Families:       <none>
IP:                10.233.63.192
IPs:               10.233.63.192
Port:              nfs  2049/TCP
TargetPort:        2049/TCP
Endpoints:         10.233.124.46:2049
Port:              mountd  20048/TCP
TargetPort:        20048/TCP
Endpoints:         10.233.124.46:20048
Port:              rpcbind  111/TCP
TargetPort:        111/TCP
Endpoints:         10.233.124.46:111
Session Affinity:  None
Events:            <none>

然后再创建nfs的pv和pvc

1
2
3
4
5
6

#kubectl create -f examples/staging/volumes/nfs/nfs-pv.yaml
#kubectl create -f examples/staging/volumes/nfs/nfs-pvc.yaml

kubectl create -f nfs/custom-nfs-pv.yaml
kubectl create -f nfs/custom-nfs-pvc.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[root@node131 k8s_pv_pvc]# kubectl create -f nfs/nfs-pv.yaml
persistentvolume/nfs created
[root@node131 k8s_pv_pvc]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
nfs    1Mi        RWX            Retain           Available                                   6s

[root@node131 k8s_pv_pvc]# kubectl describe pv nfs
Name:            nfs
Labels:          <none>
Annotations:     <none>
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Available
Claim:
Reclaim Policy:  Retain
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        1Mi
Node Affinity:   <none>
Message:
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    nfs-server.default.svc.cluster.local
    Path:      /tmp/data
    ReadOnly:  false
Events:        <none>
[root@node131 k8s_pv_pvc]#




[root@node131 k8s_pv_pvc]# kubectl create -f nfs/nfs-pvc.yaml
persistentvolumeclaim/nfs created
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM         STORAGECLASS   REASON   AGE
nfs    1Mi        RWX            Retain           Bound    default/nfs                           114s
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]# kubectl get pvc
NAME   STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs    Bound    nfs      1Mi        RWX                           16s
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]#

此时再查看pv和pvc的信息,能够看到注解信息有变化,状态为Status: Bound

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[root@node131 k8s_pv_pvc]# kubectl describe pv nfs
Name:            nfs
Labels:          <none>
Annotations:     pv.kubernetes.io/bound-by-controller: yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Bound
Claim:           default/nfs
Reclaim Policy:  Retain
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        1Mi
Node Affinity:   <none>
Message:
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    nfs-server.default.svc.cluster.local
    Path:      /tmp/data
    ReadOnly:  false
Events:        <none>
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]#
[root@node131 k8s_pv_pvc]# kubectl describe pvc nfs
Name:          nfs
Namespace:     default
StorageClass:
Status:        Bound
Volume:        nfs
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      1Mi
Access Modes:  RWX
VolumeMode:    Filesystem
Used By:       <none>
Events:        <none>
[root@node131 k8s_pv_pvc]#

  • 使用后端程序使用上面绑定好的nfs pvc,更新nfs的数据目录,即html页面
1
2
3
#kubectl create -f examples/staging/volumes/nfs/nfs-busybox-rc.yaml
kubectl create -f nfs/custom-nfs-busybox-rc.yaml

其信息显示如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[root@node131 nfs]# kubectl get po -A
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE
default       nfs-busybox-mntdb                          1/1     Running   0          35s
default       nfs-busybox-r76ml                          1/1     Running   0          35s
default       nfs-server-p6p4n                           1/1     Running   1          134m
kube-system   calico-kube-controllers-65b86747bd-c4qsp   1/1     Running   10         41d
kube-system   calico-node-lglh4                          1/1     Running   11         41d
kube-system   coredns-8677555d68-flqh4                   1/1     Running   1          90m
kube-system   kube-apiserver-node131                     1/1     Running   10         41d
kube-system   kube-controller-manager-node131            1/1     Running   10         41d
kube-system   kube-proxy-mktp9                           1/1     Running   10         41d
kube-system   kube-scheduler-node131                     1/1     Running   10         41d
kube-system   nodelocaldns-lfjzs                         1/1     Running   10         41d
[root@node131 nfs]#




[root@node131 nfs]# cat /tmp/data/index.html
Mon Feb  1 09:59:49 UTC 2021
nfs-busybox-9td8j
[root@node131 nfs]# cat /tmp/data/index.html
Mon Feb  1 09:59:49 UTC 2021
nfs-busybox-9td8j
[root@node131 nfs]#
[root@node131 nfs]#
[root@node131 nfs]#
[root@node131 nfs]#
[root@node131 nfs]# cat /tmp/data/index.html
Mon Feb  1 09:59:57 UTC 2021
nfs-busybox-9td8j
[root@node131 nfs]#


[root@node131 nfs]# kubectl exec -ndefault       nfs-busybox-r76ml -- cat /mnt/index.html
Mon Feb  1 10:17:58 UTC 2021
nfs-busybox-mntdb
[root@node131 nfs]#



每10s左右会更新web页面。这个busybox执行的是存储写操作。。。 多个busybox的运行pod表示多实例写操作

  • 启动web server(nginx)
1
2
3
4
5
6
#kubectl create -f examples/staging/volumes/nfs/nfs-web-rc.yaml
#kubectl create -f examples/staging/volumes/nfs/nfs-web-service.yaml

kubectl create -f nfs/nfs-web-rc.yaml
kubectl create -f nfs/nfs-web-service.yaml

web服务已运行,查看web服务的html页面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

[root@node131 nfs]# kubectl get po -A
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE
default       nfs-busybox-mntdb                          1/1     Running   0          35s
default       nfs-busybox-r76ml                          1/1     Running   0          35s
default       nfs-server-p6p4n                           1/1     Running   1          134m
default       nfs-web-dhmst                              1/1     Running   0          8m19s
default       nfs-web-lxlcr                              1/1     Running   0          8m19s
kube-system   calico-kube-controllers-65b86747bd-c4qsp   1/1     Running   10         41d
kube-system   calico-node-lglh4                          1/1     Running   11         41d
kube-system   coredns-8677555d68-flqh4                   1/1     Running   1          90m
kube-system   kube-apiserver-node131                     1/1     Running   10         41d
kube-system   kube-controller-manager-node131            1/1     Running   10         41d
kube-system   kube-proxy-mktp9                           1/1     Running   10         41d
kube-system   kube-scheduler-node131                     1/1     Running   10         41d
kube-system   nodelocaldns-lfjzs                         1/1     Running   10         41d
[root@node131 nfs]#
[root@node131 nfs]#



[root@node131 nfs]# kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
kubernetes   ClusterIP   10.233.0.1      <none>        443/TCP                      41d
nfs-server   ClusterIP   10.233.63.192   <none>        2049/TCP,20048/TCP,111/TCP   140m
nfs-web      ClusterIP   10.233.63.218   <none>        80/TCP                       15m
[root@node131 nfs]#
[root@node131 nfs]#

[root@node131 nfs]# kubectl exec nfs-web-dhmst -- cat /usr/share/nginx/html/index.html
Mon Feb  1 10:06:54 UTC 2021
nfs-busybox-9td8j
[root@node131 nfs]#


在busybox的pod中,http访问web服务,查看页面

直接访问web服务域名

1
kubectl exec -ndefault       nfs-busybox-mntdb -- wget -qO- http://nfs-web
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[root@node131 nfs]#
[root@node131 nfs]# kubectl exec -ndefault       nfs-busybox-mntdb -- wget -qO- http://nfs-web
Mon Feb  1 10:24:37 UTC 2021
nfs-busybox-mntdb
[root@node131 nfs]#
[root@node131 nfs]#
[root@node131 nfs]#
[root@node131 nfs]# kubectl exec -ndefault       nfs-busybox-mntdb -- wget -qO- http://nfs-web
Mon Feb  1 10:24:41 UTC 2021
nfs-busybox-r76ml
[root@node131 nfs]#


测试的pv和pvc信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[root@node131 ~]# kubectl describe pv
Name:            nfs
Labels:          <none>
Annotations:     pv.kubernetes.io/bound-by-controller: yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:
Status:          Bound
Claim:           default/nfs
Reclaim Policy:  Retain
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        1Mi
Node Affinity:   <none>
Message:
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    10.233.16.102
    Path:      /exports
    ReadOnly:  false
Events:        <none>
[root@node131 ~]#
[root@node131 ~]#
[root@node131 ~]# kubectl describe pvc
Name:          nfs
Namespace:     default
StorageClass:
Status:        Bound
Volume:        nfs
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      1Mi
Access Modes:  RWX
VolumeMode:    Filesystem
Used By:       nfs-busybox-b8wdw
               nfs-busybox-hvc8w
               nfs-web-7bnhj
               nfs-web-mxpx9
Events:        <none>


部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# create nfs server
kubectl create -f nfs/custom-nfs-server-rc.yaml
kubectl create -f nfs/nfs-server-service.yaml


# create nfs pv pvc
kubectl create -f nfs/custom-nfs-pv.yaml
kubectl create -f nfs/custom-nfs-pvc.yaml


# create busybox write
kubectl create -f nfs/custom-nfs-busybox-rc.yaml

# create web read
kubectl create -f nfs/nfs-web-rc.yaml
kubectl create -f nfs/nfs-web-service.yaml

卸载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

# remove web read
kubectl delete -f nfs/nfs-web-service.yaml
kubectl delete -f nfs/nfs-web-rc.yaml

# remove busybox write
kubectl delete -f nfs/custom-nfs-busybox-rc.yaml
# remove nfs pv pvc
kubectl delete -f nfs/nfs-pvc.yaml
kubectl delete -f nfs/custom-nfs-pv.yaml

# remove nfs server
kubectl delete -f nfs/nfs-server-service.yaml
kubectl delete -f nfs/custom-nfs-server-rc.yaml


问题

启动测试pod,进行mount时失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

Events:
  Type     Reason       Age                From               Message
  ----     ------       ----               ----               -------
  Normal   Scheduled    26s                default-scheduler  Successfully assigned default/nfs-busybox-jwshl to node131
  Warning  FailedMount  11s (x6 over 27s)  kubelet            MountVolume.SetUp failed for volume "nfs" : mount failed: exit status 32
Mounting command: mount
Mounting arguments: -t nfs nfs-server.default.svc.cluster.local:/tmp/data /var/lib/kubelet/pods/d4ca8ca9-e1c8-4610-8a74-f0fdea827fee/volumes/kubernetes.io~nfs/nfs
Output: mount: 文件系统类型错误、选项错误、nfs-server.default.svc.cluster.local:/tmp/data 上有坏超级块、
       缺少代码页或助手程序,或其他错误
       (对某些文件系统(如 nfs、cifs) 您可能需要
       一款 /sbin/mount.<类型> 助手程序)

       有些情况下在 syslog 中可以找到一些有用信息- 请尝试
       dmesg | tail  这样的命令看看。

首先检查 内核是否支持nfs文件系统格式,方法如下

cat /proc/filesystems 如果能够看到 nfs 或者nfs4字样就说明内核支持nfs格式的文件系统,否则需要重新编译新的支持nfs文件系统的内核。

如果检查内核支持nfs格式的文件系统后,检查mount.nfs是否安装:

ls /sbin/mount.* 看是否有 mount.nfs 或者 mount.nfs4 如果没有需要安装 nfs_utils

1
ls /sbin/mount.*

yum install nfs-utils (redhat系列)

1
yum install -y nfs-utils 

apt-get install common(ubuntu系列)

1
apt-get install nfs-common

挂载域名无法解析,使用ip地址标识

Output: mount.nfs: Protocol not supported

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Events:
  Type     Reason       Age                From               Message
  ----     ------       ----               ----               -------
  Normal   Scheduled    28s                default-scheduler  Successfully assigned default/nfs-busybox-k6dpc to node131
  Warning  FailedMount  11s (x6 over 27s)  kubelet            MountVolume.SetUp failed for volume "nfs" : mount failed: exit status 32
Mounting command: mount
Mounting arguments: -t nfs 10.233.124.49:/tmp/data /var/lib/kubelet/pods/e5932fde-fe05-4612-b29d-333f48b03338/volumes/kubernetes.io~nfs/nfs
Output: mount.nfs: Protocol not supported
[root@node131 nfs]#


https://stackoverflow.com/questions/35650935/output-mount-nfs-requested-nfs-version-or-transport-protocol-is-not-supported

访问挂载路径,服务端access denied

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

Events:
  Type     Reason       Age               From               Message
  ----     ------       ----              ----               -------
  Normal   Scheduled    33s               default-scheduler  Successfully assigned default/nfs-busybox-4ht5m to node131
  Warning  FailedMount  1s (x7 over 34s)  kubelet            MountVolume.SetUp failed for volume "nfs" : mount failed: exit status 32
Mounting command: mount
Mounting arguments: -t nfs 10.233.124.49:/ /var/lib/kubelet/pods/bd34361c-4883-47a2-9e70-08772437e341/volumes/kubernetes.io~nfs/nfs
Output: mount.nfs: access denied by server while mounting 10.233.124.49:/
[root@node131 nfs]#

原因挂载路径错误

pv配置的挂载路径path需跟nfs服务定义的路径一致。

1
2
3
4
5
6

nfs:
    #server: nfs-server.default.svc.cluster.local
    server: 10.233.124.49
    path: "/exports"

附录

按服务和资源配置顺序,依次如下

custom-nfs-server-rc.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: v1
kind: ReplicationController
metadata:
  name: nfs-server
spec:
  replicas: 1
  selector:
    role: nfs-server
  template:
    metadata:
      labels:
        role: nfs-server
    spec:
      containers:
      - name: nfs-server
        image: k8s.gcr.io/volume-nfs:0.8
        ports:
          - name: nfs
            containerPort: 2049
          - name: mountd
            containerPort: 20048
          - name: rpcbind
            containerPort: 111
        securityContext:
          privileged: true
        volumeMounts:
          - mountPath: /exports
            # name: mypvc
            name: data-volume
      volumes:
        # - name: mypvc
        #   persistentVolumeClaim:
        #     claimName: nfs-pv-provisioning-demo
        - name: data-volume                   #卷名
          hostPath:
            path: /tmp/data

custom-nfs-pv.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs
spec:
  capacity:
    storage: 1Mi
  accessModes:
    - ReadWriteMany
    #- ReadWriteOnce
  nfs:
    # faild: nfs-server svc name
    #server: nfs-server.default.svc.cluster.local
    # success: nfs-server svc ip
    server: 10.233.16.102
    path: "/exports"

custom-nfs-pvc.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs
spec:
  accessModes:
    - ReadWriteMany
    #- ReadWriteOnce
  storageClassName: ""
  resources:
    requests:
      storage: 1Mi

custom-nfs-busybox-rc.yaml

使用pvc进行nfs存储写操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# This mounts the nfs volume claim into /mnt and continuously
# overwrites /mnt/index.html with the time and hostname of the pod.

apiVersion: v1
kind: ReplicationController
metadata:
  name: nfs-busybox
spec:
  replicas: 2
  selector:
    name: nfs-busybox
  template:
    metadata:
      labels:
        name: nfs-busybox
    spec:
      containers:
      - image: busybox
        command:
          - sh
          - -c
          - 'while true; do date > /mnt/index.html; hostname >> /mnt/index.html; sleep $(($RANDOM % 5 + 5)); done'
        imagePullPolicy: IfNotPresent
        name: busybox
        volumeMounts:
          # name must match the volume name below
          - name: nfs
            mountPath: "/mnt"
      volumes:
      - name: nfs
        persistentVolumeClaim:
          claimName: nfs


nfs-web-rc.yaml

使用pvc进行nfs存储读操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# This pod mounts the nfs volume claim into /usr/share/nginx/html and
# serves a simple web page.

apiVersion: v1
kind: ReplicationController
metadata:
  name: nfs-web
spec:
  replicas: 2
  selector:
    role: web-frontend
  template:
    metadata:
      labels:
        role: web-frontend
    spec:
      containers:
      - name: web
        image: nginx:1.19
        ports:
          - name: web
            containerPort: 80
        volumeMounts:
            # name must match the volume name below
            - name: nfs
              mountPath: "/usr/share/nginx/html"
      volumes:
      - name: nfs
        persistentVolumeClaim:
          claimName: nfs

pv和pvc结构体

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

// Volume represents a named volume in a pod that may be accessed by any container in the pod.
type Volume struct {
    // Volume's name.
    // Must be a DNS_LABEL and unique within the pod.
    // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
    Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
    // VolumeSource represents the location and type of the mounted volume.
    // If not specified, the Volume is implied to be an EmptyDir.
    // This implied behavior is deprecated and will be removed in a future version.
    VolumeSource `json:",inline" protobuf:"bytes,2,opt,name=volumeSource"`
}

// Represents the source of a volume to mount.
// Only one of its members may be specified.
type VolumeSource struct {
    // HostPath represents a pre-existing file or directory on the host
    // machine that is directly exposed to the container. This is generally
    // used for system agents or other privileged things that are allowed
    // to see the host machine. Most containers will NOT need this.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath
    // ---
    // TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not
    // mount host directories as read/write.
    // +optional
    HostPath *HostPathVolumeSource `json:"hostPath,omitempty" protobuf:"bytes,1,opt,name=hostPath"`
    // EmptyDir represents a temporary directory that shares a pod's lifetime.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir
    // +optional
    EmptyDir *EmptyDirVolumeSource `json:"emptyDir,omitempty" protobuf:"bytes,2,opt,name=emptyDir"`
    // GCEPersistentDisk represents a GCE Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
    // +optional
    GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk,omitempty" protobuf:"bytes,3,opt,name=gcePersistentDisk"`
    // AWSElasticBlockStore represents an AWS Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
    // +optional
    AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore,omitempty" protobuf:"bytes,4,opt,name=awsElasticBlockStore"`
    // GitRepo represents a git repository at a particular revision.
    // DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an
    // EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir
    // into the Pod's container.
    // +optional
    GitRepo *GitRepoVolumeSource `json:"gitRepo,omitempty" protobuf:"bytes,5,opt,name=gitRepo"`
    // Secret represents a secret that should populate this volume.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#secret
    // +optional
    Secret *SecretVolumeSource `json:"secret,omitempty" protobuf:"bytes,6,opt,name=secret"`
    // NFS represents an NFS mount on the host that shares a pod's lifetime
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
    // +optional
    NFS *NFSVolumeSource `json:"nfs,omitempty" protobuf:"bytes,7,opt,name=nfs"`
    // ISCSI represents an ISCSI Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod.
    // More info: https://examples.k8s.io/volumes/iscsi/README.md
    // +optional
    ISCSI *ISCSIVolumeSource `json:"iscsi,omitempty" protobuf:"bytes,8,opt,name=iscsi"`
    // Glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime.
    // More info: https://examples.k8s.io/volumes/glusterfs/README.md
    // +optional
    Glusterfs *GlusterfsVolumeSource `json:"glusterfs,omitempty" protobuf:"bytes,9,opt,name=glusterfs"`
    // PersistentVolumeClaimVolumeSource represents a reference to a
    // PersistentVolumeClaim in the same namespace.
    // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
    // +optional
    PersistentVolumeClaim *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty" protobuf:"bytes,10,opt,name=persistentVolumeClaim"`
    // RBD represents a Rados Block Device mount on the host that shares a pod's lifetime.
    // More info: https://examples.k8s.io/volumes/rbd/README.md
    // +optional
    RBD *RBDVolumeSource `json:"rbd,omitempty" protobuf:"bytes,11,opt,name=rbd"`
    // FlexVolume represents a generic volume resource that is
    // provisioned/attached using an exec based plugin.
    // +optional
    FlexVolume *FlexVolumeSource `json:"flexVolume,omitempty" protobuf:"bytes,12,opt,name=flexVolume"`
    // Cinder represents a cinder volume attached and mounted on kubelets host machine.
    // More info: https://examples.k8s.io/mysql-cinder-pd/README.md
    // +optional
    Cinder *CinderVolumeSource `json:"cinder,omitempty" protobuf:"bytes,13,opt,name=cinder"`
    // CephFS represents a Ceph FS mount on the host that shares a pod's lifetime
    // +optional
    CephFS *CephFSVolumeSource `json:"cephfs,omitempty" protobuf:"bytes,14,opt,name=cephfs"`
    // Flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running
    // +optional
    Flocker *FlockerVolumeSource `json:"flocker,omitempty" protobuf:"bytes,15,opt,name=flocker"`
    // DownwardAPI represents downward API about the pod that should populate this volume
    // +optional
    DownwardAPI *DownwardAPIVolumeSource `json:"downwardAPI,omitempty" protobuf:"bytes,16,opt,name=downwardAPI"`
    // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
    // +optional
    FC *FCVolumeSource `json:"fc,omitempty" protobuf:"bytes,17,opt,name=fc"`
    // AzureFile represents an Azure File Service mount on the host and bind mount to the pod.
    // +optional
    AzureFile *AzureFileVolumeSource `json:"azureFile,omitempty" protobuf:"bytes,18,opt,name=azureFile"`
    // ConfigMap represents a configMap that should populate this volume
    // +optional
    ConfigMap *ConfigMapVolumeSource `json:"configMap,omitempty" protobuf:"bytes,19,opt,name=configMap"`
    // VsphereVolume represents a vSphere volume attached and mounted on kubelets host machine
    // +optional
    VsphereVolume *VsphereVirtualDiskVolumeSource `json:"vsphereVolume,omitempty" protobuf:"bytes,20,opt,name=vsphereVolume"`
    // Quobyte represents a Quobyte mount on the host that shares a pod's lifetime
    // +optional
    Quobyte *QuobyteVolumeSource `json:"quobyte,omitempty" protobuf:"bytes,21,opt,name=quobyte"`
    // AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.
    // +optional
    AzureDisk *AzureDiskVolumeSource `json:"azureDisk,omitempty" protobuf:"bytes,22,opt,name=azureDisk"`
    // PhotonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine
    PhotonPersistentDisk *PhotonPersistentDiskVolumeSource `json:"photonPersistentDisk,omitempty" protobuf:"bytes,23,opt,name=photonPersistentDisk"`
    // Items for all in one resources secrets, configmaps, and downward API
    Projected *ProjectedVolumeSource `json:"projected,omitempty" protobuf:"bytes,26,opt,name=projected"`
    // PortworxVolume represents a portworx volume attached and mounted on kubelets host machine
    // +optional
    PortworxVolume *PortworxVolumeSource `json:"portworxVolume,omitempty" protobuf:"bytes,24,opt,name=portworxVolume"`
    // ScaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes.
    // +optional
    ScaleIO *ScaleIOVolumeSource `json:"scaleIO,omitempty" protobuf:"bytes,25,opt,name=scaleIO"`
    // StorageOS represents a StorageOS volume attached and mounted on Kubernetes nodes.
    // +optional
    StorageOS *StorageOSVolumeSource `json:"storageos,omitempty" protobuf:"bytes,27,opt,name=storageos"`
    // CSI (Container Storage Interface) represents ephemeral storage that is handled by certain external CSI drivers (Beta feature).
    // +optional
    CSI *CSIVolumeSource `json:"csi,omitempty" protobuf:"bytes,28,opt,name=csi"`
    // Ephemeral represents a volume that is handled by a cluster storage driver (Alpha feature).
    // The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts,
    // and deleted when the pod is removed.
    //
    // Use this if:
    // a) the volume is only needed while the pod runs,
    // b) features of normal volumes like restoring from snapshot or capacity
    //    tracking are needed,
    // c) the storage driver is specified through a storage class, and
    // d) the storage driver supports dynamic volume provisioning through
    //    a PersistentVolumeClaim (see EphemeralVolumeSource for more
    //    information on the connection between this volume type
    //    and PersistentVolumeClaim).
    //
    // Use PersistentVolumeClaim or one of the vendor-specific
    // APIs for volumes that persist for longer than the lifecycle
    // of an individual pod.
    //
    // Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to
    // be used that way - see the documentation of the driver for
    // more information.
    //
    // A pod can use both types of ephemeral volumes and
    // persistent volumes at the same time.
    //
    // +optional
    Ephemeral *EphemeralVolumeSource `json:"ephemeral,omitempty" protobuf:"bytes,29,opt,name=ephemeral"`
}

// PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace.
// This volume finds the bound PV and mounts that volume for the pod. A
// PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another
// type of volume that is owned by someone else (the system).
type PersistentVolumeClaimVolumeSource struct {
    // ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume.
    // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
    ClaimName string `json:"claimName" protobuf:"bytes,1,opt,name=claimName"`
    // Will force the ReadOnly setting in VolumeMounts.
    // Default false.
    // +optional
    ReadOnly bool `json:"readOnly,omitempty" protobuf:"varint,2,opt,name=readOnly"`
}

// PersistentVolumeSource is similar to VolumeSource but meant for the
// administrator who creates PVs. Exactly one of its members must be set.
type PersistentVolumeSource struct {
    // GCEPersistentDisk represents a GCE Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod. Provisioned by an admin.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
    // +optional
    GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk,omitempty" protobuf:"bytes,1,opt,name=gcePersistentDisk"`
    // AWSElasticBlockStore represents an AWS Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
    // +optional
    AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore,omitempty" protobuf:"bytes,2,opt,name=awsElasticBlockStore"`
    // HostPath represents a directory on the host.
    // Provisioned by a developer or tester.
    // This is useful for single-node development and testing only!
    // On-host storage is not supported in any way and WILL NOT WORK in a multi-node cluster.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath
    // +optional
    HostPath *HostPathVolumeSource `json:"hostPath,omitempty" protobuf:"bytes,3,opt,name=hostPath"`
    // Glusterfs represents a Glusterfs volume that is attached to a host and
    // exposed to the pod. Provisioned by an admin.
    // More info: https://examples.k8s.io/volumes/glusterfs/README.md
    // +optional
    Glusterfs *GlusterfsPersistentVolumeSource `json:"glusterfs,omitempty" protobuf:"bytes,4,opt,name=glusterfs"`
    // NFS represents an NFS mount on the host. Provisioned by an admin.
    // More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
    // +optional
    NFS *NFSVolumeSource `json:"nfs,omitempty" protobuf:"bytes,5,opt,name=nfs"`
    // RBD represents a Rados Block Device mount on the host that shares a pod's lifetime.
    // More info: https://examples.k8s.io/volumes/rbd/README.md
    // +optional
    RBD *RBDPersistentVolumeSource `json:"rbd,omitempty" protobuf:"bytes,6,opt,name=rbd"`
    // ISCSI represents an ISCSI Disk resource that is attached to a
    // kubelet's host machine and then exposed to the pod. Provisioned by an admin.
    // +optional
    ISCSI *ISCSIPersistentVolumeSource `json:"iscsi,omitempty" protobuf:"bytes,7,opt,name=iscsi"`
    // Cinder represents a cinder volume attached and mounted on kubelets host machine.
    // More info: https://examples.k8s.io/mysql-cinder-pd/README.md
    // +optional
    Cinder *CinderPersistentVolumeSource `json:"cinder,omitempty" protobuf:"bytes,8,opt,name=cinder"`
    // CephFS represents a Ceph FS mount on the host that shares a pod's lifetime
    // +optional
    CephFS *CephFSPersistentVolumeSource `json:"cephfs,omitempty" protobuf:"bytes,9,opt,name=cephfs"`
    // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
    // +optional
    FC *FCVolumeSource `json:"fc,omitempty" protobuf:"bytes,10,opt,name=fc"`
    // Flocker represents a Flocker volume attached to a kubelet's host machine and exposed to the pod for its usage. This depends on the Flocker control service being running
    // +optional
    Flocker *FlockerVolumeSource `json:"flocker,omitempty" protobuf:"bytes,11,opt,name=flocker"`
    // FlexVolume represents a generic volume resource that is
    // provisioned/attached using an exec based plugin.
    // +optional
    FlexVolume *FlexPersistentVolumeSource `json:"flexVolume,omitempty" protobuf:"bytes,12,opt,name=flexVolume"`
    // AzureFile represents an Azure File Service mount on the host and bind mount to the pod.
    // +optional
    AzureFile *AzureFilePersistentVolumeSource `json:"azureFile,omitempty" protobuf:"bytes,13,opt,name=azureFile"`
    // VsphereVolume represents a vSphere volume attached and mounted on kubelets host machine
    // +optional
    VsphereVolume *VsphereVirtualDiskVolumeSource `json:"vsphereVolume,omitempty" protobuf:"bytes,14,opt,name=vsphereVolume"`
    // Quobyte represents a Quobyte mount on the host that shares a pod's lifetime
    // +optional
    Quobyte *QuobyteVolumeSource `json:"quobyte,omitempty" protobuf:"bytes,15,opt,name=quobyte"`
    // AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.
    // +optional
    AzureDisk *AzureDiskVolumeSource `json:"azureDisk,omitempty" protobuf:"bytes,16,opt,name=azureDisk"`
    // PhotonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine
    PhotonPersistentDisk *PhotonPersistentDiskVolumeSource `json:"photonPersistentDisk,omitempty" protobuf:"bytes,17,opt,name=photonPersistentDisk"`
    // PortworxVolume represents a portworx volume attached and mounted on kubelets host machine
    // +optional
    PortworxVolume *PortworxVolumeSource `json:"portworxVolume,omitempty" protobuf:"bytes,18,opt,name=portworxVolume"`
    // ScaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes.
    // +optional
    ScaleIO *ScaleIOPersistentVolumeSource `json:"scaleIO,omitempty" protobuf:"bytes,19,opt,name=scaleIO"`
    // Local represents directly-attached storage with node affinity
    // +optional
    Local *LocalVolumeSource `json:"local,omitempty" protobuf:"bytes,20,opt,name=local"`
    // StorageOS represents a StorageOS volume that is attached to the kubelet's host machine and mounted into the pod
    // More info: https://examples.k8s.io/volumes/storageos/README.md
    // +optional
    StorageOS *StorageOSPersistentVolumeSource `json:"storageos,omitempty" protobuf:"bytes,21,opt,name=storageos"`
    // CSI represents storage that is handled by an external CSI driver (Beta feature).
    // +optional
    CSI *CSIPersistentVolumeSource `json:"csi,omitempty" protobuf:"bytes,22,opt,name=csi"`
}


参考资料