2.13 PersistentVolume

2.13 PersistentVolume #

Pod 里的容器是由镜像产生的,而镜像文件本身是只读的,进程要读写磁盘只能用一个临时的存储空间,一旦 Pod 销毁,临时存储也就会立即回收释放,数据也就丢失了。

Kubernetes 的 Volume 对数据存储已经给出了一个很好的抽象,它只是定义了有这么一个 “存储卷”,而这个 “存储卷” 是什么类型、有多大容量、怎么存储,可以自由发挥。Pod 不需要关心那些专业、复杂的细节,只要设置好 volumeMounts,就可以把 Volume 加载进容器里使用。所以,由 Volume 的概念,延伸出了 PersistentVolume 对象,它专门用来表示持久存储设备,但隐藏了存储的底层实现,使用者只需要知道它能安全可靠地保管数据就可以了(由于 PersistentVolume 这个词很长,一般把它简称为 PV)。

作为存储的抽象,PV 实际上就是一些存储设备、文件系统,比如 Ceph、GlusterFS、NFS,甚至是本地磁盘,管理它们已经超出了 Kubernetes 的能力范围,所以,一般会由系统管理员单独维护,然后再在 Kubernetes 里创建对应的 PV。PV 属于集群的系统资源,是和 Node 平级的一种对象,Pod 对它没有管理权,只有使用权。

2.13.1 PersistentVolumeClaim/StorageClass #

由于不同存储设备的差异实在是太大了:有的速度快,有的速度慢;有的可以共享读写,有的只能独占读写;有的容量小,只有几百 MB,有的容量大到 TB、PB 级别等,这么多种存储设备,只用一个 PV 对象来管理不符合 “单一职责” 的原则,让 Pod 直接去选择 PV 也不灵活。所以 Kubernetes 就又增加了两个新对象,PersistentVolumeClaim 和 StorageClass,这种 “中间层” 的思想,把存储卷的分配管理过程再次细化。

PersistentVolumeClaim,简称 PVC,用来向 Kubernetes 申请存储资源。PVC 是给 Pod 使用的对象,它相当于是 Pod 的代理,代表 Pod 向系统申请 PV。一旦资源申请成功,Kubernetes 就会把 PV 和 PVC 关联在一起,这个动作叫做 “绑定”(bind)。

由于系统里的存储资源非常多,如果要 PVC 去直接遍历查找合适的 PV 也很麻烦,这里就用到了 StorageClass。StorageClass 抽象了特定类型的存储系统(比如 Ceph、NFS),在 PVC 和 PV 之间充当 “协调人” 的角色,帮助 PVC 找到合适的 PV。

2.13.2 描述 PersistentVolume #

Kubernetes 里有很多种类型的 PV,最容易的是本机存储 HostPath,它和 Docker 里挂载本地目录的 -v 参数非常类似。因为 Pod 会在集群的任意节点上运行,所以系统管理员需在每个节点上创建一个目录,它将会作为本地存储卷挂载到 Pod 里。

比如在 /tmp 里建一个名字是 host-10m-pv 的目录,表示一个只有 10MB 容量的存储设备。有了存储,就可以使用 YAML 来描述这个 PV 对象了。这里不能用 kubectl create 直接创建 PV 对象,只能用 kubectl api-resources、kubectl explain 查看 PV 的字段说明,手动编写 PV 的 YAML 描述文件😂。以下是一个 YAML 描述示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-10m-pv

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Mi
  hostPath:
    path: /tmp/host-10m-pv/

storageClassName 表示对存储类型的抽象 StorageClass。这个 PV 是我们手动管理的,名字可以任意起,这里写的是 host-test。

accessModes 定义了存储设备的访问模式,也就是虚拟盘的读写权限,和 Linux 的文件访问模式差不多,目前 Kubernetes 有 3 种:

  • ReadWriteOnce:存储卷可读可写,但只能被一个节点上的 Pod 挂载。

  • ReadOnlyMany:存储卷只读不可写,可以被任意节点上的 Pod 多次挂载。

  • ReadWriteMany:存储卷可读可写,也可以被任意节点上的 Pod 多次挂载。

这 3 种访问模式限制的对象是节点而不是 Pod,因为存储是系统级别的概念,不属于 Pod 里的进程。因为本地目录只能是在本机使用,所以这个 PV 使用了 ReadWriteOnce 访问模式。

capacity 表示存储设备的容量,这里设置为 10MB。

Kubernetes 里定义存储容量使用的是国际标准,日常习惯使用的 KB/MB/GB 的基数是 1024,要写成 Ki/Mi/Gi,如果写错了,单位不一致实际容量就会对不上。

hostPath 指定了存储卷的本地路径,也就是在节点上创建的目录。

2.13.3 描述 PersistentVolumeClaim #

有了 PV 就表示集群里有了一个持久化存储可以供 Pod 使用,需要再定义 PVC 对象,向 Kubernetes 申请存储。下面这个 YAML PVC 描述,要求使用一个 5MB 的存储设备,访问模式是 ReadWriteOnce:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: host-5m-pvc

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Mi

PVC 的 YAML 内容与 PV 很像,但它不表示实际的存储,而是一个 “申请” 或者 “声明”,spec 里的字段描述的是对存储的 “期望状态”。PVC 里的 storageClassName、accessModes 和 PV 是一样的,但不会有字段 capacity,而是要用 resources.request 表示希望要有多大的容量。

Kubernetes 会根据 PVC 里的描述,去找能够匹配 StorageClass 和容量的 PV,然后把 PV 和 PVC “绑定” 在一起,实现存储的分配。

2.13.4 使用 PersistentVolume #

当已经准备好了 PV 和 PVC,就可以让 Pod 实现持久化存储了。

host-path-pv.yml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-10m-pv

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Mi
  hostPath:
    path: /tmp/host-10m-pv/

host-path-pvc.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: host-5m-pvc

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Mi

从上面的截图可以看到,这个 PV 的容量是 10MB,访问模式是 RWO(ReadWriteOnce),StorageClass 是我们定义的 host-test,状态显示的是 Available,也就是处于可用状态,可以随时分配给 Pod 使用。

一旦 PVC 对象创建成功,Kubernetes 就会立即通过 StorageClass、resources 等条件在集群里查找符合要求的 PV,如果找到合适的存储对象就会把它俩 “绑定” 在一起。

PVC 对象申请的是 5MB,但现在系统里只有一个 10MB 的 PV,没有更合适的对象,所以 Kubernetes 也只能把这个 PV 分配出去,多出的容量就算是 “福利” 了。这两个对象的状态都是 Bound,也就是说存储申请成功,PVC 的实际容量就是 PV 的容量 10MB,而不是最初申请的容量 5MB。

如果把 PVC 的申请容量改大一些,比如改成 100MB,apply 后会看到 PVC 会一直处于 Pending 状态,意味着 Kubernetes 在系统里没有找到符合要求的存储,无法分配资源,只能等有满足要求的 PV 才能完成绑定。

2.13.5 Pod 挂载 PersistentVolume #

当 PV 和 PVC 绑定好了,有了持久化存储,就可以为 Pod 挂载存储卷。先要在 spec.volumes 定义存储卷,然后在 containers.volumeMounts 挂载进容器。因为用的是 PVC,所以要在 volumes 里用字段 persistentVolumeClaim 指定 PVC 的名字。

以下的 Pod 的 YAML 描述文件,把存储卷挂载到了 Nginx 容器的 /tmp 目录:

apiVersion: v1
kind: Pod
metadata:
  name: host-pvc-pod

spec:
  volumes:
    - name: host-pvc-vol
      persistentVolumeClaim:
        claimName: host-5m-pvc

  containers:
    - name: ngx-pvc-pod
      image: nginx:alpine
      ports:
        - containerPort: 80
      volumeMounts:
        - name: host-pvc-vol
          mountPath: /tmp

创建 Pod 并查看它的状态:

kubectl apply -f host-path-pod.yml
kubectl get pod -o wide

可以看到,这个 Pod 被 Kubernetes 调到了 worker 节点上。可以使用 kubectl exec 进入容器,查看 PV 是否确实挂载成功:

在容器的 /tmp 目录里生成了一个 a.txt 的文件,根据 PV 的定义,这个文件就应该落在 worker 节点的磁盘上,登录到 worker 节点上查看,在 worker 节点这台机器的 /tmp/host-10m-pv 目录下,里面有个 a.txt 文件,文件内容跟我们在容器中创建的一样,可以确认确实是刚才在 Pod 里生成的文件。其实并没有提前在 worker 节点上创建 /tmp/host-10m-pv 目录,但是当 Pod 运行起来时,/tmp/host-10m-pv 目录在 worker 节点上被自动创建了。

因为 Pod 产生的数据已经通过 PV 存在了磁盘上,如果 Pod 删除后再重新创建,挂载存储卷时会依然使用这个目录,数据保持不变,也就实现了持久化存储。但是,由于这个 PV 是 HostPath 类型,只在本节点存储,如果 Pod 重建时被调度到了其他节点上,那么即使加载了本地目录,也不会是之前的存储位置,持久化功能也就失效了。

所以,HostPath 类型的 PV 一般用来做测试,或者是用于 DaemonSet 这样与节点关系比较密切的应用。