K8S 相容 CSI 做的工作
csi 是一個標準的容器儲存介面,規定了如何實現一個容器的儲存介面,CSI 本身的定義是基於 RPC/">gRPC 的,所以有一套樣例庫可以使用,這裡分析一下 kuberntes 實現 csi 的方式,為了相容 CSI kubernete 其實搞得挺繞的,目前這個 CSI 還是定製中包括後期的 Snapshot 的介面怎麼設計等等還在討論中。kubernetes CSI 主要基於幾個外部元件和內部功能的一些改動。
CSI-Driver
ofollow,noindex">這裡 規定了 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 ,加深理解。