做者 | 惠志
來源 | 阿里巴巴雲原生公衆號html
導讀:在《一文讀懂 K8s 持久化存儲流程》一文咱們重點介紹了 K8s 內部的存儲流程,以及 PV、PVC、StorageClass、Kubelet 等之間的調用關係。接下來本文將將重點放在 CSI(Container Storage Interface)容器存儲接口上,探究什麼是 CSI 及其內部工做原理。node
K8s 原生支持一些存儲類型的 PV,如 iSCSI、NFS、CephFS 等等(詳見連接),這些 in-tree 類型的存儲代碼放在 Kubernetes 代碼倉庫中。這裏帶來的問題是 K8s 代碼與三方存儲廠商的代碼強耦合:nginx
CSI 容器存儲接口標準的出現解決了上述問題,將三方存儲代碼與 K8s 代碼解耦,使得三方存儲廠商研發人員只需實現 CSI 接口(無需關注容器平臺是 K8s 仍是 Swarm 等)。算法
在詳細介紹 CSI 組件及其接口以前,咱們先對 K8s 中 CSI 存儲流程進行一個介紹。《一文讀懂 K8s 持久化存儲流程》一文介紹了 K8s 中的 Pod 在掛載存儲卷時需經歷三個的階段:Provision/Delete(創盤/刪盤)、Attach/Detach(掛接/摘除)和 Mount/Unmount(掛載/卸載),下面以圖文的方式講解 K8s 在這三個階段使用 CSI 的流程。api
1.集羣管理員建立 StorageClass 資源,該 StorageClass 中包含 CSI 插件名稱(provisioner:pangu.csi.alibabacloud.com)以及存儲類必須的參數(parameters: type=cloud_ssd)。sc.yaml 文件以下:安全
2.用戶建立 PersistentVolumeClaim 資源,PVC 指定存儲大小及 StorageClass(如上)。pvc.yaml 文件以下:app
3.卷控制器(PersistentVolumeController)觀察到集羣中新建立的 PVC 沒有與之匹配的 PV,且其使用的存儲類型爲 out-of-tree,因而爲 PVC 打 annotation:volume.beta.kubernetes.io/storage-provisioner=[out-of-tree CSI 插件名稱](本例中即爲 provisioner:pangu.csi.alibabacloud.com)。dom
4.External Provisioner 組件觀察到 PVC 的 annotation 中包含 "volume.beta.kubernetes.io/storage-provisioner" 且其 value 是本身,因而開始創盤流程。socket
5.外部 CSI 插件返回成功後表示盤建立完成,此時External Provisioner 組件會在集羣建立一個 PersistentVolume 資源。ide
6.卷控制器會將 PV 與 PVC 進行綁定。
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 上。
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 函數。
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 函數。
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 上已沒有相關掛接信息。
1.用戶刪除相關 PVC。
2.External Provisioner 組件觀察到 PVC 刪除事件,根據 PVC 的回收策略(Reclaim)執行不一樣操做:
爲使 K8s 適配 CSI 標準,社區將與 K8s 相關的存儲流程邏輯放在了 CSI Sidecar 組件中。
Node-Driver-Registrar 組件會將外部 CSI 插件註冊到Kubelet,從而使Kubelet經過特定的 Unix Domain Socket 來調用外部 CSI 插件函數(Kubelet 會調用外部 CSI 插件的 NodeGetInfo、NodeStageVolume、NodePublishVolume、NodeGetVolumeStats 等函數)。
Node-Driver-Registrar 組件經過Kubelet 外部插件註冊機制實現註冊,註冊成功後:
Kubelet爲本節點 Node 資源打 annotation:Kubelet調用外部 CSI 插件的NodeGetInfo 函數,其返回值 [nodeID]、[driverName] 將做爲值用於 "csi.volume.kubernetes.io/nodeid" 鍵。
Kubelet更新 Node Label:將NodeGetInfo 函數返回的 [AccessibleTopology] 值用於節點的 Label。
建立/刪除實際的存儲卷,以及表明存儲卷的 PV 資源。
External-Provisioner在啓動時需指定參數 -- provisioner,該參數指定 Provisioner 名稱,與 StorageClass 中的 provisioner 字段對應。
External-Provisioner啓動後會 watch 集羣中的 PVC 和 PV 資源。
對於集羣中的 PVC 資源:
判斷 PVC 是否須要動態建立存儲卷,標準以下:
經過特定的 Unix Domain Socket 調用外部 CSI 插件的 CreateVolume 函數。
對於集羣中的 PV 資源:
判斷 PV 是否須要刪除,標準以下:
經過特定的 Unix Domain Socket 調用外部 CSI 插件的 DeleteVolume 接口。
掛接/摘除存儲卷。
External-Attacher 內部會時刻 watch 集羣中的 VolumeAttachment 資源和 PersistentVolume 資源。
對於 VolumeAttachment 資源:
從 VolumeAttachment 資源中得到 PV 的全部信息,如 volume ID、node ID、掛載 Secret 等。
對於 PersistentVolume 資源:
在掛接時爲相關 PV 打上 Finalizer:external-attacher/[driver 名稱]。
擴容存儲卷。
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。
Volume Manager(Kubelet 組件)觀察到存儲卷需在線擴容,因而經過特定的 Unix Domain Socket 調用外部 CSI 插件的NodeExpandVolume 接口實現文件系統擴容。
檢查 CSI 插件是否正常。
經過對外暴露一個 / healthz HTTP 端口以服務 kubelet 的探針探測器,內部是經過特定的 Unix Domain Socket 調用外部 CSI 插件的 Probe 接口。
三方存儲廠商需實現 CSI 插件的三大接口:IdentityServer、ControllerServer、NodeServer。
IdentityServer 主要用於認證 CSI 插件的身份信息。
// IdentityServer is the server API for Identity service. type IdentityServer interface { // 獲取CSI插件的信息,好比名稱、版本號 GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error) // 獲取CSI插件提供的能力,好比是否提供ControllerService能力 GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error) // 獲取CSI插件健康情況 Probe(context.Context, *ProbeRequest) (*ProbeResponse, error) }
ControllerServer 主要負責存儲卷及快照的建立/刪除以及掛接/摘除操做。
// ControllerServer is the server API for Controller service. type ControllerServer interface { // 建立存儲卷 CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) // 刪除存儲卷 DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error) // 掛接存儲捲到特定節點 ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error) // 從特定節點摘除存儲卷 ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error) // 驗證存儲卷能力是否知足要求,好比是否支持跨節點多讀多寫 ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error) // 列舉所有存儲卷信息 ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) // 獲取存儲資源池可用空間大小 GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error) // 獲取ControllerServer支持功能點,好比是否支持快照能力 ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error) // 建立快照 CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error) // 刪除快照 DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error) // 獲取全部快照信息 ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error) // 擴容存儲卷 ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error) }
NodeServer 主要負責存儲卷掛載/卸載操做。
// NodeServer is the server API for Node service. type NodeServer interface { // 將存儲卷格式化並掛載至臨時全局目錄 NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error) // 將存儲卷從臨時全局目錄卸載 NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error) // 將存儲卷從臨時目錄bind-mount到目標目錄 NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error) // 將存儲卷從目標目錄卸載 NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error) // 獲取存儲卷的容量信息 NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error) // 存儲卷擴容 NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error) // 獲取NodeServer支持功能點,好比是否支持獲取存儲卷容量信息 NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error) // 獲取CSI節點信息,好比最大支持卷個數 NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error) }
K8s 爲支持 CSI 標準,包含以下 API 對象:
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。
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 插件。
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 記錄了存儲卷的掛接/摘除信息以及節點信息。
在 StorageClass 中有 AllowedTopologies 字段:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-pangu provisioner: pangu.csi.alibabacloud.com parameters: type: cloud_ssd volumeBindingMode: Immediate allowedTopologies: - matchLabelExpressions: - key: topology.pangu.csi.alibabacloud.com/zone values: - zone-1 - zone-2
外部 CSI 插件部署後會爲每一個節點打標,打標內容NodeGetInfo 函數返回的 [AccessibleTopology] 值(詳見 Node Driver Registrar 部分)。
External Provisioner在調用 CSI 插件的 CreateVolume 接口以前,會在請求參數設置 AccessibilityRequirements:
對於 WaitForFirstConsumer
基於社區 1.18 版本調度器
調度器的調度過程主要有以下三步:
調度器預選階段:處理 Pod 的 PVC/PV 綁定關係以及動態供應 PV(Dynamic Provisioning),同時使調度器調度時考慮 Pod 所使用 PV 的節點親和性。詳細調度過程以下:
Pod 不包含 PVC 直接跳過。
FindPodVolumes
獲取 Pod 的 boundClaims、claimsToBind 以及 unboundClaimsImmediate。
若 len(unboundClaimsImmediate) 不爲空,表示這種 PVC 須要當即綁定 PV(即存 PVC 建立後,馬上動態建立 PV 並將其綁定到 PVC,該過程不走調度),若 PVC 處於 unbound 階段則報錯。
若 len(boundClaims) 不爲空,則檢查 PVC 對應 PV 的節點親和性與當前節點的 Label 是否衝突,若衝突則報錯(可檢查 Immediate 類型的 PV 拓撲)。
調度器優選階段不討論。
調度器 Assume 階段
調度器會先 Assume PV/PVC,再 Assume Pod。
將當前待調度的 Pod 進行深拷貝。
AssumePodVolumes(針對 WaitForFirstConsumer 類型的 PVC)
Assume Pod 完畢
調度器 Bind 階段
BindPodVolumes:
調用 Kubernetes 的 API 更新集羣中 PV/PVC 資源,使其與調度器 Cache 中的 PV/PVC 一致。
檢查 PV/PVC 狀態:
存儲卷擴容部分在 External Resizer 部分已提到,故再也不贅述。用戶只須要編輯 PVC 的 .Spec.Resources.Requests.Storage 字段便可,注意只可擴容不可縮容。
若 PV 擴容失敗,此時 PVC 沒法從新編輯 spec 字段的 storage 爲原來的值(只可擴容不可縮容)。參考 K8s 官網提供的 PVC 還原方法:
https://kubernetes.io/docs/concepts/storage/persistent-volumes/#recovering-from-failure-when-expanding-volumes
卷數量限制在 Node Driver Registrar 部分已提到,故再也不贅述。
存儲商需實現 CSI 插件的 NodeGetVolumeStats 接口,Kubelet 會調用該函數,並反映在其 metrics上:
CSI 存儲卷支持傳入 Secret 來處理不一樣流程中所須要的私密數據,目前 StorageClass 支持以下 Parameter:
Secret 會包含在對應 CSI 接口的參數中,如對於 CreateVolume 接口而言則包含在 CreateVolumeRequest.Secrets 中。
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 函數。
鑑於本文篇幅,此處不作過多原理性介紹。讀者感興趣見官方介紹:卷快照、卷克隆。
本文首先對 CSI 核心流程進行了大致介紹,並結合 CSI Sidecar 組件、CSI 接口、API 對象對 CSI 標準進行了深度解析。在 K8s 上,使用任何一種 CSI 存儲卷都離不開上面的流程,環境上的容器存儲問題也必定是其中某個環節出現了問題。本文對其流程進行梳理,以便於廣大程序猿(媛)排查環境問題。
容器存儲的坑比較多,專有云環境下尤爲如此。不過挑戰越多,機遇也越多!