Kubernetes 基础体系 · 第 37/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
StorageClass 与动态供给:Provisioner、Binding、扩容和拓扑
1. StorageClass 解决什么问题
Kubernetes 中,**PersistentVolume(PV)**描述一块已经存在或即将创建的持久化存储资源,**PersistentVolumeClaim(PVC)**描述工作负载需要什么样的存储。PVC 不直接实现存储,它只是向集群提出请求:
- 需要多少容量;
- 需要什么访问模式;
- 使用文件系统还是块设备;
- 使用哪类存储;
- 是否需要特定拓扑或节点。
**StorageClass(存储类)**则把“这类存储应该如何创建”抽象出来。它通常包含:
provisioner:负责创建卷的 CSI 驱动名称;parameters:传给驱动的存储参数;reclaimPolicy:PVC 删除后 PV 如何处理;volumeBindingMode:何时把 PVC 与 PV 绑定;allowVolumeExpansion:是否允许在线或离线扩容;mountOptions:挂载时使用的选项;allowedTopologies:允许创建卷的拓扑范围。
因此,动态供给的逻辑可以概括为:
这里的“创建 Volume”不是 Kubernetes API Server 直接调用云厂商 API,而是由某个存储实现完成。现代 Kubernetes 通常通过 CSI(Container Storage Interface)驱动完成这一过程。
需要区分三个概念:
- 声明:PVC 表示应用的存储需求;
- 供给:Provisioner 根据 PVC 创建实际存储并生成 PV;
- 使用:调度器、Attach/Mount 流程和 kubelet 让 Pod 访问已经绑定的卷。
PVC 处于 Bound 状态,只表示它已经绑定到一个 PV,并不保证卷已经成功挂载,也不保证底层存储当前可用。
2. 从 PVC 到 Pod 使用卷的完整组件链路
动态供给至少涉及以下角色:
| 组件 | 主要职责 |
|---|---|
| API Server | 保存 StorageClass、PVC、PV、Pod 等对象 |
| PV Controller | 处理 PVC/PV 的匹配、绑定和回收相关状态 |
| External Provisioner | 监听 PVC,根据 StorageClass 调用 CSI Controller 的 CreateVolume |
| CSI Controller Plugin | 调用存储系统 API,创建、删除、扩容卷,并可能处理 Attach |
| CSI Node Plugin | 在具体节点上执行 Stage、Publish、Mount 等操作 |
| Scheduler | 选择 Pod 节点,并参与延迟绑定的拓扑决策 |
| kubelet | 在节点上协调 CSI Node Plugin,把卷挂载到 Pod |
CSI 驱动通常由多个容器组成,而不是一个单独的进程。典型控制面组件包括:
external-provisioner:动态供给;external-attacher:处理需要 Attach 的卷;external-resizer:处理扩容;external-snapshotter:处理快照相关对象。
这些 sidecar 与 CSI Controller Plugin 通信,CSI Controller Plugin 再与真实存储系统通信。CSI Node Plugin 通常以 DaemonSet 形式部署在每个需要使用卷的节点上。
典型数据流如下:
sequenceDiagram
participant U as 用户
participant A as API Server
participant P as PV Controller
participant S as Scheduler
participant EP as External Provisioner
participant CC as CSI Controller
participant K as Kubelet
participant CN as CSI Node Plugin
participant ST as 存储系统
U->>A: 创建 StorageClass
U->>A: 创建 PVC
A-->>P: PVC 事件
P-->>EP: PVC 需要动态供给
EP->>CC: CreateVolume
CC->>ST: 创建真实卷
ST-->>CC: 返回 volumeHandle
CC-->>EP: 返回卷信息
EP->>A: 创建 PV
P->>A: 将 PVC 与 PV 绑定
U->>A: 创建引用 PVC 的 Pod
S->>A: 观察 Pod、PVC、PV 和拓扑
S->>A: 绑定 Pod 到节点
K->>CN: NodeStage/NodePublish
CN->>ST: 准备并挂载卷
CN-->>K: 挂载完成
K-->>U: 容器可访问挂载路径
实际环境中,如果卷需要云平台级 Attach,流程中还会出现 Attach/Detach Controller 和 external-attacher。某些存储类型不需要独立 Attach,例如部分网络文件系统;是否需要 Attach 由 CSI 驱动能力和存储实现决定。
2.1 Provisioner 不等于 CSI 驱动本身
StorageClass 中的:
provisioner: csi.example.com
表示一个 CSI 驱动名称。它通常与 CSI 驱动注册的 GetPluginInfo 名称一致。external-provisioner 监听到 PVC 后,会根据这个名称寻找对应的 CSI 控制器服务。
因此,以下配置不会凭空提供存储:
provisioner: csi.example.com
只有当集群中确实安装并运行了名称为 csi.example.com 的 CSI 驱动及其 sidecar 时,动态供给才可能成功。示例中的 csi.example.com 是占位名称,不能直接用于生产环境。
可以先检查集群实际安装了哪些 StorageClass 和 provisioner:
kubectl get storageclass
kubectl get storageclass -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner,BINDING:.volumeBindingMode,EXPAND:.allowVolumeExpansion
预期输出类似:
NAME PROVISIONER BINDING EXPAND
fast csi.example.com WaitForFirstConsumer true
shared-files nfs.csi.k8s.io Immediate true
PROVISIONER 的值必须与已安装驱动注册的名称对应。不要把云厂商文档中的 parameters 或 driver 名称直接套用到另一个 CSI 驱动。
3. StorageClass 的关键字段
下面是一个通用结构。驱动专属参数只能根据对应 CSI 驱动文档填写。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: csi.example.com
parameters:
type: premium
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
mountOptions:
- noatime
这个对象可以通过 kubectl apply 提交,但只有集群中安装了 csi.example.com,并且该驱动支持 type: premium、noatime 等配置时,后续 PVC 才能成功供给。
3.1 provisioner
provisioner 决定谁处理使用该 StorageClass 的 PVC。
动态供给失败时,常见事件包括:
failed to provision volume with StorageClass "fast":
rpc error: code = InvalidArgument desc = unsupported parameter
或者:
no storage plugin matched
第一类通常是驱动参数错误;第二类常见于驱动未安装、名称不匹配或 provisioner 控制器未正常运行。
3.2 parameters
parameters 是驱动相关的键值对。例如某个块存储驱动可能接受:
parameters:
type: premium
encrypted: "true"
但 type、encrypted 是否有效、取值是什么,都不是 Kubernetes StorageClass API 的通用保证。Kubernetes 只负责保存并传递这些参数,语义由 CSI 驱动定义。
生产中尤其要注意:
- 参数名称拼写错误可能直到创建 PVC 后才暴露;
- 某些参数会影响计费、性能等级或可用区;
- 某些驱动参数引用 Secret,Secret 命名空间和引用方式由驱动约定;
- 修改 StorageClass 通常不是修改已有卷配置的通用方式,已经创建的卷通常不会自动改变类型或性能等级。
3.3 reclaimPolicy
常见值是:
Delete:PVC 删除后,PV 通常被回收,动态创建的底层卷也通常由 CSI 驱动删除;Retain:PVC 删除后保留 PV 和底层数据,需要人工处理。
Delete 的删除链路通常是:
删除 PVC
→ PV 进入 Released 或被控制器处理
→ external-provisioner 调用 DeleteVolume
→ 底层卷删除
这里的“通常”很重要:最终是否删除底层卷取决于 CSI 驱动是否正确实现删除操作,以及回收流程是否遇到权限、网络或云平台保护策略。
Retain 不是“自动备份”。它只是避免 Kubernetes 自动删除卷,仍然需要人工确认:
- PV 是否包含敏感数据;
- 是否可以重新绑定;
- 是否需要清理旧的
claimRef; - 是否要建立新的 PVC;
- 是否要防止误挂载到错误的应用。
StorageClass 的默认回收策略通常为 Delete;生产环境应显式设置,避免依赖默认值。
3.4 allowVolumeExpansion
allowVolumeExpansion: true
只表示该 StorageClass 允许请求扩容,不代表所有卷都能扩容,也不代表扩容一定在线完成。CSI 驱动还必须实现相应的 Controller 和 Node 扩容能力。
扩容是“增加请求容量”,不是修改原容量:
spec:
resources:
requests:
storage: 200Gi
从 100Gi 改为 200Gi 合法;从 200Gi 改回 100Gi 的缩容通常不支持。Kubernetes 不会通过截断文件系统的方式自动缩容,因为这可能破坏数据。
3.5 volumeBindingMode
有两个核心值:
ImmediateWaitForFirstConsumer
它们决定 PVC 何时绑定和动态创建卷,后文会详细说明。对于具有可用区、机架、节点或区域限制的存储,WaitForFirstConsumer 往往是关键配置。
3.6 mountOptions
mountOptions 会传递给挂载流程,但 Kubernetes 不会验证每个选项是否被底层文件系统或 CSI 驱动支持。错误选项可能导致挂载失败:
MountVolume.SetUp failed ... mount failed: wrong fs type, bad option
不要把 NFS 专用选项、XFS 专用选项或云厂商专用选项无条件用于所有驱动。
4. 动态供给的前置条件:PVC 如何表达需求
一个 PVC 的典型定义如下:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-data
namespace: app
spec:
storageClassName: fast
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 100Gi
各字段的含义如下:
storageClassName: fast:请求使用名为fast的 StorageClass;accessModes:请求卷的访问模式;volumeMode: Filesystem:Pod 中看到的是文件系统目录;resources.requests.storage:请求的最小容量。
PVC 能否绑定到某个 PV,至少需要满足以下条件:
其中“拓扑约束可满足”在不同绑定时机下表现不同。尤其是 Immediate 可能先绑定,再发现 Pod 无法调度;WaitForFirstConsumer 会把卷创建或绑定推迟到 Pod 调度决策可用之后。
4.1 容量不是精确匹配
PVC 请求 100Gi,PV 容量为 120Gi,两者可以绑定:
PVC request = 100Gi
PV capacity = 120Gi
100Gi <= 120Gi
Kubernetes 不会因为容量多出 20Gi 而拒绝匹配。PV 的 capacity.storage 是可供绑定判断使用的容量声明,而不是应用一定可以精确使用的物理容量保证。
4.2 storageClassName 的三个重要状态
显式指定:
storageClassName: fast
表示使用 fast。
显式指定空字符串:
storageClassName: ""
表示该 PVC 不使用 StorageClass 动态供给,也不会被默认 StorageClass 自动填充。它通常用于静态 PV 场景。
完全省略:
spec:
resources:
requests:
storage: 100Gi
表示允许集群的默认 StorageClass 参与。默认 StorageClass 由注解标记:
metadata:
annotations:
storageclass.kubernetes.io/is-default-class: "true"
集群中最好只维护一个明确的默认 StorageClass。多个默认类会增加行为歧义,且默认类变化可能影响新建 PVC 的结果。已经绑定的 PVC 不会因为默认 StorageClass 后续变化而重新选择存储。
4.3 访问模式不是应用级并发控制
常用访问模式包括:
ReadWriteOnce(RWO):卷可被一个节点以读写方式挂载;ReadOnlyMany(ROX):卷可被多个节点只读挂载;ReadWriteMany(RWX):卷可被多个节点读写挂载;ReadWriteOncePod(RWOP):卷只能被单个 Pod 使用,适用于支持该能力的 CSI 驱动和相关组件。
ReadWriteOnce 的“一次”主要针对节点,不是 Pod。多个 Pod 位于同一个节点时,可能同时使用一个 RWO 卷。因此,RWO 不能替代数据库锁、文件锁或应用层主从协调。
相反,即使 PVC 声明了 RWX,应用也必须能够正确处理多实例并发写入。访问模式描述存储系统允许的挂载方式,不自动提供分布式一致性协议。
5. Binding:PVC 与 PV 如何建立关系
**Binding(绑定)**是 PVC 与一个 PV 建立一对一关系的过程。
绑定完成后通常可以看到:
kubectl get pvc database-data -n app
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
database-data Bound pvc-3e7... 100Gi RWO fast
PVC 的 .spec.volumeName 会指向 PV,PV 的 .spec.claimRef 会指向该 PVC。两者共同表达“这个 PV 已经被这个 PVC 占用”。
可以用以下命令查看完整关系:
kubectl get pvc database-data -n app -o yaml
kubectl get pv pvc-3e7... -o yaml
典型结果会包含:
# PVC
status:
phase: Bound
boundVolume: pvc-3e7...
# PV
spec:
claimRef:
namespace: app
name: database-data
字段的具体状态还可能包含 UID 等信息,用于避免同名对象被错误复用。
5.1 动态绑定的中间状态
创建 PVC 后,常见状态转换是:
Pending
↓
Provisioning
↓
PV 创建成功
↓
Bound
Provisioning 往往不是 PVC 的独立 phase,而是事件或控制器处理过程。PVC 的 status.phase 主要常见为:
PendingBoundLost
诊断时不要只看 phase,应同时查看事件:
kubectl describe pvc database-data -n app
可能看到:
Normal ExternalProvisioning waiting for a volume to be created
Normal Provisioning External provisioner is provisioning volume
Normal Provisioned Successfully provisioned volume
5.2 手工 PV 与动态 PV 的区别
静态 PV 是管理员先创建:
apiVersion: v1
kind: PersistentVolume
metadata:
name: static-data
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
storageClassName: ""
persistentVolumeReclaimPolicy: Retain
csi:
driver: csi.example.com
volumeHandle: preexisting-volume-id
对应 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: static-data-claim
namespace: app
spec:
storageClassName: ""
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
这里的 storageClassName: "" 很重要:它避免 PVC 被默认 StorageClass 动态供给。否则,PVC 可能得到一块新的动态卷,而不是绑定管理员准备好的静态 PV。
5.3 selector 会限制动态供给
PVC 可以设置 selector:
spec:
selector:
matchLabels:
environment: production
这会要求绑定的 PV 带有匹配标签。某些动态供给流程无法同时满足 selector 和自动创建要求,因此不能把“有 selector”与“必然能动态创建”混为一谈。使用 selector 时,应确认对应 provisioner 是否支持这一组合。
6. Immediate 与 WaitForFirstConsumer
6.1 Immediate:PVC 创建后立即供给
volumeBindingMode: Immediate
流程是:
创建 PVC
→ 立即创建真实卷
→ 创建 PV
→ PVC 绑定 PV
→ Pod 后续调度
它适合拓扑不敏感的存储,例如一个可以从所有节点访问的共享文件系统。
但对于区域型块存储,这个顺序可能产生问题。假设:
- 节点
node-a位于zone-a; - 节点
node-b位于zone-b; - CSI 驱动在
zone-a创建了卷; - Pod 由于资源、污点或亲和性只能调度到
zone-b。
那么 PVC 可能已经是 Bound,但 Pod 无法使用这个卷:
0/2 nodes are available:
1 node(s) had volume node affinity conflict
这不是 Binding 失败,而是绑定发生得太早,导致后续调度约束无法满足。
6.2 WaitForFirstConsumer:等待 Pod 的调度上下文
volumeBindingMode: WaitForFirstConsumer
典型流程变为:
创建 PVC
→ PVC 保持 Pending
→ 创建引用该 PVC 的 Pod
→ Scheduler 综合 Pod 与存储拓扑选择节点
→ Provisioner 按选择结果创建卷
→ 创建 PV 并绑定 PVC
→ Pod 在该节点挂载卷
示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zonal-block
provisioner: csi.example.com
parameters:
type: premium
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
allowVolumeExpansion: true
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
namespace: app
spec:
storageClassName: zonal-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: app
spec:
containers:
- name: app
image: busybox:1.36
command: ["/bin/sh", "-c", "echo ready; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
这个配置的前提是:
csi.example.com已安装;- 该驱动支持拓扑感知供给;
- 节点具有驱动所识别的拓扑标签;
- CSI sidecar 和调度器版本、配置能够协同处理延迟绑定。
仅仅把 volumeBindingMode 改成 WaitForFirstConsumer,并不能让不支持拓扑的驱动自动获得拓扑能力。
6.3 PVC 长时间 Pending 是否一定是故障
使用 WaitForFirstConsumer 时,只有 PVC 而没有任何 Pod 消费它,PVC 保持 Pending 是正常的:
kubectl get pvc app-data -n app
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
app-data Pending zonal-block
此时应查看:
kubectl describe pvc app-data -n app
如果事件表明正在等待消费者,说明延迟绑定正在生效。若已经有 Pod,仍然 Pending,则应继续检查 Pod:
kubectl describe pod app -n app
重点观察:
- 是否存在不可满足的节点亲和性;
- 是否有污点但没有对应 toleration;
- 节点资源是否不足;
- 是否所有候选节点都不具备 CSI 驱动;
- 是否发生
volume node affinity conflict; - 是否有调度器或 provisioner 错误事件。
7. 拓扑:卷为什么不能随便放置
**拓扑(topology)**描述资源所在的区域、可用区、机架或节点范围。常见标签键包括:
topology.kubernetes.io/region
topology.kubernetes.io/zone
kubernetes.io/hostname
具体支持哪些键由 CSI 驱动和集群配置决定。拓扑的本质是限制:
而 Pod 调度还要满足:
两者的交集为空时,Pod 就无法调度或挂载。
7.1 allowedTopologies
StorageClass 可以限制动态卷只能创建在特定拓扑中:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zone-a-only
provisioner: csi.example.com
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- zone-a
这表示动态创建的卷只能位于 zone-a。它不是把所有节点都移动到 zone-a,也不是给 Pod 自动添加节点亲和性。
如果 Pod 自身只能调度到 zone-b,而 StorageClass 只允许 zone-a,则不存在满足条件的节点。结果通常表现为 Pod Pending,事件中出现类似:
node(s) had volume node affinity conflict
或:
pod has unbound immediate PersistentVolumeClaims
7.2 PV 的 nodeAffinity
拓扑型 PV 通常会带有节点亲和性:
spec:
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
这表示该 PV 只能在满足条件的节点上使用。对于动态创建的卷,这类约束通常由 provisioner 根据存储系统返回的拓扑写入 PV。
nodeAffinity 是 PV 的可用位置约束,不是普通 Pod 的偏好设置。Pod 如果没有合适节点,调度器不能忽略它。
7.3 Immediate 的拓扑反例
假设集群有两个节点:
node-a: zone-a
node-b: zone-b
一个 Immediate StorageClass 创建卷后,存储系统返回:
volumeHandle: vol-001
volume zone: zone-a
PV nodeAffinity: zone-a
而 Pod 还有如下约束:
nodeSelector:
topology.kubernetes.io/zone: zone-b
则约束集合为:
所以:
- PVC 可以是
Bound; - PV 可以是
Available之外的正常绑定状态; - Pod 仍然无法调度;
- 删除并重建 Pod 不会改变已经绑定的 PV;
- 通常需要调整 Pod 约束,或在确认数据可删除后删除 PVC/PV/底层卷重新供给。
WaitForFirstConsumer 的价值,就是尽量在创建卷之前计算这个交集。
7.4 StatefulSet 与拓扑
StatefulSet 的 volumeClaimTemplates 会为每个 Pod 创建独立 PVC,例如:
data-db-0
data-db-1
data-db-2
每个 PVC 都可能触发独立的动态供给。对于跨可用区的 StatefulSet,需要同时考虑:
- 每个副本的 Pod 调度位置;
- 每个 PVC 的卷拓扑;
- 副本之间是否需要分散到不同区域;
- 数据库自身的复制机制;
- 存储系统是否支持跨区域访问。
一个 StatefulSet 副本的 RWO 卷不能因为增加副本数就自动变成共享卷;每个副本通常需要自己的 PVC。若数据库需要故障域级高可用,不能仅依赖 Kubernetes 把多个 RWO 卷创建出来,还要由数据库复制协议保证数据可用性。
8. Binding 不是 Attach,也不是 Mount
这三个阶段经常被混淆。
8.1 Binding
Binding 建立 Kubernetes 对象关系:
PVC ↔ PV
它主要回答:
这个 PVC 使用哪一个 PV?
完成 Binding 不代表节点已经能访问卷。
8.2 Attach
Attach 通常把卷连接到某个节点,例如云平台可能需要把云盘挂载到虚拟机。它主要回答:
这个卷是否已经连接到目标节点?
并非所有存储都需要独立 Attach。CSI 驱动通过能力声明和控制器流程决定是否需要。
8.3 Mount 与 Publish
节点侧通常包含多个逻辑阶段:
NodeStageVolume:在节点上准备卷,常见做法是格式化并挂载到 staging 目录;NodePublishVolume:把 staging 路径绑定挂载到 Pod 使用的目标路径;- kubelet 把目标路径挂入容器。
文件系统卷大致是:
块设备
→ 文件系统格式化
→ 节点 staging 目录
→ Pod 目录
→ 容器内 /data
块设备模式则不是让 kubelet把卷作为目录直接使用,而是把设备暴露给容器。PVC 示例:
spec:
volumeMode: Block
容器需要使用:
volumeDevices:
- name: data
devicePath: /dev/xvdb
如果 volumeMode 与 PV 不匹配,PVC 不能正常绑定;如果应用本身期待文件系统,却请求了 Block,即使卷已绑定,应用也无法按目录方式访问。
9. 扩容:从 PVC 请求到文件系统变大
扩容包含至少两个层次:
9.1 配置允许扩容
StorageClass:
allowVolumeExpansion: true
检查:
kubectl get sc fast -o jsonpath='{.allowVolumeExpansion}{"\n"}'
如果输出为:
true
说明该 StorageClass 允许 PVC 请求扩容。仍然要确认 CSI 驱动支持 ControllerExpandVolume,并且节点插件支持需要的 NodeExpandVolume。
9.2 执行扩容
初始 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-data
namespace: app
spec:
storageClassName: fast
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
修改为:
spec:
resources:
requests:
storage: 200Gi
可以使用:
kubectl patch pvc database-data -n app \
--type merge \
-p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
观察状态:
kubectl get pvc database-data -n app -w
检查详细事件:
kubectl describe pvc database-data -n app
成功后,通常会看到:
status:
capacity:
storage: 200Gi
但过程可能分为不同阶段:
- PVC
requests.storage已变为200Gi; - PV
capacity.storage仍暂时是100Gi; - 控制器调用 CSI
ControllerExpandVolume; - PV 容量更新;
- 如果卷已挂载,节点插件执行文件系统扩容;
- PVC 状态最终反映新容量。
不要只在修改后立即读取一次对象就判断失败。扩容是异步操作,应结合事件、CSI 控制器日志、节点插件日志和底层存储平台状态确认。
9.3 在线扩容与文件系统边界
块设备扩大后,文件系统仍可能保持旧大小。CSI 驱动必须完成节点侧扩容,文件系统还必须支持相应的扩展操作。
常见边界包括:
- 卷未挂载时,可能只完成底层块设备扩容;
- 卷已挂载时,支持在线扩容的文件系统可以继续使用;
- 某些文件系统或驱动要求卸载后扩容;
- 用户态看到的
df -h可能比 PVC 的status.capacity更新更晚; - 设备容量、分区容量、文件系统容量是三个不同观察层次。
可以在 Pod 内检查:
kubectl exec -n app database-pod -- df -h /var/lib/database
也可以在节点侧由管理员检查块设备和挂载状态,但不能把进入节点执行命令当作所有环境都适用的通用诊断方式。托管 Kubernetes 往往不允许或不建议直接修改节点设备。
9.4 扩容失败后的处理
典型错误:
failed to expand volume
rpc error: code = Unimplemented desc = volume expansion is not supported
说明驱动没有实现对应能力,或者 sidecar 与驱动版本不匹配。
另一个常见问题是请求值已经变大,但实际 PV 没有更新:
PVC request: 200Gi
PV capacity: 100Gi
此时不要尝试把请求值改回更小值来“重置”。应检查:
kubectl describe pvc database-data -n app
kubectl describe pv <pv-name>
kubectl -n <csi-namespace> logs deploy/<external-resizer>
kubectl -n <csi-namespace> logs <csi-controller-pod> -c <driver-container>
具体资源名称随 CSI 驱动安装方式变化。恢复动作通常是修复驱动权限、云平台配额、API 连通性或 sidecar 配置,然后等待控制器重试。
9.5 扩容与 StatefulSet
StatefulSet 的 volumeClaimTemplates 描述如何创建 PVC,但已经创建的 PVC 是独立对象。修改模板不应被理解为“自动修改所有现有 PVC 的容量”。
已有卷通常需要逐个修改:
kubectl patch pvc data-db-0 -n app \
--type merge \
-p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
然后验证每个副本的:
- PVC 请求值;
- PV 容量;
- 文件系统实际容量;
- 数据库是否感知到新空间;
- 扩容期间是否需要停止或切换副本。
10. 一个完整的动态供给示例
下面使用虚构的 csi.example.com,因此“可执行”的含义是 YAML 语法和 Kubernetes 对象关系完整;真正执行前,必须把 provisioner、参数和镜像环境替换为已安装驱动支持的值。
10.1 创建 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-zonal
provisioner: csi.example.com
parameters:
type: premium
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
应用:
kubectl apply -f storageclass.yaml
kubectl get sc fast-zonal
预期:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
fast-zonal csi.example.com Retain WaitForFirstConsumer true
选择 Retain 是为了演示数据保留风险:删除 PVC 后,底层数据不会按默认流程立即删除。生产中是否选择 Retain,取决于数据恢复和清理流程,不能简单认为它永远更安全,因为遗留卷也可能带来成本和敏感数据暴露。
10.2 创建 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-data
namespace: app
spec:
storageClassName: fast-zonal
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 10Gi
命令:
kubectl create namespace app
kubectl apply -f pvc.yaml
kubectl get pvc web-data -n app
在还没有 Pod 消费它时,使用 WaitForFirstConsumer 的 PVC 处于 Pending 是预期行为。
10.3 创建消费者 Pod
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: app
spec:
containers:
- name: writer
image: busybox:1.36
command:
- /bin/sh
- -c
- |
date >> /data/heartbeat
sleep 3600
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data
执行:
kubectl apply -f pod.yaml
kubectl get pod web -n app -w
kubectl get pvc web-data -n app -w
kubectl get pv
成功路径是:
- Scheduler 为 Pod 找到满足资源、污点、亲和性和卷拓扑的节点;
- PVC 的延迟绑定条件被满足;
- external-provisioner 调用 CSI
CreateVolume; - CSI 驱动返回底层卷标识;
- provisioner 创建 PV;
- PV Controller 将 PVC 绑定到 PV;
- kubelet 调用节点侧 CSI 操作;
- 容器中
/data/heartbeat可写入。
验证:
kubectl exec -n app web -- cat /data/heartbeat
如果 Pod 已调度但容器没有运行,继续查看:
kubectl describe pod web -n app
kubectl describe pvc web-data -n app
kubectl get pv
应区分故障阶段:
- PVC 仍 Pending:关注 provisioner、Binding、调度拓扑;
- PVC Bound 但 Pod Pending:关注调度器和 PV 的 nodeAffinity;
- Pod 已调度但 ContainerCreating:关注 Attach、Stage、Publish、Mount;
- 容器运行但写入失败:关注文件系统权限、只读挂载、容量和应用用户 UID。
11. 删除、回收与“数据是否还在”
删除消费者 Pod 通常不会删除 PVC:
kubectl delete pod web -n app
这通常只触发卸载流程,PVC 和 PV 仍然存在。重新创建引用同名 PVC 的 Pod,可以再次使用同一卷。
删除 PVC 才会进入回收流程:
kubectl delete pvc web-data -n app
如果 StorageClass 使用:
reclaimPolicy: Retain
可能看到:
kubectl get pv
NAME STATUS CLAIM STORAGECLASS RECLAIM POLICY
pvc-... Released fast-zonal Retain
Released 表示原 PVC 已删除,但 PV 仍保留。它通常不会自动绑定给新的 PVC,因为原 PV 仍带有旧的 claimRef,而且重新使用前必须确认数据归属和清理策略。
如果使用:
reclaimPolicy: Delete
动态创建的 PV 通常会触发底层卷删除。删除 PVC 前应确认:
- 数据是否已经备份;
- 是否仍有其他业务依赖该卷;
- CSI 驱动的删除权限是否正确;
- 云平台是否有回收站、快照或延迟删除机制;
- 数据保留要求是否允许自动删除。
生产故障中,误删 PVC 可能比 Pod 故障更严重。PV 的回收策略是生命周期控制,不是备份机制。
12. 常见失败路径与诊断顺序
12.1 PVC 一直 Pending,事件显示没有可用 PV
可能原因:
- StorageClass 名称错误;
- 没有默认 StorageClass;
- provisioner 未安装;
- CSI 控制器未运行;
parameters不被驱动支持;- 云平台配额不足;
WaitForFirstConsumer尚未出现消费者;- PVC selector、访问模式或 volumeMode 与现有 PV 不匹配。
诊断:
kubectl get pvc <name> -n <namespace>
kubectl describe pvc <name> -n <namespace>
kubectl get sc <storage-class> -o yaml
kubectl get pv
如果是动态供给,继续定位 provisioner:
kubectl get pods -A | grep -i csi
kubectl get events -A --sort-by=.lastTimestamp
CSI 部署所在命名空间和 Pod 名称因发行版不同,不应假设固定名称。
12.2 PVC Bound,但 Pod 无法调度
重点检查:
kubectl describe pod <pod> -n <namespace>
kubectl get pv <pv-name> -o yaml
特别查看:
spec:
nodeAffinity:
以及 Pod 的:
nodeSelector;nodeAffinity;tolerations;- 资源请求;
- 其他卷的拓扑限制。
如果使用 Immediate 且 PV 已落在错误可用区,调度器不能简单把卷移动到另一个区域。更换节点标签也不能改变云盘真实位置;应该根据数据保留要求决定调整 Pod 约束、迁移数据还是重建卷。
12.3 PVC Bound,但 Pod 卡在 ContainerCreating
查看事件:
kubectl describe pod <pod> -n <namespace>
常见错误:
FailedAttachVolume
FailedMount
MountVolume.SetUp failed
rpc error: code = DeadlineExceeded
诊断方向:
- CSI Node Plugin 是否运行在目标节点;
- CSI Controller 是否完成 Attach;
- 节点是否能访问存储服务;
- 节点上是否存在所需的内核模块或挂载工具;
- Secret 是否存在且权限正确;
- 文件系统类型和
mountOptions是否正确; - 卷是否已被其他节点占用;
- 云平台 API 是否限流或返回配额错误。
这里不要反复删除 Pod 作为主要诊断方法。删除 Pod 通常只会重复同一失败路径,先保留事件和 CSI 日志更有价值。
12.4 扩容请求已更新,但容量没有变
先区分三种值:
kubectl get pvc <name> -n <namespace> \
-o jsonpath='request={.spec.resources.requests.storage} capacity={.status.capacity.storage}{"\n"}'
kubectl get pv <pv-name> \
-o jsonpath='pv-capacity={.spec.capacity.storage}{"\n"}'
可能出现:
request=200Gi capacity=100Gi
pv-capacity=100Gi
这表示扩容请求已经提交,但控制器尚未成功扩展底层卷。
如果 PVC 和 PV 都显示 200Gi,但容器内仍是 100Gi:
kubectl exec -n app <pod> -- df -h /data
则问题位于节点侧扩容或文件系统刷新阶段,而不是控制器供给阶段。
13. API 与实现边界
13.1 Kubernetes 保证的部分
Kubernetes StorageClass API 定义并管理:
- StorageClass 对象及其字段;
- PVC 对 StorageClass 的引用;
- PV/PVC 的绑定关系;
- 回收策略字段;
- 延迟绑定模式;
- 扩容请求和状态表达;
- 拓扑约束对象模型。
13.2 CSI 驱动负责的部分
CSI 驱动负责具体实现:
- 如何创建和删除底层卷;
- 哪些参数有效;
- 是否支持某种访问模式;
- 是否支持 Attach;
- 是否支持快照;
- 是否支持控制器扩容和节点扩容;
- 如何处理拓扑;
- 如何执行格式化、Stage、Publish 和 Mount;
- Secret 如何认证;
- 失败如何重试或返回错误。
因此,“StorageClass 设置了 allowVolumeExpansion: true”不能推导出“该云盘一定可以在线扩容”;“PVC 设置了 RWX”也不能推导出“任意 CSI 驱动都能提供多节点读写”。
13.3 版本与兼容性
现代集群使用 storage.k8s.io/v1 的 StorageClass API。CSI 是当前主流扩展方式,早期内置卷插件和旧版 in-tree provisioner 已逐步迁移或弃用;具体迁移状态取决于 Kubernetes 版本、发行版和云厂商。
以下能力对版本和组件版本较敏感:
ReadWriteOncePod需要 CSI 驱动和相关 sidecar 支持;- 在线扩容需要 CSI Controller、Node 插件及 sidecar 协同;
- 拓扑感知供给依赖 Scheduler、external-provisioner 和 CSI 驱动共同支持;
- 快照依赖
VolumeSnapshot相关 CRD、snapshot-controller 和 CSI 驱动能力; - 云厂商的 StorageClass 参数不是 Kubernetes 通用 API。
升级 Kubernetes 或 CSI 组件时,应同时检查:
kubectl get csidrivers
kubectl get csinodes
kubectl get storageclass -o yaml
kubectl get pods -A | grep -i csi
不要只升级 Kubernetes 控制面而忽略 CSI sidecar 和节点插件的兼容性。
14. 生产中需要明确的取舍
14.1 默认 StorageClass 不是数据分类系统
默认 StorageClass 只能解决“未指定存储类的 PVC 用哪个类”。它不能表达:
- 数据库和日志使用不同性能;
- 测试环境和生产环境使用不同保留策略;
- 不同租户使用不同加密密钥;
- 不同应用必须位于不同故障域。
这些需求应通过显式 StorageClass、命名空间策略、准入控制和应用配置表达,而不是依赖默认值。
14.2 Delete 与 Retain 的选择取决于生命周期
临时环境通常适合自动删除,以避免遗留资源。生产数据可能需要 Retain,但必须配套:
- 数据归档流程;
- PV Released 清理流程;
- 底层卷盘点;
- 恢复演练;
- 删除审批或准入限制。
没有配套运营流程时,Retain 只会把删除责任从控制器转移给人工。
14.3 拓扑不是高可用的同义词
WaitForFirstConsumer 解决的是“卷创建位置与 Pod 调度位置协调”的问题,不会自动复制数据,也不会使单副本数据库跨区域高可用。
如果底层卷只存在于一个可用区,那么该卷所在区域故障时,Pod 可能无法在另一个区域直接启动。跨区域高可用需要存储系统复制、数据库复制、快照恢复或应用级容灾设计。
14.4 容量成功不代表性能成功
PVC 只表达容量和访问模式等约束。StorageClass 的 parameters 可能选择 IOPS、吞吐、介质类型或加密方式,但 Kubernetes 本身不验证业务是否达到预期性能。
验收存储时至少要分别验证:
- PVC/PV 容量;
- Pod 内文件系统容量;
- 挂载读写权限;
- 节点和区域故障下的可用性;
- 扩容流程;
- 删除后的数据行为;
- CSI 控制器和节点插件的恢复能力。
StorageClass 的核心价值,是把“存储请求”和“存储实现”解耦;动态供给的核心价值,是让 PVC 触发标准化的卷创建流程。真正理解它,必须把 Binding、调度拓扑、CSI 控制器、节点挂载和生命周期回收看成一条连续链路,而不能把 Bound 状态误认为“数据已经可用”。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes PV 与 PVC:绑定、访问模式、回收策略和生命周期
- 下一篇:Kubernetes CSI:Controller、Node、Attach、Mount、快照和故障
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论