为什么 Kubernetes 要把存储抽象拆成 PV、PVC、StorageClass 与 CSI
# 为什么 Kubernetes 要把存储抽象拆成 PV、PVC、StorageClass 与 CSI
这不是一篇名词解释文,而是要回答一个设计问题:Kubernetes 为什么没有把“我要一块盘”做成一个单层对象,而是拆成声明、供给、绑定和插件边界这几层抽象。
如果只从使用者视角看,最自然的愿望其实很简单:Pod 需要一块持久化存储,平台直接给它一块盘就好了,为什么还要让人看见 PVC、PV、StorageClass 甚至 CSI 这些层次。问题在于,Kubernetes 处理的从来都不是“单机上立刻挂一块盘”这么简单的动作,而是一个横跨应用声明、平台策略、后端供给、调度约束和节点挂载的长链路。只要这条链路里存在多个参与方、多个时机和多个失败点,单层对象就会很快失去表达能力。
先看最直观的冲突。应用作者关心的是“我要一块满足容量、访问模式和持久性要求的卷”,他并不想知道底层到底来自哪套存储系统,也不应该直接操作平台资源。平台管理员关心的却是另一件事:这块卷能否动态创建,回收策略是什么,允许落在哪些可用区,是否必须走某个企业存储后端。再往下看,真正执行卷创建和挂载的存储系统又有自己的语言,它关心的是控制器调用、节点调用、拓扑限制、设备附加和文件系统挂载。这三层诉求并不处在同一个抽象平面上。如果把它们硬塞进一个对象里,要么应用侧暴露太多底层细节,要么平台侧失去策略控制,要么后端插件根本没有稳定接入点。
这就是 Kubernetes 不把存储设计成“Pod 直接引用某块盘”的第一层原因:请求者、供给者和实现者不是同一个角色,它们需要不同的边界。PVC 先把“我要什么”从“底层怎么给”里分离出来。它代表的是消费侧声明,而不是具体卷本身。一个 PVC 真正提供的契约很克制,只描述大小、访问模式、卷模式和可能的存储类选择。它不承诺卷来自哪个云厂商、哪个存储阵列、哪个节点,也不承诺此刻已经存在一块真实设备。Kubernetes 先把需求声明单独拿出来,本质上是在保护消费侧不被基础设施细节反向污染。
但只有 PVC 还不够,因为集群里最终仍然需要一个“已经存在或即将被创建的具体卷对象”来承接这份声明,这就是 PV 的职责。PV 不是再把 PVC 重复一遍,而是把卷作为平台侧资源显式建模。它承载的是供给结果、生命周期状态和可回收语义。卷一旦成为独立对象,平台就能描述“这是预先准备好的卷还是动态创建的卷”“它释放后应保留还是删除”“它当前是否已绑定、已释放或失效”。如果没有 PV 这一层,卷一旦被消费端引用,就很难再作为基础设施资产被治理。换句话说,PVC 解决的是需求表达,PV 解决的是资源存在和生命周期管理,这两个问题虽然相邻,但并不相同。
接下来会出现第三个问题:就算有了“需求对象”和“资源对象”,系统仍然需要一种方式,把平台的供给策略批量、稳定地施加到新卷上,否则每一次创建卷都得在 PVC 里直接塞满后端参数,结果就是应用清单重新沦为存储说明书。StorageClass 的作用正是在这里显现出来。它把“如何供给”上升为一类策略模板,让卷类型、参数、回收策略、卷绑定时机和拓扑偏好不再直接挂在应用声明上,而是由平台事先定义好。这样应用方只需表达“我要这一类存储”,平台方则保留对实际供给方式的统一控制权。StorageClass 本质上是在切开“业务想要什么”和“平台决定怎么给”的边界。
到这里为止,Kubernetes 仍然只是在控制平面里整理对象关系,还没有触碰真正的后端实现问题。如果 Kubernetes 把所有存储后端逻辑都内建在核心代码里,那么每接入一种新存储系统,都意味着主仓库要理解新的控制面 API、新的挂载语义和新的节点侧实现细节。这样做短期内似乎简单,长期却会把核心项目拖进与各家存储后端的深度耦合。CSI 被引入,就是为了把“平台定义的卷生命周期”与“厂商实现的卷能力”再切开一刀。控制平面只需要表达创建、删除、附加、挂载这些阶段性动作,而具体如何调用后端、如何在节点上映射设备、如何处理多路径或文件系统格式化,都交给插件边界之外的实现去完成。这样 Kubernetes 核心维护的是统一控制语义,存储厂商维护的是各自实现细节,两者通过稳定接口对接。
如果把这几层放回一条完整链路里看,会更容易理解为什么不能塌成一层。应用先提交 PVC 表达需求;平台依据 StorageClass 决定是否动态供给、何时供给以及按什么策略供给;系统随后得到一个具体 PV 作为供给结果,并把它和 PVC 建立绑定关系;真正需要创建卷、附加卷、在节点上挂载卷时,再通过 CSI 把控制意图发给后端实现。这四层不是为了让名词变多,而是因为链路中的状态本来就分属不同平面:声明平面、资源平面、策略平面和实现平面。Kubernetes 只是把这种客观存在的分层显式化了。
这套拆分真正解决的,是声明式系统里最难处理的几个矛盾。第一,应用想描述结果,但不想持有底层实现细节。第二,平台想统一管理资源和策略,但不想把业务 YAML 变成厂商专属配置。第三,后端实现想接入 Kubernetes,但不应该反向侵入 Kubernetes 核心。第四,卷的生命周期并不等于 Pod 生命周期,卷既可能先于 Pod 存在,也可能晚于 PVC 才被动态创建,还可能在 Pod 删除后继续保留。单层对象很难同时把这些时序和边界都表达清楚,而分层建模可以让每一层各自只对自己的状态负责。
当然,抽象一旦拆开,复杂度也会变得可见。最常见的代价就是状态不再集中。PVC 显示 Pending 时,问题可能在供给策略、权限或后端创建;PVC 已经 Bound 时,问题又可能还在调度、附加或节点挂载;卷最终可用与否,经常要跨 PVC、PV、StorageClass、控制器事件和 CSI 日志联合判断。也正因为如此,很多初学者会误以为这些对象是在“重复描述同一块盘”。其实它们并不重复,它们只是分别站在不同边界上,描述同一条存储链路里的不同状态。
所以,Kubernetes 把存储抽象拆成 PV、PVC、StorageClass 与 CSI,根本原因不是喜欢把简单事情做复杂,而是它必须同时满足声明式 API、平台治理、多后端接入和异步控制循环这几组约束。只要这几组约束同时存在,存储就不可能被优雅地压缩成一个单层对象。你看到的是四个名词,Kubernetes 真正在处理的却是四类不同的问题:谁来提需求、谁来持有资源、谁来定义策略、谁来完成实现。
# 进一步讨论
【待展开:Kubernetes 动态供给为什么天然依赖控制器循环|k8s-dynamic-provisioning-needs-controller-loop】:这能继续解释声明式供给为什么不是一次同步调用。