在 [[Kubernetes CSI接口及存储插件解析]] 说明了CSI Driver结构和CSI接口调用的大体流程,但并没详细说明 CSI Driver 和 Kubernetes 其他组件的关系和调用流程。有必要再次整理一下资料,并结合代码分析一下(以Kubernetes 1.30.4为例)。
本文仅涉及创删卷,挂卸卷见 [[Kubernetes CSI 容器存储挂卸卷流程]]
总览
在 Kubernetes 中,CSI(Container Storage Interface)用于实现存储系统的插件化,使得存储供应商可以方便地集成他们的存储解决方案。CSI 组件主要包括 CSI Driver、external-provisioner、external-attacher、ad-controller(通常指 external-attacher 中的逻辑)、kubelet 以及 PV Controller。下面是简要的创删工作流程和这些组件之间的关系:
1. 创建 PersistentVolumeClaim (PVC)
用户创建一个 PVC,Kubernetes 的 PV Controller 会监视 PVC 资源。
2. PV Controller 处理 PVC
PV controller 监控PVC/PV修改,查找对应的资源进行绑定。当发现需动态创建时,写annotation通知 external-provisioner 进行创卷和创建PV;当PV解绑时,根据回收策略进行动作,当为Delete时调用 external-provisioner 删卷。
3. CSI external-provisioner 调用
PV Controller 创建 PV 时,external-provisioner 插件会被调用。
- external-provisioner:是一个 CSI sidecar 容器,负责处理 PVC 请求并调用 CSI Controller Service 的 CreateVolume RPC。
PVC -> PV Controller -> external-provisioner -> CSI Controller Service (CreateVolume)
4. PV 和 PVC 绑定
PV Controller 将 PV 和 PVC 进行绑定。
组件关系和调用流程总结
- PV Controller:监视 PVC,创建 PV。
- external-provisioner:处理 PV 创建请求,调用 CSI Controller Service 的 CreateVolume RPC。
- PV Controller:绑定 PV 和 PVC。
- VolumeAttachment:描述卷与节点的绑定关系。
- external-attacher:监视 VolumeAttachment 资源,调用 CSI Controller Service 的 ControllerPublishVolume RPC。
- kubelet:在节点上执行实际的挂载操作,调用 CSI Node Service 的 NodeStageVolume 和 NodePublishVolume RPC。
PVC -> PV Controller -> external-provisioner -> CSI Controller Service (CreateVolume)
PV Controller -> PV/PVC Bind
Pod -> Scheduler -> Node
VolumeAttachment -> external-attacher -> CSI Controller Service (ControllerPublishVolume)
Pod -> kubelet -> CSI Node Service (NodeStageVolume, NodePublishVolume)
通过以上流程,Kubernetes 可以利用 CSI 插件进行存储卷的动态供应、挂载和管理。
接下来是源码分析。
PV Controller
在 cmd/kube-controller-manager/app/controllermanager.go
中注册了 newPersistentVolumeBinderControllerDescriptor
作为 PV Controller 入口
tl;dr : pv controller 监控PVC/PV修改,查找对应的资源进行绑定。当发现需动态创建时,写annotation通知 external-provisioner 进行创卷和创建PV;当PV解绑时,根据回收策略进行动作,当为Delete时调用 external-provisioner 删卷。
PV Controller 有两个主要的工作队列:volumeQueue
和 claimQueue
。这两个队列分别用于处理 PV(PersistentVolume)和 PVC(PersistentVolumeClaim)的相关操作。
当创建 PV Controller 时,会通过 Informer 订阅 PV和PVC资源的动态,并加入以上两个队列。
p.VolumeInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { controller.enqueueWork(ctx, controller.volumeQueue, obj) },
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(ctx, controller.volumeQueue, newObj) },
DeleteFunc: func(obj interface{}) { controller.enqueueWork(ctx, controller.volumeQueue, obj) },
},
)
p.ClaimInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { controller.enqueueWork(ctx, controller.claimQueue, obj) },
UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueWork(ctx, controller.claimQueue, newObj) },
DeleteFunc: func(obj interface{}) { controller.enqueueWork(ctx, controller.claimQueue, obj) },
},
)
然后通过Run函数启动。在 Run 函数中,有两个Worker会被启动,他们分别处理两个主要的工作队列:volumeQueue
和 claimQueue
。这两个队列分别用于处理 PV(PersistentVolume)和 PVC(PersistentVolumeClaim)的相关操作。
// Run starts all of this controller's control loops
func (ctrl *PersistentVolumeController) Run(ctx context.Context) {
...
// 启动定期同步
go wait.Until(func() { ctrl.resync(ctx) }, ctrl.resyncPeriod, ctx.Done())
// 启动volume和claim的工作队列处理
go wait.UntilWithContext(ctx, ctrl.volumeWorker, time.Second)
go wait.UntilWithContext(ctx, ctrl.claimWorker, time.Second)
...
}
claimWorker
claimWorker
首先从工作队列中获取一个需要处理的 PVC 对象。检查PVC是否实际存在,如果存在则认为本次是新增或是更新动作,调用 updateClaim
进行更新,如果实际的PVC不存在,则进行 deleteClaim
updateClaim
进行 PVC 同步,通过 pv.kubernetes.io/bind-completed
注解明确区分了 PVC 是否已经绑定到 PV。
对于已绑定的PVC来说此时处理比较简单。当判断绑定PV名(claim.Spec.VolumeName)或 PV本身不存在时需抛出错误事件 ClaimLost。然后再根据找到的PV同步一下绑定状态。
- 如果绑定的PV此时没有绑定 PVC,则会打印日志,并调用
ctrl.bind
修复两者的绑定关系。 - 已绑定该PVC,也会调用
ctrl.bind
,但理论上不会有什么效果。 - 如果是其他情况,例如该PV 绑定了其他的PVC,则发出事件 ClaimMisbound。是需要关注的错误情况。
对于未绑定的PVC来说。分两种情况:
- 已指定PV
- PV not found: 更新Pending状态,等待下次查询;
- pvc is “Pending”, pv is “Available”:使用
ctrl.bind
绑定进行绑定 - pvc is “Pending”, pv is “Bound”(PVC/PV匹配):依然使用
ctrl.bind
绑定 - pvc is “Pending”, pv is “Bound”(PVC/PV不匹配):
- 存在
pv.kubernetes.io/bound-by-controller
annotation:发出事件FailedBinding, volume %q already bound to a different claim. - 不存在 annotation:社区认为不可能发生,但发出同样event
- 存在
- 未指定PV:
- 检查 PVC 是否启用了延迟绑定模式(StorageClass的 VolumeBindingMode配置为 WaitForFirstConsumer)。
- 通过
findBestMatchForClaim
查询最佳匹配。此时需要定位,修改日志等级即可。 - 找到合适的PV,则进行绑定
- 如果没有找到合适的 PV,
assignDefaultStorageClass
尝试为 PVC 分配默认存储类。 - 如果启用了延迟绑定模式且调度器未观察到使用此 PVC 的 Pod,发出事件 WaitForFirstConsumer。
- 如果 PVC 有存储类,尝试动态供应 PV。
- 添加annotation
volume.kubernetes.io/storage-provisioner
和volume.beta.kubernetes.io/storage-provisioner
为provisionerName,促使 external-provisioner 为此PVC动态创建PV。provisionerName 来自于 StorageClass。
- 添加annotation
- 如果没有存储类且没有找到合适的 PV,记录事件FailedBinding。
- 将 PVC 状态更新为
Pending
,并在下次同步时重试。
当 external-provisioner 创建 PV 后,下次执行到该PVC则会按以上流程正常绑定。
使用ctrl.bind
绑定后,会设置annotation pv.kubernetes.io/bound-by-controller
:“yes”
deleteClaim
- 删除 cache;
- 如果存在绑定,将
claim.Spec.VolumeName
加入volumeQueue
volumeWorker
volumeWorker
刚开始和claimWorker差不多,首先从工作队列中获取一个需要处理的 PV 对象。检查PV是否实际存在,如果存在则认为本次是新增或是更新动作,调用 updateVolume
进行更新,如果实际的PVC不存在,则进行 deleteVolume
updateVolume
- 未绑定PVC时,会更新 PV状态为
VolumeAvailable
; - 绑定到PVC,但
ClaimRef.UID
为空时,认为依然是VolumeAvailable
状态;
发现存在绑定时,会从claim cache中查找PVC。
此时可能由于PV是external-provisioner创建的,会存在一些同步问题,导致cache中找不到PVC。因此需要做双重检查,除了再次检查 informer 缓存外,还会访问api-server查找PVC,确保没有遗漏。
这个检查的目的是为了确保不会错误地回收(reclaim)一个 PV。如果 PV 与 PVC 的状态不同步,可能会导致错误的回收操作。只有在 PVC 不存在的情况下,且 PV 的状态不是 Released
或 Failed
时,才会将 PV 的状态更新为 Released
。这意味着如果 PV 已经处于 Released
或 Failed
状态,则不会再进行此更新。
当发现 ClaimRef.UID
与缓存的PVC UID不匹配时,也会通过api-server再次确认,如果还不存在则认为已绑定的claim不存在处理。
接下来则根据 PV中填写的claim进行处理:
- 当 claim 不存在时,若状态不为
Released
或Failed
时,需更新PV状态为Released
;且此时需根据卷的回收策略处理卷本身。- Retain:保留卷,不做处理;
- Recycle:做一些基本的擦除。已被废弃,不做分析。
- Delete
- 通过
pv.kubernetes.io/provisioned-by
查找相应的 external-provisioner,下发卷删除操作; - 删除PV资源;
- 通过
- claim存在,但claim中的 volumeName不存在时,会添加
claimQueue
,尝试修复; - 如果 claim 存在且与 PV 的绑定状态一致,更新 PV 的状态为
VolumeBound
。 - 如果 PVC 被其他卷绑定,但该 PV 是动态创建的,系统将标记 PV 为
Released
并尝试回收。
deleteVolume
- 删除 cache;
- 如果存在绑定,将
volume.Spec.ClaimRef
加入claimQueue
,和claim是类似操作。
external-provisioner
external-provisioner
是 Kubernetes 中的一种组件,主要用于支持动态存储卷的供应。它是 Kubernetes 存储架构中的一部分,允许用户在创建 PVC(Persistent Volume Claim)时自动创建和配置存储卷,而不需要手动预先创建 PV(Persistent Volume)。
上下游
- 上游 :
- Kubernetes API Server:当用户创建 PVC 时,Kubernetes API Server 会接收请求并将其存储在 etcd 中。
- StorageClass:PVC 通常会指定一个 StorageClass,
external-provisioner
根据这个类来决定如何动态创建存储卷。
- 下游 :
- 外部存储系统 :
external-provisioner
会与具体的存储提供商(如云存储服务、网络存储设备等)进行交互,实际创建和管理存储卷。 - Persistent Volume (PV) :一旦存储卷被创建,
external-provisioner
会自动生成相应的 PV,并将其与 PVC 绑定。
- 外部存储系统 :
此处仅关注该插件中的 provisionController
。
runClaimWorker
runClaimWorker -> processNextClaimWorkItem -> syncClaimHandler -> syncClaim
在 syncClaim 中,首先会通过 shouldProvision
方法查看是否应该执行动作。此时会根据 PVC 中的 annotation volume.kubernetes.io/storage-provisioner
或 volume.beta.kubernetes.io/storage-provisioner
获取 PV Controller 标记的 provisionerName,和自己对比。
只有PV Controller 指定该 provisioner 处理的情况下,才会进行处理。如果是 WaitForFirstConsumer
卷,则会需要调度之后才会处理。
接下来进行卷的动态创建,PV名会被指定为 pvc-{pvc-id} 。并通过 csiProvisioner.Provision
方法进行 provision
Provision 处理流程:
- 还会继续检查annotation中的
volume.kubernetes.io/storage-provisioner
进行确认 - 未开启 node-deployment 则无视本步骤
checkNode
。checkNode
函数主要负责检查 PVC(Persistent Volume Claim)是否可以被分配到当前节点。这一过程对于实现 PVC 的即时绑定(immediate binding)非常重要,尤其是在多节点集群中。- 选定节点后,会标记
volume.kubernetes.io/selected-node
- 选定节点后,会标记
prepareProvision
准备PVName 为 {perfix}-{PVC id} ,perfix默认时为“pvc”。准备其他创盘数据。- 调用CSI Driver ControllerService CreateVolume创建卷;
- 创建PV,填充相应数据:
- pvName:{perfix}-{PVC id} ,perfix默认时为“pvc”
- VolumeHandle: ControllerService 返回的 VolumeId
- AccessModes:PVC.AccessModes
- MountOptions: StorageClass.MountOptions
- Capacity: ControllerService 返回的大小
- ReadOnly:来自于ControllerService返回值
- PersistentVolumeReclaimPolicy:来自 StorageClass
- NodeAffinity:来自 ControllerService
- VolumeMode:来自PVC
- FSType:当不为Block时需要赋值
Provision处理完后,会将PV的claim设置为该PVC,并标记 pv.kubernetes.io/provisioned-by
至此,创盘结束。整个 runClaimWorker
用于创盘。
runVolumeWorker
runVolumeWorker -> processNextVolumeWorkItem -> syncVolumeHandler -> syncVolume
在 syncVolume
中,主要是通过删除时间戳DeletionTimestamp
/ PV状态 VolumeReleased
/ 回收策略是否为 PersistentVolumeReclaimDelete
,来判断是否需要删除。
如果需要删除,则会调用 ControllerService.DeleteVolume
删除磁盘,然后删除PV。
总结
卷的创建和删除主要是通过 PV Controller
做最初的监控,然后通过 annotation 分派给CSI external-provisioner,再由 external-provisioner 调用 CSI Driver 进行创删卷, external-provisioner 最后一步是创删 PV。PV Controller还会进行 PV/PVC绑定。
总的来说,PV Controller经过多轮次的监控,完成了创删卷的动作。
时序图待画。