本文由 CNCF + Alibaba 云原生技术公开课 整理而来
基本知识
- 存储快照产生背景:
在使用存储时,为了提高数据操作的容错性,我们通常有需要对线上数据进行 Snapshot
,以及能快速 Restore
的能力。另外,当需要对线上数据进行快速的复制以及迁移等动作,如进行环境的复制、数据开发等功能时,都可以通过存储快照来满足需求,而 Kubernetes 中通过 CSI Snapshotter Controller
来实现存储快照的功能。
Kubernetes 支持三种类型的卷插件:in-tree
、Flex
和 CSI
,仅 CSI
驱动程序支持快照(不适用于 in-tree
或 Flex
)。要使用 Kubernetes 快照功能,请确保在集群上部署实现快照的 CSI
驱动程序。
- 存储快照用户接口-
Snapshot
:
Kubernetes 中通过 PVC
以及 PV
的设计体系来简化用户对存储的使用,而存储快照的设计其实是仿照 PVC
& PV
体系的设计思想。当用户需要存储快照的功能时,可以通过 VolumeSnapshot
对象来声明,并指定相应的 VolumeSnapshotClass
对象,之后由集群中的相关组件动态生成存储快照以及存储快照对应的对象 VolumeSnapshotContent
。
VolumeSnapshotContent
是一种快照,从已提供的集群中的 Volume
获取,就像 PV
是集群中的资源一样,它也是集群中的资源。
VolumeSnapshot
是用户对快照 VolumeSnapshotContent
对象的请求,类似于 PVC
。
VolumeSnapshotClass
是管理员创建用于生成快照的模板, 允许指定属于 VolumeSnapshot
的不同属性,类似于 SC
。
动态生成 VolumeSnapshotContent
和动态生成 PV
的流程是非常相似的。
User → Cluster Admin → Dynamic Provisioning
存储:PVC → StorageClass → PV
快照:VolumeSnapshot → VolumeSnapshotClass → VolumeSnapshotContent
- 存储快照用户接口-
Restore
:
有了存储快照之后,如何将快照数据快速恢复过来呢?
PersistentVolumeClaim
的扩展字段 .spec.dataSource
可以指定为 VolumeSnapshot
对象,从而根据 PersistentVolumeClaim
对象生成新的 PersistentVolume
对象,对应的存储数据是从 VolumeSnapshot
关联的 VolumeSnapshotContent
对象中 Restore
出来的。
借助 PVC
对象将其扩展字段 .spec.dataSource
指定为 VolumeSnapshot
对象。这样当 PVC
提交之后,会由集群中的相关组件找到所指向的存储快照数据,然后将存储快照数据恢复到新创建对应的存储及 PV
对象,这样数据就恢复过来了,这就是存储快照的 Restore
。
- 拓扑含义:
这里说的拓扑是 Kubernetes 集群中为管理的 Node
划分的一种“位置”关系,可以理解为:两个 Node
只要它们的 labels
中有一个相同的 label
的,那么这两个 Node
就属于同一个拓扑。
常见的有三种:
第一种,在使用云存储服务时经常会遇到 region,也就是地区的概念,拓扑域为 Region 范围,Kubernetes 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。
这个是为了标识单个 Kubernetes 集群管理的跨 region 的 Nodes 到底属于哪个地区
第二种,比较常用的是可用区,也就是 available zone,拓扑域为 Zone 范围,Kubernetes 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。
这个是为了标识单个 Kubernetes 集群管理的跨 zone 的 Nodes 到底属于哪个可用区
第三种,是 hostname,也就是单机维度,拓扑域为 Node 范围,Kubernetes 中常通过 label kubernetes.io/hostname 来标识
上面三个拓扑是比较常见的,而拓扑还可以自定义。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的 value 其实就是 拓扑域下不同的拓扑位置。
- 存储拓扑调度产生背景:
kubernetes 中通过 PVC
& PV
体系将计算资源和存储资源分开管理了。如果创建出来的 PV
有“访问位置”的限制,即它通过 .spec.nodeAffinity
来指定哪些 Node
可以访问自己,而 kubernetes 中创建 Pod
的流程和创建 PV
的流程可以认为是并行进行的,这样就没办法保证 Pod
最终运行的 Node
能够访问到有“位置限制”的 PV
对应的存储,最终导致 Pod
无法运行。
Kubernetes 中怎样去解决这个问题呢?Kubernetes 中将 PV
、PVC
的绑定操作和动态创建 PV
的操作做了延迟,延迟到 Pod
调度结果出来之后,再去完成 PV
、PVC
的绑定操作和动态创建 PV
的操作。
这样做的好处在于:
首先,如果要使用的 PV 是预分配的,如 Local PV,其实使用这块 PV 的 Pod 它对应的 PVC 其实还没有做绑定,就可以通过调度器在调度的过程中,
结合 Pod 的计算资源需求(如 cpu/mem) 以及 Pod 的 PVC 需求,选择的 Node 既要满足计算资源的需求又要 Pod 使用的 PVC 要能绑定的 PV 的 nodeAffinity 限制
其次,对动态生成 PV 的场景其实就相当于是如果知道 Pod 运行的 Node 之后,就可以根据 Node 上记录的拓扑信息来动态的创建这个 PV,也就是保证新创建出来的 PV 的拓扑位置
与运行的 Node 所在的拓扑位置是一致的。假设知道 Pod 要运行到可用区 1,那之后创建存储时指定在可用区 1 创建即可
为了实现延迟绑定和延迟创建 PV
,需要在 Kubernetes 中的改动涉及到的组件有三个:
首先,PV Controller 需要支持延迟绑定操作
其次,动态生成 PV 的组件,如果 Pod 调度结果出来之后,它要根据 Pod 的拓扑信息去动态地创建 PV
最后,Kube-Scheduler 在为 Pod 选择 Node 时,不仅要考虑 Pod 对计算资源的需求,还要考虑 Pod 对存储的需求。
也就是根据 Pod 的 PVC 判断当前要选择的 Node ,能否满足能和 PVC 匹配的 PV 的 nodeAffinity;或者是动态生成 PV 的过程,
它要根据 StorageClass 中指定的拓扑限制来检查当前的 Node 是否满足这个拓扑先知,保证调度器最终选择出来的 Node 满足存储本身对拓扑的限制。
存储快照用法
Volume Snapshot/Restore
示例:
csi-hostpath-snapclass VolumeSnapshotClass
yaml 文件示例:
apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeSnapshotClass
metadata:
name: csi-hostpath-snapclass
annotations:
snapshot.storage.kubernetes.io/is-default-class: "true"
driver: hostpath.csi.k8s.io
deletionPolicy: Delete
parameters:
文件解析:
apiVersion: v1 表示 VolumeSnapshotClass 当前所属的组是 snapshot.storage.k8s.io,版本是 v1beta1
kind 表示 Kubernetes 资源类型是 VolumeSnapshotClass
metadata 表示 VolumeSnapshotClass 的元数据,元数据通常包含 name、labels、annotations 等
driver 表示配置 VolumeSnapshot 的 CSI Volume Plugin
deletionPolicy 表示 VolumeSnapshotClass 的删除策略,可以是 Retain 或 Delete
parameters 表示 driver 的参数
每个 VolumeSnapshotClass
都包含 driver
、deletionPolicy
和 parameters
字段, 在需要动态配置属于该 VolumeSnapshotClass
的 VolumeSnapshot
时使用。
VolumeSnapshotClass
必须指定 driver
字段,用于确定配置 VolumeSnapshot
的 CSI Volume Plugin
。parameters
用于指定 CSI Volume Plugin
特有的参数。
VolumeSnapshotClass
具有 deletionPolicy
属性。用于配置当所绑定的 VolumeSnapshot
对象将被删除时,如何处理 VolumeSnapshotContent
对象。
如果删除策略是 Delete
,那么底层的存储快照会和 VolumeSnapshotContent
对象 一起删除。如果删除策略是 Retain
,那么底层快照和 VolumeSnapshotContent
对象都会被保留。
disk-snapshot VolumeSnapshot
yaml 文件示例:
apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeSnapshot
metadata:
name: new-snapshot-test
spec:
volumeSnapshotClassName: csi-hostpath-snapclass
source:
persistentVolumeClaimName: pvc-test
apiVersion: v1 表示 VolumeSnapshot 当前所属的组是 snapshot.storage.k8s.io,版本是 v1beta1
kind 表示 Kubernetes 资源类型是 VolumeSnapshot
metadata 表示 VolumeSnapshot 的元数据,元数据通常包含 name、namespace、labels、annotations 等
spec 表示 VolumeSnapshot 的期望状态;.spec.volumeSnapshotClassName 表示指定的 VolumeSnapshotClass 对象;.spec.source 表示指定的快照数据源 PersistentVolumeClaim
restore-pvc PersistentVolumeClaim
yaml 文件示例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restore-pvc
spec:
dataSource:
name: new-snapshot-test #指定 VolumeSnapshot 名称
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: csi-disk
当在 PVC
中指定 .spec.dataSource.name
字段为 VolumeSnapshot
对象名称,即声明 PVC
对象的数据源来源于哪个 VolumeSnapshot
对象,会由集群中的相关组件动态生成新的 PV
,这个新的 PV
存储中的数据就来源于 VolumeSnapshot
绑定的 VolumeSnapshotContent
对象。
注意:
VolumeSnapshotContent
和VolumeSnapshotClass
不区分 namespace;VolumeSnapshot
区分 namespace。
Static Provisioning PV
限制拓扑示例:
local-storage StorageClass
yaml 文件示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
local-pvc PersistentVolumeClaim
yaml 文件示例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-pvc
spec:
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
local-pv PersistentVolume
yaml 文件示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /share
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node1
Local PV
大部分都会先去声明 PV
对象,如果在 PV
对象中通过 nodeAffinity
来限制 PV
只能在单个 Node
上访问,即给这个 PV
加上拓扑限制。
上面会创建一个 no-provisioner StorageClass
对象,它的 provisioner
字段指定的是 no-provisioner,相当于告诉 Kubernetes 它不会去动态创建 PV
,它的 VolumeBindingMode
字段指定为 WaitForFirstConsumer
,即延迟绑定操作。结合 PersistentVolume
对象的 .spec.storageClassName
字段,目的是告诉 PV Controller
遇到 .spec.storageClassName
为 local-storage
的 PVC
暂不做绑定操作。
而创建的 PV
对象被限制在 node1 上使用,当用户开始提交 PVC
的时候,PV Controller
在看到这个 PVC
时,它会找到相应的 storageClass,发现 volumeBindingMode
是延迟绑定,因此它就不会做任何事情。
之后当真正使用这个 PVC
的 Pod
在调度时,当它恰好调度在符合 PV nodeaffinity
的 Node
的上面后,这个 Pod
所使用的 PVC
才会真正地与 PV
做绑定操作,这样就保证 Pod
调度到这台 Node
上之后,PVC
才与 PV
绑定,最终保证的是创建出来的 Pod
能访问这块 Local PV
,也就是预分配场景下怎么去满足 PV
的拓扑限制。
Dynamic Provisioning PV
限制拓扑示例:
csi-disk StorageClass
yaml 文件示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-disk
privisioner: diskplugin.csi.alibabacloud.com
parameters:
regionId: cn-hangzhou
fsType: ext4
type: cloud_ssd
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.diskplugin.csi.alibabacloud.com/zone
values:
- cn-hangzhou-d
reclaimPolicy: Delete
disk-pvc PersistentVolumeClaim
yaml 文件示例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: disk-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: csi-disk
当该 PVC
对象被创建之后,由于对应 StorageClass
的 volumeBindingMode
字段为 WaitForFirstConsumer
,因此并不会马上动态生成 PV
对象,而是要等到使用该 PVC
对象的第一个 Pod
调度结果出来之后,而且 Kube-Scheduler
在调度 Pod
的时候会去选择满足 StorageClass.allowedTopologies
中指定的拓扑限制的 Nodes
。
拓扑限制是可用区的级别,有两层含义:
第一层含义:在动态创建 PV 时,创建出来的 PV 必须是在这个可用区可以访问的
第二层含义:因为 StorageClass 声明的是延迟绑定,Kube-Scheduler 在发现使用它的 PVC 正好对应的是该 StorageClass 时,调度 Pod 就要选择位于它指定可用区的 Nodes
总之,就是要从两方面保证,一是动态创建出来的存储要能被这个可用区访问,二是 Kube-Scheduler
在选择 Node
时,要保证存储和使用存储的这个 Pod
它所对应的 Node
,它们之间的拓扑域是在同一个拓扑域,用户在写 PVC
文件的时候主要是在 StorageClass
中要做一些拓扑限制。
处理流程
Volume Snapshot/Restore
处理流程:
Kubernetes 中对存储的扩展功能都是推荐通过 CSI out-of-tree
的方式来实现的。
CSI
实现存储扩展主要包含两部分:
第一部分:由 Kubernetes 社区推动实现的 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
做 绑定。这个绑定就类似于 PV
和 PVC
的 绑定。
有了存储快照,如何去使用存储快照恢复之前的数据呢?通过声明一个新的 PVC
对象,并且指定它的 dataSource
为 VolumeSnapshot
对象,当提交 PVC
时会被 csi-provisioner
Watch
到,之后会通过 GRPC
去创建存储。这里创建存储还指定了 VolumeSnapshot
的 ID,当去云厂商创建存储时,需要多做一步操作,即将之前的快照数据恢复到新创建的存储中。之后流程返回到 csi-provisioner
,它会将新创建的存储的相关信息写到一个新的 PV
对象中,新的 PV
对象被 PV controller
Watch
到,它会将用户提交的 PVC
与 PV
做一个绑定,之后 Pod
就可以通过 PVC
来使用 Restore
出来的数据了。这是 Kubernetes 中对存储快照的处理流程。
Volume Topology-aware Scheduling
处理流程:
第一步:通过 StorageClass 声明延迟绑定
第二步:找到 Pod 使用的所有 PVC,根据 PVC 找到满足 Pod 计算资源需求和存储资源需求的 Nodes,选出最优选的 Node
第三步:Kube-Scheduler 会更新经过预选和优选之后 Pod 的 Node信息,以及 PVC 和 PV 的 cache 信息
第四步:已经选出 Node 的 Pod,它使用的 PVC 会绑定已存在的 PV 或动态创建的 PV。Kube-Scheduler 会去触发 PV Controller 去做绑定操作,或者是触发 csi-provisioner 去做动态创建流程