一文读懂 K8s 持久化存储流程

2020-04-26 15:19:31 +08:00
 AlibabaSS

作者 | 孙志恒(惠志)  阿里巴巴开发工程师

导读:众所周知,K8s 的持久化存储( Persistent Storage )保证了应用数据独立于应用生命周期而存在,但其内部实现却少有人提及。K8s 内部的存储流程到底是怎样的? PV 、PVC 、StorageClass 、Kubelet 、CSI 插件等之间的调用关系又如何,这些谜底将在本文中一一揭晓。

K8s 持久化存储基础

在进行 K8s 存储流程讲解之前,先回顾一下 K8s 中持久化存储的基础概念。

1. 名词解释

2. 组件介绍

3. 持久卷使用

Kubernetes 为了使应用程序及其开发人员能够正常请求存储资源,避免处理存储设施细节,引入了 PV 和 PVC 。创建 PV 有两种方式:

下面我们以 NFS 共享存储为例来看二者区别。

静态创建存储卷

静态创建存储卷流程如下图所示:

第一步:集群管理员创建 NFS PV,NFS 属于 K8s 原生支持的 in-tree 存储类型。yaml 文件如下:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.4.1
    path: /nfs_storage

第二步:用户创建 PVC,yaml 文件如下:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

通过 kubectl get pv 命令可看到 PV 和 PVC 已绑定:

[root@huizhi ~]# kubectl get pvc
NAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s

第三步:用户创建应用,并使用第二步创建的 PVC 。

apiVersion: v1
kind: Pod
metadata:
  name: test-nfs
spec:
  containers:
  - image: nginx:alpine
    imagePullPolicy: IfNotPresent
    name: nginx
    volumeMounts:
    - mountPath: /data
      name: nfs-volume
  volumes:
  - name: nfs-volume
    persistentVolumeClaim:
      claimName: nfs-pvc

此时 NFS 的远端存储就挂载了到 Pod 中 nginx 容器的 /data 目录下。

动态创建存储卷

动态创建存储卷,要求集群中部署有 nfs-client-provisioner以及对应的 storageclass

动态创建存储卷相比静态创建存储卷,少了集群管理员的干预,流程如下图所示:

集群管理员只需要保证环境中有 NFS 相关的 storageclass 即可:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: nfs-sc
provisioner: example.com/nfs
mountOptions:
  - vers=4.1

第一步:用户创建 PVC,此处 PVC 的 storageClassName 指定为上面 NFS 的 storageclass 名称:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nfs
  annotations:
    volume.beta.kubernetes.io/storage-class: "example-nfs"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Mi
  storageClassName: nfs-sc

第二步:集群中的 nfs-client-provisioner 会动态创建相应 PV 。此时可看到环境中 PV 已创建,并与 PVC 已绑定。

[root@huizhi ~]# kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM         REASON    AGE
pvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b   10Mi        RWX           Delete          Bound       default/nfs             4s

第三步:用户创建应用,并使用第二步创建的 PVC,同静态创建存储卷的第三步。

K8s 持久化存储流程

1. 流程概览

此处借鉴 @郡宝云原生存储课程中的流程图

流程如下:

  1. 用户创建了一个包含 PVC 的 Pod,该 PVC 要求使用动态存储卷;

  2. Scheduler根据 Pod 配置、节点状态、PV 配置等信息,把 Pod 调度到一个合适的 Worker 节点上;

  3. PV 控制器watch 到该 Pod 使用的 PVC 处于 Pending 状态,于是调用 Volume Plugin( in-tree )创建存储卷,并创建 PV 对象( out-of-tree 由 External Provisioner 来处理);

  4. AD 控制器发现 Pod 和 PVC 处于待挂接状态,于是调用 **Volume Plugin **挂接存储设备到目标 Worker 节点上

  5. 在 Worker 节点上,Kubelet 中的 Volume Manager等待存储设备挂接完成,并通过 Volume Plugin将设备挂载到全局目录/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name](以 iscsi 为例);

  6. **Kubelet **通过 Docker 启动 Pod 的 Containers,用 bind mount 方式将已挂载到本地全局目录的卷映射到容器中。

更详细的流程如下:

2. 流程详解

不同 K8s 版本,持久化存储流程略有区别。本文基于 Kubernetes 1.14.8 版本。

从上述流程图中可看到,存储卷从创建到提供应用使用共分为三个阶段:Provision/Delete 、Attach/Detach 、Mount/Unmount 。

provisioning volumes

PV 控制器中有两个 Worker:

PV 状态迁移( UpdatePVStatus ):

PVC 状态迁移( UpdatePVCStatus ):

Provisioning 流程如下(此处模拟用户创建一个新 PVC ):

静态存储卷流程( FindBestMatch )PV 控制器首先在环境中筛选一个状态为 Available 的 PV 与新 PVC 匹配。

动态存储卷流程( ProvisionVolume ):若环境中没有合适的 PV,则进入动态 Provisioning 场景:

deleting volumes

Deleting 流程为 Provisioning 的反操作

用户删除 PVC,删除 PV 控制器改变 PV.Status.Phase 为 Released 。

当 PV.Status.Phase == Released 时,PV 控制器首先检查 Spec.PersistentVolumeReclaimPolicy 的值,为 Retain 时直接跳过,为 Delete 时:

Attaching Volumes

Kubelet 组件和 AD 控制器都可以做 attach/detach 操作,若 Kubelet 的启动参数中指定了--enable-controller-attach-detach,则由 Kubelet 来做;否则默认由 AD 控制起来做。下面以 AD 控制器为例来讲解 attach/detach 操作。

AD 控制器中有两个核心变量

Attaching 流程如下

AD 控制器根据集群中的资源信息,初始化 DSW 和 ASW 。

AD 控制器内部有三个组件周期性更新 DSW 和 ASW:

in-tree attaching:1. in-tree 的 Attacher 会实现 AttachableVolumePlugin 接口的 NewAttacher 方法,用来返回一个新的 Attacher ; 2. AD 控制器调用 Attacher 的 Attach 函数进行设备挂接; 3. 更新 ASW 。

out-of-tree attaching:1. 调用 in-tree 的 CSIAttacher 创建一个 **VolumeAttachement ( VA )**对象,该对象包含了 Attacher 信息、节点名称、待挂接 PV 信息; 2. External Attacher 会 watch 集群中的 VolumeAttachement 资源,发现有需要挂接的数据卷时,调用 Attach 函数,通过 gRPC 调用 CSI 插件的 ControllerPublishVolume 接口。

findAndRemoveDeletedPods - 遍历所有 DSW 中的 Pods,若其已从集群中删除则从 DSW 中移除; findAndAddActivePods - 遍历所有 PodLister 中的 Pods,若 DSW 中不存在该 Pod 则添加至 DSW 。

Detaching Volumes

Detaching 流程如下

in-tree detaching:1. AD 控制器会实现 AttachableVolumePlugin 接口的 NewDetacher 方法,用来返回一个新的 Detacher ; 2. 控制器调用 Detacher 的 Detach 函数,detach 对应 volume ; 3. AD 控制器更新 ASW 。

out-of-tree detaching:1. AD 控制器调用 in-tree 的 CSIAttacher 删除相关 VolumeAttachement 对象; 2. External Attacher 会 watch 集群中的 VolumeAttachement ( VA )资源,发现有需要摘除的数据卷时,调用 Detach 函数,通过 gRPC 调用 CSI 插件的 ControllerUnpublishVolume 接口; 3. AD 控制器更新 ASW 。

**Volume Manager **中同样也有两个核心变量:

Mounting/UnMounting 流程如下

全局目录( global mount path )存在的目的:块设备在 Linux 上只能挂载一次,而在 K8s 场景中,一个 PV 可能被挂载到同一个 Node 上的多个 Pod 实例中。若块设备格式化后先挂载至 Node 上的一个临时全局目录,然后再使用 Linux 中的 bind mount 技术把这个全局目录挂载进 Pod 中对应的目录上,就可以满足要求。上述流程图中,全局目录即 /var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name]

VolumeManager 根据集群中的资源信息,初始化 DSW 和 ASW 。

VolumeManager 内部有两个组件周期性更新 DSW 和 ASW:

unmountVolumes:确保 Pod 删除后 volumes 被 unmount 。遍历一遍所有 ASW 中的 Pod,若其不在 DSW 中(表示 Pod 被删除),此处以 VolumeMode=FileSystem 举例,则执行如下操作:

  1. Remove all bind-mounts:调用 Unmounter 的 TearDown 接口(若为 out-of-tree 则调用 CSI 插件的 NodeUnpublishVolume 接口);
  2. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口);
  3. 更新 ASW 。

mountAttachVolumes:确保 Pod 要使用的 volumes 挂载成功。遍历一遍所有 DSW 中的 Pod,若其不在 ASW 中(表示目录待挂载映射到 Pod 上),此处以 VolumeMode=FileSystem 举例,执行如下操作:

  1. 等待 volume 挂接到节点上(由 External Attacher or Kubelet 本身挂接);
  2. 挂载 volume 到全局目录:调用 DeviceMounter 的 MountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeStageVolume 接口);
  3. 更新 ASW:该 volume 已挂载到全局目录;
  4. bind-mount volume 到 Pod 上:调用 Mounter 的 SetUp 接口(若为 out-of-tree 则调用 CSI 插件的 NodePublishVolume 接口);
  5. 更新 ASW 。

unmountDetachDevices:确保需要 unmount 的 volumes 被 unmount 。遍历一遍所有 ASW 中的 UnmountedVolumes,若其不在 DSW 中(表示 volume 已无需使用),执行如下操作:

  1. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口);
  2. 更新 ASW 。

总结

本文先对 K8s 持久化存储基础概念及使用方法进行了介绍,并对 K8s 内部存储流程进行了深度解析。在 K8s 上,使用任何一种存储都离不开上面的流程(有些场景不会用到 attach/detach ),环境上的存储问题也一定是其中某个环节出现了故障。

容器存储的坑比较多,专有云环境下尤其如此。不过挑战越多,机遇也越多!目前国内专有云市场在存储领域也是群雄逐鹿,我们敏捷 PaaS 容器团队欢迎大侠的加入,一起共创未来!

参考链接

  1. Kubernetes 社区源码
  2. [云原生公开课] Kubernetes 存储架构及插件使用(郡宝)
  3. [云原生公开课] 应用存储和持久化数据卷 - 核心知识(至天)
  4. [ kubernetes-design-proposals ] volume-provisioning
  5. [ kubernetes-design-proposals ] CSI Volume Plugins in Kubernetes Design Doc

云原生应用团队招人啦!

阿里云原生应用平台团队目前求贤若渴,如果你满足:

简历可投递至邮箱: huizhi.szh@alibaba-inc.com ,如有疑问欢迎加微信咨询:TheBeatles1994 。

阿里巴巴云原生关注微服务、Serverless 、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

3958 次点击
所在节点    推广
1 条回复
hardysimpson1984
2020-04-26 16:13:40 +08:00
大家好,我就是惠志所在团队的 Leader,我们正在开放招聘哦,详见招聘贴: https://v2ex.com/t/659081#reply4

也可以直接加我的微信聊

[地址]( https://i.loli.net/2020/04/03/zWacUtgkdmHXNu5.jpg)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/666288

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX