9

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

 4 years ago
source link: http://dockone.io/article/10011
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

JJVVRnr.png!web

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

K8s 持久化存储基础

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

1. 名词解释

  • in-tree :代码逻辑在 K8s 官方仓库中;
  • out-of-tree :代码逻辑在 K8s 官方仓库之外,实现与 K8s 代码的解耦;
  • PV :PersistentVolume,集群级别的资源,由 集群管理员 or External Provisioner 创建。PV 的生命周期独立于使用 PV 的 Pod,PV 的 .Spec 中保存了存储设备的详细信息;
  • PVC :PersistentVolumeClaim,命名空间(namespace)级别的资源,由 用户 or StatefulSet 控制器(根据VolumeClaimTemplate) 创建。PVC 类似于 Pod,Pod 消耗 Node 资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存),而 PVC 可以请求特定存储卷的大小及访问模式(Access Mode);
  • StorageClass :StorageClass 是集群级别的资源,由集群管理员创建。SC 为管理员提供了一种动态提供存储卷的“类”模板,SC 中的 .Spec 中详细定义了存储卷 PV 的不同服务质量级别、备份策略等等;
  • CSI :Container Storage Interface,目的是定义行业标准的“容器存储接口”,使存储供应商(SP)基于 CSI 标准开发的插件可以在不同容器编排(CO)系统中工作,CO 系统包括 Kubernetes、Mesos、Swarm 等。

2. 组件介绍

  • PV Controller :负责 PV/PVC 绑定及周期管理,根据需求进行数据卷的  Provision/Delete  操作;
  • AD Controller :负责数据卷的  Attach/Detach  操作,将设备挂接到目标节点;
  • Kubelet:Kubelet 是在每个 Node 节点上运行的主要 “节点代理”,功能是 Pod 生命周期管理、容器健康检查、容器监控等;
  • Volume Manager :Kubelet 中的组件,负责管理数据卷的  Mount/Umount  操作(也负责数据卷的  Attach/Detach  操作,需配置 kubelet 相关参数开启该特性)、卷设备的格式化等等;
  • Volume Plugins :存储插件,由存储供应商开发,目的在于扩展各种存储类型的卷管理能力,实现第三方存储的各种操作能力,即是 上面蓝色操作 的实现。Volume Plugins 有 in-tree 和 out-of-tree 两种;
  • External Provioner :External Provioner 是一种 sidecar 容器,作用是调用 Volume Plugins 中的 CreateVolume 和 DeleteVolume 函数来执行  Provision/Delete  操作。因为 K8s 的 PV 控制器无法直接调用 Volume Plugins 的相关函数,故由 External Provioner 通过 gRPC 来调用;
  • External Attacher :External Attacher 是一种 sidecar 容器,作用是调用 Volume Plugins 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 函数来执行  Attach/Detach  操作。因为 K8s 的 AD 控制器无法直接调用 Volume Plugins 的相关函数,故由 External Attacher 通过 gRPC 来调用。

3. 持久卷使用

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

  • 一种是集群管理员通过手动方式 静态创建 应用所需要的 PV;
  • 另一种是用户手动创建 PVC 并由 Provisioner 组件 动态创建 对应的 PV。

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

静态创建存储卷

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

JJVVRnr.png!web

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

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 文件如下:

yaml

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: nfs-pvc

spec:

  accessModes:

  - ReadWriteOnce

  resources:

    requests:

      storage: 10Gi

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

bash

[root@huizhi ~]# kubectl get pvc

NAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE

nfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s

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

yaml

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

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

qYR3Ufi.png!web

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

yaml

kind: StorageClass

apiVersion: storage.k8s.io/v1

metadata:

  name: nfs-sc

provisioner: example.com/nfs

mountOptions:

  - vers=4.1

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

yaml

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 已绑定。

bash

[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. 流程概览

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

MNr26f2.png!web

流程如下:

  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  方式将已挂载到本地全局目录的卷映射到 容器 中。

更详细的流程如下:

JrmeYrb.png!web

2. 流程详解

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

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

provisioning volumes

bMn2Mzz.png!web

PV 控制器中有两个 Worker:

  • ClaimWorker :处理 PVC 的 add / update / delete 相关事件以及 PVC 的状态迁移;
  • VolumeWorker :负责 PV 的状态迁移。

PV 状态迁移(UpdatePVStatus):

  • PV 初始状态为 Available,当 PV 与 PVC 绑定后,状态变为 Bound;
  • 与 PV 绑定的 PVC 删除后,状态变为 Released;
  • 当 PV 回收策略为 Recycled 或手动删除 PV 的 .Spec.ClaimRef 后,PV 状态变为 Available;
  • 当 PV 回收策略未知或 Recycle 失败或存储卷删除失败,PV 状态变为 Failed;
  • 手动删除 PV 的 .Spec.ClaimRef,PV 状态变为 Available。

PVC 状态迁移(UpdatePVCStatus):

  • 当集群中不存在满足 PVC 条件的 PV 时,PVC 状态为 Pending。在 PV 与 PVC 绑定后,PVC 状态由 Pending 变为 Bound;
  • 与 PVC 绑定的 PV 在环境中被删除,PVC 状态变为 Lost;
  • 再次与一个 同名 PV  绑定后,PVC 状态变为 Bound。

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

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

  • DelayBinding :PV 控制器判断该 PVC 是否需要 延迟绑定: 1. 查看 PVC 的 annotation 中是否包含volume.kubernetes.io/selected-node,若存在则表示该 PVC 已经被调度器指定好了节点(属于  ProvisionVolume ),故不需要延迟绑定;2. 若 PVC 的 annotation 中不存在 volume.kubernetes.io/selected-node,同时没有 StorageClass,默认表示不需要延迟绑定;若有 StorageClass,查看其 VolumeBindingMode 字段,若为 WaitForFirstConsumer 则需要延迟绑定,若为 Immediate 则不需要延迟绑定;
  • FindBestMatchPVForClaim :PV 控制器尝试找一个满足 PVC 要求的环境中现有的 PV。PV 控制器会将 所有的 PV  进行一次筛选,并会从满足条件的 PV 中选择一个最佳匹配的PV。筛选规则:1. VolumeMode 是否匹配;2. PV 是否已绑定到 PVC 上;3. PV 的 .Status.Phase 是否为 Available;4. LabelSelector 检查,PV 与 PVC 的 label 要保持一致;5. PV 与 PVC 的 StorageClass 是否一致;6. 每次迭代更新最小满足 PVC requested size 的 PV,并作为最终结果返回;
  • Bind :PV 控制器对选中的 PV、PVC 进行绑定:1. 更新 PV 的 .Spec.ClaimRef 信息为当前 PVC;2. 更新 PV 的 .Status.Phase 为 Bound;3. 新增 PV 的 annotation : pv.kubernetes.io/bound-by-controller: "yes";4. 更新 PVC 的 .Spec.VolumeName 为 PV 名称;5. 更新 PVC 的 .Status.Phase 为 Bound;6. 新增 PVC 的 annotation:pv.kubernetes.io/bound-by-controller: "yes" 和 pv.kubernetes.io/bind-completed: "yes";

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

  • Before Provisioning :1. PV 控制器首先判断 PVC 使用的 StorageClass 是 in-tree 还是 out-of-tree:通过查看 StorageClass 的 Provisioner 字段是否包含  "kubernetes.io/"  前缀来判断;2. PV 控制器更新 PVC 的 annotation:claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] = storageClass.Provisioner;
  • in-tree Provisioning(internal provisioning): 1. in-tree 的 Provioner 会实现 ProvisionableVolumePlugin 接口的 NewProvisioner 方法,用来返回一个新的 Provisioner;2. PV 控制器调用 Provisioner 的 Provision 函数,该函数会返回一个 PV 对象;3. PV 控制器创建上一步返回的 PV 对象,将其与 PVC 绑定,Spec.ClaimRef 设置为 PVC,.Status.Phase 设置为 Bound,.Spec.StorageClassName 设置为与 PVC 相同的 StorageClassName;同时新增 annotation:"pv.kubernetes.io/bound-by-controller"="yes" 和 "pv.kubernetes.io/provisioned-by"=plugin.GetPluginName();
  • out-of-tree Provisioning(external provisioning): 1. External Provisioner 检查 PVC 中的 claim.Spec.VolumeName 是否为空,不为空则直接跳过该 PVC;2. External Provisioner 检查 PVC 中的 claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] 是否等于自己的 Provisioner Name(External Provisioner 在启动时会传入--provisioner 参数来确定自己的 Provisioner Name);3. 若 PVC 的 VolumeMode=Block,检查 External Provisioner 是否支持块设备;4. External Provisioner 调用 Provision 函数:通过 gRPC 调用 CSI 存储插件的 CreateVolume 接口;5. External Provisioner 创建一个 PV 来代表该 volume,同时将该 PV 与之前的 PVC 做绑定。

deleting volumes

Deleting 流程为 Provisioning 的反操作:

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

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

  • in-tree Deleting: 1. in-tree 的 Provioner 会实现 DeletableVolumePlugin 接口的 NewDeleter 方法,用来返回一个新的 Deleter;2. 控制器调用 Deleter 的 Delete 函数,删除对应 volume;3. 在 volume 删除后,PV 控制器会删除 PV 对象;
  • out-of-tree Deleting: 1. External Provisioner 调用 Delete 函数,通过 gRPC 调用 CSI 插件的 DeleteVolume 接口;2. 在 volume 删除后,External Provisioner 会删除 PV 对象

Attaching Volumes

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

vyeQVbv.png!web

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

  • DesiredStateOfWorld(DSW) :集群中预期的数据卷挂接状态,包含了 nodes->volumes->pods 的信息;
  • ActualStateOfWorld(ASW) :集群中实际的数据卷挂接状态,包含了 volumes->nodes 的信息。

Attaching 流程如下:

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

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

  • Reconciler。 通过一个 GoRoutine 周期性运行, 确保 volume 挂接/摘除完毕 。此期间不断更新 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 接口。

  • DesiredStateOfWorldPopulator。 通过一个 GoRoutine 周期性运行,主要功能是 更新 DSW:

findAndRemoveDeletedPods - 遍历所有 DSW 中的 Pods,若其已从集群中删除则从 DSW 中移除;

findAndAddActivePods - 遍历所有 PodLister 中的 Pods,若 DSW 中不存在该 Pod 则添加至 DSW。

  • PVC Worker。 watch PVC 的 add/update 事件,处理 PVC 相关的 Pod,并实时更新 DSW。

Detaching Volumes

Detaching 流程如下:

  • 当 Pod 被删除,AD 控制器会 watch 到该事件。首先 AD 控制器检查 Pod 所在的 Node 资源是否包含"volumes.kubernetes.io/keep-terminated-pod-volumes"标签,若包含则不做操作;不包含则从 DSW 中去掉该 volume;
  • AD 控制器通过  Reconciler  使 ActualStateOfWorld 状态向 DesiredStateOfWorld 状态靠近,当发现 ASW 中有 DSW 中不存在的 volume 时,会做 Detach 操作:

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。

A7FJjyi.png!web

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

  • DesiredStateOfWorld(DSW) :集群中预期的数据卷挂载状态,包含了 volumes->pods 的信息;
  • ActualStateOfWorld(ASW) :集群中实际的数据卷挂载状态,包含了 volumes->pods 的信息。

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:

  • DesiredStateOfWorldPopulator :通过一个 GoRoutine 周期性运行,主要功能是 更新 DSW;
  • Reconciler :通过一个 GoRoutine 周期性运行, 确保 volume 挂载/卸载完毕 。此期间不断更新 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

云原生应用团队招人啦!

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

  • 对容器和基础设施相关领域的云原生技术充满热情,在相关领域如 Kubernetes、Serverless 平台、容器网络与存储、运维平台等云原生基础设施其中某一方向有丰富的积累和突出成果(如产品落地,创新技术实现,开源贡献,领先的学术成果);
  • 优秀的表达能力,沟通能力和团队协作能力;对技术和业务有前瞻性思考;具备较强的 ownership,以结果为导向,善于决策;
  • 至少熟悉 Java、Golang 中的一项编程语言;
  • 本科及以上学历、3 年以上工作经验。

简历可投递至邮箱: [email protected] ,如有疑问欢迎加微信咨询:TheBeatles1994。

云原生网络研讨会邀您参加

点击立即预约直播

NnqIjqi.png!web

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

jEniiiy.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK