在高可用性的集群中,一般都会有多个容错区(实在找不到合适的中文词语来描述,下文就简称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-provisionerimage: quay.io/k8scsi/csi-provisioner:v1.0.1imagePullPolicy: "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: StorageClassapiVersion: storage.k8s.io/v1metadata:name: default-csi-sc-ad-1provisioner: myvolume.csi.example.comparameters:csi.storage.k8s.io/provisioner-secret-name: csi-provisioner-secretcsi.storage.k8s.io/provisioner-secret-namespace: kube-systemcsi.storage.k8s.io/controller-publish-secret-name: csi-publish-secretcsi.storage.k8s.io/controller-publish-secret-namespace: kube-systemreclaimPolicy: DeletevolumeBindingMode: ImmediateallowedTopologies:- matchLabelExpressions:- key: failure-domain.beta.kubernetes.io/zonevalues:- AD-1
当创建下面的PVC的时候,就会立刻触发CSI Driver的CreateVolume被调用,传入的参数TopologyRequirement.requisite就是上面StorageClass中定义的"AD-1"。CSI Driver在动态创建Volume的时候,必须满足requisite的要求,也就是必须根据requisite指定的Topology来创建Volume。
apiVersion: v1kind: PersistentVolumeClaimmetadata:name: csi-pvc-ad-1spec:accessModes:- ReadWriteOnceresources:requests:storage: 100GistorageClassName: 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: v1kind: PersistentVolumeClaimmetadata:name: csi-pvc-ad-2annotations:volume.kubernetes.io/selected-node: node2spec:accessModes:- ReadWriteOnceresources:requests:storage: 100GistorageClassName: 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/v1alpha1kind: CSINodeInfometadata:creationTimestamp: "2019-03-21T06:42:59Z"generation: 2name: node2ownerReferences:- apiVersion: v1kind: Nodename: node2uid: 11d31cc0-40bc-11e9-9d54-00001700f4earesourceVersion: "1515289"selfLink: /apis/csi.storage.k8s.io/v1alpha1/csinodeinfos/node2uid: 91118bbb-4ba4-11e9-85a7-00001700f4easpec:drivers:- name: myvolume.csi.example.comnodeID: node2topologyKeys:- failure-domain.beta.kubernetes.io/zonestatus:drivers:- available: truename: myvolume.csi.example.comvolumePluginMechanism: 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: StorageClassapiVersion: storage.k8s.io/v1metadata:name: csi-scprovisioner: myvolume.csi.example.comparameters:csi.storage.k8s.io/provisioner-secret-name: csi-provisioner-secretcsi.storage.k8s.io/provisioner-secret-namespace: kube-systemcsi.storage.k8s.io/controller-publish-secret-name: csi-publish-secretcsi.storage.k8s.io/controller-publish-secret-namespace: kube-systemreclaimPolicy: DeletevolumeBindingMode: WaitForFirstConsumer
下面的POD必须调度到属于AD-3的某个node上。当这个POD被调度到某个node上之后,Kubernetes会自动将对应的PVC中的selected-node设置成POD所在node的名称。接下来的处理逻辑就是上面介绍的通过selected-node以及 CSINodeInfo计算preferred的过程了。
kind: PodapiVersion: v1metadata:name: my-appspec:containers:- name: my-frontendimage: busyboxvolumeMounts:- mountPath: "/data"name: my-csi-volumecommand: [ "sleep", "1000000" ]nodeSelector:failure-domain.beta.kubernetes.io/zone: AD-3volumes:- name: my-csi-volumepersistentVolumeClaim:claimName: csi-pvc
为什么要返回创建的Volume的Topology信息?
前面介绍过,CreateVolume在创建完Volume之后,要将Volume的topology信息返回回去。为什么要这么做呢?换句话问,如果不返回topology信息会有什么后果?如果你有这个疑问,说明你对CSI的理解已经比一般人高出一截了,很多时候提出问题的能力比解决问题的能力更重要。
其实前面已经隐含的回答了这个问题,这里再明确回答一次。假设POD定义如下:
kind: PodapiVersion: v1metadata:name: my-appspec:containers:- name: my-frontendimage: busyboxvolumeMounts:- mountPath: "/data"name: my-csi-volumecommand: [ "sleep", "1000000" ]volumes:- name: my-csi-volumepersistentVolumeClaim: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--




