Kubernetes 兼容 CSI 做的工作

csi 是一个标准的容器存储接口,规定了如何实现一个容器的存储接口,CSI 本身的定义是基于 gRPC 的,所以有一套样例库可以使用,这里分析一下 kuberntes 实现 csi 的方式,为了兼容 CSI kubernete 其实搞得挺绕的,目前这个 CSI 还是定制中包括后期的 Snapshot 的接口怎么设计等等还在讨论中。kubernetes CSI 主要基于几个外部组件和内部功能的一些改动。

CSI-Driver

这里规定了 CSI 的标准,定义了三个 Service,也就是 RPC 的集合,但是没规定怎么写,目前看到的实现都是把这三个 service 都写在一起,比较方便,然后部署的时候有些区别将就可以。

service Identity {

  rpc GetPluginInfo(GetPluginInfoRequest)

    returns (GetPluginInfoResponse) {}

 

  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)

    returns (GetPluginCapabilitiesResponse) {}

 

  rpc Probe (ProbeRequest)

    returns (ProbeResponse) {}

}

 

service Controller {

  rpc CreateVolume (CreateVolumeRequest)

    returns (CreateVolumeResponse) {}

 

  rpc DeleteVolume (DeleteVolumeRequest)

    returns (DeleteVolumeResponse) {}

 

  rpc ControllerPublishVolume (ControllerPublishVolumeRequest)

    returns (ControllerPublishVolumeResponse) {}

 

  rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)

    returns (ControllerUnpublishVolumeResponse) {}

 

  rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)

    returns (ValidateVolumeCapabilitiesResponse) {}

 

  rpc ListVolumes (ListVolumesRequest)

    returns (ListVolumesResponse) {}

 

  rpc GetCapacity (GetCapacityRequest)

    returns (GetCapacityResponse) {}

 

  rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)

    returns (ControllerGetCapabilitiesResponse) {}

 

  rpc CreateSnapshot (CreateSnapshotRequest)

    returns (CreateSnapshotResponse) {}

 

  rpc DeleteSnapshot (DeleteSnapshotRequest)

    returns (DeleteSnapshotResponse) {}

 

  rpc ListSnapshots (ListSnapshotsRequest)

    returns (ListSnapshotsResponse) {}

}

 

service Node {

  rpc NodeStageVolume (NodeStageVolumeRequest)

    returns (NodeStageVolumeResponse) {}

 

  rpc NodeUnstageVolume (NodeUnstageVolumeRequest)

    returns (NodeUnstageVolumeResponse) {}

 

  rpc NodePublishVolume (NodePublishVolumeRequest)

    returns (NodePublishVolumeResponse) {}

 

  rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)

    returns (NodeUnpublishVolumeResponse) {}

 

  rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)

    returns (NodeGetVolumeStatsResponse) {}

 

  // NodeGetId is being deprecated in favor of NodeGetInfo and will be

  // removed in CSI 1.0. Existing drivers, however, may depend on this

  // RPC call and hence this RPC call MUST be implemented by the CSI

  // plugin prior to v1.0.

  rpc NodeGetId (NodeGetIdRequest)

    returns (NodeGetIdResponse) {

    option deprecated = true;

  }

 

  rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)

    returns (NodeGetCapabilitiesResponse) {}

 

  // Prior to CSI 1.0 - CSI plugins MUST implement both NodeGetId and

  // NodeGetInfo RPC calls.

  rpc NodeGetInfo (NodeGetInfoRequest)

    returns (NodeGetInfoResponse) {}

}

比如说 GetPluginInfo 就是用来获取 driver 的 name 等信息的,NodePublishVolume 大部分情况下就是在节点上挂载文件系统,CreateVolume 这个如果对应的是 ebs 这种块存储可能就是在 API 里面建一个 ebs,如果对应的是 glusterfs 这种文件系统存储可能就是建一个 volume,然后 ControllerPublishVolume 对应 ebs 就是把 ebs 和 instance 绑定,然后调用节点的 NodePublishVolume 来挂载,如果是文件存储,可能就不需要 `ControllerPublishVolume 了,因为不需要绑定快设备到机器上,直接挂到网络接口就可以,这一套标准的目的一个是为了兼容现有的存储方案,一个是为了让一些私有的 provider 能够比较容易的实现一套方案,而不需要做过多的迁移,甚至厂商都不需要开源代码,如果是要实现 in-tree 的存储代码肯定是要开源的,因为 kubernetes 是开源的。

device-driver-registrar

kubernetes 实现 csi 的兼容,首先需要一个外部组件 devide-driver-registrar,初始化的时候通过 csi-sock 的 RPC 获取 driver name 和 node id。

主要功能给 node 打上下面类似的 annotations,dirver 对应的是 csi driver 的名字,name 对应的是 driver 的 NodeId 基本上就是 k8s 的 node name。这样可以让 ControllerPublishVolume 调用能够获取 nodeid 到 storage nodeid 的映射,理论上一样的就可以感觉。

csi.volume.kubernetes.io/nodeid: "{ "driver1": "name1", "driver2": "name2" }

他有两个模式,一个模式是自己给 node 打上这个 annotation,并且在退出的时候把这个 annotation 去掉。

另一个模式是交给 kubelet 的 pluginswatcher 来管理, kubelet 自己会根据 device-driver-registrar 提供的 unix domain socket 然后调用 gRPC 从 registrar 获取 NodeId 和 DriverName 自己把 annotation 打上。

搜索这条路径下的 socket/var/lib/kubelet/plugins/[SanitizedCSIDriverName]/csi.sock,然后就可以自动连接 registrar 拿到 NodeId 和 DriverName。

所以 device-driver-registar 主要是注册 Node annotation 的。

external-attacher

监听 VolumeAttachments 和 PersistentVolumes 两个对象,这是和 kube-controller-manager 之间的桥梁。

实现中最后会调用 SyncNewOrUpdatedVolumeAttachment 来同步,调用 csi dirver 的 Attach 函数。

in-tree 的 attach/detach-controller

在 CSI 中扮演的角色是创建 VolumeAttachment,然后等待他的 VolumeAttachment 的 attached 的状态。

attach-controller 会创建 VolumeAttatchment.Spec.Attacher 指向的是 external-attacher

external-provisoner

Static Volume 和 Dynamic Volume 的区别是,有一个 PersistentVolumeClaim 这个会根据 claim 自动分配 PersistentVolume,不然就要自己手动创建,然后 pod 要指定这个手动创建的 volume。

external-provisoner 就是提供支持 PersistentVolumeClaim 的,一般的 provisioner 要实现 Provision 和 Delete 的接口。主要是根据 PVC 创建 PV,这是 Provisioner 的接口的定义了,不是 CSI spec 里的,这里顺带介绍一下。

external-provisoner 看到 pvc 调用 driver 的 CreateVolume,完成以后就会创建 PersistenVolume,并且绑定到 pvc。

kubelet volume manager

kubelet 有一个 volume manager 来管理 volume 的 mount/attach 操作。

desiredStateOfWorld 是从 podManager 同步的理想状态。

actualStateOfWorld 是目前 kubelet 的上运行的 pod 的状态。

每次 volume manager 需要把 actualStateOfWorld 中 volume 的状态同步到 desired 指定的状态。

volume Manager 有两个 goroutine 一个是同步状态,一个 reconciler.reconcile

rc.operationExecutor.MountVolume 会执行 MountVolume 的操作。

-> oe.operationGenerator.GenerateMountVolumeFunc

-> 首先根据 og.volumePluginMgr.FindPluginBySpec 找到对应的 VolumePlugin

-> 然后调用 volumePlugin.NewMounter

-> 然后拿到 og.volumePluginMgr.FindAttachablePluginBySpec attachableplugin

-> volumeMounter.SetUp(fsGroup) 做 mount

volume plugin

csi volume plugin 是一个 in-tree volume,以后应该会逐步迁移到都使用 csi,而不会再有 in-tree volume plugin 了。

func (c *csiMountMgr) SetUp(fsGroup *int64) error {

        return c.SetUpAt(c.GetPath(), fsGroup)

}

csi 的 mounter 调用了 NodePublish 函数。stagingTargetPath 和 targetPath 都是自动生成的。

SetUp/TearDown 的调用会执行 in-tree CSI plugin 的接口(这又是 in-tree volume plugin 的定义,确实挺绕的),对应的是 NodePublishVolume 和 NodeUnpublishVolume ,这个会通过 unix domain socket 直接调用 csi driver。

总结一个简单的具体流程

首先管理员创建 StorageClass 指向 external-provisioner,然后用户创建指向这个 StorageClass 的 pvc,然后 kube-controller-manager 里的 persistent volume manager 会把这个 pvc 打上 volume.beta.kubernetes.io/storage-provisioner 的 annotation。

externla-provisioner 看到这个 pvc 带有自己的 annotation 以后,拿到 StorageClass 提供的一些参数,并且根据 StorageClass 和 pvc 调用 CSI driver 的 CreateVolume,创建成功以后创建 pv,并且把这个 pv 绑定到对应的 pvc。

然后 kube-controller-manager 看到有 pod 含有对应的 pvc,就用调用 in-tree 的 CSI plugin 的 attach 方法。

in-tree 的 CSI plugin 实际上会创建一个 VolumeAttachment 的 object,等待这个 VolumeAttachment 被完成。

external-controller 看到 VolumeAttachment,开始 attach 一个 volume,实际上调用 CSI driver 的 ControllerPublish,成功以后更新 VoluemAttachment 以后就知道这个 Volume Attach 成功了,然后让 attach/detach-controller (kube-controller-manager) 知道这个 attach 完成。

接下来就到 kubelet 了,kubelet 看到 volume in pod 以后就会调用 in-tree 的 csi plugin WaitForAttach,然后等待 Attach 成功,之后就会调用 daemonset 里面的 csi driver 的 NodePublishVolume 做挂载操作。

整体的流程是这样的,需要反复多看几遍 kubernetes-csi 的 document,加深理解。

原文:https://ggaaooppeenngg.github.io/zh-CN/2018/10/08/K8S-兼容-CSI-做的工作/

K8S中文社区微信公众号

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址