Kubernetes 应用存储和持久化数据卷:存储快照与拓扑调度 电脑版发表于:2021/3/18 16:01  >#Kubernetes 应用存储和持久化数据卷:存储快照与拓扑调度 [TOC] 基本知识 ------------ ### 存储快照产生背景 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 1. 如何保证重要数据在误操作之后可以快速恢复,以提高数据操作容错率? 2. 如何能够快速进行复制,迁移重要数据的动作?如进行环境复制与数据开发等。 Kubernetes CSI Snapshotter controller正是为了解决这些需求而设计的。 </p> ### 存储快照用户接口 - snapshot  <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> k8s中通过pvc以及pv的设计体系来简化用户对存储的使用,而存储快照的设计其实是仿照 pvc & pv体系的思想设计。当用户需要存储快照功能时,可以通过 `VolumeSnapshot` 对象来声明,并指定相应的 `VolumeSnapshotClass`。如下对比图所示,动态生成 `VolumeSnapshotContent` 和动态生成 pv 的流程是非常相似的。 </p> ### 存储快照用户接口 - restore  <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 有了存储快照之后,如何将快照数据快速恢复过来呢? 如上图所示的流程,可以借助PVC对象将其的 `dataSource` 字段指定为 `VolumeSnapshot`对象。这样当 PVC 提交后,会由集群中的相关组件找到 `dataSource` 所指向的存储快照数据,然后新创建对应的存储以及 pv 对象,将存储快照数据恢复到新的pv中,这样数据就恢复回来了,这就是存储快照的restore用法。 </p> ### Topolopy-含义 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 首先了解一下拓扑是什么意思:这里所说的拓扑是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这个纬度来标识。属于 rack1 上的机器,node label 中都添加 rack 的标识,它的value就标识成 rack1,即 rack=rack1;另外一组机架上的机器可以标识为rack=rack2,这样就可以通过机架的纬度就来区分来k8s中的node所处的位置。 接下来就一起来看看拓扑再 K8s 存储中的使用。 </p> ### 存储拓扑调度产生背景 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> Kubernetes 中通过PVC&PV 体系将存储与计算分离,即使用不同的Controllers管理存储资源和计算资源。但如果创建的PV有访问 "位置" (.spec.nodeAffinity)的限制,也就是只在特定的一些Nodes上才能访问PV。原有的创建Pod的与创建PV的流程的分离,就无法保证新创建的Pod被调度到可以访问PV的Node上,最终导致Pod无法正常运行起来。 如场景一:Local PV只能在指定的Node上被Pod使用  场景二:单 Region 多 Zone K8s 集群,阿里云云盘当前只能被同一个Zone的Node上的Pod访问  </p> ### 存储拓扑调度 >1. 本质问题 PV在Binding或者Dynamic Provisioning时,并不知道使用它的Pod被会调度到哪些Node上?但PV本身的访问对Node的 "位置"(拓扑)有限制。 2. 流程改进 Binding/Dynamic Provisioning PV的操作 Delay 到 Pod 调度结果确定之后做,好处: - 对于pre-provisioned 的含有 Node Affinity的PV对象,可以在Pod运行的Node确认之后,根据Node找到合适的PV对象,然后与Pod中使用的PVC Binding,保证Pod运行的Node满足PV对访问 "位置"(拓扑)的要求。 - 对于dynamic provisioning PV 场景,在Pod运行的Node确认之后,在Pod运行的Node确认之后,可以结合Node的 "位置"(拓扑)信息创建可被该 Node 访问的PV对象 3. Kubernetes 相关组件改进 - PV Controller:支持延迟Binding操作 - Dynamic PV Provisioner:动态创建PV时要结合Pod待运行的Node的 "位置"(拓扑)信息 - Scheduler:选择Node时要考虑Pod的PVC Binding需求,也就是要结合 pre-provisioned 的 PV 的 Node Affinity 以及 dynamic provisioning 时 PVC 指定 StorageClass.AllowedTopologies的限制 ### Volume Snapshot/Restore示例 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 首先需要集群管理员,在集群中创建 VolumeSnapshotClass 对象,VolumeSnapshotClass 中一个重要字段就是 Snapshot,它是指定真正创建存储快照所使用的卷插件,这个卷插件是需要提前部署的,稍后再说这个卷插件。 接下来用户他如果要做真正的存储快照,需要声明一个 VolumeSnapshotClass,VolumeSnapshotClass 首先它要指定的是 VolumeSnapshotClassName,接着它要指定的一个非常重要的字段就是 source,这个 source 其实就是指定快照的数据源是啥。这个地方指定 name 为 disk-pvc,也就是说通过这个 pvc 对象来创建存储快照。提交这个 VolumeSnapshot 对象之后,集群中的相关组件它会找到这个 PVC 对应的 PV 存储,对这个 PV 存储做一次快照。 有了存储快照之后,那接下来怎么去用存储快照恢复数据呢?这个其实也很简单,通过声明一个新的 PVC 对象并在它的 spec 下面的 DataSource 中来声明我的数据源来自于哪个 VolumeSnapshot,这里指定的是 disk-snapshot 对象,当我这个 PVC 提交之后,集群中的相关组件,它会动态生成新的 PV 存储,这个新的 PV 存储中的数据就来源于这个 Snapshot 之前做的存储快照。 </p> ```yaml # 创建VolumeSnapshotClass对象 apiVersion: snapshot.storage.k8s.io/v1alpha1 kind: VolumeSnapshotClass metadata: name: disk-snapshotclass snapshotter: diskplugin.csi.alibabacloud.com # 指定Volume Snapshot时使用的Volume Plugin ``` ```yaml # 创建VolumeSnapshot对象 apiVersion: snapshot.storage.k8s.io/v1alpha1 kind: VolumeSnapshot metadata: name: disk-snapshot spec: snapshotClassName: disk-snapshotclass source: name: disk-pvc # Snapshot的数据源 kind: PersistentVolumeClaim ``` ```yaml # 从snapshot中恢复数据到新生成的PV对象中 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: restore-pvc spec: dataSource: name: disk-snapshot kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 20Gi storageClassName: csi-disk ``` ### Local PV示例 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明PV对象,即然Local PV只能是本地访问,就需要在声明PV对象的,在PV对象中通过`nodeAffinity`来限制我中国PV只能在单`node`上访问也就是给中国PV加上拓扑限制。如下列代码所示,`key`用`kubernetes.io/hostname`来做标记,也就是只能在`node1`访问。如果想用这个PV,你的`pod`必须要调度到`node1`上。 </p> ```yaml # 创建Local PV对象 apiVersion: v1 kind: PersistentVolume metadata: name: local-pv spec: capacity: storage: 60Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: path: /share nodeAffinity: # 限制该PV只能再node1上被使用 required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node1 ``` <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 即然是静态创建PV的方式,这里为什么还需要 `storageClassName` 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那即然是延迟绑定,当用户在写完PVC提交之后,即使集群中有相关的PV能与它匹配,也就是说 `PV controller` 不能马上去做binding,这个时候你就要通过一种手段来告诉`PV controller`,声明情况下是不能立即做binding。这里的`storageClass`就是为了起到这个作用,我们可以可以看到 `storageClass` 里面的 `provisioner` 指定的是 `no-provisioner`,其实就是相当于告诉 k8s 它不会去动态创建 PV,它主要用到 `storageclass` 的 `VolumeBindingMode` 字段,叫 `WaitForFirstConsumer`,可以先简单地认为他是延迟绑定。 </p> ```yaml # 创建一个no-provisioner StorageClass对象,目的是告诉PV # Controller遇到 .spec.storageClassName 为 local-storage的 # PVC暂不做Binding对象 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer # deley binding ``` <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 当用户开始提交 PVC 的时候,`PV Controller`在看到这个pvc的时候,它会找到相应的 `storageClass`,发现这个`BindingMode`是延迟绑定,它就不会做任何事情。 之后当真正使用这个 pvc 的 pod,在调度的时候,当它恰好调度在符合 `PV Nodeaffinity` 的 node 的上面后,这个pod里面所使用的PVC才会真正地与PV做绑定,这样就保证我pod调度到这台node上之后,这个PV`才与这个PV绑定,最终保证的是创建出来的pod能访问这块Local PV,也就是静态 `Provisioning` 场景下怎么去满足PV的拓扑限制。 </p> ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: local-pvc spec: storageClassName: local-storage accessModes: - ReadWriteOnce resources: requests: storage: 60Gi ``` ### 限制Dynamic Provisioning PV拓扑示例 <p style=" font-weight: 400; line-height: 1.5; color: #212529; -webkit-tap-highlight-color: transparent; box-sizing: border-box; padding: 0px 20px 20px 20px; border: 1px solid #e9ecef; border-left-width: .25rem; border-radius: .25rem; display: block; border-left-color: #5bc0de;"> 动态就是指动态创建 PV 就有拓扑位置的限制,那怎么去指定? 首先在`storageclass`还是需要指定 `BindingMode`,就是 `WaitForFirstConsumer`,就是延迟绑定。 其次特别重要的一个字段就是 `allowedTopologies`,限制就在这个地方。上图中可用看到拓扑限制是可用区的级别,这里其实有两层意思: 1. 第一层意思就是说我创建PV的时候,创建出来的PV必须是在中国可用区可以访问的; 2. 第二层含义是因为声明的是延迟绑定,那调度器在发现使用它的PVC正好对应的是该 `storageclass`的时候,调度 pod 就要选择位于该可用区的nodes。 总之,就是要从两方面保证,一是动态创建出来的存储时要能被中国可用区访问的,二是我调度器在选择node的时候,要落在中国可用区内的,这样的话就保证我的存储和我要使用存储的这个 pod 它所对应的 node,它们之间的拓扑域在同一个拓扑域,用户在写 PVC 文件的时候,写法是跟以前的写法是一样的,主要是在 `storageclass` 中要做一些拓扑限制。 </p> ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-disk provisioner: diskplugin.csi.alibabacloud.com parameters: regionId: cn-hangzhou fsType: ext4 type: cloud_ssd volumeBindingMode: WaitForFirstConsumer allowedTopologies: - matchLabelExpressions: # 拓扑域限制: 动态创建的PV只能在可用区 cn-hangzhou-d被使用 - key: topology.diskplugin.csi.alibabacloud.com/zone values: - cn-hangzhou-d reclaimPolicy: Delete ``` ```yaml # 当该PVC对象被创建之后由于对应StorageClass的BindingMode为 # WaitForFirstConsumer并不会马上动态生成PV对象,耳塞要等到使用 # 该PVC对象的第一个Pod调度结果出来之后,而且kube-scheduler在调度 # Pod的时候去选择满足StorageClass.allowedTopologies中指定的 # 拓扑限制的Nodes apiVersion: v1 kind: PersistentVolumeClaim metadata: name: disk-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 30Gi storageClassName: csi-disk ```