1. 概述
進(jìn)入 K8s 的世界,會(huì)發(fā)現(xiàn)有很多方便擴(kuò)展的 Interface,包括 CSI, CNI, CRI 等,將這些接口抽象出來,是為了更好的提供開放、擴(kuò)展、規(guī)范等能力。
K8s 持久化存儲(chǔ)經(jīng)歷了從 in-tree Volume 到 CSI Plugin(out-of-tree) 的遷移,一方面是為了將 K8s 核心主干代碼與 Volume 相關(guān)代碼解耦,便于更好的維護(hù);另一方面則是為了方便各大云廠商實(shí)現(xiàn)統(tǒng)一的接口,提供個(gè)性化的云存儲(chǔ)能力,以期達(dá)到云存儲(chǔ)生態(tài)圈的開放共贏。
本文將從持久卷 PV 的 創(chuàng)建(Create)、附著(Attach)、分離(Detach)、掛載(Mount)、卸載(Unmount)、刪除(Delete) 等核心生命周期,對(duì) CSI 實(shí)現(xiàn)機(jī)制進(jìn)行了解析。
相關(guān)術(shù)語
| Term | Definition |
|---|---|
| CSI | Container Storage Interface. |
| CNI | Container Network Interface. |
| CSI | Container Runtime Interface. |
| PV | Persistent Volume. |
| PVC | Persistent Volume Claim. |
| Volume | A unit of storage that will be made available inside of a CO-managed container, via the CSI. |
| Block Volume | A volume that will appear as a block device inside the container. |
| Mounted Volume | A volume that will be mounted using the specified file system and appear as a directory inside the container. |
| CO | Container Orchestration system, communicates with Plugins using CSI service RPCs. |
| SP | Storage Provider, the vendor of a CSI plugin implementation. |
| RPC | Remote Procedure Call. |
| Node | A host where the user workload will be running, uniquely identifiable from the perspective of a Plugin by a node ID. |
| Plugin | Aka “plugin implementation”, a gRPC endpoint that implements the CSI Services. |
| Plugin Supervisor | Process that governs the lifecycle of a Plugin, MAY be the CO. |
| Workload | The atomic unit of "work" scheduled by a CO. This MAY be a container or a collection of containers. |
本文及后續(xù)相關(guān)文章都基于 K8s v1.22

2. 從 CSI 說起
CSI(Container Storage Interface) 是由來自 Kubernetes、Mesos、Docker 等社區(qū) member 聯(lián)合制定的一個(gè)行業(yè)標(biāo)準(zhǔn)接口規(guī)范(https://github.com/container-storage-interface/spec),旨在將任意存儲(chǔ)系統(tǒng)暴露給容器化應(yīng)用程序。
CSI 規(guī)范定義了存儲(chǔ)提供商實(shí)現(xiàn) CSI 兼容的 Volume Plugin 的最小操作集和部署建議。CSI 規(guī)范的主要焦點(diǎn)是聲明 Volume Plugin 必須實(shí)現(xiàn)的接口。
先看一下 Volume 的生命周期:
CreateVolume +------------+ DeleteVolume
+------------->| CREATED +--------------+
| +---+----^---+ |
| Controller | | Controller v
+++ Publish | | Unpublish +++
|X| Volume | | Volume | |
+-+ +---v----+---+ +-+
| NODE_READY |
+---+----^---+
Node | | Node
Stage | | Unstage
Volume | | Volume
+---v----+---+
| VOL_READY |
+---+----^---+
Node | | Node
Publish | | Unpublish
Volume | | Volume
+---v----+---+
| PUBLISHED |
+------------+
The lifecycle of a dynamically provisioned volume, from
creation to destruction, when the Node Plugin advertises the
STAGE_UNSTAGE_VOLUME capability.
從 Volume 生命周期可以看到,一塊持久卷要達(dá)到 Pod 可使用狀態(tài),需要經(jīng)歷以下階段:
CreateVolume -> ControllerPublishVolume -> NodeStageVolume -> NodePublishVolume
而當(dāng)刪除 Volume 的時(shí)候,會(huì)經(jīng)過如下反向階段:
NodeUnpublishVolume -> NodeUnstageVolume -> ControllerUnpublishVolume -> DeleteVolume
上面流程的每個(gè)步驟,其實(shí)就對(duì)應(yīng)了 CSI 提供的標(biāo)準(zhǔn)接口,云存儲(chǔ)廠商只需要按標(biāo)準(zhǔn)接口實(shí)現(xiàn)自己的云存儲(chǔ)插件,即可與 K8s 底層編排系統(tǒng)無縫銜接起來,提供多樣化的云存儲(chǔ)、備份、快照(snapshot)等能力。
3. 多組件協(xié)同
為實(shí)現(xiàn)具有高擴(kuò)展性、out-of-tree 的持久卷管理能力,在 K8s CSI 實(shí)現(xiàn)中,相關(guān)協(xié)同的組件有:

3.1 組件介紹
- kube-controller-manager:K8s 資源控制器,主要通過 PVController, AttachDetach 實(shí)現(xiàn)持久卷的綁定(Bound)/解綁(Unbound)、附著(Attach)/分離(Detach);
- CSI-plugin:K8s 獨(dú)立拆分出來,實(shí)現(xiàn) CSI 標(biāo)準(zhǔn)規(guī)范接口的邏輯控制與調(diào)用,是整個(gè) CSI 控制邏輯的核心樞紐;
- node-driver-registrar:是一個(gè)由官方 K8s sig 小組維護(hù)的輔助容器(sidecar),它使用 kubelet 插件注冊(cè)機(jī)制向 kubelet 注冊(cè)插件,需要請(qǐng)求 CSI 插件的 Identity 服務(wù)來獲取插件信息;
- external-provisioner:是一個(gè)由官方 K8s sig 小組維護(hù)的輔助容器(sidecar),主要功能是實(shí)現(xiàn)持久卷的創(chuàng)建(Create)、刪除(Delete);
- external-attacher:是一個(gè)由官方 K8s sig 小組維護(hù)的輔助容器(sidecar),主要功能是實(shí)現(xiàn)持久卷的附著(Attach)、分離(Detach);
- external-snapshotter:是一個(gè)由官方 K8s sig 小組維護(hù)的輔助容器(sidecar),主要功能是實(shí)現(xiàn)持久卷的快照(VolumeSnapshot)、備份恢復(fù)等能力;
- external-resizer:是一個(gè)由官方 K8s sig 小組維護(hù)的輔助容器(sidecar),主要功能是實(shí)現(xiàn)持久卷的彈性擴(kuò)縮容,需要云廠商插件提供相應(yīng)的能力;
- kubelet:K8s 中運(yùn)行在每個(gè) Node 上的控制樞紐,主要功能是調(diào)諧節(jié)點(diǎn)上 Pod 與 Volume 的附著、掛載、監(jiān)控探測(cè)上報(bào)等;
- cloud-storage-provider:由各大云存儲(chǔ)廠商基于 CSI 標(biāo)準(zhǔn)接口實(shí)現(xiàn)的插件,包括 Identity 身份服務(wù)、Controller 控制器服務(wù)、Node 節(jié)點(diǎn)服務(wù);
3.2 組件通信
由于 CSI plugin 的代碼在 K8s 中被認(rèn)為是不可信的,因此 CSI Controller Server 和 External CSI SideCar、CSI Node Server 和 Kubelet 通過 Unix Socket 來通信,與云存儲(chǔ)廠商提供的 Storage Service 通過 gRPC(HTTP/2) 通信:

4. RPC 調(diào)用
從 CSI 標(biāo)準(zhǔn)規(guī)范可以看到,云存儲(chǔ)廠商想要無縫接入 K8s 容器編排系統(tǒng),需要按規(guī)范實(shí)現(xiàn)相關(guān)接口,相關(guān)接口主要為:

- Identity 身份服務(wù):Node Plugin 和 Controller Plugin 都必須實(shí)現(xiàn)這些 RPC 集,協(xié)調(diào) K8s 與 CSI 的版本信息,負(fù)責(zé)對(duì)外暴露這個(gè)插件的信息。
- Controller 控制器服務(wù):Controller Plugin 必須實(shí)現(xiàn)這些 RPC 集,創(chuàng)建以及管理 Volume,對(duì)應(yīng) K8s 中 attach/detach volume 操作。
- Node 節(jié)點(diǎn)服務(wù):Node Plugin 必須實(shí)現(xiàn)這些 RPC 集,將 Volume 存儲(chǔ)卷掛載到指定目錄中,對(duì)應(yīng) K8s 中的 mount/unmount volume 操作。
相關(guān) RPC 接口功能如下:

5. 創(chuàng)建/刪除 PV
K8s 中持久卷 PV 的創(chuàng)建(Create)與刪除(Delete),由 external-provisioner 組件實(shí)現(xiàn),相關(guān)工程代碼在:
https://github.com/kubernetes-csi/external-provisioner
首先,通過標(biāo)準(zhǔn)的 cmd 方式獲取命令行參數(shù),執(zhí)行 newController -> Run() 邏輯,相關(guān)代碼如下:
// external-provisioner/cmd/csi-provisioner/csi-provisioner.go
main() {
...
// 初始化控制器,實(shí)現(xiàn) Volume 創(chuàng)建/刪除接口
csiProvisioner := ctrl.NewCSIProvisioner(
clientset,
*operationTimeout,
identity,
*volumeNamePrefix,
*volumeNameUUIDLength,
grpcClient,
snapClient,
provisionerName,
pluginCapabilities,
controllerCapabilities,
...
)
...
// 真正的 ProvisionController,包裝了上面的 CSIProvisioner
provisionController = controller.NewProvisionController(
clientset,
provisionerName,
csiProvisioner,
provisionerOptions...,
)
...
run := func(ctx context.Context) {
...
// Run 運(yùn)行起來
provisionController.Run(ctx)
}
}
接著,調(diào)用 PV 創(chuàng)建/刪除流程:
PV 創(chuàng)建:runClaimWorker -> syncClaimHandler -> syncClaim -> provisionClaimOperation -> Provision -> CreateVolume
PV 刪除:runVolumeWorker -> syncVolumeHandler -> syncVolume -> deleteVolumeOperation -> Delete -> DeleteVolume

由 sigs.k8s.io/sig-storage-lib-external-provisioner 抽象了相關(guān)接口:
// 通過 vendor 方式引入 sigs.k8s.io/sig-storage-lib-external-provisioner
// external-provisioner/vendor/sigs.k8s.io/sig-storage-lib-external-provisioner/v7/controller/volume.go
type Provisioner interface {
// 調(diào)用 PRC CreateVolume 接口實(shí)現(xiàn) PV 創(chuàng)建
Provision(context.Context, ProvisionOptions) (*v1.PersistentVolume, ProvisioningState, error)
// 調(diào)用 PRC DeleteVolume 接口實(shí)現(xiàn) PV 刪除
Delete(context.Context, *v1.PersistentVolume) error
}
6. Controller 調(diào)諧
K8s 中與 PV 相關(guān)的控制器有 PVController、AttachDetachController。
6.1 PVController
PVController 通過在 PVC 添加相關(guān) Annotation(如 pv.kubernetes.io/provisioned-by),由 external-provisioner 組件負(fù)責(zé)完成對(duì)應(yīng) PV 的創(chuàng)建/刪除,然后 PVController 監(jiān)測(cè)到 PV 創(chuàng)建成功的狀態(tài),完成與 PVC 的綁定(Bound),調(diào)諧(reconcile)任務(wù)完成。然后交給 AttachDetachController 控制器進(jìn)行下一步邏輯處理。
值得一提的是,PVController 內(nèi)部通過使用 local cache,高效實(shí)現(xiàn)了 PVC 與 PV 的狀態(tài)更新與綁定事件處理,相當(dāng)于在 K8s informer 機(jī)制之外,又自己維護(hù)了一個(gè) local store 進(jìn)行 Add/Update/Delete 事件處理。
首先,通過標(biāo)準(zhǔn)的 newController -> Run() 邏輯:
// kubernetes/pkg/controller/volume/persistentvolume/pv_controller_base.go
func NewController(p ControllerParameters) (*PersistentVolumeController, error) {
...
// 初始化 PVController
controller := &PersistentVolumeController{
volumes: newPersistentVolumeOrderedIndex(),
claims: cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc),
kubeClient: p.KubeClient,
eventRecorder: eventRecorder,
runningOperations: goroutinemap.NewGoRoutineMap(true /* exponentialBackOffOnError */),
cloud: p.Cloud,
enableDynamicProvisioning: p.EnableDynamicProvisioning,
clusterName: p.ClusterName,
createProvisionedPVRetryCount: createProvisionedPVRetryCount,
createProvisionedPVInterval: createProvisionedPVInterval,
claimQueue: workqueue.NewNamed("claims"),
volumeQueue: workqueue.NewNamed("volumes"),
resyncPeriod: p.SyncPeriod,
operationTimestamps: metrics.NewOperationStartTimeCache(),
}
...
// PV 增刪改事件監(jiān)聽
p.VolumeInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { controller.enqueueWork(controller.volumeQueue, obj) },
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(controller.volumeQueue, newObj) },
DeleteFunc: func(obj interface{}) { controller.enqueueWork(controller.volumeQueue, obj) },
},
)
...
// PVC 增刪改事件監(jiān)聽
p.ClaimInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { controller.enqueueWork(controller.claimQueue, obj) },
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(controller.claimQueue, newObj) },
DeleteFunc: func(obj interface{}) { controller.enqueueWork(controller.claimQueue, obj) },
},
)
...
return controller, nil
}
接著,調(diào)用 PVC/PV 綁定/解綁邏輯:
PVC/PV 綁定:claimWorker -> updateClaim -> syncClaim -> syncBoundClaim -> bind
PVC/PV 解綁:volumeWorker -> updateVolume -> syncVolume -> unbindVolume
6.2 AttachDetachController
AttachDetachController 將已經(jīng)綁定(Bound) 成功的 PVC/PV,內(nèi)部經(jīng)過 InTreeToCSITranslator 轉(zhuǎn)換器,實(shí)現(xiàn)由 in-tree 方式管理的 Volume 向 out-of-tree 方式管理的 CSI 插件模式轉(zhuǎn)換。
接著,由 CSIPlugin 內(nèi)部邏輯實(shí)現(xiàn) VolumeAttachment 資源類型的創(chuàng)建/刪除,調(diào)諧(reconcile) 任務(wù)完成。然后交給 external-attacher 組件進(jìn)行下一步邏輯處理。
相關(guān)核心代碼在 reconciler.Run() 中實(shí)現(xiàn)如下:
// kubernetes/pkg/controller/volume/attachdetach/reconciler/reconciler.go
func (rc *reconciler) reconcile() {
// 先進(jìn)行 DetachVolume,確保因 Pod 重新調(diào)度到其他節(jié)點(diǎn)的 Volume 提前分離(Detach)
for _, attachedVolume := range rc.actualStateOfWorld.GetAttachedVolumes() {
// 如果不在期望狀態(tài)的 Volume,則調(diào)用 DetachVolume 刪除 VolumeAttachment 資源對(duì)象
if !rc.desiredStateOfWorld.VolumeExists(
attachedVolume.VolumeName, attachedVolume.NodeName) {
...
err = rc.attacherDetacher.DetachVolume(attachedVolume.AttachedVolume, verifySafeToDetach, rc.actualStateOfWorld)
...
}
}
// 調(diào)用 AttachVolume 創(chuàng)建 VolumeAttachment 資源對(duì)象
rc.attachDesiredVolumes()
...
}
7. 附著/分離 Volume
K8s 中持久卷 PV 的附著(Attach)與分離(Detach),由 external-attacher 組件實(shí)現(xiàn),相關(guān)工程代碼在:
https://github.com/kubernetes-csi/external-attacher
external-attacher 組件觀察到由上一步 AttachDetachController 創(chuàng)建的 VolumeAttachment 對(duì)象,如果其 .spec.Attacher 中的 Driver name 指定的是自己同一 Pod 內(nèi)的 CSI Plugin,則調(diào)用 CSI Plugin 的ControllerPublish 接口進(jìn)行 Volume Attach。
首先,通過標(biāo)準(zhǔn)的 cmd 方式獲取命令行參數(shù),執(zhí)行 newController -> Run() 邏輯,相關(guān)代碼如下:
// external-attacher/cmd/csi-attacher/main.go
func main() {
...
ctrl := controller.NewCSIAttachController(
clientset,
csiAttacher,
handler,
factory.Storage().V1().VolumeAttachments(),
factory.Core().V1().PersistentVolumes(),
workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax),
workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax),
supportsListVolumesPublishedNodes,
*reconcileSync,
)
run := func(ctx context.Context) {
stopCh := ctx.Done()
factory.Start(stopCh)
ctrl.Run(int(*workerThreads), stopCh)
}
...
}
接著,調(diào)用 Volume 附著/分離邏輯:
Volume 附著(Attach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncAttach -> csiAttach -> Attach -> ControllerPublishVolume
Volume 分離(Detach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncDetach -> csiDetach -> Detach -> ControllerUnpublishVolume

8. kubelet 掛載/卸載 Volume
K8s 中持久卷 PV 的掛載(Mount)與卸載(Unmount),由 kubelet 組件實(shí)現(xiàn)。
kubelet 通過 VolumeManager 啟動(dòng) reconcile loop,當(dāng)觀察到有新的使用 PersistentVolumeSource 為CSI 的 PV 的 Pod 調(diào)度到本節(jié)點(diǎn)上,于是調(diào)用 reconcile 函數(shù)進(jìn)行 Attach/Detach/Mount/Unmount 相關(guān)邏輯處理。
// kubernetes/pkg/kubelet/volumemanager/reconciler/reconciler.go
func (rc *reconciler) reconcile() {
// 先進(jìn)行 UnmountVolume,確保因 Pod 刪除被重新 Attach 到其他 Pod 的 Volume 提前卸載(Unmount)
rc.unmountVolumes()
// 接著通過判斷 controllerAttachDetachEnabled || PluginIsAttachable 及當(dāng)前 Volume 狀態(tài)
// 進(jìn)行 AttachVolume / MountVolume / ExpandInUseVolume
rc.mountAttachVolumes()
// 卸載(Unmount) 或分離(Detach) 不再需要(Pod 刪除)的 Volume
rc.unmountDetachDevices()
}
相關(guān)調(diào)用邏輯如下:
Volume 掛載(Mount):reconcile -> mountAttachVolumes -> MountVolume -> SetUp -> SetUpAt -> NodePublishVolume
Volume 卸載(Unmount):reconcile -> unmountVolumes -> UnmountVolume -> TearDown -> TearDownAt -> NodeUnpublishVolume

9. 小結(jié)
本文通過分析 K8s 中持久卷 PV 的 創(chuàng)建(Create)、附著(Attach)、分離(Detach)、掛載(Mount)、卸載(Unmount)、刪除(Delete) 等核心生命周期流程,對(duì) CSI 實(shí)現(xiàn)機(jī)制進(jìn)行了解析,通過源碼、圖文方式說明了相關(guān)流程邏輯,以期更好的理解 K8s CSI 運(yùn)行流程。
可以看到,K8s 以 CSI Plugin(out-of-tree) 插件方式開放存儲(chǔ)能力,一方面是為了將 K8s 核心主干代碼與 Volume 相關(guān)代碼解耦,便于更好的維護(hù);另一方面在遵從 CSI 規(guī)范接口下,便于各大云廠商根據(jù)業(yè)務(wù)需求實(shí)現(xiàn)相關(guān)的接口,提供個(gè)性化的云存儲(chǔ)能力,以期達(dá)到云存儲(chǔ)生態(tài)圈的開放共贏。
PS: 更多內(nèi)容請(qǐng)關(guān)注 k8s-club