做者 | 至天 阿里巴巴高级研发工程师node
在使用存储时,为了提升数据操做的容错性,咱们一般有须要对线上数据进行 snapshot ,以及能快速 restore 的能力。另外,当须要对线上数据进行快速的复制以及迁移等动做,如进行环境的复制、数据开发等功能时,均可以经过存储快照来知足需求,而 K8s 中经过 CSI Snapshotter controller 来实现存储快照的功能。服务器
咱们知道,K8s 中经过 pvc 以及 pv 的设计体系来简化用户对存储的使用,而存储快照的设计实际上是仿照 pvc & pv 体系的设计思想。当用户须要存储快照的功能时,能够经过 VolumeSnapshot 对象来声明,并指定相应的 VolumeSnapshotClass 对象,以后由集群中的相关组件动态生成存储快照以及存储快照对应的对象 VolumeSnapshotContent 。以下对比图所示,动态生成 VolumeSnapshotContent 和动态生成 pv 的流程是很是类似的。微信
有了存储快照以后,如何将快照数据快速恢复过来呢?以下图所示:网络
如上所示的流程,能够借助 PVC 对象将其的 dataSource 字段指定为 VolumeSnapshot 对象。这样当 PVC 提交以后,会由集群中的相关组件找到 dataSource 所指向的存储快照数据,而后新建立对应的存储以及 pv 对象,将存储快照数据恢复到新的 pv 中,这样数据就恢复回来了,这就是存储快照的 restore 用法。less
首先了解一下拓扑是什么意思:这里所说的拓扑是 K8s 集群中为管理的 nodes 划分的一种“位置”关系,意思为:能够经过在 node 的 labels 信息里面填写某一个 node 属于某一个拓扑。<br /> <br />常见的有三种,这三种在使用时常常会遇到的:dom
第一种,在使用云存储服务的时候,常常会遇到 region,也就是地区的概念,在 K8s 中常经过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪一个地区;socket
第二种,比较经常使用的是可用区,也就是 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 所处的位置。
接下来就一块儿来看看拓扑在 K8s 存储中的使用。
上一节课咱们说过,K8s 中经过 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 协议来实现多副本高可用。
接下来看一下 Local PV 场景可能若是没有对 PV 作“访问位置”的限制会遇到什么问题?
当用户在提交完 PVC 的时候,K8s PV controller可能绑定的是 node2 上面的 PV。可是,真正使用这个 PV 的 pod,在被调度的时候,有可能调度在 node1 上,最终致使这个 pod 在起来的时候没办法去使用这块存储,由于 pod 真实状况是要使用 node2 上面的存储。
第二个(若是不对 PV 作“访问位置”的限制会出问题的)场景:
若是搭建的 K8s 集群管理的 nodes 分布在单个区域多个可用区内。在建立动态存储的时候,建立出来的存储属于可用区 2,但以后在提交使用该存储的 pod,它可能会被调度到可用区 1 了,那就可能没办法使用这块存储。所以像阿里云的云盘,也就是块存储,当前不能跨可用区使用,若是建立的存储其实属于可用区 2,可是 pod 运行在可用区 1,就没办法使用这块存储,这是第二个常见的问题场景。
接下来咱们来看看 K8s 中如何经过存储拓扑调度来解决上面的问题的。
首先总结一下以前的两个问题,它们都是 PV 在给 PVC 绑定或者动态生成 PV 的时候,我并不知道后面将使用它的 pod 将调度在哪些 node 上。但 PV 自己的使用,是对 pod 所在的 node 有拓扑位置的限制的,如 Local PV 场景是我要调度在指定的 node 上我才能使用那块 PV,而对第二个问题场景就是说跨可用区的话,必需要在将使用该 PV 的 pod 调度到同一个可用区的 node 上才能使用阿里云云盘服务,那 K8s 中怎样去解决这个问题呢?
简单来讲,在 K8s 中将 PV 和 PVC 的 binding 操做和动态建立 PV 的操做作了 delay,delay 到 pod 调度结果出来以后,再去作这两个操做。这样的话有什么好处?
为了实现上面所说的延迟绑定和延迟建立 PV,须要在 K8s 中的改动涉及到的相关组件有三个:
这就是存储拓扑调度的相关知识。
接下来经过 yaml 用例来解读一下第一部分的基本知识。
下面来看一下存储快照如何使用:首先须要集群管理员,在集群中建立 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 以前作的存储快照。
以下图看一下 Local PV 的 yaml 示例:
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 的拓扑限制。
再看一下动态 Provisioning PV 的时候,怎么去作拓扑限制的?
动态就是指动态建立 PV 就有拓扑位置的限制,那怎么去指定?
首先在 storageclass 仍是须要指定 BindingMode,就是 WaitForFirstConsumer,就是延迟绑定。
其次特别重要的一个字段就是 allowedTopologies,限制就在这个地方。上图中能够看到拓扑限制是可用区的级别,这里其实有两层意思:
总之,就是要从两方面保证,一是动态建立出来的存储时要能被这个可用区访问的,二是我调度器在选择 node 的时候,要落在这个可用区内的,这样的话就保证个人存储和我要使用存储的这个 pod 它所对应的 node,它们之间的拓扑域是在同一个拓扑域,用户在写 PVC 文件的时候,写法是跟之前的写法是同样的,主要是在 storageclass 中要作一些拓扑限制。
本节将在线上环境来演示一下前面讲解的内容。
首先来看一下个人阿里云服务器上搭建的 K8s 服务。总共有 3 个 node 节点。一个 master 节点,两个 node。其中 master 节点是不能调度 pod 的。
再看一下,我已经提早把我须要的插件已经布好了,一个是 snapshot 插件 ( csi-external-snapshot* ) ,一个是动态云盘的插件 ( csi-disk* ) 。
如今开始 snapshot 的演示。首先去动态建立云盘,而后才能作 snapshot。动态建立云盘须要先建立 storageclass,而后去根据 PVC 动态建立 PV,而后再建立一个使用它的 pod 了。
有个以上对象,如今就能够作 snapshot 了,首先看一下作 snapshot 须要的第一个配置文件:snapshotclass.yaml。
其实里面就是指定了在作存储快照的时候须要使用的插件,这个插件刚才演示了已经部署好了,就是 csi-external-snapshot-0 这个插件。
接下来建立 volume-snapshotclass 文件,建立完以后就开始了 snapshot。
而后看 snapshot.yaml,Volumesnapshot 声明建立存储快照了,这个地方就指定刚才建立的那个 PVC 来作的数据源来作 snapshot,那咱们开始建立。
咱们看一下 Snapshot 有没有建立好,以下图所示,content 已经在 11 秒以前建立好了。
能够看一下它里面的内容,主要看 volumesnapshotcontent 记录的一些信息,这个是我 snapshot 出来以后,它记录的就是云存储厂商那边返回给个人 snapshot 的 ID。而后是这个 snapshot 数据源,也就是刚才指定的 PVC,能够经过它会找到对应的 PV。
snapshot 的演示大概就是这样,把刚才建立的 snapshot 删掉,仍是经过 volumesnapshot 来删掉。而后看一下,动态建立的这个 volumesnapshotcontent 也被删掉。
接下来看一下动态 PV 建立的过程加上一些拓扑限制,首先将的 storageclass 建立出来,而后再看一下 storageclass 里面作的限制,storageclass 首先仍是指定它的 BindingMode 为 WaitForFirstConsumer,也就是作延迟绑定,而后是对它的拓扑限制,我这里面在 allowedTopologies 字段中配置了一个可用区级别的限制。
来尝试建立一下的 PVC,PVC 建立出来以后,理论上它应该处在 pending 状态。看一下,它如今由于它要作延迟绑定,因为如今没有使用它的 pod,暂时没办法去作绑定,也没办法去动态建立新的 PV。
接下来建立使用该 pvc 的 pod 看会有什么效果,看一下 pod,pod 也处在 pending了。
那来看一下 pod 为啥处在 pending 状态,能够看一下是调度失败了,调度失败缘由:一个 node 因为 taint 不能调度,这个实际上是 master,另外两个 node 也是没有说是可绑定的 PV。
为何会有两个 node 出现没有可绑定的 pv 的错误?不是动态建立么?
咱们来仔细看看 storageclass 中的拓扑限制,经过上面的讲解咱们知道,这里限制使用该 storageclass 建立的 PV 存储必须在可用区 cn-hangzhou-d 可访问的,而使用该存储的 pod 也必须调度到 cn-hangzhou-d 的 node 上。
那就来看一下 node 节点上有没有这个拓扑信息,若是它没有固然是不行了。
看一下第一个 node 的全量信息吧,主要找它的 labels 里面的信息,看 lables 里面的确有一个这样的 key。也就是说有一个这样的拓扑,可是这指定是 cn-hangzhou-b,刚才 storageclass 里面指定的是 cn-hangzhou-d。
那看一下另外的一个 node 上的这个拓扑信息写的也是 hangzhou-b,可是咱们那个 storageclass 里面限制是 d。
这就致使最终没办法将 pod 调度在这两个 node 上。如今咱们修改一下 storageclass 中的拓扑限制,将 cn-hangzhou-d 改成 cn-hangzhou-b。
改完以后再看一下,其实就是说我动态建立出来的 PV 要能被 hangzhou-b 这个可用区访问的,使用该存储的 pod 要调度到该可用区的 node 上。把以前的 pod 删掉,让它从新被调度看一下有什么结果,好,如今这个已经调度成功了,就是已经在启动容器阶段。
说明刚才把 storageclass 它里面的对可用区的限制从 hangzhou-d 改成 hangzhou-b 以后,集群中就有两个 node,它的拓扑关系是和 storageclass 里要求的拓扑关系是相匹配的,这样的话它就能保证它的 pod 是有 node 节点可调度的。上图中最后一点 Pod 已经 Running 了,说明刚才的拓扑限制改动以后能够 work 了。
接下来看一下 K8s 中对存储快照与拓扑调度的具体处理流程。以下图所示:
首先来看一下存储快照的处理流程,这里来首先解释一下 csi 部分。K8s 中对存储的扩展功能都是推荐经过 csi out-of-tree 的方式来实现的。
csi 实现存储扩展主要包含两部分:
两部分部件经过 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 中对存储快照的处理流程。
接下来看一下存储拓扑调度的处理流程:
第一个步骤其实就是要去声明延迟绑定,这个经过 StorageClass 来作的,上面已经阐述过,这里就不作详细描述了。
接下来看一下调度器,上图中红色部分就是调度器新加的存储拓扑调度逻辑,咱们先来看一下不加红色部分时调度器的为一个 pod 选择 node 时,它的大概流程:
那如今看一下加上卷相关的调度的时候,筛选 node(第二个步骤)又是怎么作的?
通过这上面步骤以后,就找到了全部即知足 pod 计算资源需求又知足 pod 存储资源需求的全部 nodes。<br /> <br />当 node 选出来以后,第三个步骤就是调度器内部作的一个优化。这里简单过一下,就是更新通过预选和优选以后,pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中作的一些 cache 信息。
第四个步骤也是重要的一步,已经选择出来 node 的 Pod,无论其使用的 PVC 是要 binding 已经存在的 PV,仍是要作动态建立 PV,这时就能够开始作。由调度器来触发,调度器它就会去更新 PVC 对象和 PV 对象里面的相关信息,而后去触发 PV controller 去作 binding 操做,或者是由 csi-provisioner 去作动态建立流程。
阿里巴巴云原生微信公众号(ID:Alicloudnative)关注微服务、Serverless、容器、Service Mesh等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术公众号。