欢迎光临

Kubernetes CSI(Container Storage Interface)深度解析:从架构原理到自建存储驱动的完整实战指南

引言:Kubernetes 存储的演进之路

在 Kubernetes 的早期版本中,存储插件的集成方式非常原始——所有云厂商的存储驱动代码都硬编码在 Kubernetes 主仓库中,以一个庞大的 “in-tree” 插件列表存在。随着 AWS、GCP、Azure、vSphere 等云厂商的增多,这种模式带来了严重的维护灾难:每个存储驱动的 Bug 修复都需要等待 Kubernetes 发版,第三方存储厂商如果想接入 K8s,就必须向 Kubernetes 主仓库提交 PR,经过漫长的审查周期才能被合并。

这种紧耦合架构显然无法适应云原生生态的爆炸式增长。于是,Container Storage Interface(CSI) 作为行业标准应运而生。CSI 是一个统一的存储接口规范,让存储厂商可以编写完全独立于 Kubernetes 的驱动,以 DaemonSet / Deployment 的形式部署在集群中,Kubernetes 只需要通过 gRPC 调用即可与这些驱动交互。自 Kubernetes v1.13 起 CSI 进入 GA(正式可用)阶段,v1.23 起移除了所有 in-tree 存储驱动的代码路径,CSI 成为 K8s 中唯一推荐的存储扩展方式。

本文将深入剖析 CSI 的架构设计、核心组件、工作流程,并手把手教你如何部署 CSI 驱动、排查常见问题,最后用一个极简的自定义 CSI 驱动示例,帮你彻底理解这个”存储界的 OCI 标准”。

Kubernetes CSI 架构示意图

CSI 架构全景:三个核心组件

CSI 定义了一套标准的 gRPC 接口,整个实现被拆分为三个独立的 Sidecar 容器(通常运行在同一个 Pod 中),职责清晰分离:

组件 运行位置 核心职责 实现的 CSI 接口
CSI Controller Controller(Deployment) 卷的创建/删除/快照/克隆等控制面操作
1
CreateVolume

,

1
DeleteVolume

,

1
ControllerPublishVolume

,

1
CreateSnapshot
CSI Node Node(DaemonSet) 卷的挂载/卸载/格式化、设备发现
1
NodePublishVolume

,

1
NodeUnpublishVolume

,

1
NodeGetInfo
CSI Identity Controller + Node 均有 插件能力声明、探针健康检查
1
GetPluginInfo

,

1
Probe

CSI Controller(控制面组件)

CSI Controller 以 Deployment 的形式在集群中运行,副本数通常为 1 或者 2(主备模式)。它负责处理所有”不依赖于具体节点”的存储操作:创建卷、删除卷、创建快照、扩容卷大小等。Kubernetes 中的

1
PersistentVolumeClaim

触发卷创建时,由

1
external-provisioner

这个 Sidecar 容器监听到 PVC 事件,然后通过 gRPC 调用 CSI Controller 的

1
CreateVolume

接口,最终在底层存储系统(比如 Ceph RBD、NFS 服务器、云厂商的 EBS 卷)上真正分配空间。

关键接口解析(gRPC proto 定义):


1
2
3
4
5
6
7
8
9
10
11
12
service Controller {
  rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
  rpc DeleteVolume(DeleteVolumeRequest) returns (DeleteVolumeResponse);
  rpc ControllerPublishVolume(ControllerPublishVolumeRequest)
      returns (ControllerPublishVolumeResponse);
  rpc ControllerUnpublishVolume(ControllerUnpublishVolumeRequest)
      returns (ControllerUnpublishVolumeResponse);
  rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
  rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse);
  rpc ControllerExpandVolume(ControllerExpandVolumeRequest)
      returns (ControllerExpandVolumeResponse);
}

CSI Node(节点面组件)

CSI Node 以 DaemonSet 的形式在每个工作节点上运行。它负责实际的数据面操作:将卷格式化、挂载到 Pod 可以访问的路径。

当 Kubernetes 调度器决定将某个 Pod 调度到 Node A 上,并且该 Pod 声明了一个 CSI 卷时,

1
external-attacher

Sidecar 会先调用 Controller 的

1
ControllerPublishVolume

(对于块存储而言,相当于将云硬盘挂载到宿主机),然后

1
node-driver-registrar

会通知 kubelet 调用 CSI Node 的

1
NodePublishVolume

,完成最终的 mount 操作。


1
2
3
4
5
6
7
8
9
10
11
service Node {
  rpc NodePublishVolume(NodePublishVolumeRequest)
      returns (NodePublishVolumeResponse);
  rpc NodeUnpublishVolume(NodeUnpublishVolumeRequest)
      returns (NodeUnpublishVolumeResponse);
  rpc NodeGetVolumeStats(NodeGetVolumeStatsRequest)
      returns (NodeGetVolumeStatsResponse);
  rpc NodeExpandVolume(NodeExpandVolumeRequest)
      returns (NodeExpandVolumeResponse);
  rpc NodeGetInfo(NodeGetInfoRequest) returns (NodeGetInfoResponse);
}

Kubernetes CSI Sidecar 容器

CSI 驱动本身通常只实现了上述三个 gRPC 服务。为了将 CSI 驱动与 Kubernetes 的资源体系对接,社区提供了几个标准的 Sidecar 容器,与 CSI 驱动容器部署在同一个 Pod 中:

  • external-provisioner:监听 PVC 对象,调用 CSI Controller 的
    1
    CreateVolume

    /

    1
    DeleteVolume

    来动态创建和删除卷,并自动创建对应的 PV 对象。

  • external-attacher:监听
    1
    VolumeAttachment

    对象,调用 CSI Controller 的

    1
    ControllerPublishVolume

    /

    1
    ControllerUnpublishVolume

    完成卷与节点的绑定/解绑。

  • external-resizer:监听 PVC 大小变更请求,调用 CSI Controller 的
    1
    ControllerExpandVolume

    实现在线扩容。

  • external-snapshotter:监听
    1
    VolumeSnapshot

    /

    1
    VolumeSnapshotContent

    对象,调用 CSI Controller 的

    1
    CreateSnapshot

    /

    1
    DeleteSnapshot

  • node-driver-registrar:在节点上向 kubelet 注册 CSI 驱动的 Unix Socket 路径,并上报驱动信息。
  • livenessprobe:定期对 CSI 驱动执行
    1
    Probe()

    gRPC 调用,确保驱动健康,配置 Pod 的 liveness probe。

CSI 卷的完整生命周期

理解了组件之后,让我们追踪一个 PVC 从创建到挂载到 Pod 使用的完整流程:

  1. 用户创建 PVC:用户提交
    1
    PersistentVolumeClaim

    YAML,指定

    1
    storageClassName

    指向某个 CSI 驱动的 StorageClass。

  2. external-provisioner 感知:Sidecar 监听到 PVC 事件,读取其 StorageClass 中的
    1
    provisioner

    字段(如

    1
    rbd.csi.ceph.com

    ),确认由哪个 CSI 驱动处理。

  3. 调用 CreateVolume:external-provisioner 通过 Unix Socket 调用 CSI Controller 的
    1
    CreateVolume

    ,传入容量、参数(如

    1
    pool

    1
    fsType

    )等。

  4. 创建 PV:底层存储卷创建成功后,external-provisioner 自动创建一个
    1
    PersistentVolume

    对象与 PVC 绑定。

  5. Pod 调度 & 创建 VolumeAttachment:当使用了该 PVC 的 Pod 被调度到某个节点上时,K8s 创建
    1
    VolumeAttachment

    对象,表明该卷需要被挂载到特定节点。

  6. external-attacher 调用 ControllerPublishVolume:Attacher Sidecar 调用 CSI Controller,将底层卷挂载到目标节点(对块存储而言,相当于将 EBS 卷 attach 到 EC2 实例)。
  7. kubelet 调用 NodePublishVolume:节点上的 kubelet(通过 Unix Socket)调用 CSI Node 的
    1
    NodePublishVolume

    ,完成格式化(如果需要)和最终的 mount,将卷暴露到宿主机的 staging 路径。

  8. CSI Node 执行 mount:驱动内部调用
    1
    mount

    1
    mkfs

    等系统命令,完成后 kubelet 将卷绑定到 Pod 的容器文件系统中。

至此,Pod 中的应用就可以通过挂载路径正常读写持久化数据了。整个过程对用户完全透明——用户只看到了 PVC 和 Pod,中间所有的 orchestration 由 Sidecar 们自动完成。

实战部署:用 Ceph CSI(RBD)搭建持久化存储

理论讲再多也不如动手部署一次。下面我们以 Ceph RBD 为例,完整演示如何部署 CSI 驱动并使用动态供给创建 PV。

前提条件

你需要一个运行中的 Ceph 集群(至少一个 mon 和一个 OSD),以及

1
ceph-csi

的 Helm Chart:


1
2
3
4
5
6
7
# 添加 Helm 仓库
helm repo add ceph-csi https://ceph.github.io/csi-charts
helm repo update

# 获取 Ceph 集群的 mon 地址和 keyring
ceph mon dump | grep "mon." | awk '{print $1}' | cut -d' ' -f1
ceph auth get-key client.admin | base64

创建一个 Ceph Secret,用于 CSI 驱动访问 Ceph 集群:


1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: ceph-csi-secret
  namespace: ceph-csi
stringData:
  userID: admin
  userKey: QVFDVnd...base64-encoded-key=

部署 CSI 驱动


1
2
3
4
5
6
7
helm install ceph-csi-rbd ceph-csi/ceph-csi-rbd \
  --namespace ceph-csi \
  --create-namespace \
  --set csiConfig[0].clusterID=ceph-cluster-1 \
  --set csiConfig[0].monitors="192.168.1.10:6789,192.168.1.11:6789" \
  --set secret.create=false \
  --set secret.name=ceph-csi-secret

验证 CSI 驱动 Pod 是否正常运行:


1
2
3
4
5
kubectl -n ceph-csi get pods
NAME                                        READY   STATUS    RESTARTS
csi-rbdplugin-controller-749f6f547-pq8cr   5/5     Running   0
csi-rbdplugin-8rt6d                         3/3     Running   0
csi-rbdplugin-hq45x                         3/3     Running   0

这里 Controller Pod 有 5 个容器(CSI 驱动 + 4 个 Sidecar),Node Pod 有 3 个容器(CSI 驱动 + node-driver-registrar + livenessprobe)。

创建 StorageClass


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-rbd-sc
provisioner: rbd.csi.ceph.com
parameters:
  clusterID: ceph-cluster-1
  pool: kubernetes
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: ceph-csi-secret
  csi.storage.k8s.io/controller-expand-secret-name: ceph-csi-secret
  csi.storage.k8s.io/node-stage-secret-name: ceph-csi-secret
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate

测试动态供给


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: csi-rbd-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: csi-rbd-sc
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: csi-rbd-test
spec:
  containers:
  - name: app
    image: busybox
    command: ["sleep", "3600"]
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: csi-rbd-pvc

创建后验证:


1
2
3
4
5
6
7
kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   STATUS   CLAIM
pvc-8a3b2c1d-...                          10Gi       RWO            Bound    default/csi-rbd-pvc

kubectl exec csi-rbd-test -- df -h /data
Filesystem      Size  Used Avail Use% Mounted on
/dev/rbd0       9.8G   44M  9.8G   1% /data

CSI 驱动开发速览:构建一个极简的 Demo 驱动

为了更好地理解 CSI 的内部机制,我们来动手写一个最简单的 CSI 驱动——”Memory CSI Driver”,它不真正管理存储,只是在内存中模拟卷的创建和挂载,但完整实现了 Identity、Controller 和 Node 三个服务。

项目结构


1
2
3
4
5
6
7
8
9
10
memory-csi/
├── cmd/
│   └── main.go          # 入口,启动 gRPC Server
├── pkg/
│   ├── identity.go      # Identity 服务
│   ├── controller.go    # Controller 服务
│   └── node.go          # Node 服务
├── deploy/
│   └── csi-memory.yaml  # Kubernetes 部署清单
└── go.mod / go.sum

核心代码示例:Controller 的 CreateVolume


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// pkg/controller.go
func (cs *controllerServer) CreateVolume(
    ctx context.Context,
    req *csi.CreateVolumeRequest,
) (*csi.CreateVolumeResponse, error) {
    name := req.GetName()
    caps := req.GetVolumeCapabilities()
    reqBytes := req.GetCapacityRange().GetRequiredBytes()

    log.Printf("CreateVolume called: name=%s, size=%d bytes", name, reqBytes)

    // 在实际驱动中,这里会调用云 API 或存储后端
    // 在 Demo 中,我们只是在内存中记录一下
    volume := &csi.Volume{
        VolumeId:      uuid.New().String(),
        CapacityBytes: reqBytes,
        VolumeContext: map[string]string{
            "driver": "memory-csi",
            "name":   name,
        },
    }

    return &csi.CreateVolumeResponse{Volume: volume}, nil
}

核心代码示例:Node 的 NodePublishVolume


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pkg/node.go
func (ns *nodeServer) NodePublishVolume(
    ctx context.Context,
    req *csi.NodePublishVolumeRequest,
) (*csi.NodePublishVolumeResponse, error) {
    targetPath := req.GetTargetPath()
    volumeID := req.GetVolumeId()

    log.Printf("NodePublishVolume: volume=%s, target=%s", volumeID, targetPath)

    // 保证目标目录存在
    if err := os.MkdirAll(targetPath, 0755); err != nil {
        return nil, status.Errorf(codes.Internal,
            "mkdir %s failed: %v", targetPath, err)
    }

    // 挂载 tmpfs(实际生产驱动这里会 mount 真实的存储设备)
    if err := syscall.Mount("tmpfs", targetPath, "tmpfs", 0, ""); err != nil {
        return nil, status.Errorf(codes.Internal,
            "mount tmpfs at %s failed: %v", targetPath, err)
    }

    log.Printf("Successfully mounted volume %s at %s", volumeID, targetPath)
    return &csi.NodePublishVolumeResponse{}, nil
}

完整的 Demo 驱动需要处理

1
NodeGetCapabilities

1
ControllerGetCapabilities

等能力声明方法,以及错误处理和资源回收逻辑。你可以在 csi-test 项目中找到更多的开发参考和测试框架。

常见问题与排障指南

1. PVC 一直处于 Pending 状态

这是 CSI 使用中最常见的问题。排查步骤:


1
2
3
4
5
6
7
8
# 查看 PVC 事件
kubectl describe pvc my-pvc

# 检查 CSI Controller Pod 日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin-controller -c csi-provisioner

# 检查 CSI 驱动自身的 Controller 日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin-controller -c csi-rbdplugin

常见原因包括:

  • StorageClass 中的
    1
    provisioner

    名称写错,与 CSI 驱动的注册名称不匹配

  • CSI 驱动的 Secret 配置错误,无法通过存储后端的认证
  • Ceph pool 不存在或已满(针对 Ceph 场景)
  • CSI Controller 未正确启动或被 OOMKill

2. Pod 创建失败:Failed to mount volume

挂载阶段的错误通常定位到 Node 侧的日志:


1
2
3
4
5
6
7
8
9
10
# 查看 kubelet 日志(需要 SSH 到工作节点)
journalctl -u kubelet -n 50 | grep -i csi

# 查看 CSI Node 插件日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin -c csi-rbdplugin

# 检查节点是否有需要的工具(如 mkfs.ext4, mount, rbd)
which mount
which mkfs.ext4
which rbd

一个容易被忽略的点:CSI Node 驱动的容器镜像中可能不包含

1
mkfs.ext4

1
xfs_repair

等文件系统工具。你需要在 DaemonSet 中挂载宿主机的

1
/sbin/mkfs.*

或者在驱动镜像中预装这些工具。

3. 在线扩容不生效

要支持在线扩容,需要同时满足三个条件:

  • StorageClass 中设置了
    1
    allowVolumeExpansion: true
  • CSI 驱动实现了
    1
    ControllerExpandVolume

    1
    NodeExpandVolume

    接口

  • 部署了
    1
    external-resizer

    Sidecar


1
2
3
# 检查 CSI 驱动是否声明了 EXPAND_VOLUME 能力
kubectl -n ceph-csi exec -it deploy/csi-rbdplugin-controller \
  -c csi-rbdplugin -- csc --endpoint /csi/csi.sock controller-get-capabilities

CSI 最佳实践与性能调优

资源限制与调优

CSI 驱动的资源规划直接影响存储操作的并发性能和稳定性:

组件 推荐 CPU 限制 推荐内存限制 关键调优参数
CSI Controller 200m – 500m 256Mi – 512Mi
1
--worker-threads

(默认 10)

CSI Node 100m – 300m 128Mi – 256Mi 影响 Pod 启动速度
external-provisioner 100m – 200m 128Mi
1
--retry-interval-start

重试间隔

external-attacher 100m – 200m 128Mi
1
--worker-threads

并行数

安全加固建议

  • 最小权限原则:CSI 驱动的 ServiceAccount 只需要对 PV、PVC、VolumeAttachment 等资源的必要操作权限,不要使用
    1
    cluster-admin

  • Secret 隔离:存储后端凭据必须单独放在 CSI 驱动所在的 Namespace 中,不要混入业务 Namespace。
  • gRPC Socket 权限:CSI 驱动的 Unix Socket 文件应设置
    1
    0600

    权限,确保只有 kubelet 和同 Pod 的 Sidecar 可以访问。

  • 加密传输:对于敏感数据场景,建议启用存储后端(如 Ceph 的
    1
    rbd

    加密或云厂商的 EBS 加密)的静态数据加密功能。

总结

CSI 的出现彻底改变了 Kubernetes 存储生态的面貌。它将存储驱动的开发从 Kubernetes 主仓库中解耦出来,让每一个存储厂商都可以独立迭代自己的驱动版本。作为 K8s 用户和运维人员,理解 CSI 的架构和工作流程至关重要——当你遇到 PVC 挂载失败、扩容异常或性能瓶颈时,知道应该查看 Controller 还是 Node 的日志、理解每个 Sidecar 的角色,能帮你将排障时间从小时级缩短到分钟级。

如果你正在考虑为自建存储系统接入 Kubernetes,推荐从社区已有的 CSI 驱动开始(Ceph RBD/NFS、Longhorn、Rook、OpenEBS 等都有成熟的实现),只有在特殊需求确实无法被现有驱动满足时,才需要自行开发 CSI 驱动。CSI Spec 目前版本为 v1.9+,接口已经非常稳定,官方提供了完整的 csi-test 测试套件来保证实现的合规性。

最后,强烈建议在生产环境中启用 CSI 的 Topology-Aware Provisioning(拓扑感知调度),让你的 PV 创建在离 Pod 最近的可用区,减少跨 AZ 的网络延迟和流量费用。通过配置 StorageClass 的

1
volumeBindingMode: WaitForFirstConsumer

,可以让卷的创建延迟到 Pod 被调度之后再做,确保存储位置与计算位置一致。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Kubernetes CSI(Container Storage Interface)深度解析:从架构原理到自建存储驱动的完整实战指南
分享到: 更多 (0)