【ArchSummit】如何通过AIOps推动可量化的业务价值增长和效率提升?>>> 了解详情
写点什么

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

  • 2020-04-21
  • 本文字数:7730 字

    阅读完需:约 25 分钟

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

1 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 有两种方式:


  1. 一种是集群管理员通过手动方式静态创建应用所需要的 PV;

  2. 另一种是用户手动创建 PVC 并由 Provisioner 组件动态创建对应的 PV。


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


静态创建存储卷


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



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


apiVersion: v1kind: PersistentVolumemetadata:  name: nfs-pvspec:  capacity:    storage: 10Gi  accessModes:    - ReadWriteOnce  persistentVolumeReclaimPolicy: Retain  nfs:    server: 192.168.4.1    path: /nfs_storag
复制代码


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


apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: nfs-pvcspec:  accessModes:  - ReadWriteOnce  resources:    requests:      storage: 10Gi
复制代码


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


[root@huizhi ~]# kubectl get pvcNAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGEnfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s
复制代码


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


apiVersion: v1kind: Podmetadata:  name: test-nfsspec:  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: StorageClassapiVersion: storage.k8s.io/v1metadata:  name: nfs-scprovisioner: example.com/nfsmountOptions:  - vers=4.1
复制代码


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


kind: PersistentVolumeClaimapiVersion: v1metadata:  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 pvNAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM         REASON    AGEpvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b   10Mi        RWX           Delete          Bound       default/nfs             4s
复制代码


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

2 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


  • 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。


当 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 操作。



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 流程如下:


  1. 当 Pod 被删除,AD 控制器会 watch 到该事件。首先 AD 控制器检查 Pod 所在的 Node 资源是否包含"volumes.kubernetes.io/keep-terminated-pod-volumes"标签,若包含则不做操作;不包含则从 DSW 中去掉该 volume;

  2. 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。


Mounting/Unmounting Volumes



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。

3 总结

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


参考链接:


https://github.com/kubernetes/kubernetes


https://edu.aliyun.com/lesson_1651_13092#_13092


https://edu.aliyun.com/lesson_1651_13085#_13085


https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-provisioning.md


https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md


原文链接


https://mp.weixin.qq.com/s?__biz=MzIzNjUxMzk2NQ==&mid=2247493879&idx=2&sn=8f9dc34c8c46f70bf80121b2f155b00c&chksm=e8d41735dfa39e23ece26f4101f38000a6fda196ae3290c793163ca05428d063068d31efce9b&token=1951287211&lang=zh_CN#rd


2020-04-21 10:062555

评论

发布
暂无评论
发现更多内容

游戏夜读 | 如何制作互动剧?

game1night

认识数据产品经理(二 数据产品经理的稀缺性)

马踏飞机747

大数据 互联网 数据分析 产品经理

Linux学习-2020.05.11

Flychen

工具集系列|值得收藏的几个免费在线学习国外网站

一尘观世界

学习 工具 网站 提升

Java 为什么需要包装类

Rayjun

Java

错过了初恋,别错过WebFlux

稻草鸟人

stream Spring5 WebFlux Reactive

ShedLock:一个轻量级的定时任务协调组件

kk

定时任务 shedlock

你的团队属于部落的哪个阶段?

Yanel 说敏捷产品

敏捷 敏捷开发 敏捷精髓

Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!

牧码哥

Java spring 事务

追光逐影:读《我们这一代》

北风

Git clone过慢问题

JDoe

git

用Go替代Python在生产环境中进行数据分析

良少

人工智能 大数据 数据分析 pandas Go 语言

良好的工作习惯——及时存档、备份

小匚

工作效率

【解析+示例】2种方法,通过SpreadJS在前端实现甘特图

葡萄城技术团队

大前端 甘特图 SpreadJS 表格控件

你真的懂"看板文化"么?

Yanel 说敏捷产品

敏捷 敏捷开发 敏捷精髓

Python程序性能分析和火焰图

ElvinYang

回"疫"录(12):一“罩”难求

小天同学

疫情 回忆录 现实纪录 纪实

功不唐捐

Janenesome

读书笔记 思考 坚持

ITerm2 + Oh my ZSH + Powerlevel10k

JDoe

配置

医院陪护5天的四点感受

赵新龙

身心健康 医院

接口限流算法有哪些,看完这篇又能和面试官互扯了~

不才陈某

Java 分布式 后端

从技术层面理解对于区块链技术的10.24集体学习讲话

Tux Hu

区块链 智能合约 以太坊 加密货币 去中心化网络

带你吃透原型设计

Yanel 说敏捷产品

产品 产品经理 产品设计 产品开发 产品推荐

危机过后,「表格文档协同」需要具备什么能力?

葡萄城技术团队

大前端 开发者工具 Excel

NIO 看破也说破(三)—— 不同的IO模型

小眼睛聊技术

Java 学习 深度思考 程序员 架构

每个人都应该知道的性能参数

ElvinYang

也谈程序员的核心竞争力

我心依然

学习 程序员 竞争力 独立思考 清晰表达

如何让团队产生“多米诺骨牌”效应?

Yanel 说敏捷产品

项目管理 敏捷 敏捷开发 敏捷精髓

目光聚集之处,金钱必将追随

Tom

学习 个人成长 思考 读书

如何高效阅读

ElvinYang

DDD 实践手册(6. Bounded Context - 限界上下文)

Joshua

企业架构 设计模式 领域驱动设计 DDD 架构模式

一文读懂 K8s 持久化存储流程_容器_孙志恒(惠志)_InfoQ精选文章