命令记不住

登陆

k8s 存储快照和拓扑

## 存储快照和拓扑

1.存储快照产生的背景

为了提高数据操作的容错性,我们通常有需要对线上数据进行snapshot,以及能快速restore的能力。另外,当需要对线上数据进行快速的复制以及迁移等动作,如进行环境的复制、数据开发等功能时,都可以通过存储快照来满足需求,而 K8s 中通过 CSI Snapshotter controller 来实现存储快照的功能。

2.存储快照用户接口-Snapshot

K8s 中通过 pvc 以及 pv 的设计体系来简化用户对存储的使用,而存储快照的设计其实是仿照 pvc & pv 体系的设计思想。当用户需要存储快照的功能时,可以通过 VolumeSnapshot 对象来声明,并指定相应的 VolumeSnapshotClass 对象,之后由集群中的相关组件动态生成存储快照以及存储快照对应的对象 VolumeSnapshotContent。如下对比图所示,动态生成 VolumeSnapshotContent 和动态生成 pv 的流程是非常相似的

3.存储快照用户接口-Restore

借助 PVC 对象将其的 dataSource 字段指定为 VolumeSnapshot 对象。这样当 PVC 提交之后,会由集群中的相关组件找到 dataSource 所指向的存储快照数据,然后新创建对应的存储以及 pv 对象,将存储快照数据恢复到新的 pv 中,这样数据就恢复回来了,这就是存储快照的restore用法。

4.Topolopy-含义

拓扑是什么意思:这里所说的拓扑是 K8s 集群中为管理的 nodes 划分的一种“位置”关系,意思为:可以通过在 node 的 labels 信息里面填写某一个 node 属于某一个拓扑。

常见的有三种,这三种在使用时经常会遇到的:

  • 第一种,在使用云存储服务的时候,经常会遇到 region,也就是地区的概念,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区;
  • 第二种,比较常用的是可用区,也就是 available zone,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区;
  • 第三种,是 **hostname,**就是单机维度,是拓扑域为 node 范围,在 K8s 中常通过 label kubernetes.io/hostname 来标识,这个在文章的最后讲 local pv 的时候,会再详细描述。

上面讲到的三个拓扑是比较常用的,而拓扑其实是可以自己定义的。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的值其实就是拓扑域下不同的拓扑位置。

举个例子:可以用 **rack,**也就是机房中的机架这个纬度来做一个拓扑域。这样就可以将不同机架 (rack) 上面的机器标记为不同的拓扑位置,也就是说可以将不同机架上机器的位置关系通过 rack 这个纬度来标识。属于 rack1 上的机器,node label 中都添加 rack 的标识,它的 value 就标识成 rack1,即 rack=rack1;另外一组机架上的机器可以标识为 rack=rack2,这样就可以通过机架的纬度就来区分来 K8s 中的 node 所处的位置。

5.存储拓扑调度产生背景

8s 中通过 PV 的 PVC 体系将存储资源和计算资源分开管理了。如果创建出来的 PV有"访问位置"的限制,也就是说,它通过 nodeAffinity 来指定哪些 node 可以访问这个 PV。为什么会有这个访问位置的限制?

因为在 K8s 中创建 pod 的流程和创建 PV 的流程,其实可以认为是并行进行的,这样的话,就没有办法来保证 pod 最终运行的 node 是能访问到 有位置限制的 PV 对应的存储,最终导致 pod 没法正常运行。这里来举两个经典的例子:

Local PV 的例子,Local PV 是将一个 node 上的本地存储封装为 PV,通过使用 PV 的方式来访问本地存储。为什么会有 Local PV 的需求呢?简单来说,刚开始使用 PV 或 PVC 体系的时候,主要是用来针对分布式存储的,分布式存储依赖于网络,如果某些业务对 I/O 的性能要求非常高,可能通过网络访问分布式存储没办法满足它的性能需求。这个时候需要使用本地存储,刨除了网络的 overhead,性能往往会比较高。但是用本地存储也是有坏处的!分布式存储可以通过多副本来保证高可用,但本地存储就需要业务自己用类似 Raft 协议来实现多副本高可用。

6.存储拓扑调度

在 K8s 中将 PV 和 PVC 的 binding 操作和动态创建 PV 的操作做了 delay,delay 到 pod 调度结果出来之后,再去做这两个操作。这样的话有什么好处?

  • 首先,如果要是所要使用的 PV 是预分配的,如 Local PV,其实使用这块 PV 的 pod 它对应的 PVC 其实还没有做绑定,就可以通过调度器在调度的过程中,结合 pod 的计算资源需求(如 cpu/mem) 以及 pod 的 PVC 需求,选择的 node 既要满足计算资源的需求又要 pod 使用的 pvc 要能 binding 的 pv 的 nodeaffinity 限制;
  • 其次对动态生成 PV 的场景其实就相当于是如果知道 pod 运行的 node 之后,就可以根据 node 上记录的拓扑信息来动态的创建这个 PV,也就是保证新创建出来的 PV 的拓扑位置与运行的 node 所在的拓扑位置是一致的,如上面所述的阿里云云盘的例子,既然知道 pod 要运行到可用区 1,那之后创建存储时指定在可用区 1 创建即可。

为了实现上面所说的延迟绑定和延迟创建 PV,需要在 K8s 中的改动涉及到的相关组件有三个:

  • PV Controller 也就是 persistent volume controller,它需要支持延迟 Binding 这个操作。
  • 另一个是动态生成 PV 的组件,如果 pod 调度结果出来之后,它要根据 pod 的拓扑信息来去动态的创建 PV。
  • 第三组件,也是最重要的一个改动点就是 kube-scheduler。在为 pod 选择 node 节点的时候,它不仅要考虑 pod 对 CPU/MEM 的计算资源的需求,它还要考虑这个 pod 对存储的需求,也就是根据它的 PVC,它要先去看一下当前要选择的 node,能否满足能和这个 PVC 能匹配的 PV 的 nodeAffinity;或者是动态生成 PV 的过程,它要根据 StorageClass 中指定的拓扑限制来 check 当前的 node 是不是满足这个拓扑限制,这样就能保证调度器最终选择出来的 node 就能满足存储本身对拓扑的限制。

这就是存储拓扑调度的相关知识。

Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明 PV 对象,既然 Local PV 只能是本地访问,就需要在声明 PV 对象的,在 PV 对象中通过 nodeAffinity 来限制我这个 PV 只能在单 node 上访问,也就是给这个 PV 加上拓扑限制。如上图拓扑的 key 用 kubernetes.io/hostname 来做标记,也就是只能在 node1 访问。如果想用这个 PV,你的 pod 必须要调度到 node1 上。

既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding,这个时候你就要通过一种手段来告诉 PV controller,什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 no-provisioner,其实就是相当于告诉 K8s 它不会去动态创建 PV,它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer,可以先简单地认为它是延迟绑定。

当用户开始提交 PVC 的时候,pv controller 在看到这个 pvc 的时候,它会找到相应的 storageClass,发现这个 BindingMode 是延迟绑定,它就不会做任何事情。

之后当真正使用这个 pvc 的 pod,在调度的时候,当它恰好调度在符合 pv nodeaffinity 的 node 的上面后,这个 pod 里面所使用的 PVC 才会真正地与 PV 做绑定,这样就保证我 pod 调度到这台 node 上之后,这个 PVC 才与这个 PV 绑定,最终保证的是创建出来的 pod 能访问这块 Local PV,也就是静态 Provisioning 场景下怎么去满足 PV 的拓扑限制。

7.存储快照的处理流程

csi 实现存储扩展主要包含两部分:

  • 第一部分是由 K8s 社区推动实现的 csi controller 部分,也就是这里的 csi-snapshottor controller 以及 csi-provisioner controller,这些主要是通用的 controller 部分;
  • 另外一部分是由特定的云存储厂商用自身 OpenAPI 实现的不同的 csi-plugin 部分,也叫存储的 driver 部分。

两部分部件通过 unix domain socket 通信连接到一起。有这两部分,才能形成一个真正的存储扩展功能。

当用户提交 VolumeSnapshot 对象之后,会被 csi-snapshottor controller watch 到。之后它会通过 GPPC 调用到 csi-plugin,csi-plugin 通过 OpenAPI 来真正实现存储快照的动作,等存储快照已经生成之后,会返回到 csi-snapshottor controller 中,csi-snapshottor controller 会将存储快照生成的相关信息放到 VolumeSnapshotContent 对象中并将用户提交的 VolumeSnapshot 做 bound。这个 bound 其实就有点类似 PV 和 PVC 的 bound 一样。

有了存储快照,如何去使用存储快照恢复之前的数据呢?前面也说过,通过声明一个新的 PVC 对象,并且指定他的 dataSource 为 Snapshot 对象,当提交 PVC 的时候会被 csi-provisioner watch 到,之后会通过 GRPC 去创建存储。这里创建存储跟之前讲解的 csi-provisioner 有一个不太一样的地方,就是它里面还指定了 Snapshot 的 ID,当去云厂商创建存储时,需要多做一步操作,即将之前的快照数据恢复到新创建的存储中。之后流程返回到 csi-provisioner,它会将新创建的存储的相关信息写到一个新的 PV 对象中,新的 PV 对象被 PV controller watch 到它会将用户提交的 PVC 与 PV 做一个 bound,之后 pod 就可以通过 PVC 来使用 Restore 出来的数据了。这是 K8s 中对存储快照的处理流程。

推荐阅读