背景
K8s 原生支持一些存儲類型的 PV,如 iSCSI、NFS、CephFS 等等,這些 in-tree 類型的存儲代碼放在 Kubernetes 代碼倉庫中。這里帶來的問題是 K8s 代碼與三方存儲廠商的代碼強耦合:
- 更改 in-tree 類型的存儲代碼,用戶必須更新 K8s 組件,成本較高
- in-tree 存儲代碼中的 bug 會引發 K8s 組件不穩定
- K8s 社區需要負責維護及測試 in-tree 類型的存儲功能
- in-tree 存儲插件享有與 K8s 核心組件同等的特權,存在安全隱患
- 三方存儲開發者必須遵循 K8s 社區的規則開發 in-tree 類型存儲代碼
CSI 容器存儲接口標准的出現解決了上述問題,將三方存儲代碼與 K8s 代碼解耦,使得三方存儲廠商研發人員只需實現 CSI 接口(無需關注容器平台是 K8s 還是 Swarm 等)。
CSI 核心流程介紹
K8s 中的 Pod 在掛載存儲卷時需經歷三個的階段:
- Provision/Delete(創盤/刪盤)
- Attach/Detach(掛接/摘除)
- Mount/Unmount(掛載/卸載),
下面以圖文的方式講解 K8s 在這三個階段使用 CSI 的流程。
1.Provisioning Volumes(External Provisioner)
1.集群管理員創建 StorageClass 資源,該 StorageClass 中包含 CSI 插件名稱。
2.用戶創建 PersistentVolumeClaim 資源,PVC 指定存儲大小及 StorageClass。
3.卷控制器(PersistentVolumeController)觀察到集群中新創建的 PVC 沒有與之匹配的 PV,且其使用的存儲類型為 out-of-tree,於是為 PVC 打 annotation:volume.beta.kubernetes.io/storage-provisioner=[out-of-tree CSI 插件名稱]
4.External Provisioner 組件觀察到 PVC 的 annotation 中包含 “volume.beta.kubernetes.io/storage-provisioner” 且其 value 是自己,於是開始創盤流程。
獲取相關 StorageClass 資源並從中獲取參數,用於后面 CSI 函數調用。
通過 unix domain socket 調用外部 CSI 插件的CreateVolume 函數。
5.外部 CSI 插件返回成功后表示盤創建完成,此時External Provisioner 組件會在集群創建一個 PersistentVolume 資源。
6.卷控制器會將 PV 與 PVC 進行綁定。
2.Attaching Volumes(External Attacher)
1.AD 控制器(AttachDetachController)觀察到使用 CSI 類型 PV 的 Pod 被調度到某一節點,此時AD 控制器會調用內部 in-tree CSI 插件(csiAttacher)的 Attach 函數。
2.內部 in-tree CSI 插件(csiAttacher)會創建一個 VolumeAttachment 對象到集群中。
3.External Attacher 觀察到該 VolumeAttachment 對象,並調用外部 CSI插件的ControllerPublish 函數以將卷掛接到對應節點上。外部 CSI 插件掛載成功后,External Attacher會更新相關 VolumeAttachment 對象的 .Status.Attached 為 true。
4.AD 控制器內部 in-tree CSI 插件(csiAttacher)觀察到 VolumeAttachment 對象的 .Status.Attached 設置為 true,於是更新AD 控制器內部狀態(ActualStateOfWorld),該狀態會顯示在 Node 資源的 .Status.VolumesAttached 上。
3.Mounting Volumes(Kubelet)
1.Volume Manager(Kubelet 組件)觀察到有新的使用 CSI 類型 PV 的 Pod 調度到本節點上,於是調用內部 in-tree CSI 插件(csiAttacher)的 WaitForAttach 函數。
2.內部 in-tree CSI 插件(csiAttacher)等待集群中 VolumeAttachment 對象狀態 .Status.Attached 變為 true。
3.in-tree CSI 插件(csiAttacher)調用 MountDevice 函數,該函數內部通過 unix domain socket 調用外部 CSI 插件的NodeStageVolume 函數;之后插件(csiAttacher)調用內部 in-tree CSI 插件(csiMountMgr)的 SetUp 函數,該函數內部會通過 unix domain socket 調用外部 CSI 插件的NodePublishVolume 函數。
4.Unmounting Volumes(Kubelet)
1.用戶刪除相關 Pod。
2.Volume Manager(Kubelet 組件)觀察到包含 CSI 存儲卷的 Pod 被刪除,於是調用內部 in-tree CSI 插件(csiMountMgr)的 TearDown 函數,該函數內部會通過 unix domain socket 調用外部 CSI 插件的 NodeUnpublishVolume 函數。
3.Volume Manager(Kubelet 組件)調用內部 in-tree CSI 插件(csiAttacher)的 UnmountDevice 函數,該函數內部會通過 unix domain socket 調用外部 CSI 插件的 NodeUnpublishVolume 函數。
5.Detaching Volumes(External Attacher)
1.AD 控制器觀察到包含 CSI 存儲卷的 Pod 被刪除,此時該控制器會調用內部 in-tree CSI 插件(csiAttacher)的 Detach 函數。
2.csiAttacher會刪除集群中相關 VolumeAttachment 對象(但由於存在 finalizer,va 對象不會立即刪除)。
3.External Attacher觀察到集群中 VolumeAttachment 對象的 DeletionTimestamp 非空,於是調用外部 CSI 插件的ControllerUnpublish 函數以將卷從對應節點上摘除。外部 CSI 插件摘除成功后,External Attacher會移除相關 VolumeAttachment 對象的 finalizer 字段,此時 VolumeAttachment 對象被徹底刪除。
4.AD 控制器中內部 in-tree CSI 插件(csiAttacher)觀察到 VolumeAttachment 對象已刪除,於是更新AD 控制器中的內部狀態;同時AD 控制器更新 Node 資源,此時 Node 資源的 .Status.VolumesAttached 上已沒有相關掛接信息。
6.Deleting Volumes(External Provisioner)
1.用戶刪除相關 PVC。
2.External Provisioner 組件觀察到 PVC 刪除事件,根據 PVC 的回收策略(Reclaim)執行不同操作:
Delete:調用外部 CSI 插件的DeleteVolume 函數以刪除卷;一旦卷成功刪除,Provisioner會刪除集群中對應 PV 對象。
Retain:Provisioner不執行卷刪除操作。
CSI Sidecar 組件介紹
為使 K8s 適配 CSI 標准,社區將與 K8s 相關的存儲流程邏輯放在了 CSI Sidecar 組件中。
1. Node Driver Registrar
1)功能
Node-Driver-Registrar 組件會將外部 CSI 插件注冊到Kubelet,從而使Kubelet通過特定的 Unix Domain Socket 來調用外部 CSI 插件函數(Kubelet 會調用外部 CSI 插件的 NodeGetInfo、NodeStageVolume、NodePublishVolume、NodeGetVolumeStats 等函數)。
2)原理
Node-Driver-Registrar 組件通過Kubelet 外部插件注冊機制實現注冊,注冊成功后:
Kubelet為本節點 Node 資源打 annotation:Kubelet調用外部 CSI 插件的NodeGetInfo 函數,其返回值 [nodeID]、[driverName] 將作為值用於 “csi.volume.kubernetes.io/nodeid” 鍵。
Kubelet更新 Node Label:將NodeGetInfo 函數返回的 [AccessibleTopology] 值用於節點的 Label。
Kubelet更新 Node Status:將NodeGetInfo 函數返回的 maxAttachLimit(節點最大可掛載卷數量)更新到 Node 資源的 Status.Allocatable:attachable-volumes-csi-[driverName]=[maxAttachLimit]。
Kubelet更新 CSINode 資源(沒有則創建):將 [driverName]、[nodeID]、[maxAttachLimit]、[AccessibleTopology] 更新到 Spec 中(拓撲僅保留 Key 值)。
2. External Provisioner
1)功能
創建/刪除實際的存儲卷,以及代表存儲卷的 PV 資源。
2)原理
External-Provisioner在啟動時需指定參數 — provisioner,該參數指定 Provisioner 名稱,與 StorageClass 中的 provisioner 字段對應。
External-Provisioner啟動后會 watch 集群中的 PVC 和 PV 資源。
對於集群中的 PVC 資源:
判斷 PVC 是否需要動態創建存儲卷,標准如下:
PVC 的 annotation 中是否包含 “volume.beta.kubernetes.io/storage-provisioner” 鍵(由卷控制器創建),並且其值是否與 Provisioner 名稱相等。
PVC 對應 StorageClass 的 VolumeBindingMode 字段若為 WaitForFirstConsumer,則 PVC 的 annotation 中必須包含 “volume.kubernetes.io/selected-node” 鍵(詳見調度器如何處理 WaitForFirstConsumer),且其值不為空;若為 Immediate 則表示需要 Provisioner 立即提供動態存儲卷。
通過特定的 Unix Domain Socket 調用外部 CSI 插件的 CreateVolume 函數。
創建 PV 資源,PV 名稱為 [Provisioner 指定的 PV 前綴] – [PVC uuid]。
對於集群中的 PV 資源:
判斷 PV 是否需要刪除,標准如下:
判斷其 .Status.Phase 是否為 Release。
判斷其 .Spec.PersistentVolumeReclaimPolicy 是否為 Delete。
判斷其是否包含 annotation(pv.kubernetes.io/provisioned-by),且其值是否為自己。
通過特定的 Unix Domain Socket 調用外部 CSI 插件的 DeleteVolume 接口。
刪除集群中的 PV 資源。
3. External Attacher
1)功能
掛接/摘除存儲卷。
2)原理
External-Attacher 內部會時刻 watch 集群中的 VolumeAttachment 資源和 PersistentVolume 資源。
對於 VolumeAttachment 資源:
從 VolumeAttachment 資源中獲得 PV 的所有信息,如 volume ID、node ID、掛載 Secret 等。
判斷 VolumeAttachment 的 DeletionTimestamp 字段是否為空來判斷其為卷掛接或卷摘除:若為卷掛接則通過特定的 Unix Domain Socket 調用外部 CSI 插件的ControllerPublishVolume 接口;若為卷摘除則通過特定的 Unix Domain Socket 調用外部 CSI 插件的ControllerUnpublishVolume 接口。
對於 PersistentVolume 資源:
在掛接時為相關 PV 打上 Finalizer:external-attacher/[driver 名稱]。
當 PV 處於刪除狀態時(DeletionTimestamp 非空),刪除 Finalizer:external-attacher/[driver 名稱]。
4. External Resizer
1)功能
擴容存儲卷。
2)原理
External-Resizer內部會 watch 集群中的 PersistentVolumeClaim 資源。
對於 PersistentVolumeClaim 資源:
判斷 PersistentVolumeClaim 資源是否需要擴容:PVC 狀態需要是 Bound 且 .Status.Capacity 與 .Spec.Resources.Requests 不等。
更新 PVC 的 .Status.Conditions,表明此時處於 Resizing 狀態。
通過特定的 Unix Domain Socket 調用外部 CSI 插件的 ControllerExpandVolume 接口。
更新 PV 的 .Spec.Capacity。
若 CSI 支持文件系統在線擴容,ControllerExpandVolume 接口返回值中 NodeExpansionRequired 字段為 true,External-Resizer更新 PVC 的 .Status.Conditions 為 FileSystemResizePending 狀態;若不支持則擴容成功,External-Resizer更新 PVC 的 .Status.Conditions 為空,且更新 PVC 的 .Status.Capacity。
Volume Manager(Kubelet 組件)觀察到存儲卷需在線擴容,於是通過特定的 Unix Domain Socket 調用外部 CSI 插件的NodeExpandVolume 接口實現文件系統擴容。
5. livenessprobe
1)功能
檢查 CSI 插件是否正常。
2)原理
通過對外暴露一個 / healthz HTTP 端口以服務 kubelet 的探針探測器,內部是通過特定的 Unix Domain Socket 調用外部 CSI 插件的 Probe 接口。
CSI 接口介紹
三方存儲廠商需實現 CSI 插件的三大接口:
- IdentityServer
- ControllerServer
- NodeServer。
1. IdentityServer
IdentityServer 主要用於認證 CSI 插件的身份信息。
2. ControllerServer
ControllerServer 主要負責存儲卷及快照的創建/刪除以及掛接/摘除操作。
3. NodeServer
NodeServer 主要負責存儲卷掛載/卸載操作。
K8s CSI API 對象
K8s 為支持 CSI 標准,包含如下 API 對象:
- CSINode
- CSIDriver
- VolumeAttachment
1. CSINode
apiVersion: storage.k8s.io/v1beta1
kind: CSINode
metadata:
name: node-10.212.101.210
spec:
drivers:
- name: yodaplugin.csi.alibabacloud.com
nodeID: node-10.212.101.210
topologyKeys:
- kubernetes.io/hostname
- name: pangu.csi.alibabacloud.com
nodeID: a5441fd9013042ee8104a674e4a9666a
topologyKeys:
- topology.pangu.csi.alibabacloud.com/zone
作用:
判斷外部 CSI 插件是否注冊成功。在 Node Driver Registrar 組件向 Kubelet 注冊完畢后,Kubelet 會創建該資源,故不需要顯式創建 CSINode 資源。
將 Kubernetes 中 Node 資源名稱與三方存儲系統中節點名稱(nodeID)一一對應。此處Kubelet會調用外部 CSI 插件NodeServer 的 GetNodeInfo 函數獲取 nodeID。
顯示卷拓撲信息。CSINode 中 topologyKeys 用來表示存儲節點的拓撲信息,卷拓撲信息會使得Scheduler在 Pod 調度時選擇合適的存儲節點。
2. CSIDriver
apiVersion: storage.k8s.io/v1beta1
kind: CSIDriver
metadata:
name: pangu.csi.alibabacloud.com
spec:
# 插件是否支持卷掛接(VolumeAttach)
attachRequired: true
# Mount階段是否CSI插件需要Pod信息
podInfoOnMount: true
# 指定CSI支持的卷模式
volumeLifecycleModes:
- Persistent
作用:
簡化外部 CSI 插件的發現。由集群管理員創建,通過 kubectl get csidriver 即可得知環境上有哪些 CSI 插件。
自定 義Kubernetes 行為,如一些外部 CSI 插件不需要執行卷掛接(VolumeAttach)操作,則可以設置 .spec.attachRequired 為 false。
3. VolumeAttachment
apiVersion: storage.k8s.io/v1
kind: VolumeAttachment
metadata:
annotations:
csi.alpha.kubernetes.io/node-id: 21481ae252a2457f9abcb86a3d02ba05
finalizers:
- external-attacher/pangu-csi-alibabacloud-com
name: csi-0996e5e9459e1ccc1b3a7aba07df4ef7301c8e283d99eabc1b69626b119ce750
spec:
attacher: pangu.csi.alibabacloud.com
nodeName: node-10.212.101.241
source:
persistentVolumeName: pangu-39aa24e7-8877-11eb-b02f-021234350de1
status:
attached: true
作用:VolumeAttachment 記錄了存儲卷的掛接/摘除信息以及節點信息。
支持特性
1. 拓撲支持
2. 存儲卷擴容
3. 單節點卷數量限制
4. 存儲卷監控
存儲商需實現 CSI 插件的 NodeGetVolumeStats 接口,Kubelet 會調用該函數,並反映在其 metrics上:
kubelet_volume_stats_capacity_bytes:存儲卷容量
kubelet_volume_stats_used_bytes:存儲卷已使用容量
kubelet_volume_stats_available_bytes:存儲卷可使用容量
kubelet_volume_stats_inodes:存儲卷 inode 總量
kubelet_volume_stats_inodes_used:存儲卷 inode 使用量
kubelet_volume_stats_inodes_free:存儲卷 inode 剩余量
5. Secret
CSI 存儲卷支持傳入 Secret 來處理不同流程中所需要的私密數據,目前 StorageClass 支持如下 Parameter:
csi.storage.k8s.io/provisioner-secret-name
csi.storage.k8s.io/provisioner-secret-namespace
csi.storage.k8s.io/controller-publish-secret-name
csi.storage.k8s.io/controller-publish-secret-namespace
csi.storage.k8s.io/node-stage-secret-name
csi.storage.k8s.io/node-stage-secret-namespace
csi.storage.k8s.io/node-publish-secret-name
csi.storage.k8s.io/node-publish-secret-namespace
csi.storage.k8s.io/controller-expand-secret-name
csi.storage.k8s.io/controller-expand-secret-namespace
Secret 會包含在對應 CSI 接口的參數中,如對於 CreateVolume 接口而言則包含在 CreateVolumeRequest.Secrets 中。
6. 塊設備
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-example
spec:
selector:
matchLabels:
app: nginx
serviceName: "nginx"
volumeClaimTemplates:
- metadata:
name: html
spec:
accessModes:
- ReadWriteOnce
volumeMode: Block
storageClassName: csi-pangu
resources:
requests:
storage: 40Gi
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeDevices:
- devicePath: "/dev/vdb"
name: html
三方存儲廠商需實現 NodePublishVolume 接口。Kubernetes 提供了針對塊設備的工具包(”k8s.io/kubernetes/pkg/util/mount”),在 NodePublishVolume 階段可調用該工具的 EnsureBlock 和 MountBlock 函數。