详解 Kubernetes 中的数据管理

作者: 王炳明 分类: Kubernetes 发布时间: 2021-04-12 08:29 热度:621

1. emptyDir Volume

准备 empty-dir.yaml

apiVersion: v1
kind: Pod
metadata:
  name: producer-consumer

spec:
  containers:
  - name: producer
    image: busybox
    resources:
      limits:
        memory: "128Mi"
        CPU: "500m"
    volumeMounts:
      - mountPath: /data
        name: pod-volume
    args:
      - /bin/sh
      - -c
      - echo "hello, world" > /data/hello; sleep 10000

  - name: comsumer
    image: busybox
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
    volumeMounts:
      - mountPath: /data
        name: pod-volume
    args:
      - /bin/sh
      - -c
      - cat /data/hello; sleep 10000

  volumes:
    - name: pod-volume
      emptyDir: {}

使用 kubectl apply -f empty-dir.yaml 创建一个拥有两个容器的 Pod,其中一个容器负责生产,往 /data/hello 中写入内容,另外一个容器负责消费,从 /data/hello 中读取内容并打印出来。

打印的内容,可以通过 kubectl logs producer-consumer comsumer 进行查看。

在容器中的 /data/hello 文件,映射到宿主机上,是放在哪个目录下呢?

你可以使用 Docker inspect <container_id> 查看到更详细的内容,其中就包括 Mounts 的内容

详解 Kubernetes 中的数据管理插图

详解 Kubernetes 中的数据管理插图(1)

如果把 Pod 给删除掉,那么这个 volume 也会随之消失,而如果是 Pod 正常关闭的话,Volume 也是会存在的,由此可见,emptyDir Volume 的生命周期与 Pod 一致。

验证结论

  1. 一个 Pod 内的所有容器可以共享 Volume ;
  2. emptyDir Volume 在宿主机上仅是个临时目录,并不具备持久性
  3. emptyDir Volume 的生命周期与 Pod 一致,Pod 没了,emptyDir Volume 也会随之消失

2. hostPath Volume

将 Node 上的目录直接挂载到 Pod 中去,一般来说,很少有应用会使用这种模式,因为这种方法,会增加 Pod 与 Node 的耦合。

但也有一些比较特殊的应用仍然需要采用这种方式,比如 kube-apiserver 和 kube-controller-manager 中就有这样的应用。

它们需要访问 Kubernetes 和 Docker 的内部数据,而这些也只有宿主机上才有。

3. 外部的 Storage Provider

K8S 支持多种主流的存储后端,比如 AWS,Azure,Ceph

下面我以 Ceph 为例,为大家展示

首先准备好 ceph-backend.yaml

apiVersion: v1
kind: Pod
metadata:
  name: using-ceph
spec:
  containers:
  - name: using-ceph
    image: busybox
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
  volumes:
    - name: ceph-volume
      cephfs:
        path: /data/hello
        monitors: "10.16.154.78:6789"
        secretFile: "/etc/ceph/admin.secret"

这种存储后端的优势在于,数据的存储完全与 K8S 集群独立开来,即使 K8S 集群崩溃了,数据也不会受影响。当然也有它的劣势, Ceph 的运维需要额外的人才投入。

4. 最优解:使用 PVC

先总结一下,以上介绍的几种方案:

  • emptyDir Volume:不能持久化数据
  • hostPath Volume:与节点强耦合,不适合大部分应用
  • 外部的 Storage Provider:在创建 Pod 前需要事先创建好 Volume 并且得到 Volume-id,无法对大规模的Volume 进行管理

可以看到他们都有自己的痛点所在,对此 Kubernetes 提出的解决方案是使用 PV (PersistentVolume)和 PVC (PersistentVolumeClaim)。

PV 是外部存储系统中的一块存储空间(具备持久性、生命周期独立于 Pod),我们可以事先创建出一堆的 PV,并且为他们打上标签(storageClassName),比如是哪一类型的存储,是 nfs 还是 ceph,是 ssd 还是 hdd,然后等到 Pod 需要用的时候,指定这个标签去创建 PVC 申请,Kubernetes 就会为其自动寻找满足条件的 PV。

对于 Pod 来说,它并不需要关注 PV 的更多细节,比如 PV 是怎样的存储资源,空间从哪里分配。

准备 pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: ssd-pv
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: ssd
  nfs:
    path: /var/lib/nfs/
    server: 192.168.56.201

使用 kubectl apply -f pv.yaml 去创建它

详解 Kubernetes 中的数据管理插图(2)

再准备 pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ssd-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  storageClassName: ssd

使用 kubectl apply -f pvc.yaml 去创建它

详解 Kubernetes 中的数据管理插图(3)

再次查看 pv, 会发现 刚刚那块 pv 已经绑定我们新创建 pvc 了,Status 为 Bound

详解 Kubernetes 中的数据管理插图(4)

然后就可以在 Pod 中直接使用这个 pvc 啦。

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: busybox
    args:
      - /bin/sh
      - -c 
      - sleep 1000
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
    volumeMounts:
      - mountPath: "/mnt/nfs"
        name: mydata
  volumes:
    - name: mydata
      persistentVolumeClaim: 
        claimName: ssd-pvc

再使用 kubectl apply -f mypod.yaml 创建它,然后在 pod 中往 nfs 目录中创建一个文件 123 ,如果出现权限问题无法创建,可以去 nfs 执行 chmod 777 /var/lib/nfs

详解 Kubernetes 中的数据管理插图(5)

在 pod 中创建 123 文件,同样在 nfs 服务器中也可以看得到,只是用户名有点奇怪(有精力的可以去 pod 中调整用户 id)。

详解 Kubernetes 中的数据管理插图(6)

5. PV 的回收机制

上面在我们创建 PV 的时候,有指定回收策略的值为 persistentVolumeReclaimPolicy: Recycle ,这意味着当 PVC 删除后,PV 里的数据也会清空。

回收策略有三种选项:

  1. Recycle :PVC 删除后,PV 里的数据也会清空。
  2. Retain:PVC 删除后,PV 会一直处于 Release ,你将 PV 也删除后,里面的数据会保留下来
  3. Delete:会删除 PV 在 Storage Provider 上对应存储空间(AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 支持,NFS 不支持)。

这里我沿着上面的场景继续演示(回收策略设置为 Recycle )当你把占用 PVC 的 Pod 删除后,再把 PVC 给删除

kubectl delete pod mypod
kcbectl delete pvc ssd-pvc

Kubernetes 就会启动一个名叫 recycler-for-{pv-name} 的 Pod 来清理PV 中的数据,在清理的过程中,PV 处于 Release 的状态,此时的 PV 还不可用,等清理完毕后,PV 的状态变成 Available ,就可以重新被申请。

6. StorageClass:卷的动态供给

卷的供给分为两种方式:

  1. 静态供给:提前创建好 PV,然后通过 PVC 进行申请使用
  2. 动态供给:直接使用 PVC 进行申请,如果没有满足条件的 PV ,会自动创建 PV

动态供给是通过 StorageClass 实现的,同时它需要有对应的 Provisioner

  • NFS:nfs-client-provisioner
  • AWS:aws-ebs

更多的 provisioner 可以查阅:provisioner

关于本地盘的支持,在官方文档中并没有看到,但在阿里的文档中有查到一些资料,分别是 LVM数据卷LocalVolume数据卷

下面以 NFS 为例,为你演示一下 StorageClass 动态供给的整个过程

准备 namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
   name: storage-class
   labels:
     name: storage-class

创建它

$ kubectl create -f namespace.yaml
$ kubectl get namespace

准备 rabc.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: storage-class
---
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
    # replace with namespace where provisioner is deployed
    namespace: storage-class
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
    # replace with namespace where provisioner is deployed
  namespace: storage-class
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: storage-class
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: storage-class
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io

再准备 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: storage-class
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  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: guopeiyuan/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes/
          env:
            - name: PROVISIONER_NAME
              value: iswbm.com/nfs
            - name: NFS_SERVER
              value: 192.168.56.201
            - name: NFS_PATH
              value: /var/lib/images
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.56.201
            path: /var/lib/images

创建它们

$ kubectl create -f deployment.yaml

准备 storageclass.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: iswbm-nfs-storage
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: iswbm.com/nfs # or choose another name, must match deployment's env PROVISIONER_NAME'
reclaimPolicy: Retain
parameters:
  archiveOnDelete: "false"

准备 pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test
  labels:
    app: test
spec:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: iswbm-nfs-storage
  resources:
    requests:
      storage: "10Mi"

再创建它

$ kubectl create -f pvc.yaml

这里要注意一下,我们在创建 pvc 之前,并没有先创建 pv,按照之前的逻辑,这时候应该失败的,但是由于我们使用 StorageClass 动态供给,k8s会自动为我们创建 10Mi 的 PV

详解 Kubernetes 中的数据管理插图(7)

当 PV 被绑定后,在 NFS 后端目录中会自动创建一个目录(命名规则是:DOMAIN + PVC_NAME + VOLUME_NAME)用来存储数据

详解 Kubernetes 中的数据管理插图(8)

创建一个 Pod 应用(mypod.yaml)来使用这个 PVC

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: busybox
    args:
      - /bin/sh
      - -c
      - sleep 1000
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
    volumeMounts:
      - mountPath: "/mnt/share"
        name: mydata
  volumes:
    - name: mydata
      persistentVolumeClaim:
        claimName: test

再执行如下命令在 Pod 里的 /mnt/share 目录下新建一个 hello 文件,就能在 NFS 后端中看到该目录啦

详解 Kubernetes 中的数据管理插图(9)

7. 本地卷的供给

宿主机上的 LVM 存储池和裸磁盘,都属于本地卷。在 Kubernetes 中,他们还不能支持动态供给。具体的内容可查看:PV、PVC体系是不是多此一举?从本地持久化卷谈起

要想使用本地卷,流程是这样子的:

  1. 需要提前针对宿主机上的本地盘,提前创建到 PV (好像有 DeamonSet 可以做到自动创建)
  2. 创建 StorageClass ,注意 provisioner 要选 kubernetes.io/no-provisioner(因为不能动态供给),并且要开启延迟绑定,将volumeBindingMode 要设置为 WaitForFirstConsumer
  3. 然后创建 PVC,此时并不会绑定任何的 PV
  4. 指定 PVC创建 Pod,这时候才会真正的绑定 PV
明哥公众号

文章有帮助,请作者喝杯咖啡?

发表评论