k8s存储 pv pvc

Pod Volumes

首先来看一下 Pod Volumes 的使用场景:

  • 场景一:如果 pod 中的某一个容器在运行时异常退出,被 kubelet 重新拉起之后,如何保证之前容器产生的重要数据没有丢失?
  • 场景二:如果同一个 pod 中的多个容器想要共享数据,应该如何去做?

以上两个场景,其实都可以借助 Volumes 来很好地解决,接下来首先看一下 Pod Volumes 的常见类型:

  1. 本地存储,常用的有 emptydir/hostpath;
  2. 网络存储:网络存储当前的实现方式有两种,一种是 in-tree,它的实现的代码是放在 K8s 代码仓库中的,随着k8s对存储类型支持的增多,这种方式会给k8s本身的维护和发展带来很大的负担;而第二种实现方式是 out-of-tree,它的实现其实是给 K8s 本身解耦的,通过抽象接口将不同存储的driver实现从k8s代码仓库中剥离,因此out-of-tree 是后面社区主推的一种实现网络存储插件的方式;
  3. Projected Volumes:它其实是将一些配置信息,如 secret/configmap 用卷的形式挂载在容器中,让容器中的程序可以通过POSIX接口来访问配置数据;
  4. PV 与 PVC 就是今天要重点介绍的内容。

image-20211218201651773

容器磁盘上的文件的生命周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先,当容器崩溃时,kubelet 会重启它,但是容器中的文件将丢失——容器以干净的状态(镜像最初的状态)重新启动。其次,在 Pod 中同时运行多个容器时,这些容器之间通常需要共享文件。Kubernetes 中的 Volume 抽象就很好的解决了这些问题。

Kubernetes 中的卷有明确的寿命 —— 与封装它的 Pod 相同。所以,卷的生命比 Pod 中的所有容器都长,当这个容器重启时数据仍然得以保存。当然,当 Pod 不再存在时,卷也将不复存在。也许更重要的是,Kubernetes 支持多种类型的卷,Pod 可以同时使用任意数量的卷。

Kubernetes 支持以下类型的卷

1
2
3
4
5
6
nfs、emptyDir、local
awsElasticBlockStore、azureDisk、azureFile
cephfs、csi、downwardAPI、fc、flocker、scaleIO
gcePersistentDisk、gitRepo、glusterfs、hostPath、iscsi
persistentVolumeClaim、projected、portworxVolume
quobyte、rbd、secret、storageos、vsphereVolume

image-20211218203502722

hostpath pod删除后目录仍然存在,emptyDir会删除

emptyDir

EmptyDir 是一个空目录,他的生命周期和所属的 Pod 是完全一致的,它用处是把同一 Pod 内的不同容器之间共享工作过程产生的文件。当 Pod 被分配给节点时,首先创建 emptyDir 卷,并且只要该 Pod 在该节点上运行,该卷就会存在。正如卷的名字所述,它最初是空的。

Pod 中的容器可以读取和写入 emptyDir 卷中的相同文件,尽管该卷可以挂载到每个容器中的相同或不同路径上。当出于任何原因从节点中删除 Pod 时,emptyDir 中的数据将被永久删除。emptyDir 的用法有:

  • 暂存空间,例如用于基于磁盘的合并排序
  • 用作长时间计算崩溃恢复时的检查点
  • Web 服务器容器提供数据时,保存内容管理器容器提取的文件除了所需的 path 属性之外,用户还可以为 hostPath 卷指定 type。使用这种卷类型是请注意,因为:
    • 由于每个节点上的文件都不同,具有相同配置(例如从 podTemplate 创建的)的 pod 在不同节点上的行为可能会有所不同。
    • 当 Kubernetes 按照计划添加资源感知调度时,将无法考虑 hostPath 使用的资源。
    • 在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入 hostPath 卷。
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
apiVersion: v1
kind: Pod
metadata:
  name: pod-demo
  namespace: default
  labels:
    app: myapp
    tier: frontend
  annotations:
    youmen.com/created-by: "youmen admin"
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
      ports:
        - name: http
          containerPort: 80
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html/

    - name: busybox
      image: busybox:latest
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - name: html
          mountPath: /data/
      command: [ "/bin/sh", "-c" ]
      args:
        - "while true; do echo $(date) >> /data/index.html; sleep 3; done"
  volumes:
    - name: html
      emptyDir: { }
1
2
3
4
5
6
7
8
$ kubectl apply -f pod_volume.demo1.yaml
curl  10.244.3.34 -s
Tue Dec 24 15:37:09 UTC 2020
Tue Dec 24 15:37:12 UTC 2020
Tue Dec 24 15:37:15 UTC 2020
Tue Dec 24 15:37:18 UTC 2020
Tue Dec 24 15:37:21 UTC 2020
Tue Dec 24 15:37:24 UTC 2020

hostPath

hostPath 为静态存储机制 - 主机目录挂载,同一 Pod 内的不同容器之间共享工作过程

hostPath 卷将主机节点的文件系统中的文件或目录挂载到集群中,hostPath 的用途如下所示:

运行需要访问 Docker 内部的容器

  • 使用 /var/lib/docker 的 hostPath

在容器中运行 cAdvisor 监控服务

  • 使用 /dev/cgroups 的 hostPath

允许 pod 指定给定的 hostPath

  • 是否应该在 pod 运行之前存在,是否应该创建,以及它应该以什么形式存在

除了所需的 path 属性之外,用户还可以为 hostPath 卷指定 type。使用这种卷类型是请注意,因为:

  • 由于每个节点上的文件都不同,具有相同配置(例如从 podTemplate 创建的)的 pod 在不同节点上的行为可能会有所不同。
  • 当 Kubernetes 按照计划添加资源感知调度时,将无法考虑 hostPath 使用的资源。
  • 在底层主机上创建的文件或目录只能由 root 写入。您需要在特权容器中以 root 身份运行进程,或修改主机上的文件权限以便写入 hostPath 卷。

image-20211017133347145

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
    - name: test-container
      image: k8s.gcr.io/test-webserver
      volumeMounts:
        - mountPath: /test-pd
          name: test-volume
  volumes:
    - name: test-volume
      hostPath:
        path: /data
        type: Directory

nfs

nfs-client-provisioner 是一个 Kubernetes 的简易 NFS 的外部 provisioner,本身不提供 NFS,需要现有的 NFS 服务器提供存储。

  • 动态 PV 又叫做动态供给,就是在创建 PVC 以后,自动创建出 PV
  • PV${namespace}-${pvcName}-${pvName} 的命名格式提供(在 NFS 服务器上)
  • PV 回收的时候以 archieved-${namespace}-${pvcName}-${pvName} 的命名格式(在 NFS 服务器上)

Kubernetes之存储 - nfs

kubernetes 中要实现动态创建 pv 必须先要创建 StorageClass,每一个 StorageClass 对应了一个 provisioner。在 kubernetes 中内置了很多 provisioner 类型可供选择,但是很难受的是没有内置的 nfsprovisioner。对普通用户而言,部署 nfs 是实现后端存储最简单直接的方法。虽然 k8s 并不提供,但其允许提供外部的 provisioner,而对应迫切需要使用 nfs 的用户,可以使用 nfs-client-provisioner 这个插件完成。

  • 获取 NFS 的配置代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 克隆项目
$ git clone https://github.com/kubernetes-retired/external-storage.git

# 进入对应目录
$ cd nfs-client/deploy/

# 文件列表如下所示
$ ls -lh
-rw-r--r-- 1 root root  225 Jan 22 14:26 class.yaml
-rw-r--r-- 1 root root 1030 Jan 22 13:17 deployment-arm.yaml
-rw-r--r-- 1 root root 1040 Jan 22 13:30 deployment.yaml
drwxr-xr-x 2 root root  214 Jan 22 13:32 objects
-rw-r--r-- 1 root root 1834 Jan 22 13:19 rbac.yaml
-rw-r--r-- 1 root root  241 Jan 22 13:55 test-claim.yaml
-rw-r--r-- 1 root root  399 Jan 22 13:38 test-pod.yaml
  • 在 K8S 中配置 NFS 插件
    • 创建一个独立 nfs-namespace.yaml 的命名空间
    • 修改 nfs-deployment.yamlnamespace 为你创建的 namespace
    • 修改 nfs-rbac.yamlnamespace 为你创建的 namespace
1
2
3
4
5
6
7
# nfs-namespace.yaml
# kubectl apply -f nfs-namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: nfs-client
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
# nfs-deployment.yaml
# kubectl apply -f nfs-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: nfs-client
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 192.168.30.40
            - name: NFS_PATH
              value: /ifs/kubernetes
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.30.40
            path: /ifs/kubernetes
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
# nfs-rbac.yaml
# kubectl apply -f nfs-rbac.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: nfs-client

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-client
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-client
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-client
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-client
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
  • 创建完成之后再来创建 StorageClass
    • provisioner 就是刚才 deployment 中的 PROVISIONER_NAME
    • 还有一个比较关键的参数是 archiveOnDelete,如果你想在删除了 pvc 之后还保留数据的话需要把这个参数改为 true,不然你删除了 pvc 同时 pv 也会删除,然后数据也会丢失。
1
2
3
4
5
6
7
8
9
10
11
# nfs-class.yaml
# kubectl apply -f nfs-class.yaml
# kubectl get storageclass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs
parameters:
  archiveOnDelete: "false"
  • 测试是否正常
    • 官方仓库提供了对应的测试配置文件,我们只需要执行即可
    • 只要有了 pvc,那么就会动态创建 pv
    • 测试创建 PVC,测试创建 POD
    • 删除测试 PVC,删除测试 POD
1
2
3
4
# 查看创建的nfs容器
$ kubectl get pods
NAME                                       READY  STATUS   RESTARTS  AGE
nfs-client-provisioner-565b4456f6-v9b97    1/1    Running  0         67s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# test-claim.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
  annotations:
    volume.beta.kubernetes.io/storage-class: "managed-nfs-storage"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# test-pod.yaml

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
    - name: test-pod
      image: gcr.io/google_containers/busybox:1.24
      command:
        - "/bin/sh"
      args:
        - "-c"
        - "touch /mnt/SUCCESS && exit 0 || exit 1"
      volumeMounts:
        - name: nfs-pvc
          mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 测试效果(并非上述服务效果)
# 用于StatefulSet服务使用: nfs服务器会自动创建pv数据
# https://www.cnblogs.com/xiangsikai/p/11424245.html

# 查看Pod数量
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 3m21s
nginx-statefulset-1 1/1 Running 0 3m16s
nginx-statefulset-2 1/1 Running 0 3m11s

# 查看动态pv,pvc存储
$ kubectl get pv, pvc

# nfs服务器会自动创建pv数据
default-www-nginx-statefulset-0-pvc-8063e4f9-c8a1-11e9-8b0e-000c29400317
default-www-nginx-statefulset-1-pvc-836c1466-c8a1-11e9-8b0e-000c29400317
default-www-nginx-statefulset-2-pvc-868a4a51-c8a1-11e9-8b0e-000c29400317

PersistentVolume

image-20211218202435294

虽然 pod 也可以直接挂载存储,但是管理不方便,特别是 pod 的数量越来越多。而且 pod 可能是由开发维护的,而存储却是由运维负责。通过 PV、PVC 分开就方便多了。

  • PersistentVolume(PV)

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

  • PersistentVolumeClaim(PVC)

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

  • StorageClass(SC)

    充当 PV 的模板,自动为 PVC 创建 PV。

    常见的情况是针对不同的 问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。 集群管理员需要能够提供不同性质的 PersistentVolume,并且这些 PV 卷之间的差别不 仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。

Kubernetes之存储

基本概念

静态 PV

image-20211218202802142

静态 Provisioning:由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些 PV;然后用户在提交自己的存储需求(也就是 PVC)的时候,K8s 内部相关组件会帮助它把 PVC 和 PV 做绑定;之后用户再通过 pod 去使用存储的时候,就可以通过 PVC 找到相应的 PV,它就可以使用了。

静态产生方式有什么不足呢?可以看到,首先需要集群管理员预分配,预分配其实是很难预测用户真实需求的。举一个最简单的例子:如果用户需要的是 20G,然而集群管理员在分配的时候可能有 80G 、100G 的,但没有 20G 的,这样就很难满足用户的真实需求,也会造成资源浪费。有没有更好的方式呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 静态的配置方式 - 一个连接NFS的PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0003
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  mountOptions:
    - noatime
    - _netdev
  nfs:
    path: /data/nfs
    server: 172.100.102.66

动态 PV

现在集群管理员不预分配 PV,他写了一个模板文件,这个模板文件是用来表示创建某一类型存储(块存储,文件存储等)所需的一些参数,这些参数是用户不关心的,给存储本身实现有关的参数。用户只需要提交自身的存储需求,也就是PVC文件,并在 PVC 中指定使用的存储模板(StorageClass)。

K8s 集群中的管控组件,会结合 PVC 和 StorageClass 的信息动态,生成用户所需要的存储(PV),将 PVC 和 PV 进行绑定后,pod 就可以使用 PV 了。通过 StorageClass 配置生成存储所需要的存储模板,再结合用户的需求动态创建 PV 对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。

image-20211218203108093

  • StorageClass: 指定一些 PV 的回收策略以及挂载选项

    • 最主要的是指定 Provisioner
  • Provisioner: 用来动态创建和管理 PV 的插件

    • 每个存储都有不同的插件来管理 PV,不同存储指定 PV 参数和是从存储里清除数据的方式都是不一样的
  • Kubernetes 内置了一些存储的 Provisioner,而一些没有内置的

    • 不支持的常用的有: NFS、CerphFS,则需要引入外部的 Provisioner 来完成
  • 外部仓库地址

    • kubernetes-retired
    • kubernetes-incubator
  • 关于 PVC 的扩容

    • 只有动态供应的 pvc 可以调整大小,供应 pvc 的存储类必须支持调整大小,即手动创建不支持
    • kubernetes 中用 NFS 做后端存储支不支持 PVC 扩容
    • 目前支持 resize 的存储卷 Azure Disk, Azure File, Glusterfs, Ceph RBD 等
    • kubernetes PVC 支持 resize 扩容

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

要启用基于存储级别的动态存储配置,集群管理员需要启用 API server 上的 DefaultStorageClass 准入控制器。例如,通过确保 DefaultStorageClass 位于 API server 组件的 –admission-control 标志,使用逗号分隔的有序值列表中,可以完成此操作。

1
2
3
4
5
6
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client-storageclass
provisioner: rookieops/nfs
allowVolumeExpansion: true

绑定

master 中的控制环路监视新的 PVC,寻找匹配的 PV(如果可能),并将它们绑定在一起。如果为新的 PVC 动态调配 PV,则该环路将始终将该 PV 绑定到 PVC。否则,用户总会得到他们所请求的存储,但是容量可能超出要求的数量。一旦 PV 和 PVC 绑定后,PersistentVolumeClaim 绑定是排他性的,不管它们是如何绑定的。PVC 跟 PV 绑定是一对一的映射。

  • 根据Labels匹配PV与PVC
  • 根据storageClassName类型进行匹配

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

pv状态流转

image-20211218215150634

image-20211218221649651

image-20211218222526702

主要分为三个阶段:

  • 第一个阶段(Create阶段)是用户提交完 PVC,由 csi-provisioner 创建存储,并生成 PV 对象,之后 PV controller 将 PVC 及生成的 PV 对象做 bound,bound 之后,create 阶段就完成了;
  • 之后用户在提交 pod yaml 的时候,首先会被调度选中某一个 合适的node,等 pod 的运行 node 被选出来之后,会被 AD Controller watch 到 pod 选中的 node,它会去查找 pod 中使用了哪些 PV。然后它会生成一个内部的对象叫 VolumeAttachment 对象,从而去触发 csi-attacher去调用csi-controller-server 去做真正的 attache 操作,attach操作调到云存储厂商OpenAPI。这个 attach 操作就是将存储 attach到 pod 将会运行的 node 上面。第二个阶段 —— attach阶段完成;
  • 然后我们接下来看第三个阶段。第三个阶段 发生在kubelet 创建 pod的过程中,它在创建 pod 的过程中,首先要去做一个 mount,这里的 mount 操作是为了将已经attach到这个 node 上面那块盘,进一步 mount 到 pod 可以使用的一个具体路径,之后 kubelet 才开始创建并启动容器。这就是 PV 加 PVC 创建存储以及使用存储的第三个阶段 —— mount 阶段。

总的来说,有三个阶段:第一个 create 阶段,主要是创建存储;第二个 attach 阶段,就是将那块存储挂载到 node 上面(通常为将存储load到node的/dev下面);第三个 mount 阶段,将对应的存储进一步挂载到 pod 可以使用的路径。这就是我们的 PVC、PV、已经通过CSI实现的卷从创建到使用的完整流程。

持久化卷

  • Capacity:这个很好理解,就是存储对象的大小;
  • AccessModes:也是用户需要关心的,就是说我使用这个 PV 的方式。它有三种使用方式。

    • 一种是单 node 读写访问;
    • 第二种是多个 node 只读访问,是常见的一种数据的共享方式;
    • 第三种是多个 node 上读写访问。

用户在提交 PVC 的时候,最重要的两个字段 —— Capacity 和 AccessModes。在提交 PVC 后,k8s 集群中的相关组件是如何去找到合适的 PV 呢?首先它是通过为 PV 建立的 AccessModes 索引找到所有能够满足用户的 PVC 里面的 AccessModes 要求的 PV list,然后根据PVC的 Capacity,StorageClassName, Label Selector 进一步筛选 PV,如果满足条件的 PV 有多个,选择 PV 的 size 最小的,accessmodes 列表最短的 PV,也即最小适合原则。

  • ReclaimPolicy:这个就是刚才提到的,我的用户方 PV 的 PVC 在删除之后,我的 PV 应该做如何处理?常见的有三种方式。

    • 第一种方式我们就不说了,现在 K8s 中已经不推荐使用了;
    • 第二种方式 delete,也就是说 PVC 被删除之后,PV 也会被删除;
    • 第三种方式 Retain,就是保留,保留之后,后面这个 PV 需要管理员来手动处理。
  • StorageClassName:StorageClassName 这个我们刚才说了,我们动态 Provisioning 时必须指定的一个字段,就是说我们要指定到底用哪一个模板文件来生成 PV ;
  • NodeAffinity:就是说我创建出来的 PV,它能被哪些 node 去挂载使用,其实是有限制的。然后通过 NodeAffinity 来声明对node的限制,这样其实对 使用该PV的pod调度也有限制,就是说 pod 必须要调度到这些能访问 PV 的 node 上,才能使用这块 PV,这个字段在我们下一讲讲解存储拓扑调度时在细说。

持久化卷声明的保护

PVC 保护的目的是确保由 pod 正在使用的 PVC 不会从系统中移除,因为如果被移除的话可能会导致数据丢失。

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

持久化卷类型

  • GCEPersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk
  • FlexVolume、Flocker、NFS、iSCSI、RBD、CephFS
  • Cinder、Glusterfs、VsphereVolume、Quobyte、FC
  • HostPath、VMware、Photon、StorageOS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv
spec:
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  mountOptions:
    - hard
    - nfsvers=4.
  capacity:
    storage: 5Gi
  nfs:
    path: /tmp
    server: 172.17.0.2

PV 访问模式

PersistentVolume 可以以资源提供者支持的任何方式挂载到主机上。如下表所示,供应商具有不同的功能,每个 PV 的访问模式都将被设置为该卷支持的特定模式。例如,NFS 可以支持多个读/写客户端,但特定的 NFS PV 可能以只读方式导出到服务器上。每个 PV 都有一套自己的用来描述特定功能的访问模式。

  • ReadWriteOnce

    • 该卷可以被单个节点以读/写模式挂载,允许运行在同一节点上的多个 Pod 访问卷
    • 在命令行中访问模式缩写为:RWO
  • ReadOnlyMany

    • 该卷可以被多个节点以只读模式挂载
    • 在命令行中访问模式缩写为:ROX
  • ReadWriteMany

    • 该卷可以被多个节点以读/写模式挂载
    • 在命令行中访问模式缩写为:RWX

图片

回收策略

  • Retain(保留)—— 用户可以手动回收资源。当 PersistentVolumeClaim 对象被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为”已释放(released),由于卷上仍然存在这前一申领人的数据,该卷还不能用于其他申领。
  • Recycle(回收)—— 废弃
  • Delete(删除)—— 删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中移除所关联的存储资产。 动态供应的卷会继承其 StorageClass 中设置的回收策略,该策略默认 为 Delete

状态

卷可以处于以下的某种状态(命令行会显示绑定到 PV 的 PVC 的名称):

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

实例演示

演示 NFS 模式下的持久化方案!

  • 安装 NFS 服务器端服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装相关工具
$ yum install -y nfs-common nfs-utils rpcbind

# 创建共享目录
$ mkdir /nfs-data
$ chmod -R 666 /nfs-data
$ chown -R nfs-data /nfs-data

# 配置挂载
$ cat /etc/exports
/nfs-data    *(rw,no_root_squash,no_all_squash,sync)

# 重启服务生效
$ systemctl start rpcbind
$ systemctl start nfs
  • 安装 NFS 客户端服务
1
2
3
4
5
6
7
8
9
10
# 安装客户端工具
$ yum install -y nfs-common nfs-utils rpcbind

# 查看服务端挂载地址
$ showmount -e 192.168.66.100
/nfs-data    *

# 尝试客户端手动挂载
$ mkdir -pv /test
$ mount -t -nfs 192.168.66.100:/nfs-data /test/
  • 部署 PV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv1
spec:
  persistentVolumeReclaimPolicy: Retain
  # 我们可以给PV起名字;比如高速/低速等
  storageClassName: nfs-fast
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Gi
  nfs:
    path: /nfs-data
    server: 192.168.66.100
# 查看当前可用PV(我们这里多创建几个)
$ kubectl get pv -n default
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM    STORAGECLASS   REASON   AGE
nfspv1    10Gi       RWO            Retain           Available            nfs-fast                3m
nfspv2    5Gi        RWO            Retain           Available            nfs-fast                4m
nfspv3    5Gi        RWO            Retain           Available            nfs-slow                5m
nfspv4    1Gi        ROX            Retain           Available            nfs-fast                6m
  • 创建服务并使用 PVC

通过如下配置文件,我们创建了三个 Nginx 的副本。但是,观察发现只有一个 web-0 的 POD 绑定到了属于自己的 PV 数据存储,而 web-2 显示没有符合自己要求的 PV,而后续的 POD 压根都没有开始创建。

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
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  clusterIP: None
  selector:
    app: nginx
  ports:
    - name: web
      port: 80

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  template:
    metadata:
      labels:
      app: nginx
    spec:
      containers:
        - name: nginx
          image: k8s.gcr.io/nginx-slim:0.1
          ports:
            - name: web
              containerPort: 80
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
      name: www
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: "nfs-fast"
        resources:
          requests:
            storage: 1Gi
# 查看当前可用PV(我们这里多创建几个)
$ kubectl get pv -n default
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                STORAGECLASS   REASON   AGE
nfspv1    10Gi       RWO            Retain           Bound       default/www-web-0    nfs-fast                3m
nfspv2    5Gi        RWO            Retain           Available                        nfs-fast                4m
nfspv3    5Gi        RWO            Retain           Available                        nfs-slow                5m
nfspv4    1Gi        ROX            Retain           Available                        nfs-fast                6m

StorageClass

StorageClass 为管理员提供了描述存储 “类” 的方法。 不同的类型可能会映射到不同的服务质量等级或备份策略,或是由集群管理员制定的任意策略

  • provisioner 每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。
  • parameters取决于制备器,可以接受不同的参数。
  • reclaimPolicy 由 StorageClass 动态创建的 PersistentVolume 会在类的 reclaimPolicy 字段中指定回收策略,可以是 Delete 或者 Retain。如果 StorageClass 对象被创建时没有指定 reclaimPolicy,它将默认为 Delete

使用方式

PV 可以看作可用的存储资源,PVC 则是对存储资源的需求,PV 和 PVC 的互相关系遵循如下图!

由于容器本身是非持久化的,因此需要解决在容器中运行应用程序遇到的一些问题。首先,当容器崩溃时,kubelet 将重新启动容器,但是写入容器的文件将会丢失,容器将会以镜像的初始状态重新开始;第二,在通过一个 Pod 中一起运行的容器,通常需要共享容器之间一些文件。Kubernetes 通过存储卷解决上述的两个问题。

在 Docker 有存储卷的概念卷,但 Docker 中存储卷只是磁盘的或另一个容器中的目录,并没有对其生命周期进行管理。Kubernetes 的存储卷有自己的生命周期,它的生命周期与使用的它 Pod 生命周期一致。因此,相比于在 Pod 中运行的容器来说,存储卷的存在时间会比的其中的任何容器都长,并且在容器重新启动时会保留数据。当然,当 Pod 停止存在时,存储卷也将不再存在。在 Kubernetes 支持多种类型的卷,而 Pod 可以同时使用各种类型和任意数量的存储卷。

图片

资源供应:Kubernetes 支持两种资源的供应模式:静态模式(Staic)和动态模式(Dynamic)。

  • 静态模式

    • 运维管理人员手动创建多个 PV,每个 PV 在定义的时需要将后端存储的特性进行设置,然后让 PVC 进行选择。
    • 静态资源下,通过 PV 和 PVC 完成绑定,并供 Pod 使用的存储管理机制。

图片

  • 动态模式

    • 运维管理人员无需手动创建 PV,而是通过 StorageClass 的设置对后端存储进行描述,标记为某种”类型(Class)”,此时要求 PVC 对存储的类型进行声明,系统将自动完成 PV 的创建及 PVC 的绑定,PVC 可以声明为 Class 为 ““,说明该 PVC 禁止使用动态模式。
    • 动态资源下,通过 StorageClass 和 PVC 完成资源动态绑定,系统自动生成 PV,并供 Pod 使用的存储管理机制。

图片

资源绑定:在用户定义好 PVC 后,系统将根据 PVC 对存储资源的请求(存储空间和访问模式)在已存在的 PV 中选择一个满足 PVC 要求的 PV,一旦找到,就将 PV 与用户定义的 PVC 进行绑定,然后用户的应用就可以使用这个 PVC 了。如果系统中没有满足 PVC 要求的 PV,PVC 则会无限期处于 Pending 状态,直到等到系统管理员创建了一个符合要求的 PV。PV 一旦绑定在某个 PVC 上,就被这个 PVC 独占,不能再与其他 PVC 进行绑定了。在这种情况下,当 PVC 申请的存储空间比 PV 的少时,整个 PV 的空间都能够为 PVC 所用,可能会造成资源的浪费。如果资源供应使用的是动态模式,则系统在 PVC 找到合适的 StorageClass 后,将会自动创建 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
# 根据Labels匹配PV与PVC
#也可以根据storageClassName类型进行匹配

# PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv2
  labels: # 这里将PV设置一个labels
    app: nfs
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    path: /data/volumes
    server: nfs1

# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-nfs
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  selector: # PVC匹配标签为app=nfs的PV
    matchLabels:
      app: nfs

# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pv-nfs-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pv-nfs-nginx
  template:
    metadata:
      labels:
        app: pv-nfs-nginx
    spec:
      containers:
      - name: pv-nfs-nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts: # 挂载,首先添加需要挂载的目录
        - name: pv-nginx # 挂载点的名称
          mountPath: /usr/share/nginx/html # 挂载点的路径
      volumes: # 绑定
      - name: pv-nginx
        persistentVolumeClaim: # 将镜像中的nginx目录挂载到下面名称的PVC中
          claimName: pvc-nfs # PVC名称

# Service
apiVersion: v1
kind: Service
metadata:
  name: nfs-pvc
  labels:
    app: pv-nfs-nginx
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: pv-nfs-nginx
[root@master storage]# kubectl get pods,svc |grep pv
pod/pv-nfs-nginx-6b4759b5b8-95vhk   1/1     Running   0          74s
pod/pv-nfs-nginx-6b4759b5b8-mjwhn   1/1     Running   0          58s
pod/pv-nfs-nginx-6b4759b5b8-p4jhn   1/1     Running   0          11s
service/nfs-pvc      NodePort    10.0.0.57    <none>        80:31850/TCP   4m23s

资源使用:Pod 使用 volume 的定义,将 PVC 挂载到容器内的某个路径进行使用。volume 的类型为 persistentVoulumeClaim,在容器应用挂载了一个 PVC 后,就能被持续独占使用。不过,多个 Pod 可以挂载同一个 PVC,应用程序需要考虑多个实例共同访问一块存储空间的问题。

资源释放:当用户对存储资源使用哪个完毕后,用户可以删除 PVC,与该 PVC 绑定的 PV 将会被标记为已释放,但还不能立刻与其他 PVC 进行绑定。通过之前 PVC 写入的数据可能还留在存储设备上,只有在清除之后该 PV 才能继续使用。

资源回收:对于 PV,管理员可以设定回收策略(Reclaim Policy)用于设置与之绑定的 PVC 释放资源之后,对于遗留数据如何处理。只有 PV 的存储空间完成回收,才能供新的 PVC 绑定和使用。

我们如果部署多个服务使用 PVC 的时候,要么对每个服务划分一个单独的 PVC 资源,要么花一个大的 PVC 资源,然后通过 subPath 字段指定分割资料目录,这样就不会造成我们数据目录的文件混淆的问题了。

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
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pv-nfs-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pv-nfs-nginx
  template:
    metadata:
      labels:
        app: pv-nfs-nginx
    spec:
      containers:
        - name: pv-nfs-nginx
          image: nginx
          ports:
            - containerPort: 80
          volumeMounts: # 挂载,首先添加需要挂载的目录
            - name: pv-nginx # 挂载点的名称
              mountPath: /usr/share/nginx/html # 挂载点的路径
              subPath: nginx-pvc
      volumes: # 绑定
        - name: pv-nginx
          persistentVolumeClaim: # 将镜像中的nginx目录挂载到下面名称的PVC中
            claimName: pvc-nfs # PVC名称

image-20211219112658979

image-20211219123110842

image-20211219164915389