PVC 供应与绑定流程

本文档描述 Kubernetes 中 PVC(PersistentVolumeClaim)、PV(PersistentVolume)和 StorageClass 的完整交互流程。

概述

PVC 供应与绑定涉及以下核心组件:

  • PV Controller:管理 PV 和 PVC 的绑定
  • StorageClass:定义动态供应参数
  • Provisioner:实际创建存储卷(in-tree 或 CSI)
  • Scheduler:处理 WaitForFirstConsumer 模式
flowchart TD
    subgraph User["用户操作"]
        A["创建 PVC"]
    end

    subgraph APIServer["API Server"]
        B["PVC 资源"]
        C["PV 资源"]
        D["StorageClass"]
    end

    subgraph PVController["PV Controller"]
        E["Watch PVC"]
        F["syncClaim"]
        G["FindMatchingVolume"]
        H["provisionClaim"]
    end

    subgraph Provisioner["Provisioner"]
        I["In-tree Provisioner"]
        J["CSI External Provisioner"]
    end

    subgraph StorageBackend["存储后端"]
        K["Cloud Disk"]
        L["NFS"]
        M["Local Path"]
    end

    A --> B
    B -->|触发| E
    E --> F

    alt 静态供应
        F --> G
        G -->|匹配现有 PV| C
    else 动态供应
        F --> H
        H --> I
        H --> J
        I -->|创建| K
        I -->|创建| L
        J -->|创建| K
        K -->|创建 PV| C
        M -->|创建 PV| C
    end

    C -->|绑定| B

    style F fill:#c8e6c9
    style H fill:#fff9c4

流程详解

1. PV Controller 架构

代码路径: pkg/controller/volume/persistentvolume/pv_controller.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type PersistentVolumeController struct {
volumeLister corelisters.PersistentVolumeLister
volumeSynced cache.InformerSynced
claimLister corelisters.PersistentVolumeClaimLister
claimSynced cache.InformerSynced
classLister storagelisters.StorageClassLister
classSynced cache.InformerSynced

kubeClient clientset.Interface

// 事件记录
eventRecorder record.EventRecorder

// Provisioner 插件
volumePlugins []volume.VolumePlugin
cloud cloudprovider.Interface

// 工作队列
queue workqueue.RateLimitingInterface
}

2. PVC 绑定状态机

stateDiagram-v2
    [*] --> Pending: 创建 PVC
    Pending --> Bound: 找到匹配 PV (静态供应)
    Pending --> Bound: 动态供应完成
    Bound --> Released: 删除 PVC
    Released --> Available: ReclaimPolicy=Retain
    Released --> [*]: ReclaimPolicy=Delete
    Pending --> Lost: 绑定失败

3. syncClaim 核心逻辑

代码路径: pkg/controller/volume/persistentvolume/pv_controller.go:250

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (ctrl *PersistentVolumeController) syncClaim(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
logger := klog.FromContext(ctx)
logger.V(4).Info("Synchronizing PersistentVolumeClaim", "PVC", klog.KObj(claim))

// 更新迁移注解
newClaim, err := ctrl.updateClaimMigrationAnnotations(ctx, claim)
claim = newClaim

// 判断绑定状态
if !metav1.HasAnnotation(claim.ObjectMeta, storagehelpers.AnnBindCompleted) {
// 未绑定的 PVC
return ctrl.syncUnboundClaim(ctx, claim)
} else {
// 已绑定的 PVC
return ctrl.syncBoundClaim(ctx, claim)
}
}

4. 未绑定 PVC 处理 (syncUnboundClaim)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (ctrl *PersistentVolumeController) syncUnboundClaim(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
// 检查是否已绑定到 PV
if claim.Spec.VolumeName != "" {
// 已指定 VolumeName,验证绑定
return ctrl.bindClaimToVolume(ctx, claim)
}

// 1. 尝试匹配现有 PV (静态供应)
volume, err := ctrl.findBestMatchForClaim(ctx, claim)
if err != nil {
return err
}

if volume != nil {
// 找到匹配的 PV,执行绑定
return ctrl.bindClaimToVolume(ctx, claim, volume)
}

// 2. 没有找到匹配的 PV,尝试动态供应
if ctrl.shouldProvision(claim) {
return ctrl.provisionClaim(ctx, claim)
}

// 3. 无法供应,保持 Pending 状态
return nil
}

5. 静态供应 vs 动态供应

5.1 静态供应

管理员预先创建 PV:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.1.100
path: /data

PVC 绑定到预创建的 PV:

sequenceDiagram
    participant Admin as 管理员
    participant API as API Server
    participant PVC as PV Controller
    participant PV as PV 资源

    Admin->>API: 创建 PV
    API->>PV: Available 状态

    Note over API: 用户创建 PVC
    API->>PVC: syncClaim 触发
    PVC->>PV: findBestMatchForClaim
    PV->>PVC: 返回匹配的 PV
    PVC->>API: 更新 PVC.Spec.VolumeName
    PVC->>API: 更新 PV.Spec.ClaimRef
    API->>PV: Bound 状态
    API->>PVC: Bound 状态

5.2 动态供应

根据 StorageClass 自动创建 PV:

1
2
3
4
5
6
7
8
9
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd
reclaimPolicy: Delete
volumeBindingMode: Immediate
sequenceDiagram
    participant User as 用户
    participant API as API Server
    participant PVC as PV Controller
    participant Prov as Provisioner
    participant Backend as 存储后端

    User->>API: 创建 PVC (指定 StorageClass)
    API->>PVC: syncClaim 触发
    PVC->>PVC: findBestMatchForClaim (无匹配)
    PVC->>PVC: shouldProvision (返回 true)
    PVC->>Prov: provisionClaim

    Prov->>Backend: 创建存储卷
    Backend->>Prov: 返回卷 ID
    Prov->>API: 创建 PV
    API->>PVC: PV Available

    Note over PVC: 下一次 syncClaim
    PVC->>API: 绑定 PV 到 PVC
    API->>PVC: Bound 状态

6. FindMatchingVolume 逻辑

代码路径: staging/src/k8s.io/component-helpers/storage/volume/pv_helpers.go:185

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func FindMatchingVolume(
claim *v1.PersistentVolumeClaim,
volumes []*v1.PersistentVolume,
node *v1.Node,
excludedVolumes map[string]*v1.PersistentVolume,
delayBinding bool) (*v1.PersistentVolume, error) {

var smallestVolume *v1.PersistentVolume
requestedQty := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
requestedClass := GetPersistentVolumeClaimClass(claim)

for _, volume := range volumes {
// 检查容量
volumeQty := volume.Spec.Capacity[v1.ResourceStorage]
if volumeQty.Cmp(requestedQty) < 0 {
continue
}

// 检查 VolumeMode
if CheckVolumeModeMismatches(&claim.Spec, &volume.Spec) {
continue
}

// 检查 PV 是否正在删除
if volume.ObjectMeta.DeletionTimestamp != nil {
continue
}

// 检查 NodeAffinity (WaitForFirstConsumer 模式)
if node != nil {
err := CheckNodeAffinity(volume, node.Labels)
if err != nil {
continue
}
}

// 检查 StorageClass
if GetPersistentVolumeClass(volume) != requestedClass {
continue
}

// 检查 AccessModes
if !CheckAccessModes(claim, volume) {
continue
}

// 选择最小的匹配卷
if smallestVolume == nil || smallestVolumeQty.Cmp(volumeQty) > 0 {
smallestVolume = volume
}
}

return smallestVolume, nil
}

7. VolumeBindingMode

7.1 Immediate(立即绑定)

1
volumeBindingMode: Immediate

PVC 创建后立即绑定,不考虑 Pod 调度位置。

7.2 WaitForFirstConsumer(延迟绑定)

1
volumeBindingMode: WaitForFirstConsumer

等待使用该 PVC 的 Pod 被调度后再绑定:

sequenceDiagram
    participant User as 用户
    participant API as API Server
    participant PVC as PV Controller
    participant Sched as Scheduler
    participant Prov as Provisioner

    User->>API: 创建 PVC (WaitForFirstConsumer)
    API->>PVC: syncClaim
    PVC->>PVC: IsDelayBindingMode (true)
    Note over PVC: 不立即绑定,等待调度

    User->>API: 创建 Pod (使用 PVC)
    API->>Sched: 调度 Pod
    Sched->>Sched: 选择节点
    Sched->>API: 添加 AnnSelectedNode 注解

    API->>PVC: PVC 更新事件
    PVC->>Prov: provisionClaim (with selected node)
    Prov->>API: 创建 PV (with node affinity)
    API->>PVC: 绑定完成

相关注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const (
// AnnSelectedNode 表示调度器选中的节点
AnnSelectedNode = "volume.kubernetes.io/selected-node"

// AnnStorageProvisioner 表示应该供应此 PVC 的 provisioner
AnnStorageProvisioner = "volume.kubernetes.io/storage-provisioner"

// AnnBindCompleted 表示绑定已完成
AnnBindCompleted = "pv.kubernetes.io/bind-completed"

// AnnBoundByController 表示由控制器完成绑定
AnnBoundByController = "pv.kubernetes.io/bound-by-controller"

// AnnDynamicallyProvisioned 表示动态供应的 PV
AnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
)

8. Reclaim Policy

代码路径: pkg/controller/volume/persistentvolume/pv_controller.go:1154

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (ctrl *PersistentVolumeController) reclaimVolume(ctx context.Context, volume *v1.PersistentVolume) error {
logger := klog.FromContext(ctx)

// 检查是否迁移到 CSI
if migrated := volume.Annotations[storagehelpers.AnnMigratedTo]; len(migrated) > 0 {
// CSI provisioner 负责处理
return nil
}

switch volume.Spec.PersistentVolumeReclaimPolicy {
case v1.PersistentVolumeReclaimRetain:
// 保留 PV,需要手动处理
return ctrl.retainVolume(ctx, volume)

case v1.PersistentVolumeReclaimDelete:
// 删除 PV 和后端存储
return ctrl.deleteVolume(ctx, volume)

case v1.PersistentVolumeReclaimRecycle:
// 已废弃,执行回收脚本
return ctrl.recycleVolume(ctx, volume)
}

return nil
}
Reclaim Policy 行为
Retain 保留 PV 和数据,需手动清理
Delete 删除 PV 和后端存储
Recycle 已废弃,执行 rm -rf /volume/*

9. syncVolume 核心逻辑

代码路径: pkg/controller/volume/persistentvolume/pv_controller.go:556

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (ctrl *PersistentVolumeController) syncVolume(ctx context.Context, volume *v1.PersistentVolume) error {
logger := klog.FromContext(ctx)
logger.V(4).Info("Synchronizing PersistentVolume", "volumeName", volume.Name)

// 更新迁移注解和 Finalizer
newVolume, err := ctrl.updateVolumeMigrationAnnotationsAndFinalizers(ctx, volume)
volume = newVolume

if volume.Spec.ClaimRef == nil {
// PV 未绑定,设置为 Available
return ctrl.updateVolumePhase(ctx, volume, v1.VolumeAvailable, "")
}

// 获取关联的 PVC
claim, err := ctrl.claimLister.PersistentVolumeClaims(volume.Spec.ClaimRef.Namespace).Get(volume.Spec.ClaimRef.Name)

if apierrors.IsNotFound(err) {
// PVC 不存在,执行 Reclaim
return ctrl.reclaimVolume(ctx, volume)
}

// 检查绑定是否有效
if !IsVolumeBoundToClaim(volume, claim) {
// 绑定无效,重置 PV
return ctrl.unbindVolume(ctx, volume)
}

// 更新 PV 状态为 Bound
return ctrl.updateVolumePhase(ctx, volume, v1.VolumeBound, "")
}

10. CSI 动态供应

现代 Kubernetes 推荐使用 CSI (Container Storage Interface):

flowchart TD
    subgraph Kubernetes["Kubernetes 集群"]
        A["PVC"]
        B["StorageClass"]
        C["CSIDriver"]
    end

    subgraph ExternalProvisioner["external-provisioner"]
        D["Watch PVC"]
        E["调用 CSI 接口"]
    end

    subgraph CSI["CSI Driver"]
        F["CreateVolume"]
        G["DeleteVolume"]
        H["ControllerPublishVolume"]
    end

    subgraph Storage["存储系统"]
        I["云磁盘 / NFS / 本地存储"]
    end

    A -->|指定| B
    B -->|provisioner:csi.xxx| C
    A -->|触发| D
    D --> E
    E --> F
    F -->|创建| I
    I -->|返回卷 ID| F
    F -->|创建 PV| A

关键代码锚点

功能 文件路径
PV Controller 结构 pkg/controller/volume/persistentvolume/pv_controller.go
syncClaim 入口 pkg/controller/volume/persistentvolume/pv_controller.go:250
syncVolume 入口 pkg/controller/volume/persistentvolume/pv_controller.go:556
reclaimVolume pkg/controller/volume/persistentvolume/pv_controller.go:1154
FindMatchingVolume staging/src/k8s.io/component-helpers/storage/volume/pv_helpers.go:185
注解常量 staging/src/k8s.io/component-helpers/storage/volume/pv_helpers.go:33

完整流程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 1. 创建 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
---
# 2. 创建 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 10Gi
---
# 3. 创建 Pod 使用 PVC
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc

常见问题

PVC 一直 Pending

  • 检查是否有匹配的 PV 或 StorageClass
  • 检查 Provisioner 是否正常运行
  • 检查 WaitForFirstConsumer 模式是否有 Pod 使用

绑定失败

  • 检查 PV 容量是否满足 PVC 请求
  • 检查 AccessModes 是否匹配
  • 检查 StorageClass 是否一致

动态供应失败

  • 检查 Provisioner 日志
  • 检查存储后端配额和权限
  • 检查 CSI Driver 状态

高频面试题

Q1: PV 和 PVC 的绑定流程是怎样的?

参考答案:

  1. 静态供应:

    • 管理员预先创建 PV(状态为 Available)
    • 用户创建 PVC
    • PV Controller 执行 syncClaim()
    • 调用 findBestMatchForClaim() 查找匹配的 PV
    • 匹配条件:容量、AccessMode、StorageClass、VolumeMode、Selector
    • 执行双向绑定:更新 PV.Spec.ClaimRef 和 PVC.Spec.VolumeName
    • 状态更新:PV → Bound,PVC → Bound
  2. 动态供应:

    • 用户创建 PVC(指定 StorageClass)
    • PV Controller 发现没有匹配的 PV
    • 调用 provisionClaim() 触发动态供应
    • Provisioner 创建后端存储,然后创建 PV
    • 下一次 syncClaim 时完成绑定

Q2: StorageClass 的 VolumeBindingMode 有哪几种?

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
# 1. Immediate(立即绑定)
volumeBindingMode: Immediate
# - PVC 创建后立即尝试绑定/供应
# - 不考虑 Pod 调度位置
# - 可能导致 Pod 调度到与 PV 不同的 Zone

# 2. WaitForFirstConsumer(延迟绑定)
volumeBindingMode: WaitForFirstConsumer
# - 等待使用该 PVC 的 Pod 被调度
# - 调度器选择节点后,添加 AnnSelectedNode 注解
# - Provisioner 在选定节点所在的 Zone 创建 PV
# - 适用于 Local PV、跨 Zone 存储等场景

Q3: PV 的 ReclaimPolicy 有哪几种?分别是什么行为?

参考答案:

ReclaimPolicy 行为 适用场景
Retain 保留 PV 和数据,需手动清理 生产环境、重要数据
Delete 删除 PV 和后端存储 动态供应的临时数据
Recycle 已废弃,执行 rm -rf /volume/* 旧版本兼容

Retain 模式下的处理流程:

  1. PVC 删除后,PV 状态变为 Released
  2. ClaimRef 保留(指向已删除的 PVC)
  3. 管理员手动处理:
    • 删除 PV(保留数据)
    • 或清除 ClaimRef 后 PV 重新 Available

Q4: PVC 一直处于 Pending 状态,可能的原因有哪些?

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 排查命令
kubectl describe pvc <pvc-name>
kubectl get pv
kubectl get storageclass
kubectl get events --field-selector involvedObject.name=<pvc-name>

# 常见原因:

# 1. 没有匹配的 PV(静态供应)
# - PV 容量不足
# - AccessMode 不匹配
# - StorageClass 不一致
# - Selector 不匹配

# 2. 动态供应失败
# - StorageClass 不存在
# - Provisioner 未运行或异常
# - 存储后端配额不足
# - 权限问题

# 3. WaitForFirstConsumer 模式
# - 没有使用该 PVC 的 Pod
# - Pod 未被调度

# 4. CSI 驱动问题
# - CSI Driver 未安装
# - external-provisioner 未运行

Q5: CSI 动态供应的流程是怎样的?

参考答案:

  1. PV Controller 处理:

    • 发现 PVC 需要动态供应
    • 在 PVC 上设置 volume.kubernetes.io/storage-provisioner annotation
    • 发送 ExternalProvisioning 事件
  2. external-provisioner(CSI Sidecar):

    • Watch PVC,发现 annotation 匹配自己的 driver name
    • 调用 CSI Driver 的 CreateVolume gRPC 接口
    • 获取后端存储返回的 Volume ID
  3. 创建 PV:

    • external-provisioner 创建 PV 对象
    • 设置 pv.kubernetes.io/provisioned-by annotation
    • 设置 CSI 卷源(包含 Volume ID)
  4. 完成绑定:

    • PV Controller 监听到新 PV
    • 执行正常的绑定流程

Q6: PV Controller 是如何保证绑定一致性的?

参考答案:

  1. 双向绑定:

    1
    2
    3
    4
    5
    // bind() 函数执行顺序
    // 1. 绑定 PV 到 PVC(设置 PV.Spec.ClaimRef)
    // 2. 更新 PV 状态为 Bound
    // 3. 绑定 PVC 到 PV(设置 PVC.Spec.VolumeName)
    // 4. 更新 PVC 状态为 Bound
  2. Annotation 标记:

    • pv.kubernetes.io/bind-completed:绑定已完成
    • pv.kubernetes.io/bound-by-controller:由控制器绑定
  3. syncBoundClaim 验证:

    • 检查 PVC.Spec.VolumeName 是否为空
    • 检查 PV 是否存在
    • 检查 PV.ClaimRef.UID 是否与 PVC.UID 匹配
    • 不匹配则设置 PVC 状态为 Lost
  4. Finalizer 保护:

    • kubernetes.io/pv-controller:in-tree PV 删除保护
    • external-provisioner.volume.kubernetes.io/finalizer:CSI PV 保护

Q7: 如何实现存储的跨 Zone 高可用?

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 使用 WaitForFirstConsumer 模式
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: cross-zone-storage
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- us-west-2a
- us-west-2b
- us-west-2c

# 2. 工作原理:
# - Pod 调度时,调度器选择一个 Zone
# - 在 PVC 上设置 AnnSelectedNode annotation
# - Provisioner 在 Pod 所在 Zone 创建 PV
# - 确保 Pod 和存储在同一 Zone,减少延迟

# 3. 对于已存在的 PV(静态供应):
# - PV 设置 nodeAffinity
# - 调度器根据 PV 的 nodeAffinity 选择节点

延伸阅读