基于 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.go与pkg/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 有状态应用部署的关键。