基于 Kubernetes v1.37 源码(pkg/volume/pkg/controller/volume/

一、为什么需要存储抽象? @

容器本身是无状态的,重启后数据丢失。但大多数应用需要持久化存储:

容器文件系统(tmpfs)→ 重启丢失
       ↓
需要持久化存储 → Volume 抽象
       ↓
但 Volume 类型繁多(NFS、EBS、Ceph...)→ 需要统一抽象
       ↓
PV/PVC 体系 → 解耦存储供应与消费
       ↓
CSI → 标准化存储接口,第三方存储无需修改 k8s 代码

二、存储体系全景 @

┌─────────────────────────────────────────────────────────────┐
│                      用户层                                  │
│  Pod.spec.volumes[].persistentVolumeClaim.claimName         │
│       ↓                                                      │
│  PVC (PersistentVolumeClaim)                                │
│       ↓                                                      │
│  PV (PersistentVolume)                                      │
│       ↓                                                      │
│  StorageClass (动态供应)                                    │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   供应层(Provisioning)                     │
│  静态供应:管理员手动创建 PV                                 │
│  动态供应:StorageClass + Controller 自动创建 PV            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   控制层(Bind)                             │
│  Bind:PV Controller 将 PVC 绑定到 PV                       │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   挂载层(Attach/Mount)                     │
│  Attach:将存储卷连接到节点(如 EBS Attach)                │
│  Mount:将卷挂载到 Pod 目录                                 │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   存储后端                                   │
│  NFS / Ceph / EBS / GCE PD / Azure Disk / CSI Driver       │
└─────────────────────────────────────────────────────────────┘

三、核心概念 @

3.1 Volume(卷) @

Volume 是 Pod 中容器可访问的存储目录。

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test
    image: nginx
    volumeMounts:
    - name: data
      mountPath: /usr/share/nginx/html
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-pvc

Volume 生命周期:Volume 的生命周期与 Pod 绑定,Pod 删除后 Volume 也随之删除(除非使用持久化存储)。

3.2 PersistentVolume(PV) @

PV 是集群中的一块持久化存储,由管理员提供或动态创建。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-001
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: standard
  nfs:
    path: /exports/data
    server: 192.168.1.100

关键属性

属性 说明
capacity.storage 存储容量
accessModes 访问模式(RWO/ROX/RWX/RWOP)
persistentVolumeReclaimPolicy 回收策略(Retain/Delete;Recycle 已废弃,推荐用动态供应 + Delete 替代)
storageClassName 存储类名称
nodeAffinity 节点亲和性(限制可挂载节点)

访问模式

模式 缩写 说明
ReadWriteOnce RWO 单节点读写
ReadOnlyMany ROX 多节点只读
ReadWriteMany RWX 多节点读写
ReadWriteOncePod RWOP 单 Pod 读写(1.22 alpha / 1.27 beta / 1.29 GA)

3.3 PersistentVolumeClaim(PVC) @

PVC 是用户对存储的请求。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard
  selector:
    matchLabels:
      disk-type: ssd

PVC 状态

状态 说明
Pending 等待绑定
Bound 已绑定到 PV
Lost PV 已丢失

3.4 StorageClass @

StorageClass 描述了"如何动态创建 PV"。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: ebs.csi.aws.com  # in-tree 的 kubernetes.io/aws-ebs 已于 1.27 移除
parameters:
  type: gp2
  fsType: ext4
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

关键参数

参数 说明
provisioner 供应器(内置或 CSI)
parameters 后端特定参数
reclaimPolicy PVC 删除后 PV 的处理策略
allowVolumeExpansion 是否允许扩容
volumeBindingMode 绑定时机(Immediate/WaitForFirstConsumer)

四、控制器 @

k8s 有多个控制器协同管理存储生命周期:

4.1 PersistentVolumeController(PV 控制器) @

PV 控制器负责绑定 PVC 到 PV管理回收

// pkg/controller/volume/persistentvolume/pv_controller.go(框架在 pv_controller_base.go)
func (ctrl *PersistentVolumeController) syncClaim(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
    if claim.Spec.VolumeName == "" {
        // 未绑定:寻找匹配的 PV(匹配逻辑在 syncUnboundClaim 中)
        return ctrl.syncUnboundClaim(ctx, claim)
    }
    // 已绑定:检查绑定的 PV 是否仍然有效...
}

func (ctrl *PersistentVolumeController) syncUnboundClaim(ctx context.Context, claim *v1.PersistentVolumeClaim) error {
    // 1. 寻找匹配的 PV
    pv := findMatchingVolume(claim) // 简化表示

    if pv == nil {
        // 2. 没有匹配的 PV,尝试动态供应
        return ctrl.provisionClaim(ctx, claim)
    }
    // 3. 绑定 PVC 到 PV
    return ctrl.bind(ctx, pv, claim)
}

func (ctrl *PersistentVolumeController) syncVolume(ctx context.Context, volume *v1.PersistentVolume) error {
    // 根据回收策略处理已释放的 PV
    switch volume.Spec.PersistentVolumeReclaimPolicy {
    case v1.PersistentVolumeReclaimRecycle:
        // 回收:已废弃,推荐用动态供应 + Delete 替代
    case v1.PersistentVolumeReclaimDelete:
        // 删除:删除 PV 及其后端存储
    case v1.PersistentVolumeReclaimRetain:
        // 保留:保留数据,需手动处理
    }
}

4.2 AD Controller(附加/分离控制器) @

AD Controller 负责将存储卷 Attach 到节点从节点分离

// pkg/controller/volume/attachdetach/attach_detach_controller.go
// 启动时用 informer 数据填充期望状态与实际状态
func (adc *attachDetachController) populateDesiredStateOfWorld(logger klog.Logger) error {
    // 遍历所有节点上的 Pod,把需要 Attach 的卷加入期望状态
    adc.desiredStateOfWorld.AddPod(podName, pod, volumeSpec, nodeName)
}

func (adc *attachDetachController) populateActualStateOfWorld(logger klog.Logger) error {
    // 从 Node.Status.VolumesAttached 初始化实际状态
    adc.actualStateOfWorld.AddVolumeNode(logger, volumeName, volumeSpec, nodeName, devicePath, attached)
}

// 独立的 Reconciler 组件(reconciler/reconciler.go)周期性对比两个状态:
// desired 有而 actual 未 Attached → 执行 Attach
// actual 已 Attached 而 desired 没有 → 执行 Detach

4.3 PVC Protection Controller @

PVC Protection Controller 防止正在使用的 PVC 被删除:

// pkg/controller/volume/pvcprotection/pvc_protection_controller.go
// 控制器类型名为 Controller
func (c *Controller) processPVC(ctx context.Context, pvcNamespace, pvcName string, lazyLivePodList *LazyLivePodList) error {
    // 如果 PVC 正在被 Pod 使用,添加 finalizer
    isUsed, err := c.isBeingUsedWith(ctx, pvc, lazyLivePodList, c.podUsesPVCForDeletion)
    if isUsed {
        return c.addFinalizer(ctx, pvc)
    }
    return c.removeFinalizer(ctx, pvc)
}

五、Volume 插件 @

5.1 内置插件 @

k8s 内置了多种 Volume 插件:

类型 说明 适用场景
emptyDir 临时目录 缓存、临时数据
hostPath 主机目录 节点级数据
configMap ConfigMap 挂载 配置文件
secret Secret 挂载 敏感配置
downwardAPI Pod 元数据挂载 环境变量注入
nfs NFS 挂载 共享存储
fc Fibre Channel SAN 存储
iscsi iSCSI 挂载 块存储
csi CSI 驱动 外部存储

注:cephfs 的 in-tree 插件已在 1.31 移除(pkg/volume/ 下无 cephfs 目录),v1.37 使用 CephFS 需通过 CSI 驱动(如 cephfs.csi.ceph.com)。

5.2 插件接口 @

注:只列出核心方法,完整定义见 pkg/volume/plugins.gopkg/volume/volume.go

// pkg/volume/plugins.go
type VolumePlugin interface {
    Init(host VolumeHost) error
    GetPluginName() string
    GetVolumeName(spec *Spec) (string, error)
    CanSupport(spec *Spec) bool
    RequiresRemount(spec *Spec) bool
    NewMounter(spec *Spec, podRef *v1.Pod) (Mounter, error)
    NewUnmounter(name string, podUID types.UID) (Unmounter, error)
    ConstructVolumeSpec(volumeName, volumePath string) (ReconstructedVolume, error)
    SupportsMountOption() bool
    SupportsSELinuxContextMount(spec *Spec) (bool, error)
}

// 可扩容插件实现扩展接口(RequiresFSResize 在这里,不在 VolumePlugin 中)
type ExpandableVolumePlugin interface {      // 控制面扩容(plugins.go:250)
    VolumePlugin
    ExpandVolumeDevice(spec *Spec, newSize, oldSize resource.Quantity) (resource.Quantity, error)
    RequiresFSResize() bool
}

type NodeExpandableVolumePlugin interface {  // 节点侧扩容(plugins.go:258)
    VolumePlugin
    RequiresFSResize() bool
    NodeExpand(resizeOptions NodeResizeOptions) (bool, error)
}

// pkg/volume/volume.go
type Mounter interface {
    Volume  // 内嵌 Volume 接口
    SetUp(mounterArgs MounterArgs) error
    SetUpAt(dir string, mounterArgs MounterArgs) error
    GetAttributes() Attributes
}

type Unmounter interface {
    Volume
    TearDown() error
    TearDownAt(dir string) error
}

5.3 挂载流程 @

// VolumeHost 接口定义在 pkg/volume/plugins.go(不存在 volume_host.go),
// 提供 GetMounter()、GetPodVolumeDir() 等辅助方法,本身没有 MountDevice。
// MountDevice 属于 DeviceMounter 接口(pkg/volume/volume.go:308),
// 负责把设备挂载到节点全局目录,之后由 Mounter 绑定挂载到 Pod 目录:
type DeviceMounter interface {
    GetDeviceMountPath(spec *Spec) (string, error)
    MountDevice(spec *Spec, devicePath string, deviceMountPath string, deviceMounterArgs DeviceMounterArgs) error
}

六、CSI(容器存储接口) @

CSI 是存储的标准化接口,允许第三方存储提供商编写自己的驱动,无需修改 k8s 代码。

6.1 CSI 架构 @

┌─────────────────────────────────────────────────────────────┐
│                    Kubernetes 核心                          │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              CSI Sidecar Containers                   │  │
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐     │  │
│  │  │ external-  │  │ external-  │  │ liveness-  │     │  │
│  │  │ provisioner│  │ attacher   │  │ probe      │     │  │
│  │  └────────────┘  └────────────┘  └────────────┘     │  │
│  │  ┌────────────┐  ┌────────────┐                     │  │
│  │  │ external-  │  │ external-  │                     │  │
│  │  │ resizer    │  │ snapshotter│                     │  │
│  │  └────────────┘  └────────────┘                     │  │
│  └──────────────────────────────────────────────────────┘  │
│                            ↓                                │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              CSI Driver (gRPC)                       │  │
│  │  Controller Service: CreateVolume/DeleteVolume/      │  │
│  │                      ControllerPublishVolume/...     │  │
│  │  Node Service: NodeStageVolume/NodePublishVolume/... │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

6.2 CSI Sidecar Containers @

Sidecar 职责
external-provisioner 监听 PVC 创建,调用 CSI CreateVolume
external-attacher 监听 VolumeAttachment,调用 CSI ControllerPublishVolume
external-resizer 监听 PVC 扩容,调用 CSI ControllerExpandVolume
external-snapshotter 监听 VolumeSnapshot,调用 CSI CreateSnapshot
liveness-probe 监控 CSI Driver 健康状态
node-driver-registrar 在节点注册 CSI Driver

6.3 CSI 接口定义 @

注:rpc 方法与消息类型使用大写驼峰命名(如 CreateVolume),snake_case 只用于消息字段名。

// CSI 规范由外部项目 github.com/container-storage-interface/spec 定义,
// 本仓库 vendor 中的生成代码:vendor/github.com/container-storage-interface/spec/lib/go/csi/csi.pb.go
service Identity {
    rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse);
    rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse);
    rpc Probe(ProbeRequest) returns (ProbeResponse);
}

service Controller {
    rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
    rpc DeleteVolume(DeleteVolumeRequest) returns (DeleteVolumeResponse);
    rpc ControllerPublishVolume(ControllerPublishVolumeRequest) returns (ControllerPublishVolumeResponse);
    rpc ControllerUnpublishVolume(ControllerUnpublishVolumeRequest) returns (ControllerUnpublishVolumeResponse);
    rpc ValidateVolumeCapabilities(ValidateVolumeCapabilitiesRequest) returns (ValidateVolumeCapabilitiesResponse);
    rpc ListVolumes(ListVolumesRequest) returns (ListVolumesResponse);
    rpc GetCapacity(GetCapacityRequest) returns (GetCapacityResponse);
    rpc ControllerGetCapabilities(ControllerGetCapabilitiesRequest) returns (ControllerGetCapabilitiesResponse);
    rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
    rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse);
    rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse);
    rpc ControllerExpandVolume(ControllerExpandVolumeRequest) returns (ControllerExpandVolumeResponse);
    rpc ControllerGetVolume(ControllerGetVolumeRequest) returns (ControllerGetVolumeResponse);
}

service Node {
    rpc NodeStageVolume(NodeStageVolumeRequest) returns (NodeStageVolumeResponse);
    rpc NodeUnstageVolume(NodeUnstageVolumeRequest) returns (NodeUnstageVolumeResponse);
    rpc NodePublishVolume(NodePublishVolumeRequest) returns (NodePublishVolumeResponse);
    rpc NodeUnpublishVolume(NodeUnpublishVolumeRequest) returns (NodeUnpublishVolumeResponse);
    rpc NodeGetVolumeStats(NodeGetVolumeStatsRequest) returns (NodeGetVolumeStatsResponse);
    rpc NodeExpandVolume(NodeExpandVolumeRequest) returns (NodeExpandVolumeResponse);
    rpc NodeGetCapabilities(NodeGetCapabilitiesRequest) returns (NodeGetCapabilitiesResponse);
    rpc NodeGetInfo(NodeGetInfoRequest) returns (NodeGetInfoResponse);
}

6.4 CSI 调用流程 @

1. 用户创建 PVC
       ↓
2. external-provisioner 监听到 PVC
       ↓
3. 调用 CSI Driver 的 CreateVolume
       ↓
4. 后端存储创建卷,返回 VolumeHandle
       ↓
5. external-provisioner 创建 PV 对象
       ↓
6. PV Controller 绑定 PVC 到 PV
       ↓
7. Pod 调度到节点
       ↓
8. AD Controller 创建 VolumeAttachment
       ↓
9. external-attacher 调用 ControllerPublishVolume(Attach)
       ↓
10. kubelet 调用 NodeStageVolume(格式化+挂载到全局目录)
       ↓
11. kubelet 调用 NodePublishVolume(绑定挂载到 Pod 目录)

七、Volume 绑定模式 @

7.1 Immediate @

volumeBindingMode: Immediate:PVC 创建后立即绑定 PV。

问题:如果 PV 有节点亲和性,Pod 可能无法调度到该节点。

7.2 WaitForFirstConsumer @

volumeBindingMode: WaitForFirstConsumer:等待 Pod 调度后再绑定 PV。

优势:调度器可以根据 PV 的节点亲和性选择节点。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
  - key: topology.kubernetes.io/zone
    values:
    - us-east-1a

八、Volume 扩容 @

8.1 在线扩容 @

PVC 扩容自 1.11 进入 beta(当时要求卷未被使用,即离线扩容);在线(in-use)扩容由 ExpandInUsePersistentVolumes 特性提供:1.15 alpha、1.16 beta、1.24 GA(无需重启 Pod):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
allowVolumeExpansion: true  # 允许扩容
# 扩容 PVC
kubectl patch pvc my-pvc -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

8.2 扩容流程 @

1. 用户修改 PVC 的 storage 请求
       ↓
2. external-resizer 监听到 PVC 变化
       ↓
3. 调用 CSI ControllerExpandVolume
       ↓
4. 后端存储扩容
       ↓
5. kubelet 调用 NodeExpandVolume
       ↓
6. 文件系统扩容(resize2fs/xfs_growfs)
       ↓
7. PVC 状态更新为 FileSystemResizePending → 扩容完成

九、Volume 快照 @

9.1 快照 API @

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: ebs-snapshot-class
driver: ebs.csi.aws.com
deletionPolicy: Delete
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: my-snapshot
spec:
  volumeSnapshotClassName: ebs-snapshot-class
  source:
    persistentVolumeClaimName: my-pvc

9.2 快照恢复 @

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: restore-pvc
spec:
  dataSource:
    name: my-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: ebs-sc

十、CSI 内联临时卷(CSI Ephemeral Inline Volume) @

k8s 1.16+ 支持 CSI 内联临时卷,生命周期与 Pod 绑定。要求 CSI 驱动的 CSIDriver 对象声明 volumeLifecycleModes: Ephemeral

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test
    image: nginx
    volumeMounts:
    - name: scratch
      mountPath: /scratch
  volumes:
  - name: scratch
    csi:
      driver: inline.storage.example.com
      volumeAttributes:
        size: 1Gi

特点

  • 卷属性直接写在 Pod spec 的 volumes[].csi 中,不创建 PV/PVC 对象
  • 数据随 Pod 删除而销毁
  • 仅适用于实现了 Ephemeral 生命周期模式的 CSI 驱动

注:1.19 起引入的通用临时卷ephemeral.volumeClaimTemplate,1.21 beta、1.23 GA)见第十二章。


十一、原始块设备卷(Raw Block Volume) @

原始块设备卷:1.9 alpha、1.13 beta、1.18 GA:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test
    image: nginx
    volumeDevices:  # 使用 volumeDevices 而非 volumeMounts
    - name: raw-disk
      devicePath: /dev/xvda
  volumes:
  - name: raw-disk
    persistentVolumeClaim:
      claimName: block-pvc

用途

  • 数据库直接访问裸设备(绕过文件系统)
  • 高性能存储场景

十二、通用临时卷(Generic Ephemeral Volume) @

通用临时卷(Generic Ephemeral Volume):1.19 alpha、1.21 beta、1.23 GA。通过 ephemeral.volumeClaimTemplate 用 PVC 模板为每个 Pod 创建临时 PVC,Pod 删除后临时 PVC 自动删除,支持所有支持动态供应的存储后端:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test
    image: nginx
    volumeMounts:
    - name: scratch
      mountPath: /scratch
  volumes:
  - name: scratch
    ephemeral:
      volumeClaimTemplate:
        metadata:
          labels:
            type: scratch
        spec:
          accessModes: ["ReadWriteOnce"]
          storageClassName: "standard"
          resources:
            requests:
              storage: 1Gi

十三、生产最佳实践 @

13.1 使用 WaitForFirstConsumer @

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer  # 避免调度问题
allowedTopologies:
- matchLabelExpressions:
  - key: topology.kubernetes.io/zone
    values:
    - us-east-1a

13.2 设置回收策略 @

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
reclaimPolicy: Retain  # PVC 删除后保留数据

13.3 启用扩容 @

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
allowVolumeExpansion: true  # 允许在线扩容

13.4 定期快照 @

# 创建快照
kubectl apply -f snapshot.yaml

# 恢复快照
kubectl apply -f restore-pvc.yaml

十四、设计亮点总结 @

  • 分层抽象:Pod 层通过 Volume 引用存储,PVC 层解耦使用与供应,PV 层解耦供应与后端,StorageClass 提供动态供应模板。
  • CSI 标准化:通过 gRPC 接口标准化存储驱动,第三方存储无需修改 k8s 代码。
  • Sidecar 模式:CSI 采用 Sidecar 容器设计,每个职责独立,易于维护和升级。
  • WaitForFirstConsumer:解决存储拓扑感知问题,确保 Pod 调度到可访问存储的节点。
  • 在线扩容:无需重启 Pod 即可扩容,提高可用性。

十五、总结 @

Kubernetes 存储体系的设计体现了几个核心思想:

  • 分层解耦:Pod → PVC → PV → StorageClass → 后端存储
  • 标准化接口:CSI 统一存储驱动接口
  • 自动化:动态供应、Attach/Mount、扩容、快照
  • 高可用:WaitForFirstConsumer、在线扩容、快照恢复

理解存储体系,就掌握了 Kubernetes 有状态应用部署的关键。