作者
王成,騰訊雲研發工程師,Kubernetes contributor,從事資料庫產品容器化、資源管控等工作,關注 Kubernetes、Go、雲原生領域。
概述
進入 K8s 的世界,會發現有很多方便擴充套件的 Interface,包括 CSI, CNI, CRI 等,將這些介面抽象出來,是為了更好的提供開放、擴充套件、規範等能力。
K8s 持久化儲存經歷了從 in-tree Volume 到 CSI Plugin(out-of-tree) 的遷移,一方面是為了將 K8s 核心主幹程式碼與 Volume 相關程式碼解耦,便於更好的維護;另一方面則是為了方便各大雲廠商實現統一的介面,提供個性化的雲端儲存能力,以期達到雲端儲存生態圈的開放共贏。
本文將從持久卷 PV 的 建立(Create)、附著(Attach)、分離(Detach)、掛載(Mount)、解除安裝(Unmount)、刪除(Delete) 等核心生命週期,對 CSI 實現機制進行了解析。
相關術語
Term | Definition |
---|---|
CSI | Container Storage Interface. |
CNI | Container Network Interface. |
CRI | Container Runtime Interface. |
PV | Persistent Volume. |
PVC | Persistent Volume Claim. |
StorageClass | Defined by provisioner(i.e. Storage Provider), to assemble Volume parameters as a resource object. |
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. |
本文及後續相關文章都基於 K8s v1.22
流程概覽
PV 建立核心流程:
apiserver
建立 Pod,根據PodSpec.Volumes
建立 Volume;PVController
監聽到 PV informer,新增相關 Annotation(如 pv.kubernetes.io/provisioned-by),調諧實現 PVC/PV 的繫結(Bound);- 判斷
StorageClass.volumeBindingMode
:WaitForFirstConsumer
則等待 Pod 排程到 Node 成功後再進行 PV 建立,Immediate
則立即呼叫 PV 建立邏輯,無需等待 Pod 排程; external-provisioner
監聽到 PV informer, 呼叫 RPC-CreateVolume 建立 Volume;AttachDetachController
將已經繫結(Bound) 成功的 PVC/PV,經過 InTreeToCSITranslator 轉換器,由 CSIPlugin 內部邏輯實現VolumeAttachment
資源型別的建立;external-attacher
監聽到 VolumeAttachment informer,呼叫 RPC-ControllerPublishVolume 實現 AttachVolume;kubelet
reconcile 持續調諧:通過判斷controllerAttachDetachEnabled || PluginIsAttachable
及當前 Volume 狀態進行 AttachVolume/MountVolume,最終實現將 Volume 掛載到 Pod 指定目錄中,供 Container 使用;
從 CSI 說起
CSI(Container Storage Interface) 是由來自 Kubernetes、Mesos、Docker 等社群 member 聯合制定的一個行業標準介面規範(https://github.com/container-storage-interface/spec),旨在將任意儲存系統暴露給容器化應用程式。
CSI 規範定義了儲存提供商實現 CSI 相容的 Volume Plugin 的最小操作集和部署建議。CSI 規範的主要焦點是宣告 Volume Plugin 必須實現的介面。
先看一下 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 生命週期可以看到,一塊持久卷要達到 Pod 可使用狀態,需要經歷以下階段:
CreateVolume -> ControllerPublishVolume -> NodeStageVolume -> NodePublishVolume
而當刪除 Volume 的時候,會經過如下反向階段:
NodeUnpublishVolume -> NodeUnstageVolume -> ControllerUnpublishVolume -> DeleteVolume
上面流程的每個步驟,其實就對應了 CSI 提供的標準介面,雲端儲存廠商只需要按標準介面實現自己的雲端儲存外掛,即可與 K8s 底層編排系統無縫銜接起來,提供多樣化的雲端儲存、備份、快照(snapshot)等能力。
多元件協同
為實現具有高擴充套件性、out-of-tree 的持久卷管理能力,在 K8s CSI 實現中,相關協同的元件有:
元件介紹
- kube-controller-manager:K8s 資源控制器,主要通過 PVController, AttachDetach 實現持久卷的繫結(Bound)/解綁(Unbound)、附著(Attach)/分離(Detach);
- CSI-plugin:K8s 獨立拆分出來,實現 CSI 標準規範介面的邏輯控制與呼叫,是整個 CSI 控制邏輯的核心樞紐;
- node-driver-registrar:是一個由官方 K8s sig 小組維護的輔助容器(sidecar),它使用 kubelet 外掛序號產生器制向 kubelet 註冊外掛,需要請求 CSI 外掛的 Identity 服務來獲取外掛資訊;
- external-provisioner:是一個由官方 K8s sig 小組維護的輔助容器(sidecar),主要功能是實現持久卷的建立(Create)、刪除(Delete);
- external-attacher:是一個由官方 K8s sig 小組維護的輔助容器(sidecar),主要功能是實現持久卷的附著(Attach)、分離(Detach);
- external-snapshotter:是一個由官方 K8s sig 小組維護的輔助容器(sidecar),主要功能是實現持久卷的快照(VolumeSnapshot)、備份恢復等能力;
- external-resizer:是一個由官方 K8s sig 小組維護的輔助容器(sidecar),主要功能是實現持久卷的彈性擴縮容,需要雲廠商外掛提供相應的能力;
- kubelet:K8s 中執行在每個 Node 上的控制樞紐,主要功能是調諧節點上 Pod 與 Volume 的附著、掛載、監控探測上報等;
- cloud-storage-provider:由各大雲端儲存廠商基於 CSI 標準介面實現的外掛,包括 Identity 身份服務、Controller 控制器服務、Node 節點服務;
元件通訊
由於 CSI plugin 的程式碼在 K8s 中被認為是不可信的,因此 CSI Controller Server 和 External CSI SideCar、CSI Node Server 和 Kubelet 通過 Unix Socket 來通訊,與雲端儲存廠商提供的 Storage Service 通過 gRPC(HTTP/2) 通訊:
RPC 呼叫
從 CSI 標準規範可以看到,雲端儲存廠商想要無縫接入 K8s 容器編排系統,需要按規範實現相關介面,相關介面主要為:
- Identity 身份服務:Node Plugin 和 Controller Plugin 都必須實現這些 RPC 集,協調 K8s 與 CSI 的版本資訊,負責對外暴露這個外掛的資訊。
- Controller 控制器服務:Controller Plugin 必須實現這些 RPC 集,建立以及管理 Volume,對應 K8s 中 attach/detach volume 操作。
- Node 節點服務:Node Plugin 必須實現這些 RPC 集,將 Volume 儲存卷掛載到指定目錄中,對應 K8s 中的 mount/unmount volume 操作。
相關 RPC 介面功能如下:
建立/刪除 PV
K8s 中持久卷 PV 的建立(Create)與刪除(Delete),由 external-provisioner 元件實現,相關工程程式碼在:【https://github.com/kubernetes-csi/external-provisioner】
首先,通過標準的 cmd 方式獲取命令列引數,執行 newController -> Run() 邏輯,相關程式碼如下:
// external-provisioner/cmd/csi-provisioner/csi-provisioner.go
main() {
...
// 初始化控制器,實現 Volume 建立/刪除介面
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 執行起來
provisionController.Run(ctx)
}
}
接著,呼叫 PV 建立/刪除流程:
PV 建立:runClaimWorker -> syncClaimHandler -> syncClaim -> provisionClaimOperation -> Provision -> CreateVolume
PV 刪除:runVolumeWorker -> syncVolumeHandler -> syncVolume -> deleteVolumeOperation -> Delete -> DeleteVolume
由 sigs.k8s.io/sig-storage-lib-external-provisioner 抽象了相關介面:
// 通過 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 {
// 呼叫 PRC CreateVolume 介面實現 PV 建立
Provision(context.Context, ProvisionOptions) (*v1.PersistentVolume, ProvisioningState, error)
// 呼叫 PRC DeleteVolume 介面實現 PV 刪除
Delete(context.Context, *v1.PersistentVolume) error
}
Controller 調諧
K8s 中與 PV 相關的控制器有 PVController、AttachDetachController。
PVController
PVController 通過在 PVC 新增相關 Annotation(如 pv.kubernetes.io/provisioned-by),由 external-provisioner 元件負責完成對應 PV 的建立/刪除,然後 PVController 監測到 PV 建立成功的狀態,完成與 PVC 的繫結(Bound),調諧(reconcile)任務完成。然後交給 AttachDetachController 控制器進行下一步邏輯處理。
值得一提的是,PVController 內部通過使用 local cache,高效實現了 PVC 與 PV 的狀態更新與繫結事件處理,相當於在 K8s informer 機制之外,又自己維護了一個 local store 進行 Add/Update/Delete 事件處理。
首先,通過標準的 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 增刪改事件監聽
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 增刪改事件監聽
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
}
接著,呼叫 PVC/PV 繫結/解綁邏輯:
PVC/PV 繫結:claimWorker -> updateClaim -> syncClaim -> syncBoundClaim -> bind
PVC/PV 解綁:volumeWorker -> updateVolume -> syncVolume -> unbindVolume
AttachDetachController
AttachDetachController 將已經繫結(Bound) 成功的 PVC/PV,內部經過 InTreeToCSITranslator 轉換器,實現由 in-tree 方式管理的 Volume 向 out-of-tree 方式管理的 CSI 外掛模式轉換。
接著,由 CSIPlugin 內部邏輯實現 VolumeAttachment
資源型別的建立/刪除,調諧(reconcile) 任務完成。然後交給 external-attacher 元件進行下一步邏輯處理。
相關核心程式碼在 reconciler.Run() 中實現如下:
// kubernetes/pkg/controller/volume/attachdetach/reconciler/reconciler.go
func (rc *reconciler) reconcile() {
// 先進行 DetachVolume,確保因 Pod 重新排程到其他節點的 Volume 提前分離(Detach)
for _, attachedVolume := range rc.actualStateOfWorld.GetAttachedVolumes() {
// 如果不在期望狀態的 Volume,則呼叫 DetachVolume 刪除 VolumeAttachment 資源物件
if !rc.desiredStateOfWorld.VolumeExists(
attachedVolume.VolumeName, attachedVolume.NodeName) {
...
err = rc.attacherDetacher.DetachVolume(attachedVolume.AttachedVolume, verifySafeToDetach, rc.actualStateOfWorld)
...
}
}
// 呼叫 AttachVolume 建立 VolumeAttachment 資源物件
rc.attachDesiredVolumes()
...
}
附著/分離 Volume
K8s 中持久卷 PV 的附著(Attach)與分離(Detach),由 external-attacher 元件實現,相關工程程式碼在:【https://github.com/kubernetes-csi/external-attacher】
external-attacher 元件觀察到由上一步 AttachDetachController 建立的 VolumeAttachment 物件,如果其 .spec.Attacher 中的 Driver name 指定的是自己同一 Pod 內的 CSI Plugin,則呼叫 CSI Plugin 的ControllerPublish 介面進行 Volume Attach。
首先,通過標準的 cmd 方式獲取命令列引數,執行 newController -> Run() 邏輯,相關程式碼如下:
// 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)
}
...
}
接著,呼叫 Volume 附著/分離邏輯:
Volume 附著(Attach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncAttach -> csiAttach -> Attach -> ControllerPublishVolume
Volume 分離(Detach):syncVA -> SyncNewOrUpdatedVolumeAttachment -> syncDetach -> csiDetach -> Detach -> ControllerUnpublishVolume
kubelet 掛載/解除安裝 Volume
K8s 中持久卷 PV 的掛載(Mount)與解除安裝(Unmount),由 kubelet 元件實現。
kubelet 通過 VolumeManager 啟動 reconcile loop,當觀察到有新的使用 PersistentVolumeSource 為CSI 的 PV 的 Pod 排程到本節點上,於是呼叫 reconcile 函式進行 Attach/Detach/Mount/Unmount 相關邏輯處理。
// kubernetes/pkg/kubelet/volumemanager/reconciler/reconciler.go
func (rc *reconciler) reconcile() {
// 先進行 UnmountVolume,確保因 Pod 刪除被重新 Attach 到其他 Pod 的 Volume 提前解除安裝(Unmount)
rc.unmountVolumes()
// 接著通過判斷 controllerAttachDetachEnabled || PluginIsAttachable 及當前 Volume 狀態
// 進行 AttachVolume / MountVolume / ExpandInUseVolume
rc.mountAttachVolumes()
// 解除安裝(Unmount) 或分離(Detach) 不再需要(Pod 刪除)的 Volume
rc.unmountDetachDevices()
}
相關呼叫邏輯如下:
Volume 掛載(Mount):reconcile -> mountAttachVolumes -> MountVolume -> SetUp -> SetUpAt -> NodePublishVolume
Volume 解除安裝(Unmount):reconcile -> unmountVolumes -> UnmountVolume -> TearDown -> TearDownAt -> NodeUnpublishVolume
小結
本文通過分析 K8s 中持久卷 PV 的 建立(Create)、附著(Attach)、分離(Detach)、掛載(Mount)、解除安裝(Unmount)、刪除(Delete) 等核心生命週期流程,對 CSI 實現機制進行了解析,通過原始碼、圖文方式說明了相關流程邏輯,以期更好的理解 K8s CSI 執行流程。
可以看到,K8s 以 CSI Plugin(out-of-tree) 外掛方式開放儲存能力,一方面是為了將 K8s 核心主幹程式碼與 Volume 相關程式碼解耦,便於更好的維護;另一方面在遵從 CSI 規範介面下,便於各大雲廠商根據業務需求實現相關的介面,提供個性化的雲端儲存能力,以期達到雲端儲存生態圈的開放共贏。
PS: 更多內容請關注 k8s-club
相關資料
關於我們
更多關於雲原生的案例和知識,可關注同名【騰訊雲原生】公眾號~
福利:
①公眾號後臺回覆【手冊】,可獲得《騰訊雲原生路線圖手冊》&《騰訊雲原生最佳實踐》~
②公眾號後臺回覆【系列】,可獲得《15個系列100+篇超實用雲原生原創乾貨合集》,包含Kubernetes 降本增效、K8s 效能優化實踐、最佳實踐等系列。
【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多幹貨!!