共计 11205 个字符,预计需要花费 29 分钟才能阅读完成。
作者
王成,腾讯云研发工程师,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…】
首先,通过规范的 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…】
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
相干材料
- CSI 标准
- Kubernetes 源码
- kubernetes-csi 源码
- kubernetes-sig-storage 源码
- K8s CSI 概念
- K8s CSI 介绍
对于咱们
更多对于云原生的案例和常识,可关注同名【腾讯云原生】公众号~
福利:
①公众号后盾回复【手册】,可取得《腾讯云原生路线图手册》&《腾讯云原生最佳实际》~
②公众号后盾回复【系列】,可取得《15 个系列 100+ 篇超实用云原生原创干货合集》,蕴含 Kubernetes 降本增效、K8s 性能优化实际、最佳实际等系列。
【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!