暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

CSI系列四:你真的理解Topology吗?

零君聊软件 2019-03-29
1559

在高可用性的集群中,一般都会有多个容错区(实在找不到合适的中文词语来描述,下文就简称zone)。Kubernetes为zone定义了一个知名的label(如下),同一个zone内的node都会相同的值。

    failure-domain.beta.kubernetes.io/zone

    另外,还有一个知名的label(如下),主要用来区分不同的数据中心(data center)。

      failure-domain.beta.kubernetes.io/region

      有时对存储Volume的访问不能跨越zone,也就是POD只能访问同一个zone内的volume。所以在创建Volume的时候,需要确定要在哪个zone来创建。这就是Topology要解决的问题。


      CSI规范同样定义了对Topology的支持,但要完全理解这里面的机制,还是要费一番心思。本文详细总结了其中涉及的各种容易被人忽视的细节,希望对在此问题上迷茫的童鞋有一些帮助。


      Feature gates

      支持Topology的feature gate主要就是CSINodeInfo。在Kubernetes 1.13中是alpha版本,默认是disable的。但是在刚刚发布的Kubernetes 1.14中已经是beta版本了,默认则是enable的。


      如果是用Kubernetes 1.13或者1.12,那么就需要在kube-apiserver和kubelet中enable该feature gate。enable的方法就是在相应的配置文件增加下面的配置:

        --feature-gates=CSINodeInfo=true


        kube-apiserver是修改下面的文件:

          /etc/kubernetes/manifests/kube-apiserver.yaml


          kubelet是修改下面的文件:

            /etc/systemd/system/kubelet.service.d/10-kubeadm.conf


            另外external-provisioner中的一个feature gate也是alpha版的,也需要需要enable,那就是"Topology"。

                    containers:
              - name: csi-provisioner
              image: quay.io/k8scsi/csi-provisioner:v1.0.1
              imagePullPolicy: "IfNotPresent"
              args:
                          - "--provisioner=myvolume.csi.example.com"
              - "--csi-address=$(ADDRESS)"
              - "--connection-timeout=120s"
              - "--v=5"
              - "--feature-gates=Topology=true"


              Topology in CSI Spec

              CSI Spec中的三组接口IdentityService/ControllerService/NodeService都涉及到了Topology。在IdentityService中与Topology相关的接口函数是GetPluginCapabilities,我们在实现该接口函数时,需要明确告诉CO(Container Orchestration)我们实现的CSI Driver是否支持Topology。例如下面的实现就明确表示CSI Driver支持Topology,

                func (d *Driver) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
                  return &csi.GetPluginCapabilitiesResponse{
                Capabilities: []*csi.PluginCapability{
                {
                Type: &csi.PluginCapability_Service_{
                Service: &csi.PluginCapability_Service{
                Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
                },
                },
                },
                {
                Type: &csi.PluginCapability_Service_{
                Service: &csi.PluginCapability_Service{
                Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS,
                },
                },
                },
                },
                  }, nil
                }


                ControllerService中与Topology相关的接口函数是CreateVolume。这里又涉及到两个方面,首先是传入的参数中可能会指定Topology信息,其次在返回信息中也需要包含Topology信息。


                CreateVolumeRequest里的参数accessibility_requirement包含topology信息,表示用户希望在哪儿(如region, zone等信息)创建volume。这个信息通常由用户指定,后面会详细说明。

                  message CreateVolumeRequest {
                    ....
                  TopologyRequirement accessibility_requirements = 7;
                  }


                  动态创建完Volume之后,需要将Volume的实际Topology信息包含在CreateVolumeResponse中,以便返回给CO(其实是external-provisioner)。CreateVolume返回后,external-provisioner会将返回的Topology信息保存到创建的PersistentVolume中的nodeAffinity中。这一步非常关键,因为后面CO在调度POD时,这是非常重要的参考信息。这一点后文还会通过实际例子进一步阐述。

                    message CreateVolumeResponse {
                    Volume volume = 1;
                    }
                    message Volume {
                    ....
                    repeated Topology accessible_topology = 5;
                    }


                    NodeService中与Topology相关的接口函数是NodeGetInfo。实现该接口函数时,需要返回与当前node相关的Topology信息。这个信息也很重要,CO在调度POD时同样也会参考这个信息。后面会详细阐述。

                      message NodeGetInfoResponse {
                        ....
                      Topology accessible_topology = 3;  
                      }


                      TopologyRequirement

                      TopologyRequirement是调用CSI Driver的接口函数CreateVolume的部分传入参数,主要包含requisite和preferred两种Topology信息,

                        message TopologyRequirement {
                          repeated Topology requisite = 1;
                        repeated Topology preferred = 2;
                        }


                        其中requisite的值来自于在StorageClass中的allowedTopologies里定义的信息,例如下面的例子中要求在"AD-1"这个zone中创建Volume,

                          kind: StorageClass
                          apiVersion: storage.k8s.io/v1
                          metadata:
                            name: default-csi-sc-ad-1
                          provisioner: myvolume.csi.example.com
                          parameters:
                            csi.storage.k8s.io/provisioner-secret-name: csi-provisioner-secret
                          csi.storage.k8s.io/provisioner-secret-namespace: kube-system
                            csi.storage.k8s.io/controller-publish-secret-name: csi-publish-secret
                          csi.storage.k8s.io/controller-publish-secret-namespace: kube-system
                          reclaimPolicy: Delete
                          volumeBindingMode: Immediate
                          allowedTopologies:
                          - matchLabelExpressions:
                          - key: failure-domain.beta.kubernetes.io/zone
                          values:
                              - AD-1


                          当创建下面的PVC的时候,就会立刻触发CSI Driver的CreateVolume被调用,传入的参数TopologyRequirement.requisite就是上面StorageClass中定义的"AD-1"。CSI Driver在动态创建Volume的时候,必须满足requisite的要求,也就是必须根据requisite指定的Topology来创建Volume。

                            apiVersion: v1
                            kind: PersistentVolumeClaim
                            metadata:
                            name: csi-pvc-ad-1
                            spec:
                            accessModes:
                            - ReadWriteOnce
                            resources:
                            requests:
                            storage: 100Gi
                            storageClassName: default-csi-sc-ad-1


                            如果在StorageClass中没有定义allowedTopologies,那么requisite的值就是cluster内所有存在的topology。


                            而TopologyRequiremet中的preferred的值,则是间接来自于PVC中"volume.kubernetes.io/selected-node"这个annotation的值。例如下面的例子中,selected-node的值是"node2",是k8s cluster中的一个node的名字,

                              apiVersion: v1
                              kind: PersistentVolumeClaim
                              metadata:
                                name: csi-pvc-ad-2
                              annotations:
                                  volume.kubernetes.io/selected-node: node2
                              spec:
                              accessModes:
                              - ReadWriteOnce
                              resources:
                              requests:
                              storage: 100Gi
                                storageClassName: csi-sc


                              TopologyRequiremet.preferred的值就是根据selected-node的值计算出来的。虽然计算过程稍微有点绕,但还是比较清晰的。在说明这个计算过程之前,先插叙一下Node和CSINodeInfo这两个API Object的关系。我们知道,Node是Kubernetes内置的一个API Object,表示一个cluster中的一个工作节点。而CSINodeInfo则是用来存储CSI Driver产生的一些与Node相关的信息,主要是存储topologyKeys。本来开始Kubernetes Storage SIG团队是想将这些信息直接保存在Node中,但由于Node里面包含的信息已经很多了,所以就单独创建了一个新的CRD来保存这些信息,也就是CSINodeInfo。Kubelet会自动为cluster中的每一个node创建一个对应的CSINodeInfo对象。


                              接下来通过例子介绍preferred的计算过程。首先根据PVC中定义的selected-node的值,来查到对应的CSINodeInfo对象。上面的PVC中的selected-node的值为"node2",所以就会找名称也为"node2"的CSINodeInfo对象,找到的对象大致如下:

                                apiVersion: csi.storage.k8s.io/v1alpha1
                                kind: CSINodeInfo
                                metadata:
                                creationTimestamp: "2019-03-21T06:42:59Z"
                                generation: 2
                                  name: node2
                                ownerReferences:
                                - apiVersion: v1
                                kind: Node
                                    name: node2
                                uid: 11d31cc0-40bc-11e9-9d54-00001700f4ea
                                resourceVersion: "1515289"
                                  selfLink: /apis/csi.storage.k8s.io/v1alpha1/csinodeinfos/node2
                                uid: 91118bbb-4ba4-11e9-85a7-00001700f4ea
                                spec:
                                drivers:
                                  - name: myvolume.csi.example.com
                                    nodeID: node2
                                topologyKeys:
                                - failure-domain.beta.kubernetes.io/zone
                                status:
                                drivers:
                                - available: true
                                    name: myvolume.csi.example.com
                                volumePluginMechanism: in-tree


                                接下来就从CSINodeInfo找出该node所对应的topologyKeys,上面的例子中就是“failure-domain.beta.kubernetes.io/zone”,这也是一个知名的label。


                                最后在“node2"这个节点上,查出topologyKeys中定义的key对应的value,也就是查出“failure-domain.beta.kubernetes.io/zone”对应的值。假设查出的value是"AD-2"。那么preferred包含的topology信息就为:

                                  failure-domain.beta.kubernetes.io/zone: AD-2


                                  注意requisite是强制要求,而preferred只是需要优先考虑的topology信息,并不是强制要求。包含在preferred中的topology信息,一定也包含在requisite中。


                                  如果没有在PVC中定义selected-node,那么会根据PVC的名字的hash值来确定preferred的值,基本是带有一定的随机性。具体细节可查看external-provisioner的源码。


                                  WaitForFirstConsumer

                                  当既没有在StorageClass中定义allowedTopology,也没有在PVC中定义selected-node时,用户仅仅在POD中定义nodeSelector或者nodeAffinity,那么当CSI Driver的CreateVolume被调用时,也可以传入正确的topology信息。


                                  这种场景下,一定要将StorageClass中的volumeBindingMode设置成“WaitForFirstConsumer”,也就是等到第一个POD使用PVC时,才动态创建Volume。因为我们需要保证所创建的Volume与POD属于同一个zone。

                                    kind: StorageClass
                                    apiVersion: storage.k8s.io/v1
                                    metadata:
                                      name: csi-sc
                                    provisioner: myvolume.csi.example.com
                                    parameters:
                                    csi.storage.k8s.io/provisioner-secret-name: csi-provisioner-secret
                                    csi.storage.k8s.io/provisioner-secret-namespace: kube-system
                                    csi.storage.k8s.io/controller-publish-secret-name: csi-publish-secret
                                    csi.storage.k8s.io/controller-publish-secret-namespace: kube-system
                                    reclaimPolicy: Delete
                                    volumeBindingMode: WaitForFirstConsumer


                                    下面的POD必须调度到属于AD-3的某个node上。当这个POD被调度到某个node上之后,Kubernetes会自动将对应的PVC中的selected-node设置成POD所在node的名称。接下来的处理逻辑就是上面介绍的通过selected-node以及 CSINodeInfo计算preferred的过程了。

                                      kind: Pod
                                      apiVersion: v1
                                      metadata:
                                      name: my-app
                                      spec:
                                      containers:
                                      - name: my-frontend
                                      image: busybox
                                      volumeMounts:
                                      - mountPath: "/data"
                                      name: my-csi-volume
                                      command: [ "sleep", "1000000" ]
                                      nodeSelector:
                                          failure-domain.beta.kubernetes.io/zone: AD-3
                                      volumes:
                                      - name: my-csi-volume
                                      persistentVolumeClaim:
                                              claimName: csi-pvc


                                      为什么要返回创建的Volume的Topology信息?

                                      前面介绍过,CreateVolume在创建完Volume之后,要将Volume的topology信息返回回去。为什么要这么做呢?换句话问,如果不返回topology信息会有什么后果?如果你有这个疑问,说明你对CSI的理解已经比一般人高出一截了,很多时候提出问题的能力比解决问题的能力更重要。


                                      其实前面已经隐含的回答了这个问题,这里再明确回答一次。假设POD定义如下:

                                        kind: Pod
                                        apiVersion: v1
                                        metadata:
                                        name: my-app
                                        spec:
                                        containers:
                                        - name: my-frontend
                                        image: busybox
                                        volumeMounts:
                                        - mountPath: "/data"
                                        name: my-csi-volume
                                        command: [ "sleep", "1000000" ]
                                        volumes:
                                        - name: my-csi-volume
                                        persistentVolumeClaim:
                                                claimName: csi-pvc


                                        上面的POD中没有定义nodeSelector,也没有定义nodeAffinity,那么这个POD岂不是要被Kubernetes随机调度某个node上?非也,因为POD引用了PVC,那么对应的PV中如果包含Topology信息,那么Kubernetes就会自动将POD调度到与PV具有相同topology的node上。这就是为什么CreateVolume要返回Volume的Topology信息的原因。


                                        另外,Kubernetes是如何知道每个node的Topology的信息的呢?这里就是前面提到的NodeService中的NodeGetInfo为什么也要返回node的topology信息的原因。看到这里,是否一切焕然大悟!且慢,Kubernetes完全可以从Node上直接获取topology信息,但为什么偏偏要通过我们实现的CSI Driver中的接口函数NodeGetInfo来获取呢?这个问题是留给各位童鞋课外作业。:)


                                        --END--

                                        文章转载自零君聊软件,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                        评论