Kubernetes 存储利器:CSI 插件机制解析 – wiki基地


Kubernetes 存储利器:CSI 插件机制深度解析

引言:容器化时代的存储挑战

在以 Kubernetes 为代表的容器编排技术席卷全球的今天,应用部署和管理的模式发生了翻天覆地的变化。容器提供了轻量级、可移植、自包含的运行环境,极大地提高了应用交付的效率和一致性。然而,当我们将目光投向有状态应用(如数据库、消息队列、缓存系统等)时,一个核心挑战浮出水面:持久化存储

容器本身是短暂的(Ephemeral),其生命周期可能随时结束并被新的实例替换。但应用程序的数据却需要持久存在,并且能够在容器漂移、节点故障等场景下被稳定地访问。Kubernetes 通过 Volume 抽象和 PersistentVolume (PV) / PersistentVolumeClaim (PVC) 机制,为容器提供了使用持久化存储的接口。但在早期,管理和集成各种存储解决方案的方式存在诸多弊端。

最初,Kubernetes 采用的是 “In-Tree”(树内) 的卷插件模式。这意味着所有存储提供商的驱动逻辑代码都需要直接集成到 Kubernetes 的核心代码库(kubernetes/kubernetes)中。这种模式随着支持的存储类型增多,暴露出了一系列严重的问题,促使社区寻求一种更现代化、更解耦的解决方案——这便是我们今天要深入探讨的主角:容器存储接口(Container Storage Interface, CSI)

本文将详细解析 Kubernetes CSI 插件机制,从其诞生的背景、核心设计理念、架构组成、工作流程,到其带来的优势和实际应用,力求为您呈现一幅清晰而全面的 CSI 图景。

一、告别 In-Tree 之痛:CSI 的诞生背景

在 CSI 出现之前,Kubernetes 的 In-Tree 卷插件模式主要存在以下痛点:

  1. 开发与发布周期绑定: 存储插件的代码与 Kubernetes 核心代码紧密耦合。任何存储插件的 Bug 修复、功能增强或新存储系统的支持,都必须遵循 Kubernetes 的发布周期(通常几个月一次)。这对于需要快速迭代和响应市场变化的存储厂商来说,是难以接受的。
  2. 代码库臃肿与维护困难: 随着支持的存储类型越来越多,Kubernetes 核心代码库变得异常庞大,增加了编译、测试和维护的复杂性。核心开发者需要评审他们可能并不熟悉的特定存储系统的代码。
  3. 稳定性和安全风险: 任何一个 In-Tree 存储插件中的 Bug 都可能影响到 Kubernetes 核心组件(如 kubeletkube-controller-manager)的稳定性和安全性,潜在影响范围巨大。
  4. 供应商依赖与测试负担: Kubernetes 维护者需要为所有 In-Tree 插件设置和维护测试环境,这是一个沉重的负担。同时,存储供应商也需要依赖 Kubernetes 社区来合并和发布他们的驱动更新。
  5. 扩展性受限: 添加新的存储系统支持需要修改 Kubernetes 核心代码,流程复杂且门槛较高。

为了解决这些问题,社区迫切需要一种标准化的、与 Kubernetes 核心解耦的存储插件机制。借鉴了容器网络接口(CNI)和容器运行时接口(CRI)的成功经验,容器存储接口(Container Storage Interface, CSI) 应运而生。

CSI 是一套标准的 API 规范,旨在将存储系统的控制逻辑从核心容器编排系统(如 Kubernetes, Mesos, Cloud Foundry 等)中分离出来。它定义了容器编排系统如何与存储插件进行交互,以完成卷的生命周期管理(创建、删除、挂载、卸载、快照等)操作,而无需关心底层存储的具体实现细节。

二、CSI 核心设计理念与优势

CSI 的设计核心在于 标准化解耦

  • 标准化: CSI 定义了一套基于 gRPC 的接口规范。存储提供商只需要按照这套规范实现自己的 CSI 驱动程序,就能接入任何支持 CSI 标准的容器编排系统。
  • 解耦: CSI 驱动程序作为独立的应用运行在 Kubernetes 集群中(通常是 Pod),与 Kubernetes 核心组件彻底分离。

这种设计带来了显著的优势:

  1. 独立开发与发布: 存储供应商可以独立于 Kubernetes 发布周期来开发、测试、打包和发布他们的 CSI 驱动,大大加快了迭代速度和问题修复效率。
  2. 代码库清晰: Kubernetes 核心代码库不再包含特定存储的逻辑,变得更加精简和稳定。
  3. 提升稳定性与安全性: CSI 驱动的故障通常只会影响到使用该驱动的存储卷,而不会危及 Kubernetes 核心组件的运行。驱动可以独立进行安全加固和更新。
  4. 生态繁荣: 标准化的接口降低了存储厂商接入 Kubernetes 生态的门槛,促进了更多存储解决方案的支持,用户拥有更广泛的选择。
  5. 功能扩展性强: CSI 规范本身可以独立演进,添加新的存储功能(如卷克隆、卷扩容、拓扑感知、临时卷等),而无需修改 Kubernetes 核心代码。

三、CSI 架构深度解析:组件协同的艺术

理解 CSI 的工作原理,首先需要了解其架构中的关键组件以及它们之间的交互关系。一个典型的 Kubernetes CSI 部署包含以下几个部分:

  1. CSI Driver (存储驱动程序):

    • 这是由存储供应商开发和维护的核心组件,负责实现 CSI gRPC 接口规范。
    • 通常包含两个主要部分:
      • Controller Plugin (控制器插件): 一般部署为 DeploymentStatefulSet,通常只需要一个或少数几个实例。它负责处理无需在特定节点上执行的操作,如卷的创建(Provisioning)、删除(Deleting)、附加(Attaching)、分离(Detaching)、快照(Snapshotting)等。这些操作通常需要与存储系统的控制平面 API 交互。
      • Node Plugin (节点插件): 通常部署为 DaemonSet,确保在需要使用该存储的每个 Kubernetes 工作节点上都运行一个实例。它负责处理需要在特定节点上执行的操作,如将卷暂存(Stage)到节点、将卷发布(Publish,即挂载)到 Pod、取消发布(Unpublish)、取消暂存(Unstage)、获取节点上的卷统计信息等。这些操作通常涉及节点操作系统级别的命令(如 iSCSI 登录、设备发现、格式化、挂载等)。
    • Controller 和 Node Plugin 通过 Unix Domain Socket (UDS) 或 TCP 暴露其实现的 gRPC 服务端点。
  2. Kubernetes CSI Sidecar Containers (辅助容器):

    • 由于 CSI 驱动本身并不直接理解 Kubernetes API 对象(如 PVC, PV, VolumeAttachment 等),Kubernetes 社区提供了一组标准的 Sidecar 容器,它们作为 Kubernetes API 和 CSI Driver gRPC 接口之间的“翻译官”和“协调者”。
    • 这些 Sidecar 容器会 Watch(监听)特定的 Kubernetes API 资源变化,并在合适的时机调用 CSI Driver 暴露的相应 gRPC 接口。
    • 常见的 Sidecar 容器包括:
      • external-provisioner: 监听 PersistentVolumeClaim (PVC) 对象。当发现一个需要动态创建卷(基于某个 StorageClass)的 PVC 时,它会调用 CSI Driver Controller Plugin 的 CreateVolume gRPC 接口。成功后,它会创建对应的 PersistentVolume (PV) 对象。
      • external-attacher: 监听 VolumeAttachment 对象。当 Kubernetes 调度器决定将一个 Pod 调度到某个节点,并且该 Pod 需要使用某个 CSI 卷时,kube-controller-manager 中的 AttachDetach 控制器会创建一个 VolumeAttachment 对象。external-attacher 监听到这个对象后,会调用 CSI Driver Controller Plugin 的 ControllerPublishVolume gRPC 接口,将存储卷“附加”到目标节点(例如,在 SAN 存储中将 LUN 映射给目标节点的 WWN/IQN)。
      • external-resizer: 监听 PersistentVolumeClaim (PVC) 对象的 spec.resources.requests.storage 字段的变化。当用户修改 PVC 请求更大的容量时,它会调用 CSI Driver Controller Plugin 的 ControllerExpandVolume gRPC 接口(如果驱动支持)来扩展底层存储卷。
      • external-snapshotter: 与 VolumeSnapshotVolumeSnapshotContentVolumeSnapshotClass 这些 CRD (Custom Resource Definition) 配合工作。它监听这些对象,并在用户请求创建或删除卷快照时,调用 CSI Driver Controller Plugin 的 CreateSnapshotDeleteSnapshot gRPC 接口。
      • node-driver-registrar: 运行在每个节点上(通常与 Node Plugin Pod 在一起)。它的主要职责是向该节点上的 kubelet 注册 CSI Driver。它会调用 CSI Driver Node Plugin 的 GetNodeId 获取节点标识,并调用 NodeGetInfo 获取驱动名称和拓扑信息,然后通过 Kubelet Plugin Registration 机制(监听在 kubeletplugins_registry 目录下创建的 UDS)将驱动信息(驱动名称、gRPC 服务端点地址)告知 kubelet
      • livenessprobe: 一个可选的 Sidecar,用于定期调用 CSI Driver 的 Probe gRPC 接口,检查驱动的健康状况,可以集成到 Kubernetes 的 Liveness Probe 机制中。
  3. Kubernetes 内建组件与 API 对象:

    • kubelet: 运行在每个节点上的 Kubernetes 代理。在 CSI 场景下,当需要为一个 Pod 挂载卷时,kubelet 会直接(通过 node-driver-registrar 注册的 UDS)调用相应 CSI Driver Node Plugin 的 NodeStageVolumeNodePublishVolume gRPC 接口。卸载时则调用 NodeUnpublishVolumeNodeUnstageVolume
    • kube-controller-manager: 包含多个控制器,其中 AttachDetach 控制器负责创建和管理 VolumeAttachment 对象,触发卷的附加/分离流程。PersistentVolume 控制器负责 PV 和 PVC 的绑定。
    • kube-apiserver: Kubernetes 的核心 API 服务,所有组件通过它来交互和持久化状态。
    • 相关 API 对象:
      • PersistentVolumeClaim (PVC): 用户对存储资源的需求声明。
      • PersistentVolume (PV): 集群中已存在的存储资源的表示,可以静态创建或由 external-provisioner 动态创建。PV 中包含 csi 字段,指定了驱动名称和卷句柄(Volume Handle,存储系统中的唯一标识符)。
      • StorageClass: 定义了存储的“类别”。对于 CSI,StorageClass 中的 provisioner 字段指定了负责动态创建卷的 CSI 驱动名称。还可以包含 parameters 字段,传递给 CSI Driver 的 CreateVolume 调用。
      • VolumeAttachment: 一个内部对象,表示一个卷已经或正在被附加到哪个节点。由 AttachDetach 控制器创建,由 external-attacher 处理。
      • CSIDriver (CRD): 代表集群中安装的一个 CSI 驱动。它声明了驱动的名称、是否需要附加操作(attachRequired)、Pod 信息挂载(podInfoOnMount)以及驱动支持的卷生命周期模式等。kubelet 和其他组件会查询这个对象来了解驱动的能力。
      • CSINode (CRD): 代表一个节点上关于 CSI 驱动的信息,由 node-driver-registrarkubelet 更新,包含节点 ID、驱动名称、拓扑信息等。调度器可以使用拓扑信息来做出更优的 Pod 放置决策。
      • VolumeSnapshotClass, VolumeSnapshot, VolumeSnapshotContent (CRDs): 用于卷快照功能,由 external-snapshotter 管理。

四、CSI 工作流程:从 PVC 到 Pod 挂载

让我们通过一个典型的动态卷供应和挂载流程,来串联理解 CSI 各组件是如何协同工作的:

  1. 用户创建 PVC: 用户创建一个 PersistentVolumeClaim 对象,指定了一个引用 CSI 驱动的 StorageClass,并请求所需存储容量。
    yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: my-csi-pvc
    spec:
    accessModes:
    - ReadWriteOnce
    resources:
    requests:
    storage: 10Gi
    storageClassName: my-csi-sc # 引用了 StorageClass
    ---
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
    name: my-csi-sc
    provisioner: csi.my-storage.com # CSI 驱动的名称
    parameters:
    type: ssd
    replication: "2"
    reclaimPolicy: Delete
    volumeBindingMode: Immediate # 或 WaitForFirstConsumer

  2. external-provisioner 介入: external-provisioner Sidecar 监听到这个新的 PVC,并且其 storageClassName 指向它所管理的 CSI 驱动 (csi.my-storage.com)。

  3. 调用 CreateVolume: external-provisioner 通过配置的 UDS 或 TCP 地址,调用 CSI Driver Controller Plugin 的 CreateVolume gRPC 接口。请求中包含了容量、访问模式、StorageClass 中的 parameters 等信息。

  4. CSI Driver 创建卷: CSI Driver Controller Plugin 接收到请求后,与底层存储系统的控制平面交互,执行实际的卷创建操作(例如,在云平台上创建一个磁盘,或在 Ceph 集群中创建一个 RBD 镜像)。

  5. 返回卷信息: 存储系统创建成功后,CSI Driver 将新创建卷的唯一标识符(Volume Handle)、实际容量、拓扑信息等返回给 external-provisioner

  6. 创建 PV 对象: external-provisioner 收到成功的响应后,使用返回的信息创建一个 PersistentVolume (PV) 对象。PV 对象中会包含 csi 字段,指定驱动名称和 Volume Handle。
    yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
    name: pv-abcde # 自动生成或基于某种模式
    spec:
    capacity:
    storage: 10Gi
    volumeMode: Filesystem
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: my-csi-sc
    csi:
    driver: csi.my-storage.com
    volumeHandle: xyz-123-789 # 存储系统返回的唯一 ID
    # fsType: ext4 (可选)
    # volumeAttributes: {...} (可选)

  7. PV-PVC 绑定: Kubernetes 的 PersistentVolume 控制器发现新创建的 PV 满足了 my-csi-pvc 的需求,于是将它们绑定在一起。PVC 的状态变为 Bound

  8. Pod 调度: 用户创建一个 Pod,并在其 volumes 字段中引用了 my-csi-pvc。Kubernetes 调度器选择一个合适的节点来运行这个 Pod。如果 StorageClassvolumeBindingMode 设置为 WaitForFirstConsumer,则卷的创建和绑定会推迟到这一步之后,以确保卷创建在 Pod 将要运行的节点的拓扑约束区域内(如果驱动支持拓扑)。

  9. 触发 Attach (如果需要): 如果 CSIDriver 对象表明该驱动需要 Attach/Detach 操作 (attachRequired: true),kube-controller-manager 中的 AttachDetach 控制器会为这个卷和目标节点创建一个 VolumeAttachment 对象。

  10. external-attacher 介入: external-attacher Sidecar 监听到 VolumeAttachment 对象,调用 CSI Driver Controller Plugin 的 ControllerPublishVolume gRPC 接口,将卷附加到目标节点(例如,将云磁盘附加到 VM 实例,或执行 iSCSI/FC 的 LUN Mapping)。

  11. kubelet 准备挂载: 目标节点上的 kubelet 看到 Pod 被调度到本节点,并且需要 my-csi-pvc 对应的卷。它确认 VolumeAttachment 状态表明卷已成功附加(如果需要 Attach)。

  12. 调用 NodeStageVolume: kubelet 通过 UDS 调用本节点上 CSI Driver Node Plugin 的 NodeStageVolume gRPC 接口。这个调用通常只在卷第一次被挂载到该节点时发生。Node Plugin 会执行一些节点级别的准备工作,例如:

    • 发现附加到节点的设备(如 /dev/sdX)。
    • 如果需要,格式化设备(例如,使用 PV 中指定的 fsType)。
    • 将设备挂载到一个全局的、临时的暂存目录(Staging Path)。
  13. 调用 NodePublishVolume: kubelet 接着调用 CSI Driver Node Plugin 的 NodePublishVolume gRPC 接口。Node Plugin 会将之前暂存目录中的卷(或直接是块设备,如果是 Raw Block Volume 模式)通过绑定挂载(Bind Mount)或设备映射的方式,挂载到 Pod 实际需要的挂载点(Target Path),即 Pod 定义中 volumeMounts 指定的路径。

  14. Pod 启动: 至此,卷已经成功挂载到 Pod 内部。kubelet 启动 Pod 中的容器,容器内的应用可以通过指定的路径访问持久化存储了。

卸载和删除流程 则大致是上述过程的逆向操作:

  • Pod 删除时,kubelet 调用 NodeUnpublishVolume (解除 Pod 内挂载点) 和 NodeUnstageVolume (解除节点暂存点,可选)。
  • 如果需要 Detach,AttachDetach 控制器删除 VolumeAttachmentexternal-attacher 调用 ControllerUnpublishVolume (从节点分离卷)。
  • 当 PVC 被删除时 (且 reclaimPolicyDelete),external-provisioner 调用 DeleteVolume 来删除底层存储资源。

五、CSI 的高级功能与未来展望

CSI 不仅仅解决了基本的卷供应和挂载问题,其标准化的接口也为许多高级存储功能的实现铺平了道路:

  • 卷快照 (Volume Snapshotting): 通过 external-snapshotter 和相关的 CRD,用户可以为 CSI 卷创建一致性快照,用于备份、恢复或创建新卷。
  • 卷克隆 (Volume Cloning): CSI 允许从一个已有的 PVC 创建一个新的 PVC,新卷的内容是源卷在某个时间点的精确副本。
  • 卷扩容 (Volume Expansion): 用户可以修改 PVC 的 storage 请求来在线或离线增加卷的容量,由 external-resizer 和 CSI Driver 的 ControllerExpandVolume / NodeExpandVolume 实现。
  • 拓扑感知 (Topology Awareness): CSI Driver 可以上报其存储资源的拓扑信息(如可用区、机架等),CSINode 对象记录节点拓扑,调度器可以利用这些信息将 Pod 调度到能够最佳访问所需存储的节点上,特别适用于本地存储或有区域限制的存储。StorageClassvolumeBindingMode: WaitForFirstConsumer 是实现拓扑感知的关键。
  • 裸块设备卷 (Raw Block Volumes): CSI 支持将存储卷作为块设备直接暴露给 Pod,而不是挂载文件系统。这对于需要直接访问块设备的特定应用(如某些数据库)很有用。
  • 临时卷 (Ephemeral Volumes): CSI 也支持创建与 Pod 生命周期绑定的临时卷,数据在 Pod 删除时丢失。这可以用于需要高性能临时存储(优于 emptyDir)的场景。

未来展望:

CSI 规范仍在不断发展中。社区正在积极探索和标准化更多的功能,例如:
* 卷健康监控与修复。
* 更细粒度的访问控制和 QoS (Quality of Service) 管理。
* 跨集群的卷复制和迁移。
* 与备份工具的更深度集成。

六、选择与部署 CSI 驱动

如今,几乎所有主流的存储供应商(包括公有云提供商如 AWS EBS, GCE PD, Azure Disk/File;分布式存储系统如 Ceph, GlusterFS;以及商业存储阵列厂商)都提供了自己的 CSI 驱动。还有一些通用的 CSI 驱动,如 nfs-subdir-external-provisioner 的 CSI 版本、hostpath-csi (用于测试或本地存储) 等。

部署 CSI 驱动通常涉及:

  1. 应用 CRDs: 如果驱动使用了如 CSIDriver, CSINode, 或快照相关的 CRD,需要先将这些 CRD 定义应用到集群。
  2. 部署 Controller Plugin: 通常是一个 Deployment,包含 CSI Driver Controller 镜像和相关的 Sidecar 容器 (external-provisioner, external-attacher, external-resizer, external-snapshotter 等)。需要配置 RBAC 权限。
  3. 部署 Node Plugin: 通常是一个 DaemonSet,包含 CSI Driver Node 镜像和相关的 Sidecar 容器 (node-driver-registrar, livenessprobe 等)。需要配置 RBAC 权限,并且通常需要以特权模式运行(或具有特定的 securityContext)来执行节点级别的操作。
  4. 创建 StorageClass: 定义一个或多个 StorageClass,将 provisioner 指向部署的 CSI 驱动名称,并根据需要配置 parameters

许多 CSI 驱动都提供了 Helm Chart 或 Operator 来简化部署过程。

结论

Kubernetes CSI 插件机制是容器化存储领域的一项重大进步。它通过标准化接口和解耦架构,彻底解决了早期 In-Tree 插件模式的诸多弊端,为 Kubernetes 用户带来了前所未有的灵活性、稳定性和选择空间。

CSI 不仅使得存储供应商能够快速、独立地集成其解决方案到 Kubernetes 生态,也让 Kubernetes 本身能够专注于核心编排能力,同时通过 Sidecar 模式和标准 API 对象优雅地协调存储操作。从动态卷供应、挂载卸载,到快照、克隆、扩容和拓扑感知等高级功能,CSI 提供了一个强大而可扩展的框架。

理解 CSI 的架构、组件和工作流程,对于任何在 Kubernetes 环境中管理有状态应用的工程师或架构师来说都至关重要。随着 CSI 标准的持续演进和社区生态的日益繁荣,它将继续作为 Kubernetes 存储体系的基石,支撑起云原生时代多样化、高性能、高可靠的持久化存储需求。掌握 CSI,无疑是驾驭 Kubernetes 存储挑战的关键利器。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部