容器化RDS:PersistentLocalVolumes和VolumeScheduling

|  导语

数据库的高可用方案非常依赖底层的存储架构,这也是集中式存储作为核心数据库业务标配的原因之一。现在,越来越多的用户开始在生产环境中使用基于数据库层的复制技术来保障数据多副本和数据一致性,该架构使用户可以使用“Local”存储构建“Zero Data Lost”的高可用集群,而不依赖“Remote” 的集中式存储。
“Local”和“Remote”隐含着Kube-Scheduler在调度时需要对Volume的“位置”可见。
本文尝试从使用本地存储的场景出发,分享“VolumeScheduling”在代码中的具体实现和场景局限,以下的总结来自于(能力有限,不尽之处,不吝赐教):
  • https://github.com/kubernetes/community/pull/1054
  • https://github.com/kubernetes/community/pull/1140
  • https://github.com/kubernetes/community/pull/1105
  • Kubernetes 1.9和1.10部分代码

 

| 本地卷

相比“Remote”的卷,本地卷:
  • 更好的利用本地高性能介质(SSD,Flash)提升数据库服务能力 QPS/TPS(其实这个结论未必成立,后面会有赘述)
  • 更闭环的运维成本,现在越来越多的数据库支持基于Replicated的技术实现数据多副本和数据一致性(比如MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster的),DBA可以处理所有问题,而不在依赖存储工程师或者SA的支持。
MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster 方案详见:《容器化RDS:计算存储分离还是本地存储?
在1.9之后,可以通过Feature Gate “PersistentLocalVolumes”使用本地卷。
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
    capacity:
      storage: 10Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1
目前local.path可以是MountPoint或者是BlockDevice。这是我们要使用 MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster架构的基础。
但还不够,因为Scheduler并不感知卷的“位置”。
这里需要从PVC绑定和Pod调度说起。

| 原有调度机制的问题

当申请匹配workload需求的资源时,可以简单的把资源分为“计算资源”和“存储资源”。
以Kubernetes申请Statefulset资源YAML为例:
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: mysql-5.7
spec:
  replicas: 1
  template:
    metadata:
      name: mysql-5.7
    spec:
      containers:
        name: mysql
        resources:
          limits:
            cpu: 5300m
            memory: 5Gi
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi
YAML中定义了Pod对“计算”和“存储”资源的要求。随后Statefulset Controller创建需要的PVC和Pod:
func (spc *realStatefulPodControl) CreateStatefulPod(set *apps.StatefulSet, pod *v1.Pod) error {
    // Create the Pod's PVCs prior to creating the Pod
    if err := spc.createPersistentVolumeClaims(set, pod); err != nil {
        spc.recordPodEvent("create", set, pod, err)
        return err
    }
    // If we created the PVCs attempt to create the Pod
    _, err := spc.client.CoreV1().Pods(set.Namespace).Create(pod)
    // sink already exists errors
    if apierrors.IsAlreadyExists(err) {
        return err
    }
    spc.recordPodEvent("create", set, pod, err)
    return err
}
PVC绑定
Pod借用PVC描述需要的存储资源,PVC是PV的抽象,就像VFS是Linux对具体文件系统的抽象,所以在PVC创建之后,还需要将PVC跟卷进行绑定,也即是PV。PersistentVolume Controller会遍历现有PV和可以动态创建的StorageClass(譬如NFS、Ceph、EBS),找到满足条件(访问权限、容量等)进行绑定。
大致流程如下:

Pod调度
Scheduler基于资源要求找到匹配的节点,以过滤和打分的方式选出“匹配度”最高的Node,流程大致如此:

Scheduler通用调度策略详见:《容器化RDS:调度策略
问题
总结以上流程:
  • PVC绑定在Pod调度之前,PersistentVolume Controller不会等待Scheduler调度结果,在Statefulset中PVC先于Pod创建,所以PVC/PV绑定可能完成在Pod调度之前。
  • Scheduler不感知卷的“位置”,仅考虑存储容量、访问权限、存储类型、还有第三方CloudProvider上的限制(譬如在AWS、GCE、Aure上使用Disk数量的限制)
当应用对卷的“位置”有要求,比如使用本地卷,可能出现Pod被Scheduler调度到NodeB,但PersistentVolume Controller绑定了在NodeD上的本地卷,以致PV和Pod不在一个节点,如下图所示:

不仅仅是本地卷,如果对存储“位置”(譬如:Rack、Zone)有要求,都会有类似问题。
 
好比,Pod作为下属,它实现自身价值的核心资源来自于两个上级Scheduler和PersistentVolume Controller,但是这两个上级从来不沟通,甚至出现矛盾的地方。作为下属,要解决这个问题,无非如下几种选择:
  1. 尝试让两个老板沟通
  2. 站队,挑一个老板,只听其中一个的指挥
  3. 辞职
Kubernetes做出了“正常人”的选择:站队。
如果Pod使用的Volume对“位置”有要求(又叫Topology-Aware Volume),通过延时绑定(DelayBinding)使PersistentVolume Controller不再参与,PVC绑定的工作全部由Scheduler完成。
在通过代码了解特性“VolumeScheduling”的具体实现时,还可以先思考如下几个问题:
  • 如何标记Topology-Aware Volume
  • 如何让PersistentVolume Controller不再参与,同时不影响原有流程

| Feature:VolumeScheduling

Kubernetes在卷管理中通过策略VolumeScheduling重构Scheduler以支持Topology-Aware Volume,步骤大致如下:
预分配使用本地卷的PV。
通过NodeAffinity方式标记Topology-Aware Volume和“位置”信息:
"volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["Node1"]
                        }
                    ]}
                 ]}
              }'
创建StorageClass,通过StorageClass间接标记PVC的绑定需要延后(绑定延时)。
标记该PVC需要延后到Node选择出来之后再绑定:
  • 创建StorageClass “X”(无需Provisioner),并设置StorageClass.VolumeBindingMode = VolumeBindingWaitForFirstConsumer
  • PVC.StorageClass设置为X
依照原有流程创建PVC和Pod,但对于需要延时绑定的PVC,PersistentVolume Controller不再参与。
通过PVC.StorageClass,PersistentVolume Controller得知PVC是否需要延时绑定。
return *class.VolumeBindingMode == storage.VolumeBindingWaitForFirstConsumer
如需延时绑定,do nothing。
if claim.Spec.VolumeName == "" {
        // User did not care which PV they get.
        delayBinding, err := ctrl.shouldDelayBinding(claim)
                        ….
                        switch {
            case delayBinding:
                                    do nothing
  • 执行原有Predicates函数
  • 执行添加Predicate函数CheckVolumeBinding校验候选Node是否满足PV物理拓扑(主要逻辑由FindPodVolumes提供):

已绑定PVC:对应PV.NodeAffinity需匹配候选Node,否则该节点需要pass

未绑定PVC:该PVC是否需要延时绑定,如需要,遍历未绑定PV,其NodeAffinity是否匹配候选Node,如满足,记录PVC和PV的映射关系到缓存bindingInfo中,留待节点最终选出来之后进行最终的绑定。

以上都不满足时 : PVC.StorageClass是否可以动态创建 Topology-Aware Volume(又叫 Topology-aware dynamic provisioning)

  • 执行原有Priorities函数
  • 执行添加Priority函数PrioritizeVolumes。Volume容量匹配越高越好,避免本地存储资源浪费。
  • Scheduler选出Node
  • 由Scheduler进行API update,完成最终的PVC/PV绑定(异步操作,时间具有不确定性,可能失败)
  • 从缓存bindingInfo中获取候选Node上PVC和PV的绑定关系,并通过API完成实际的绑定
  • 如果需要StorageClass动态创建,被选出Node将被赋值给StorageClass.topologyKey,作为StorageClass创建Volume的拓扑约束,该功能的实现还在讨论中。
  • 绑定被调度Pod和Node
Scheduler的流程调整如下(依据Kubernetes 1.9代码):
从代码层面,对Controller Manager和Scheduler都有改造,如下图所示(依据Kubernetes 1.9代码):

举个例子
先运行一个例子。
预分配的方式创建使用本地存储的PV。
为了使用本地存储需要启动FeatureGate:PersistentLocalVolumes支持本地存储,1.9是alpha版本,1.10是beta版,默认开启。
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
  annotations:
        "volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["k8s-node1-product"]
                        }
                    ]}
                 ]}
              }'
spec:
    capacity:
      storage: 10Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1
创建Storage Class。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
创建使用本地存储的Statefulset(仅列出关键信息)。
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: mysql-5.7
spec:
  replicas: 1
  template:
    metadata:
      name: mysql-5.7
    spec:
      containers:
        name: mysql
        resources:
          limits:
            cpu: 5300m
            memory: 5Gi
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: data
  volumeClaimTemplates:
  - metadata:
      annotations:
        volume.beta.kubernetes.io/storage-class: local-storage
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi
该Statefulset的Pod将会调度到k8s-node1-product,并使用本地存储“local-pv”。

|“PersistentLocalVolumes”和“VolumeScheduling”的局限性

具体部署时。
使用局限需要考虑:
  • 资源利用率降低。一旦本地存储使用完,即使CPU、Memory剩余再多,该节点也无法提供服务;
  • 需要做好本地存储规划,譬如每个节点Volume的数量、容量等,就像原来使用存储时需要把LUN规划好一样,在一个大规模运行的环境,存在落地难度。
高可用风险需要考虑:
当Pod调度到某个节点后,将会跟该节点产生亲和,一旦Node发生故障,Pod不能调度到其他节点,只能等待该节点恢复,你能做的就是等待“Node恢复”,如果部署3节点MySQL集群,再挂一个Node,集群将无法提供服务,你能做的还是“等待Node恢复”。这么设计也是合理的,社区认为该Node为Stateful节点,Pod被调度到其他可用Node会导致数据丢失。当然,你的老板肯定不会听这套解释。
而且还要思考,初衷更好的利用本地高性能介质(SSD,Flash)提升数据库服务能力QPS/TPS。
真的成立吗?
数据库是IO延时敏感型应用,同时它也极度依赖系统的平衡性,基于Replicated架构的数据库集群对集群网络的要求会很高,一旦网路成为瓶颈影响到数据的sync replication,都会极大的影响数据库集群服务能力。目前,Kubernetes的网络解决方案还无法提供高吞吐,低延时的网络能力。
当然,我们可以做点什么解决“等待Node恢复”的问题。
以MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster架构为例,完全可以在这个基础上,结合Kubernetes现有的Control-Plane做进一步优化。
  • Node不可用后,等待阈值超时,以确定Node无法恢复
  • 如确认Node不可恢复,删除PVC,通过解除PVC和PV绑定的方式,解除Pod和Node的绑定
  • Scheduler将Pod调度到其他可用Node,PVC重新绑定到可用Node的PV。
  • Operator查找MySQL最新备份,拷贝到新的PV
  • MySQL集群通过增量同步方式恢复实例数据
  • 增量同步变为实时同步,MySQL集群恢复
最后,借用Google工程师Kelsey Hightower的一句话:
“We very receptive this Kubernetes can’t be everything to everyone.”
Kubernetes并不是“鸿茅药酒”包治百病,了解边界是使用的开始,泛泛而谈Cloud-Native无法获得任何收益,在特定的场景和领域,很多问题还需要我们自己解决。最后说下个人理解,如无特别必要,尽量不选用Local Volume。

| 作者简介

熊中哲,沃趣科技产品及研发负责人
曾就职于阿里巴巴和百度,超过10年关系型数据库工作经验,目前致力于将云原生技术引入到关系型数据库服务中。
K8S中文社区微信公众号

评论 1

登录后评论

立即登录  

  1. #1

    赞,但感觉引入localPVC却带来了更多的问题,性能与高可用方面见仁见智吧,如果不差钱还是上共享存储吧。

    大容量人形自走式装饭机6年前 (2018-05-25)