Kubernetes 最早是用来运行无状态服务的。Pod 可以随时销毁、重建、漂移到别的节点,反正不丢数据。

后来用户开始在上面跑数据库、消息队列、需要持久化的文件系统。Kubernetes 不得不解决一个问题:怎么让有状态应用安全地跑在容器里?

答案在存储上。Kubernetes 的存储架构经历了三次大的变动。


第一章:In-tree 插件——一开始能用,后来不够用了

什么是 In-tree

Kubernetes 早期(v1.0 到 v1.7),所有存储驱动的源码都直接写在 K8s 核心仓库里。AWS EBS、GCE PD、Ceph RBD、NFS、iSCSI……每种存储后端对应 pkg/volume/ 下的一个子目录。

kubernetes/
└── pkg/
    └── volume/
        ├── aws_ebs/
        ├── gce_pd/
        ├── ceph/
        ├── nfs/
        ├── iscsi/
        └── ... (40+ 种存储插件)

用户在 Pod 或 PVC 里指定 volume 类型,Kubelet 就调用对应的插件完成挂载。

为什么一开始这么设计

因为快。

早期 K8s 生态还没成型,集中开发是最直接的方式。核心贡献者熟悉代码结构,合并 PR 也快,用户体验统一。

后来出了什么问题

1. 发布节奏被绑死

存储厂商修复一个 bug 或加一个功能,得等 K8s 的版本发布周期。K8s 每季度发一个大版本,厂商的排期完全被动。

2. 代码库膨胀

每个存储插件的依赖都会被编译进 kube-controller-managerkubelet。即使用户只装 AWS EBS,NFS、Ceph、GlusterFS 的依赖也一起打进去了。编译时间越来越长,二进制文件越来越大。

3. 质量把控困难

核心维护者不可能精通所有存储系统。Ceph 插件出了 bug,得等 Ceph 社区的专家来修,但那个专家不一定熟悉 K8s 的合入流程。

4. 存储厂商的开发体验差

你是一家存储公司的工程师,想让自己的产品支持 K8s。fork 了整个仓库,写了几千行 Go 代码,提了 PR——然后等了三个月,因为核心团队的代码审查排期太长。

存储生态的多样性和 K8s 核心代码的稳定性之间有冲突。社区得选一条路。


第二章:FlexVolume——折中的过渡方案

FlexVolume 怎么做

2017 年前后,Kubernetes 推出了 FlexVolume。思路是把存储驱动从核心代码中剥离出来,作为独立的可执行文件部署在节点上。

Kubelet 在预定义路径下找驱动,通过 JSON 格式的命令行调用通信:

/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver init
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver attach '{"kubernetes.io/pv/name":"pv-001",...}'
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver mount '{"kubernetes.io/pv/name":"pv-001",...}'

改进之处

  • 存储驱动不再需要修改 K8s 源码,独立开发、独立部署
  • 存储厂商可以自己发版本
  • 只在需要的节点上装对应驱动

遗留的问题

FlexVolume 解决了"出树"的问题,但本身还是个过渡方案:

问题 说明
必须部署到每个节点 驱动是节点上的可执行文件
生命周期绑定 Kubelet 驱动的更新和重启依赖 Kubelet
功能有限 最初只支持 attach/mount/detach/unmount,后来才补上 resize、snapshot
接口设计粗糙 基于 JSON + 命令行通信,没有类型安全和结构化错误处理
权限模型粗糙 驱动以 Kubelet 的权限运行,没有细粒度的 RBAC

"出树"方向是对的,但实现方式还不够好。


第三章:CSI——容器存储接口

CSI 是什么

CSI(Container Storage Interface)是一套行业标准,由 Kubernetes、Docker、Containerd、Mesos 等多个容器编排项目的社区共同制定。它定义了一套 gRPC API,存储供应商可以用同一套接口为任何容器平台提供驱动。
核心想法很简单:存储驱动不应该跑在编排系统的进程内,应该作为独立的标准服务跑。
CSI 的架构通常由两部分组成:K8s 官方提供的通用组件(Sidecar) 和 存储厂商实现的驱动(Driver)。

CSI 架构

在这里插入图片描述

Sidecar

CSI 把不同的存储操作拆分到独立的容器中,每个 Sidecar 负责与 K8s API 交互,通过 gRPC 调用存储厂商的CSI 驱动。

Sidecar 容器 职责 监听的事件
external-provisioner 创建/删除卷 PVC 创建事件
external-attacher 挂载/卸载卷到节点 VolumeAttachment 事件
external-resizer 在线扩容卷 PVC 扩容请求
external-snapshotter 卷快照 VolumeSnapshot 事件

好处:

  • 如果存储不支持快照,就不部署 snapshotter
  • sidecar 版本和驱动版本可以各自升级
  • 每个组件只做一件事
  • 同一个 sidecar 可以和任何 CSI 驱动配合

这些Sidecar组件大部分都是K8s社区维护的。
Github仓库链接:
https://github.com/kubernetes-csi。

CSI 的 gRPC 接口

厂商编写的驱动通常会实现三个gRPC服务:

Controller Service——Control Plane 侧:

  • CreateVolume / DeleteVolume
  • ControllerPublishVolume / ControllerUnpublishVolume
  • ValidateVolumeCapabilities
  • ControllerExpandVolume
  • CreateSnapshot / DeleteSnapshot
  • ListVolumes

Node Service——节点上:

  • NodeStageVolume——预挂载(格式化、文件系统准备)
  • NodePublishVolume——最终挂载到 Pod 目录
  • NodeUnpublishVolume——卸载
  • NodeUnstageVolume——清理预挂载状态
  • NodeExpandVolume——节点侧扩容
  • NodeGetVolumeStats——获取卷容量和使用量

Identity Service

  • GetPluginInfo
  • Probe

卷的生命周期

1. 用户创建 PVC
       ↓
2. external-provisioner 监听到 PVC 事件
       ↓
3. 调用 CSI Driver 的 CreateVolume → 存储后端创建卷
       ↓
4. K8s 调度器将 Pod 调度到某个节点
       ↓
5. external-attacher 调用 ControllerPublishVolume → 卷挂载到目标节点
       ↓
6. Kubelet 调用 NodeStageVolume → 格式化、准备文件系统
       ↓
7. Kubelet 调用 NodePublishVolume → 挂载到 Pod 目录
       ↓
8. Pod 运行,读写数据
       ↓
9. Pod 删除 → NodeUnpublishVolume
       ↓
10. 节点不再需要卷 → ControllerUnpublishVolume
       ↓
11. PVC 删除 → DeleteVolume

这个顺序非常重要,客户端发起PVC创建请求后,调度器先将要运行Pod的节点选择出来,然后厂商驱动调用后端存储API创建卷并将卷挂到目标节点,这些动作由“Controller Service”完成。之后Pod才能真正起来,随后运行在节点上的“Node Service”完成卷的初始化并将可以使用的卷挂载到Pod的指定目录中。

这样K8s核心代码不需要针对每个存储系统写逻辑。

CSI 控制器部署示例

在实际部署中,存储厂商实现的 “Controller Service” 是和 K8s 官方的多个 Sidecar 容器运行在同一个 Pod 中。

这个 Pod 通常被称为 “CSI Controller Pod”(或 CSI Plugin Controller),一般以 Deployment 的形式部署,在集群中运行 1 个或多个副本(通过 Leader Election 实现高可用)。

下面是一个示例:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ebs-csi-controller
  namespace: kube-system
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ebs-csi-controller
  template:
    spec:
      serviceAccountName: ebs-csi-controller-sa
      containers:
        - name: csi-provisioner
          image: registry.k8s.io/sig-storage/csi-provisioner:v4.0.0
          args:
            - --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock
            - --feature-gates=Topology=true

        - name: csi-attacher
          image: registry.k8s.io/sig-storage/csi-attacher:v4.5.0
          args:
            - --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock

        - name: csi-resizer
          image: registry.k8s.io/sig-storage/csi-resizer:v1.10.0
          args:
            - --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock

        - name: csi-snapshotter
          image: registry.k8s.io/sig-storage/csi-snapshotter:v7.0.0
          args:
            - --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock

        - name: ebs-plugin
          image: amazon/aws-ebs-csi-driver:v1.28.0
          args:
            - --endpoint=$(CSI_ENDPOINT)
          env:
            - name: CSI_ENDPOINT
              value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/

      volumes:
        - name: socket-dir
          emptyDir: {}

为什么它们要挤在同一个Pod中?

这是一种非常经典的 Sidecar 设计模式。其核心逻辑是:“分工合作,本地通信”。

官方 Sidecar 负责“听”:external-provisioner、external-attacher 等容器各自各司其职,专门负责监听 K8s 集群里的 API 对象(如 PVC、VolumeAttachment)的变化。

厂商 Driver 负责“干”:存储厂商的 Controller Service 容器(比如 aws-ebs-csi-driver)只负责接收具体的指令并操作底层存储。

通过 localhost (Unix Socket) 通信:

  • 当 Sidecar 监听到需要创建卷时,它会通过 gRPC 协议调用厂商的 Controller Service。
  • 因为它们在同一个 Pod 内,它们可以通过共享的 Unix Domain Socket(通常挂载一个特殊的 emptyDir 卷)进行极高效率的本地通信,而不需要走复杂的网络。

第四章:三者对比

维度 In-tree FlexVolume CSI
代码位置 K8s 核心仓库 节点上的可执行文件 独立容器/进程
通信方式 函数调用 JSON 命令行 gRPC
部署方式 内置 每个节点装可执行文件 DaemonSet + Deployment
开发门槛 高(需理解 K8s 内部) 中(写脚本处理 JSON) 低(实现标准 gRPC)
高级功能 需 K8s 版本支持 不完整 完整(快照/扩容/拓扑)
权限模型 共享 K8s 内部权限 以 Kubelet 权限运行 独立 RBAC
跨平台 仅 Kubernetes 部分支持 全容器生态通用
生命周期 随 K8s 发布 绑定 Kubelet 完全独立
社区状态 已废弃 已废弃 当前标准

第五章:迁移过程

CSI 再好,也不能一夜之间替换掉所有 In-tree 插件。社区采取了渐进式迁移策略。

迁移时间线

  • 2018(v1.9):CSI Alpha
  • 2019(v1.13):CSI Beta,开始支持 In-tree 到 CSI 的迁移
  • 2020(v1.17):CSI GA,AWS EBS、GCE PD 等核心插件完成迁移
  • 2021(v1.21):大部分 In-tree 插件标记为 Deprecated
  • 2022(v1.26):In-tree 存储插件正式移除

迁移机制:CSI Migration Feature Gate

K8s 引入了 CSIMigration 特性门控,用户不需要修改 YAML,In-tree 插件的请求会自动转发到对应的 CSI 驱动:

# PVC 依然用 in-tree 的 provisioner 标识
kind: StorageClass
metadata:
  name: my-sc
provisioner: kubernetes.io/aws-ebs   # in-tree 标识符
# 内部:CSIMigration 自动转发到 ebs.csi.aws.com

平台团队部署好 CSI 驱动,打开特性门控,业务方的 YAML 不用改。


第六章:常用 CSI 驱动

主流驱动

驱动 存储后端 成熟度 备注
aws-ebs-csi-driver AWS EBS GA EKS 官方推荐
gcp-compute-persistent-disk-csi-driver GCE PD GA GKE 官方推荐
azuredisk-csi-driver Azure Disk GA AKS 官方推荐
ceph-csi Ceph RBD/CephFS GA 开源存储
nfs-subdir-external-provisioner NFS 社区 开发测试常用
longhorn Longhorn GA 轻量级分布式块存储
rook-ceph Ceph (via Rook) GA 全栈 Ceph on K8s
portworx Portworx GA 企业级方案
juicefs-csi-driver JuiceFS 社区 高性能 POSIX 文件系统

常见问题

provisioner 写错了

kubectl get storageclass -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner

provisioner 名称和 CSI 驱动注册的不匹配,PVC 会卡在 Pending

CSI Node 服务没有在目标节点运行

CSI Node Service 通常以 DaemonSet 部署。如果某个节点没有对应 Pod,该节点上的 Pod 挂载卷会失败:

kubectl get pods -n kube-system -l app=csi-node -o wide

权限不足

kubectl get clusterrolebinding | grep csi
kubectl logs -n kube-system -l app=ebs-csi-controller -c ebs-plugin --tail=50

卷容量不匹配

某些存储后端不支持精确容量请求。请求 50Gi,后端可能创建 50.5Gi。CSI 规格允许实际容量略大于请求值。

排错清单

# PVC 状态
kubectl describe pvc <name>

# PV 事件
kubectl describe pv <name>

# CSI 驱动日志
kubectl logs -n kube-system -l app=<csi-driver> -c <container>

# Kubelet 日志(节点上)
journalctl -u kubelet | grep -i csi

# 查看 CSI 驱动是否注册
kubectl get csidrivers.storage.k8s.io

第七章:对平台团队的影响

存储插件的架构变动也是平台工程治理的问题。

  1. 多租户隔离:CSI 的独立 RBAC 让不同租户可以用不同存储驱动,互不干扰。In-tree 时代所有插件共享 Kubelet 的权限。

  2. 升级灵活:CSI 驱动独立于 K8s 版本。升级 EBS CSI 驱动不需要升级整个集群。

  3. 可观测性:CSI 驱动作为独立进程,可以暴露自己的 Prometheus metrics,独立做日志采集。In-tree 的监控数据混在 Kubelet 日志里。

  4. 供应商锁定降低:CSI 是跨平台标准。如果从 K8s 迁移到其他编排平台,CSI 驱动可以复用。

  5. 开发者体验:平台团队封装 StorageClass,业务团队只要声明需要多少存储,不需要管底层是 EBS、Ceph 还是 NFS。


回顾

Kubernetes 存储插件走了这么一条路:

  • In-tree:所有驱动写进核心仓库,快但臃肿
  • FlexVolume:驱动拆到节点上,解决了独立开发但实现方式粗糙
  • CSI:定义标准 gRPC 接口,驱动作为独立服务运行

In-tree 时期,K8s 核心团队试图覆盖所有存储场景。FlexVolume 先跑起来再说。CSI 则划清了边界,让生态在标准接口上各自发展。

现在用 kubectl create pvc 的时候,背后可能是 AWS、Ceph、Longhorn 或任何存储厂商的 CSI 驱动在跑。不同团队开发,同一套接口。


参考资料

  • CSI Specification:
    https://github.com/container-storage-interface/spec
  • Kubernetes CSI Documentation: https://kubernetes.io/docs/concepts/storage/volumes/#csi
  • CSI Migration Design Doc: https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1495-csi-migration
Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐