K8S 基础知识:PersistentVolume 持久卷(Provisioner 自动创建)

Published: 2023-08-23

Tags: k8s Redis

本文总阅读量

无状态的 Pod 一般通过 Deployment 创建,而有状态的服务通过 StatefulSet 创建,例如数据库就是典型的有状态服务,本文通过部署 Redis 来测试验证如何手动、自动创建持久卷,并了解相关概念。

基础概念

PersistentVolumeClaim(PVC)持久卷声明

PVC 是一种资源对象,用于申请存储,声明需要的存储容量和访问模式等。

一般由开发者创建,系统会自动将 PVC 绑定到 PV,而后在 Pod 中指定 PVC 即可挂载使用存储。

如果是通过 StatefulSet 创建 Pod,StatefulSet 提供了 volumeClaimTemplates 字段,用于为 StatefulSet 中的每个 Pod 动态生成 PVC,跟手动创建 PVC 效果相同。

PersistentVolume(PV)持久卷

PVC 声明所需要的存储,由 PV 提供实际的存储。

PV 通过 storageClassName 声明所属的存储类,PVC 通过指定 storageClassName 选择匹配的 PV。

举个例子:管理员分别创建了 1GB、3GB、5GB 大小的 PV,并且定义这些 PV 的 storageClassName 为 “local-storage”(多个 PV 的类名可以是相同的),在 PVC 中指定 storageClassName 为 “local-storage”,当 PVC 中声明需用 2GB 存储时,系统会自动分配 3GB 的 PV 给 PVC 使用,这样我们无需关注具体的存储名,只需要声明我们对存储的要求即可。

PersistentVolume Provisioner 持久卷提供者

初步了解了 PV 和 PVC,我们知晓每个 Pod 所使用的存储通过 PVC 声明,PCV 自动绑定到 PV,如果每个 Pod 所需的 PV 都手动创建,这肯定是个枯燥和容易出错的步骤。

PersistentVolume Provisioner 插件可以解决这个问题,它可以根据 StorageClass 动态创建 PersistentVolume,实现自动创建 PV。

后文会安装 local-path-provisioner,并且创建 Pod 进行测试。

StorageClass 存储类

PVC 通过 StorageClass 来绑定到具体的 PV(更具体的说:PVC 通过指定 storageClassName 选择使用的 StorageClass,而 StorageClass 定义了供应机制和配置决定了最终为 PVC 配备的具体 PV 资源,实现了声明式的动态供应。)

StorageClass 好比于存储的分类,可以有不同维度的分组

如磁盘速度(standard - 普通本地存储、ssd - 固态硬盘存储、high-speed - 高速存储)

也可以根据环境(dev - 开发和测试环境存储、prod - 生产环境存储)

也可以根据供应商(google-disk - GCP磁盘、aws-ebs - AWS弹性块存储)等

手动创建 PersistentVolume(PV)

手动创建基于本次磁盘的 PV

pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  storageClassName: local-storage
  capacity: 
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /redis-data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - worker-01

metadata.name 定义了名为 “local-pv” 的 PV,它提供了 5GB 存储空间,存储使用的是本地磁盘的 “/redis-data” 路径。

下方会有例子创建 StorageClass 并使用 provisioner 自动创建 PV 的示例。

创建基于本地存储的 PV 时,可以通过配置中的 nodeAffinity 节点亲和性配置指定 PV 在哪些符合条件的节点创建,以上配置定义 的 PV 会被创建在名为 “worker-01” 节点。

执行

$ kubectl apply -f pv.yaml

查看

$ kubectl get pv

输出

NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    REASON   AGE
local-pv   5Gi        RWO            Retain           Available           local-storage            2m2s

当存储被使用后,状态会改变。

删除

$ kubectl delete pv local-pv

启动 StatefulSet Redis 实例

sts.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: "redis"
  replicas: 1 
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:6
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: redis-data
          mountPath: /redis-data
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "local-storage"
      resources:
        requests:
          storage: 1Gi

以上 PVC 配置声明使用 "local-storage" 存储类提供存储,需要 1GB 大小的存储空间,并且访问模式是单实例读写访问。

此处通过 volumeClaimTemplates 字段声明所需存储,其效果等同于先创建 PVC,而后在 StatefulSet 指定 PVC。

执行

$ kubectl apply -f sts.yaml

查看

$ kubectl get statefulsets

删除

$ kubectl delete statefulsets redis

Redis 启动后,可以通过命令查看 Pod 节点的 IP,而后通过 redis-cli 连接到服务

$ root@mater:~# redis-cli -h 172.11.22.33
172.11.22.33:6379> ping
PONG
172.11.22.33:6379>

写入一些数据后,Redis 存盘时会将 dump.rdb 文件存储在 worker-01 机器的 /redis-data 目录下,也可以手动写入一些数据到容器的 /data 目录进行验证

$ kubectl exec redis-statefulset-0 -- /bin/sh -c "echo 'test-data' > /data/test.txt"

在 worker-01 节点查看

root@worker-01:~# ls /redis-data/
dump.rdb  test.txt

这样 StatefulSet 类型的 Redis 就部署好了,能通过 cli 访问并且存储到了 PV 符合我们的预期,接下来通过 Provisioner 来自动创建 PV

安装 Local Path Provisioner

集群中一般默认没有 Provisioner,这里安装使用的是 Rancher 提供的 Local Path Provisioner

安装

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

可以打开 Local Path Provisioner 的仓库找最新的 Yaml 地址

输出

namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created

通过命令 kubectl get pods -n local-path-storage 可以查询到 provisioner 对应的 Deployment Pod

root@master:~/# kubectl get pods -n local-path-storage
NAME                                      READY   STATUS    RESTARTS   AGE
local-path-provisioner-8559f79bcf-2mpgv   1/1     Running   0          109s

通过命令 kubectl get storageclass 查看 StorageClass

NAME          PROVISIONER              RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path    rancher.io/local-path    Delete          WaitForFirstConsumer   false                  1h

查看日志

$ kubectl -n local-path-storage logs -f -l app=local-path-provisioner

验证 Local Path Provisioner

创建 PVC,并由 Pod 使用

$ kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pvc/pvc.yaml
$ kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pod/pod.yaml

这里是手动创建的 PVC(在文章上半部分,我们创建的 StatefulSet Redis 是通过 volumeClaimTemplates 字段生成的 PVC),它声明了 128MB 的存储空间,storageClassName 指定的名为 “local-path” 的 StorageClass,由其 Provisioner 分配 PV

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

而后 Pod 的配置如下,Pod 名为 volume-test,通过 persistentVolumeClaim 设置刚创建的 PVC,将由 PVC 通过 StorageClass 获得的 PV 绑定到 /data 目录,留意这里的 volumes 参数,一个 Pod 支持绑定多个 Volume

apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: volume-test
    image: nginx:stable-alpine
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: volv
      mountPath: /data
    ports:
    - containerPort: 80
  volumes:
  - name: volv
    persistentVolumeClaim:
      claimName: local-path-pvc

接下来查看由 Provisioner 自动创建的 PV 和刚创建的 PVC

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS        CLAIM                                    STORAGECLASS    REASON   AGE
pvc-e452f51a-abd7-4d0a-a699-eca3adca8127   128Mi      RWO            Delete           Bound         default/local-path-pvc                   local-path               1h

$ kubectl get pvc
NAME                             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
local-path-pvc                   Bound    pvc-e452f51a-abd7-4d0a-a699-eca3adca8127   128Mi      RWO            local-path      1h

PV 及 PVC 中输出的 STATUS = Bound 表示 PVC 成功找到匹配的 PV 并被绑定。

查看 Pod 并通过 Pod 写入数据到存储

$ kubectl get pod
NAME                                   READY   STATUS      RESTARTS      AGE
volume-test                            1/1     Running     0             1h

$ kubectl exec volume-test -- sh -c "echo local-path-test > /data/test"

在 /opt/local-path-provisioner 目录下可以找到生成的文件夹,我这里示例文件夹如下

root@master:~# cat /opt/local-path-provisioner/pvc-e452f51a-abd7-4d0a-a699-eca3adca8127_default_local-path-pvc/test
local-path-test

接下来初步下 provisioner 的工作原理,理解 StorageClass

Provisioner 工作流程

  • Provisioner Pod 向 K8s 的 API Server 注册自己支持的 provisioner 类型
  • StorageClass 通过 provisioner 字段指定要使用的 provisioner(rancher.io/local-path)
  • K8s 的 Persistent Volume Controller,收到 PVC 的创建通知,匹配 provisioner 后通过 RPC 接口下发请求到 Pod
  • Provisioner Pod 根据 StorageClass 参数创建对应存储资源作为 PV
  • PV 和 PVC 绑定后,应用 Pod 即可使用该 PV 存储卷

我们打开 local-path-storage.yaml 就可以看到其定义的很多类,NameSpace、权限、Deployment、StorageClass 等,接下来摘录部分配置。

先看下 StorageClass 配置,它名为 local-path,指定 provisioner 为 rancher.io/local-path

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

另外 Rancher 提供的 Provisioner 有用到 ConfigMap,可以看到参数通过 ConfigMap 提供,未集成到 Pod 中,这样可以方便的修改配置。

kind: ConfigMap
apiVersion: v1
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-
    {
            "nodePathMap":[
            {
                    "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
                    "paths":["/opt/local-path-provisioner"]
            }
            ]
    }

nodePathMap 配置每个节点的存储路径,node 指代节点,此处配置的 DEFAULT_PATH_FOR_NON_LISTED_NODES 表示未配置的节点设置默认路径,paths 指定了 /opt/local-path-provisioner 为存储默认路径,Paths 可以指定多个路径,可以利用多磁盘空间、定义优先级、配置冗余备选,但需要注意同一个 PV 只会使用路径中的一个。

{
         "node":"worder-01",
         "paths":["/opt/local-path-provisioner", "/data1"]
 }

总结

本篇通过部署 StatefulSet Redis 进而了解到了如何手动创建本地存储卷并使用,同时安装 Local Path Provisioner 用于自动创建本地存储卷并做了验证。

存储及 StatefulSet 还有很多知识点及细节可探索,待后续学习。