阶段三 · 存储:抽象与接口

CSI:存储插件的标准接口

一句话总结

CSI(容器存储接口)是 Kubernetes 把"对接存储"标准化外包出去的接口--和网络侧的 CNI 是一对孪生兄弟.
它把"给 Pod 准备好一块盘"拆成三段式: provision(造盘) → attach(挂到节点) → mount(挂进 Pod), 并定义了快照, 扩容等能力, 由各家存储插件实现.

前置回顾

第 06 篇我们讲了存储抽象: PVC 指定 StorageClass, K8s 就自动"造盘"绑定. 但一直没说: 那块盘到底是谁, 怎么造出来的? AWS 云盘, Ceph, NFS 各不相同, K8s 怎么统一对接?

答案是 CSI--它和第 03 篇的 CNI 是同一种设计的孪生兄弟. 如果理解了 CNI, 这一篇会有强烈的既视感: 那正是云原生"接口外包"哲学的对称之美.

又一次外包: K8s 也不自己对接存储

问题和网络如出一辙: 存储后端千差万别--AWS EBS, GCP PD, 阿里云盘, Ceph, NFS, 各家厂商的存储... K8s 不可能, 也不应该把对接每一种存储的代码都内置进核心. 于是它做了和 CNI 完全一样的决定:

K8s 定义一个标准接口 CSI(Container Storage Interface), 规定"一个存储插件必须能做到哪些操作(造盘, 挂载, 删除, 快照...)", 至于"怎么对接具体某种存储"由插件实现. 任何存储厂商只要实现 CSI, 就能接入 K8s.

网络(第 03 篇) 存储(本篇)
接口 CNI CSI
外包什么 "怎么给 Pod 配网络" "怎么给 Pod 备好存储"
K8s 只定义 四条网络规则(机制) 存储操作接口(机制)
插件提供 Flannel/Calico/Cilium EBS/Ceph/NFS... 的 CSI driver

CNI, CSI, CRI: K8s 的'三大外包接口'

第 03 篇的补充问答提过这个三件套, 这里正好闭环: CRI 外包运行时, CNI 外包网络, CSI 外包存储. 它们是同一种"机制与策略分离"哲学的三个方向, 让 K8s 核心保持精简, 而运行时/网络/存储生态各自百花齐放.

看懂 CNI 再看 CSI, 会发现连"控制面 + 节点面"的分工方式都高度对称--这种对称不是巧合, 是好设计的自我复制.

CSI 插件的两个组件: 控制面 + 节点面

"给 Pod 准备一块盘"这件事, 其实发生在两个不同层面, 所以 CSI 插件通常拆成两部分(像网络里"控制面下发, 数据面转发"的分工):

组件 跑在哪 管什么
Controller 插件(控制面) 集群里跑一份(Deployment) "资源管理"层面的事: 创建/删除底层存储卷, 把卷 attach 到某节点, 做快照, 扩容--调用存储后端的 API
Node 插件(节点面) 每个节点一份(DaemonSet) "本节点挂载"层面的事: 把已 attach 到本节点的卷, 格式化, 挂载到 Pod 能用的目录

为什么这么拆? 因为"造一块云盘"是集群级的全局操作(调云 API, 跟具体节点无关), 而"把盘挂进某个容器"必须在Pod 所在的那个节点本地执行. 两件事天然属于不同层面, 故分两个组件.

核心流程: provision → attach → mount 三段式

这是本篇的重点, 也是把第 06 篇"自动造盘"彻底讲透的地方. 当一个 Pod 要用动态供给的存储时, CSI 把整个过程分成三段, 各由不同组件负责:

┌────────────────────────────────────────────┐
│ Pod 创建, 引用一个 PVC(PVC 指定了 StorageClass) │
└────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────────┐
│ ① Provision 造盘 │
│ Controller 插件调存储后端 API, 创建一块真实的卷(如一块 EBS 云盘) │
└────────────────────────────────────────────────────────────────────┘


┌───────────────────────────────────────────────────────────────────────────────────┐
│ ② Attach 挂到节点 │
│ Controller 插件把这块卷挂载(attach)到 Pod 所在的节点上(此时是节点上的一个块设备) │
└───────────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ ③ Mount 挂进 Pod │
│ Node 插件在该节点上格式化(首次)并 mount 到 Pod 的目录 │
└──────────────────────────────────────────────────────────────┘


┌──────────────────┐
│ Pod 容器里就能读写这块存储了 │
└──────────────────┘
阶段 做什么 谁干的 类比
① Provision 创建出一块真实的存储卷 Controller 插件 仓库里造出一个新柜子
② Attach 把卷接到 Pod 所在节点(成为节点上的块设备) Controller 插件 把柜子搬到所在的楼层
③ Mount 在节点上格式化并挂载到 Pod 的目录 Node 插件 把柜子装进房间, 打开门能用

三段式像叫一份外卖到工位:
Provision 是餐厅做好这份餐(创建卷).
Attach 是骑手送到公司楼下(卷到达节点).
Mount 是前台帮送到工位, 拆好包装(挂进 Pod 能直接吃). 三步分属餐厅, 骑手, 前台--对应 Controller, Controller, Node 三个角色的分工.

为什么 attach 和 mount 要分开?

初学者常觉得"挂载"是一件事, 其实是两件: attach 是把存储卷"接到节点这台机器上"(让节点的操作系统能看到这个块设备, 类似插了块硬盘), 这是云平台/存储后端层面的操作; mount 是在节点操作系统里把这个块设备"挂到某个目录"(格式化文件系统, 挂载点), 让容器能通过文件路径读写.

一块盘可能先 attach 到节点, 但还没 mount 给任何 Pod. 分开这两步, K8s 才能精细控制卷在节点间的迁移(如 Pod 漂移时: 先从旧节点 detach, 再 attach 到新节点).

CSI 带来的高级能力: 快照与扩容

CSI 不只管"挂盘". 因为它是个标准接口, K8s 还在上面定义了通用的高级存储能力, 各插件按需实现:

  • 卷快照(Snapshot): 给某个卷在某一时刻"拍快照", 可用于备份或克隆出新卷. 通过 VolumeSnapshot 资源声明式地创建--这是第 08 篇数据备份的重要手段.
  • 卷扩容(Expansion): 在线把卷扩大(第 06 篇提过的 allowVolumeExpansion), CSI 驱动负责调底层扩容 + 扩展文件系统.
  • 拓扑感知(Topology): 让 K8s 知道"这块卷只能在某个可用区用", 调度 Pod 时把它放到能访问该卷的节点上(避免"卷在 A 区, Pod 调度到 B 区却挂不上").

标准接口的红利: 能力一次定义, 所有插件共享

快照, 扩容这些能力, K8s 在 CSI 层面定义一次标准 API(如 VolumeSnapshot 资源), 所有实现了对应 CSI 能力的存储插件就都能用同一套 K8s 操作来驱动--不用为 EBS 和 Ceph 学两套快照命令.

这正是"标准接口"的复利: 定义一次, 生态共享. 和 OpenTelemetry(可观测性系列)用一套标准统一采集, CNI 用一套接口统一网络, 是完全相同的价值.

不是每个 CSI 驱动都支持全部能力

CSI 定义了一堆可选能力, 但具体某个存储插件支不支持快照, 支不支持在线扩容, 支不支持 RWX(第 06 篇), 取决于该驱动的实现和底层存储本身的能力. 比如本地盘的 CSI 驱动通常不支持跨节点 attach, 简单的 NFS 驱动可能不支持快照.

选型时务必查清要用的 CSI 驱动支持哪些能力--这直接决定了第 08 篇能不能做快照备份, 能不能扩容. 接口定义了"可能性", 实现决定了"现实".

快速回顾

  • CSI 是存储版的 CNI: K8s 不自己对接各种存储, 定义 CSI 接口外包给插件; 与 CNI, CRI 并称三大外包接口, 同一种机制/策略分离哲学
  • 两个组件: Controller 插件(集群一份, 管造盘/attach/快照/扩容, 调存储 API) + Node 插件(每节点一份 DaemonSet, 管本节点的格式化与挂载)
  • 三段式流程: Provision(造出真实卷) → Attach(卷接到 Pod 所在节点) → Mount(节点上格式化并挂进 Pod); attach 是"接到机器", mount 是"挂到目录", 分开才能支持卷在节点间迁移
  • 高级能力: 快照(备份/克隆, 第 08 篇用), 在线扩容, 拓扑感知; K8s 在 CSI 层定义标准 API, 所有插件共享
  • 能力因驱动而异: 快照/扩容/RWX 支不支持取决于具体 CSI 驱动 + 底层存储, 选型必查

CSI 出现之前, K8s 怎么对接存储的?

早期是 in-tree(树内) 插件--把对接 AWS, GCE 等的存储代码直接写进 K8s 主代码库. 问题很明显: 存储厂商想加个新功能或修 bug, 得给 K8s 主项目提 PR, 等 K8s 发版, 节奏完全被绑死; K8s 核心也被一堆厂商代码拖累得越来越臃肿.

CSI 把这些代码移到 K8s 之外(out-of-tree), 厂商自己维护, 自己发版, K8s 核心瘦身. 这和 CNI 把网络实现移出核心是同一个动机--解耦核心与生态, 各自独立演进. 如今 in-tree 插件已基本被迁移/弃用.

三段式里, 如果 Pod 从节点 A 漂移到节点 B, 存储怎么跟过去?

这正是 attach/mount 分离的用武之地. 流程大致是: 旧节点 A 上先 unmount(从 Pod 目录卸载)再 detach(从节点 A 摘下卷), 然后在新节点 B 上重新 attach(把卷接到 B)再 mount(挂进新 Pod). 卷本身(provision 出来的那块)不用重造, 数据自然跟着走.

但注意: 这要求底层存储支持"被不同节点先后挂载"--块存储是 RWO(单节点), 所以同一时刻只能在一个节点, 漂移时串行迁移没问题; 而本地盘(绑死在某台机器)就没法这样漂移, 这会影响第 08 篇的有状态应用调度.

动手练习

  1. 找到 CSI 组件: 在集群里 kubectl get pods -n kube-system | grep csi, 找到 CSI 的 Controller(Deployment)和 Node(DaemonSet)组件, 对照本篇的两组件分工
  2. 观察三段式事件: 创建一个 PVC + Pod, 用 kubectl describe pvckubectl get events 观察 Provisioning → 绑定的事件, 把它和三段式对应起来
  3. 去云控制台验证: 在云环境里, 创建 PVC 后去云控制台看是不是真的多了一块云盘(Provision), 并查看它 attach 到了哪台节点
  4. 查驱动能力: 查看 StorageClass / CSI 驱动文档, 确认它是否支持快照在线扩容; 若支持, 创建一个 VolumeSnapshot 给卷拍个快照
  5. 观察 Pod 漂移时的卷迁移(进阶): 把一个挂了 RWO 卷的 Pod 删除并让它重新调度到另一个节点, 观察卷的 detach/attach 过程(看 events), 体会 Pod 漂移时存储如何跟随