在 [[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 进行绑定。

组件关系和调用流程总结

  1. PV Controller:监视 PVC,创建 PV。
  2. external-provisioner:处理 PV 创建请求,调用 CSI Controller Service 的 CreateVolume RPC。
  3. PV Controller:绑定 PV 和 PVC。
  4. VolumeAttachment:描述卷与节点的绑定关系。
  5. external-attacher:监视 VolumeAttachment 资源,调用 CSI Controller Service 的 ControllerPublishVolume RPC。
  6. 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:发出事件FailedBindingvolume %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-provisionervolume.beta.kubernetes.io/storage-provisioner 为provisionerName,促使 external-provisioner 为此PVC动态创建PV。provisionerName 来自于 StorageClass。
    • 如果没有存储类且没有找到合适的 PV,记录事件FailedBinding
    • 将 PVC 状态更新为 Pending,并在下次同步时重试。

当 external-provisioner 创建 PV 后,下次执行到该PVC则会按以上流程正常绑定。

使用ctrl.bind 绑定后,会设置annotation pv.kubernetes.io/bound-by-controller:“yes”

deleteClaim

  1. 删除 cache;
  2. 如果存在绑定,将 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

  1. 删除 cache;
  2. 如果存在绑定,将 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-provisionervolume.beta.kubernetes.io/storage-provisioner 获取 PV Controller 标记的 provisionerName,和自己对比。

只有PV Controller 指定该 provisioner 处理的情况下,才会进行处理。如果是 WaitForFirstConsumer 卷,则会需要调度之后才会处理。

接下来进行卷的动态创建,PV名会被指定为 pvc-{pvc-id} 。并通过 csiProvisioner.Provision 方法进行 provision

Provision 处理流程:

  1. 还会继续检查annotation中的 volume.kubernetes.io/storage-provisioner 进行确认
  2. 未开启 node-deployment 则无视本步骤 checkNodecheckNode 函数主要负责检查 PVC(Persistent Volume Claim)是否可以被分配到当前节点。这一过程对于实现 PVC 的即时绑定(immediate binding)非常重要,尤其是在多节点集群中。
    1. 选定节点后,会标记 volume.kubernetes.io/selected-node
  3. prepareProvision 准备PVName 为 {perfix}-{PVC id} ,perfix默认时为“pvc”。准备其他创盘数据。
  4. 调用CSI Driver ControllerService CreateVolume创建卷;
  5. 创建PV,填充相应数据:
    1. pvName:{perfix}-{PVC id} ,perfix默认时为“pvc”
    2. VolumeHandle: ControllerService 返回的 VolumeId
    3. AccessModes:PVC.AccessModes
    4. MountOptions: StorageClass.MountOptions
    5. Capacity: ControllerService 返回的大小
    6. ReadOnly:来自于ControllerService返回值
    7. PersistentVolumeReclaimPolicy:来自 StorageClass
    8. NodeAffinity:来自 ControllerService
    9. VolumeMode:来自PVC
    10. 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经过多轮次的监控,完成了创删卷的动作。

时序图待画。