Li Sheng | Backend / Distributed Storage Engineer Li Sheng | Backend / Distributed Storage Engineer
Home
Resume
Projects
Topics
Notes
GitHub (opens new window)
Home
Resume
Projects
Topics
Notes
GitHub (opens new window)
  • Go语言

  • C++

  • 算法题

  • 存储系统

  • CephFS

  • 分布式系统

  • 计算机网络

  • Redis与缓存

  • Kubernetes

    • 存储

      • 待完成专题池
      • 为什么 Kubernetes 要把存储抽象拆成 PV、PVC、StorageClass 与 CSI
      • 一个 PVC 从创建到 Pod 成功挂载,中间经过了哪些控制链路
      • 为什么 PVC 已经 Bound,Pod 仍然可能挂载失败
      • 本地盘、网络盘与拓扑约束,是如何共同影响 Kubernetes 调度的
      • Kubernetes 存储故障为什么常表现为应用启动超时
      • 为什么 Kubernetes CSI 插件架构要拆成 Controller、Node 与 Sidecar
  • 技术笔记
  • Kubernetes
  • 存储
lisheng
2026-04-28

为什么 Kubernetes CSI 插件架构要拆成 Controller、Node 与 Sidecar

# 为什么 Kubernetes CSI 插件架构要拆成 Controller、Node 与 Sidecar

这篇文章想回答的不是“CSI 有哪些组件”,而是一个更关键的架构问题:为什么 Kubernetes 接入存储时,没有把逻辑做成一个能统一处理创建、附加、挂载和扩容的单体插件,而是拆成 Controller、Node 和一组围绕在旁边的 sidecar。

如果只从“把一块盘挂给 Pod”这个直觉目标出发,单体插件看起来当然更简单。平台把请求交给插件,插件自己完成创建卷、把卷接到目标节点、在节点上格式化并挂给容器,似乎一步到位。但 Kubernetes 真正面对的是一条跨控制平面和节点平面的长链路。动态供给发生在控制平面,卷附加通常也依赖控制平面与后端存储系统交互,而最终把卷变成容器可见挂载点的动作,却只能在目标节点本地由 kubelet 协调完成。只要这些动作天然分布在不同机器、不同权限边界和不同时序里,单体插件就不再只是“不优雅”,而是很难准确表达谁应该在什么时候负责什么。

先看最硬的一条边界:控制面能决定“集群应当拥有一块什么样的卷”,但它并不能替节点完成本地挂载;节点能看到设备、执行 mount、准备目录和文件系统,但它又不适合承担整个集群范围的卷供给和生命周期管理。也就是说,存储链路里天然存在两类完全不同的执行现场。Controller 侧面对的是集群级资源状态,它关心的是 CreateVolume、DeleteVolume、ControllerPublish、扩容协商这类“让后端世界发生变化”的动作;Node 侧面对的是节点本地状态,它关心的是设备是否可见、是否需要格式化、全局路径是否准备好、最终能否把卷发布到 Pod 目录里。Kubernetes 把 CSI 拆成这两面,本质上是在承认“同一块卷的生命周期,本来就跨越两个权限域”。

这也是为什么很多人第一次看 CSI 时会误解为“插件被人为拆碎了”。其实真正被拆开的,不是一个原本简单的程序,而是两条本就不同步的状态推进。控制平面先决定卷是否存在、是否附加、是否允许扩容;节点平面随后才有条件把这块卷变成某个 Pod 真正可用的文件系统。PVC 已经 Bound 不代表 NodePublish 可以立刻成功,卷已经附加到节点也不代表 kubelet 马上能把它交付给容器。Controller 和 Node 分离之后,每一侧只需要对自己那段收敛负责,系统也才能接受“控制面状态已推进,但节点面还在等待”的现实。

光有这两类 gRPC 能力仍然不够,因为 Kubernetes 并没有把所有存储控制逻辑直接写进 kube-controller-manager 和 kubelet。这里就轮到 sidecar 出场了。它们看起来像是围着 driver 额外长出的一圈进程,实际上承担的是“把 Kubernetes 对象世界翻译成 CSI 调用”的适配责任。external-provisioner 盯住的是 PVC 与 StorageClass 的供给事件,把“需要新卷”翻译成 CreateVolume;external-attacher 盯住的是附加状态,把“某卷应当接到某节点”翻译成控制器侧发布调用;external-resizer 关心的是扩容请求何时需要进入后端;节点侧的 node-driver-registrar 则负责让 kubelet 知道本机有哪些 CSI driver 可以被调用。换句话说,很多 sidecar 并不是在“扩展存储能力”,而是在“扩展 Kubernetes 对不同 CSI 能力的控制回路”。

如果没有这些 sidecar,最直接的替代方案只有两个。要么把所有监听 PVC、VolumeAttachment、扩容请求和驱动注册的逻辑重新塞回 Kubernetes 核心;要么要求每家存储厂商自己重复实现一整套对 Kubernetes API 的监听、选主、重试和状态回写机制。这两条路都很重。前者会让 Kubernetes 核心再次和各类存储行为深度耦合,后者则会让每个 driver 不只实现存储能力,还得重写一遍控制器生态。sidecar 的真正价值就在于把“通用的控制循环骨架”从“厂商特有的存储实现细节”里剥离出来。Kubernetes 社区维护控制循环适配器,存储厂商只需要把精力放在 CSI Controller 和 CSI Node 能力实现上。

从部署形态上看,这种分工也正好对应不同的运行位置。控制器相关 sidecar 往往和 CSI Controller driver 一起运行在 Deployment 或 StatefulSet 中,因为它们处理的是集群范围事件,不需要贴着每台节点常驻。节点相关 driver 则通常以 DaemonSet 形式部署,因为只有这样,kubelet 才能在 Pod 真正落到某台机器上时,就地调用该节点上的 CSI Node 服务。于是整套架构形成了一个很明确的执行链:控制器 sidecar 监听 Kubernetes 对象并驱动集群级 CSI 调用,后端卷状态收敛后,kubelet 再在目标节点上调用本地 CSI Node,把“集群里存在一块卷”继续推进成“容器里出现一个挂载点”。

这套拆分还顺手解决了一个很容易被忽略的问题:不同卷能力并不是强制同时存在的。某些后端只需要节点挂载,不需要真正的控制器附加;某些场景支持在线扩容,但不支持快照;有些本地盘方案几乎全部逻辑都落在节点侧。CSI 把能力切分成一组可声明的接口,再让不同 sidecar 只围绕自己关心的能力工作,结果就是 Kubernetes 不必假设“所有存储插件都长得一样”。架构被拆开以后,异构性反而更容易被容纳,因为系统协商的是能力边界,而不是某个固定的单体实现模板。

当然,代价也很明显。一次看似简单的挂载,背后可能跨越 PVC 事件、sidecar 日志、CSI Controller 响应、VolumeAttachment 状态、kubelet 卷管理和 CSI Node 调用。排障时经常让人感觉状态散落在很多地方,不像单机程序那样有一条完整调用栈。但这种“分散”不是架构多此一举造成的,而是集群存储本来就在分布式控制、节点本地执行和后端设备系统之间来回穿梭。CSI 只是把这些真实边界显式暴露出来了。

所以,Kubernetes CSI 插件架构之所以要拆成 Controller、Node 与 sidecar,核心原因不是组件化本身,而是它必须同时处理三类不同职责:谁负责监听 Kubernetes 中的声明变化,谁负责把这些变化变成后端存储动作,谁负责在目标节点上把卷交付给具体 Pod。只要这三类职责仍然分属不同的时机、位置和权限域,CSI 就不可能优雅地收缩成一个“万能插件”。你看到的是三个名字,系统真正换来的是三种边界被清楚地区分开:控制循环边界、存储能力边界,以及节点交付边界。

Edit (opens new window)
Last Updated: 2026/04/29, 13:43:32
Kubernetes 存储故障为什么常表现为应用启动超时

← Kubernetes 存储故障为什么常表现为应用启动超时

最近更新
01
待完成专题池
04-28
02
待完成专题池
04-28
03
Kubernetes 存储故障为什么常表现为应用启动超时
04-28
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式