Kubernetes Storage : 淺談如何實現一個 CSI 插件
現在,我將繼續和大家聊一聊關于 K8s 存儲的一個重要組成部分:Container Storage Interface (CSI)。在接下來的內容中,我們將會了解到 CSI 的工作原理、核心概念以及如何將其集成到你的容器化環境中。
為什么需要 CSI ?它解決了什么問題?
在學習 CSI 之前,了解其產生的背景以及它能夠解決的問題我覺得是很有必要的。
為什么需要 CSI
雖然 Kubernetes 平臺它本身支持了非常多的存儲插件,但是畢竟也是有限的,永遠無法滿足用戶日益增長的需求,比方說有客戶要求我們的 Paas 平臺必須接國產的存儲怎么辦?
面臨的問題,如何做集成?
Kubernetes 本身提供了一個強大的 Volume 插件系統,最直接的方式就是向這個 Volume Plugin 增加新的插件。
但是,想必大家也知道 Kubernetes 太復雜了,它有一定的學習曲線,這樣做一來成本比較高,再者直接集成第三方代碼,可能會對 Kubernetes 平臺系統的可靠性和安全性產生隱患。
另外,這種方法它也不方便測試和維護,比方說第三方存儲服務如果有變更,我們就需要提交變更代碼到 Kubernetes,等待 Code Review。換句話說,我們必須要等到 Kubernetes 發布才能將存儲服務的變動上線,這就意味著存儲的集成與 Kubernetes 的發布周期捆綁在一起了。
所以直接和 Kubernetes 做集成是非常不方便的。
讓我們重新回過來看下,上面反復提到過的 In-Tree 和 Out-Of-Tree 這兩個概念,我相信從字面意思上大家都已經理解了,再結合下面這張表格,大家心里是否都已經有了答案?
圖片
解決了什么痛點?
CSI 將三方存儲代碼與 K8s 代碼解耦,不同的存儲插件只要實現這些統一的接口,就能對接 K8s,用戶無需接觸核心的 K8s 代碼。
最重要的是,CSI 規范現在是業界容器編排統一的存儲接入標準。
圖片
Container Orchestrators(CO)
那么,什么是 CSI?
以下是 ChatGPT 給出的回答:
CSI(Container Storage Interface) 是一個規范化的接口,用于容器編排引擎與存儲供應商之間的通信。它允許存儲供應商編寫標準的插件,以便容器編排引擎可以與其集成,從而實現更加靈活和可擴展的存儲解決方案。
CSI 驅動器由三個主要組件組成,每個組件都扮演著特定的角色:
Node Service: 運行在每個 Kubernetes 節點上,負責在節點上掛載和卸載存儲卷,并處理節點級別的存儲操作。
Controller Service: 運行在 Kubernetes 控制平面中,負責管理存儲卷的生命周期,包括創建、刪除和擴容等操作。
Identity Service: 它是 CSI 驅動器的第三個組件,它在 CSI 驅動器注冊時提供標識信息,并向 Kubernetes 集群公開驅動器的支持能力。它負責告知 Kubernetes 驅動器的存在,提供驅動器的基本信息和功能支持。
CSI 的設計思想是將存儲管理和容器編排系統解耦,使得新的存儲系統可以通過實現一組標準化的接口來與 Kubernetes 進行集成,而無需修改 Kubernetes 的核心代碼。
CSI 驅動器的出現為 Kubernetes 用戶帶來了更多的存儲選擇,同時也為存儲供應商和開發者提供了更方便的接入點,使得集群的存儲管理更加靈活和可擴展。
圖片
Storage in Cloud Native Environment
CSI 適配工作是由容器編排系統(CO)和存儲提供商(SP)共同完成的,CO 通過 gRPC 與 CSI 插件進行通信。相信大家也都觀察到了,CSI 在這里充當了連接的紐帶,上層連接容器編排系統,下層操作三方存儲服務。
CSI 的工作原理,它是如何工作的?
Typical CSI driver architecture
下面是 CSI 的一個典型架構,雖然 CSI 對于存儲提供商來說只需實現三個模塊即可,但是整個編排流程可以說是相當復雜的。
圖片
Kubernetes cluster with CSI
CSI 的整個運轉流程會涉及到兩方面的組件:
?? 由 Kubernetes 官方維護的一組標準 external 組件,他們主要負責監聽 K8s 里的資源對象,從而向 CSI Driver 發起 gRPC 調用,詳見:Kubernetes CSI Sidecar Containers[1]。它們是與 CSI 驅動器一起部署在同一個 Pod 中,用于輔助 CSI Driver 完成一些額外的任務和功能。?? 各存儲廠商開發的組件(需要實現 Identity Service,Controller Service,Node Service)
我們來看下左邊的 CSI Driver Controller 部分,它是通過多個 Sidecar 組件配合第三方實現的插件(Controller Service)來完成的。
正如上面提到的,它負責管理存儲卷的生命周期,包括創建、刪除和擴容等操作。換句話說,我們的存儲廠商能夠提供什么樣的能力,部署 Controller 的時候,就需要提供與之對應的 Sidecar 容器一起部署。
好比說我的 CSI Driver 只提供了 Dynamic provisioning 的能力,其他能力的接口我都沒實現,在控制器部署的時候,我只需要將 external-provisioner 和我的 Controller Service 部署在一個 Pod 即可,組件的選擇完全取決于三方的實現。
Kubernetes CSI Sidecar Containers
#1. external-provisioner
external-provisioner 是一個 Sidecar 容器,用于在 Kubernetes 中動態地創建和刪除外部存儲卷。
當一個新的 PVC (PersistentVolumeClaim) 被創建時,external-provisioner 會向外部存儲系統發起請求,以創建相應的存儲卷,并將其與 PVC 關聯,從而滿足 Pod 對持久化存儲的需求。
external-provisioner 實際上會執行檢查,以驗證 PVC 中是否存在注解 volume.kubernetes.io/storage-provisioner,并且該注解的值是否與 CSI 驅動程序的名稱相匹配。整個流程貫穿了 PV Controller 這個組件。
圖片
provision
這里涉及到的兩個操作分別對應著 Controller Service 中的 CreateVolume 和 DeleteVolume 兩個接口的實現,它們的調用者正是 External Provisoner。
這一流程的核心是,external-provisioner 充當了中間人,通過 Kubernetes 的 PVC 和 StorageClass 機制,將 Pod 的持久存儲需求傳遞給外部存儲系統。這使得存儲卷的創建和管理能夠無縫集成到 Kubernetes 集群中,為應用提供了持久性的存儲解決方案。
思考
假設每個 PVC 背后對應的 Volume 都需要獨立加密,并且加密密鑰也各不相同,PVC 的 Spec 中已經沒有額外的參數來提供這些信息了,那么我們如何將這些加密密鑰傳遞給 CSI 接口呢?
這里有必要提一下 CSI Operation Secrets 這個概念,它允許針對每種不同的 CSI 操作定制不同的 Secret,并且通過 StorageClass 與之配合使用。
讓我們來看下面這個 StorageClass 的定義作為例子:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: xfs2-sc-4-per-volume
provisioner: xfs2.csi.basebit.ai
parameters:
csi.storage.k8s.io/provisioner-secret-name: ${pvc.name}
csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}需要關注的是 parameters 字段中的 csi.storage.k8s.io/provisioner-secret-name 字段的值,它使用了變量 ${pvc.name}。怎么理解呢?
具體而言,當我們使用 ${pvc.name} 作為 csi.storage.k8s.io/provisioner-secret-name 參數的值時,每次創建 PVC 后,都可以為它創建一個對應的 Provisioner Secret,并將該 PVC 的名稱作為 Provisioner Secret 的名稱。
這樣,每個 PVC 都會擁有一個唯一的 Provisioner Secret,用于身份驗證和認證。Secret 的具體定義取決于每個 CSI 驅動器的實現。在針對創建存儲卷的場景中,CreateVolumeRequest 可以獲取到 Secret 的詳細信息。
這種參數化技術是 Kubernetes 中允許的一種靈活方式,可用于在運行時動態生成配置文件
provision、delete、expand、attach 和 detach 等操作通常也需要 CSI 驅動程序在存儲后端使用憑證,后面就不再多做贅述了。
如需了解更多高級用法,請參考文檔:StorageClass Secrets[2]
#2. external-attacher
external-attacher 是一個 Sidecar 容器,其作用是在 Kubernetes 節點上動態地進行外部存儲卷的 掛載(Attach) 和 卸載(Detach) 操作。
它是通過監聽 Kubernetes API Server 中的 VolumeAttachment 對象,來觸發 Controller Service 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 兩個接口調用。
然而,并不是所有情況下都需要使用 attach/detach 操作,尤其是在一些分布式文件系統的 CSI 驅動程序中,這樣的操作可能并不適用。因此,可以說 attach/detach 是一項可選的特性。
#3. external-resizer
external-resizer 是一個 Sidecar 容器,用于調整外部存儲卷的大小。
當 PVC 的存儲需求發生變化時,external-resizer 可以根據需求調整外部存儲卷的大小,確保存儲資源得到最優的利用。
它會調用 Controller Service 中的 ControllerExpandVolume 接口。
#4. external-snapshotter
external-snapshotter 是一個 Sidecar 容器,用于實現外部存儲卷的快照功能。
它是通過監聽 Kubernetes Snapshot CRD 對象,來觸發 Controller Service 中的 CreateSnapshot 和 DeleteSnapshot 兩個接口調用。
它負責在外部存儲系統上創建、刪除和管理快照,以便于實現數據備份、恢復和復制等功能。
思考
這玩意兒有什么用呢?VolumeSnapshot 允許在 Kubernetes 集群中創建卷的快照,這些快照可以用于數據備份和應用程序恢復。當應用程序出現故障或數據損壞時,我們可以使用先前創建的快照來還原應用程序的狀態。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data-my-redis-master-0
spec:
dataSource:
name: my-server-snapshot-0
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi#5. liveness-probe
liveness-probe 是一個 Sidecar 容器,用于監控 CSI 驅動程序的運行狀況,并通過 Liveness Probe 機制向 Kubernetes 報告。
如果驅動器出現故障或停止響應,Kubernetes 將重啟相關的 Pod(Controller Service/Node Service) 以確保服務的可用性。
有關具體的用法和配置示例,可以參考這里[3]的說明文檔。
思考
實際上 livenessProbe 是通過 CSI 驅動程序在容器上設置的。liveness-probe sidecar 容器的主要作用是提供了 /healthz 這個 HTTP 端點。它背后 checkProbe 最終向 CSI 驅動程序的 Identity Service 中的 Probe 接口發起調用,用來檢測插件是否處于健康狀態。
這種架構使得插件的健康狀態檢查與應用程序分離,通過 CSI 驅動程序的 Probe 接口進行通信。
事實上 Kubernetes 從 v1.23 開始具有內置 gRPC 健康探測,已經不需要這么麻煩了。
#6. node-driver-registrar
node-driver-registrar 是一個作為 Sidecar 容器運行的組件,其主要職責是通過直接調用 CSI 驅動程序的 Node Service 中的 NodeGetInfo 接口,獲取驅動程序的信息。
然后,它會利用 kubelet 的插件注冊機制,將這些驅動程序的信息注冊到相應節點的 kubelet 中。
小結
我們需要記住這張能力關系組合表,它對部署 CSI 驅動程序和排查問題非常的有用。
圖片
如何實現一個 CSI 插件?
要實現一個 CSI 驅動程序,確實只需要完成一系列接口的實現即可,但僅僅完成這些接口的實現還不足以構建一個穩健、可用的 CSI 驅動程序,構建一個穩健的驅動程序還需要考慮方方面面。
CSI Plugin components
下面是每一個 CSI 驅動程序要實現的接口清單。
其中 Identity Service 負責提供 CSI 驅動程序的身份信息,Controller Service 負責 Volume 的管理,Node Service 負責將 Volume 掛載到 Pod 中。
圖片
正如前面提到的,一個 CSI 驅動程序能提供什么樣的能力,取決于各自存儲廠商的實現,三個組件都有對外暴露能力的接口,比如
- Identity Service 中的 GetPluginCapabilities 方法,表示該 CSI 驅動程序主要提供了哪些功能。
- Controller Service 中的 ControllerGetCapabilities 方法,實際上告訴 K8s,CSI 驅動程序具備哪些能力。這些能力可以包括卷的創建、刪除、擴容、快照等操作。
- Node Service 中的 NodeGetCapabilities 方法,提供 Node plugin 的能力列表。
CSI lifecycle
在通常情況下,每個 Volume 都會經歷完整的生命周期過程。
圖片
從創建 PersistentVolumeClaim(PVC)開始,接著被 Pod 所使用,這個過程包括三個主要階段:Provision -> Attach -> Mount。
隨后,從 Pod 開始被刪除,直到 PVC 被刪除,整個過程又經歷了另外三個關鍵階段: Unmount -> Detach -> Delete。
然而,存在一種特殊的存儲卷,它就是 Ephemeral Inline Volumes,它可以通過改變 CSIDriver 的規范中的 volumeLifecycleModes 參數來改變其生命周期。
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
name: xfs2.csi.basebit.ai
spec:
...
volumeLifecycleModes:
- EphemeralEphemeral 模式表示存儲卷是臨時的,會隨著 Pod 的生命周期結束而被釋放。對于這種類型的存儲卷,Kubelet 在向 CSI 驅動請求卷時,只會調用 NodePublishVolume,省略了其他階段(例如 CreateVolume、NodeStageVolume)的調用。而在 Pod 結束需要釋放存儲卷時,只會調用 NodeUnpublishVolume。
具體的 Pod 規范如下所示:
kind: Pod
apiVersion: v1
metadata:
name: my-csi-app
spec:
containers:
- name: my-frontend
image: busybox
volumeMounts:
- mountPath: "/data"
name: my-csi-inline-vol
command: ["sleep","1000000"]
volumes:
- name: my-csi-inline-vol
csi:
driver: xfs2.csi.basebit.ai
volumeAttributes:
foo: bar這里的 volumeAttributes 用于指定驅動程序需要準備的卷的屬性。這些屬性在每個驅動程序中都是特定的,沒有標準化的實現方法。
Ephemeral 使用案例
Secrets Store CSI Driver[4]允許用戶將 Secret 作為內聯卷從外部掛載到一個 Pod 中。當密鑰存儲在外部管理服務或 Vault 實例中時,這可能很有用。
Cert-Manager CSI Driver[5] 與 cert-manager[6] 協同工作, 無縫地請求和掛載證書密鑰對到一個 Pod 中。這使得證書可以在應用 Pod 中自動更新。
通過這個特性,再一次說明了我們并不要求所有的接口都需要實現,取決于插件實現方提供什么樣的能力,我們再去實現相應的邏輯即可。
CSI idempotent
我們應該確保所有的 CSI 操作都是冪等的,這意味著同一操作被多次調用時,結果始終保持一致,不會因為多次調用而導致狀態變化或產生額外的副作用。這種冪等性是保證系統穩定性和一致性的關鍵因素。
舉個例子,假設我們做一個 DeleteVolume 的操作,如果底層的 Volume 已經不存在了,依然不能報錯。無論是第一次執行 DeleteVolume 還是多次重試,操作的最終結果都應該是相同的。
這里不得不提一下 CSI Sanity Test,它可用于驗證 CSI 驅動程序的基本功能和穩定性。它會模擬不同的錯誤和異常情況,例如創建已存在的卷、卸載不存在的卷等,以驗證驅動程序對這些情況的處理是否正確。
它能夠驗證 CSI 驅動程序是否符合 Kubernetes CSI 規范并且可以正確運行,對開發 CSI 驅動程序非常的有幫助。比如說可以幫助開發人員快速定位和修復常見問題,減少在生產環境中出現意外問題的可能性。
官方文檔中詳細闡述了規范(Container Storage Interface,CSI)的內容,同時還提供了與開發相關的注意事項。這些注意事項涵蓋了規范中的一些關鍵要點,以及在開發過程中可能會遇到的挑戰和解決方案。我們可以在 Container Storage Interface (CSI) Specification [7] 找到這些詳細信息。
如何部署 CSI?
標準的 CSI 驅動程序部署架構如下圖所示,其中包括一個由 DaemonSet 運行的 CSI Node 組件,以及一個運行在 StatefulSet 內的 CSI Controller 組件。
?? 這兩個容器通過本地 Socket (Unix Domain Socket, UDS)進行通信,并使用 gRPC 協議。CSI 插件直接與同一宿主機上的 K8s 組件進行交互,通過本機進程之間的 Unix 域套接字通信,相較于 TCP 套接字,具備更高的通信效率和性能。
圖片
在部署 CSI Node 時,需要將宿主機上的 kubelet 目錄(/var/lib/kubelet)掛載到驅動程序的容器內,且需將 Mount Propagation 設置為 Bidirectional。這樣,驅動程序容器內的后續 Mount/Umount 操作能夠傳播到宿主機上。
請注意,這只是一個高層次的架構概述,具體的實施細節可能會因不同的 CSI 插件和環境而有所變化。
CSI 在集群部署成功后,可以用以下兩個命令來做下檢查:
#1. 查看集群內安裝的 CSI Driver
? kubectl get csidrivers#2. 列出哪些節點具有 CSI
? kubectl get csinodes總結
CSI 是 Kubernetes 存儲體系中的核心組件,為存儲供應商提供了靈活且可擴展的集成方式,也為 Kubernetes 用戶提供了高效穩定的存儲解決方案。
通過標準化容器編排器與存儲供應商之間的接口,CSI 構建了一種統一的范式,確保所有與 CSI 兼容的存儲系統都遵循相同的實現規范。事實上,通過編寫一個 CSI 驅動程序,我們不僅為 Kubernetes 存儲架構增添了新的維度,還深化了對存儲資源管理的理解。
下一期,我將繼續與大家分享在實際工作中使用 CSI Driver 遇到的問題和挑戰。
參考資料
[1]Kubernetes CSI Sidecar Containers: https://kubernetes-csi.github.io/docs/sidecar-containers.html
[2]StorageClass Secrets: https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html
[3]這里: https://github.com/kubernetes-csi/livenessprobe/blob/master/deployment/kubernetes/livenessprobe-sidecar.yaml
[4]Secrets Store CSI Driver: https://github.com/kubernetes-sigs/secrets-store-csi-driver
[5]Cert-Manager CSI Driver: https://github.com/cert-manager/csi-driver
[6]cert-manager: https://cert-manager.io/
[7]Container Storage Interface (CSI) Specification : https://github.com/container-storage-interface/spec/blob/master/spec.md
[8]CSI Documentation: https://kubernetes-csi.github.io/docs/
[9]CloudNativeCon EU 2018 CSI Jie Yu: https://schd.ws/hosted_files/kccnceu18/fb/CloudNativeCon%20EU%202018%20CSI%20Jie%20Yu.pdf



























