Kubernetes 存储机制深度解析:CSI、EmptyDir 与 HostPath

Kubernetes 作为一个强大的容器编排平台,其存储子系统是支撑有状态应用的关键。理解 Kubernetes 如何管理存储、如何与底层存储系统交互至关重要。本文将深入探讨 Kubernetes 存储的核心概念,重点解析容器存储接口(CSI)标准,以及两种内建的卷类型:EmptyDir 和 HostPath,分析它们的实现原理、适用场景和潜在风险。


一、Kubernetes 存储核心概念回顾

在深入具体实现之前,我们先回顾一下 Kubernetes 存储的几个核心抽象:

  1. Volume(卷): Pod 内可访问的文件系统,生命周期可能与 Pod 绑定,也可能独立于 Pod。它是 Pod 内各容器间共享数据、持久化数据的基础。
  2. PersistentVolume (PV): 由管理员配置的集群存储资源,代表了一块具体的网络存储(如 NFS、iSCSI、云硬盘)或本地存储。它独立于 Pod 生命周期。
  3. PersistentVolumeClaim (PVC): 用户(应用开发者)对存储资源的请求。PVC 会绑定到满足其请求条件的 PV 上。Pod 通过挂载 PVC 来使用存储。
  4. StorageClass: 定义了动态存储分配(Dynamic Provisioning)的策略。当用户创建 PVC 时,如果指定了 StorageClass,系统会根据其定义自动创建(Provision)一个匹配的 PV。

这些抽象解耦了应用对存储的需求与底层存储的具体实现。而 CSI 正是连接这些抽象与具体存储实现的标准化桥梁。


二、CSI(Container Storage Interface):存储标准化的基石

CSI 是 Kubernetes(以及其他容器编排系统如 Mesos, Cloud Foundry)为了统一存储插件开发而制定的标准接口规范。它旨在将存储驱动(Volume Plugin)的逻辑从 Kubernetes 核心代码中解耦出来,允许第三方存储厂商独立开发、部署和更新其存储驱动。

1. CSI 架构原理

CSI 采用典型的控制器-节点(Controller-Node)架构,通过 gRPC 接口进行通信,主要包含三个组件:

  1. Kubernetes 核心组件(与 CSI 交互部分)

    • kube-controller-manager: 内含 PersistentVolumeController,负责处理 PV/PVC 的绑定、生命周期管理。通过 external-attacher sidecar 调用 CSI Controller Plugin 的 ControllerPublishVolume/ControllerUnpublishVolume
    • kubelet: 负责 Pod 的卷挂载。通过 Unix Domain Socket (/var/lib/kubelet/plugins/<driver-name>/csi.sock) 调用 CSI Node Plugin 的 NodeStageVolume/NodePublishVolume/NodeUnstageVolume/NodeUnpublishVolume 等接口。
    • API Server: 存储 PV, PVC, StorageClass, VolumeAttachment 等资源对象的状态。
  2. CSI Controller Plugin(控制平面)

    • 通常以 Deployment 或 StatefulSet 形式部署,包含 CSI 驱动的核心控制逻辑和一个或多个 Sidecar 容器
      • external-provisioner: 监听 PVC 创建事件,调用 CSI Controller 的 CreateVolume/DeleteVolume 接口来动态创建/删除底层存储卷。
      • external-attacher: 监听 VolumeAttachment 对象,调用 CSI Controller 的 ControllerPublishVolume/ControllerUnpublishVolume 接口,负责将卷附加(Attach)到指定节点或分离(Detach)。对于某些存储(如 Ceph RBD),这步是必要的;对于 NFS 等则可能为空操作。
      • external-resizer (可选): 监听 PVC 编辑事件(请求扩容),调用 CSI Controller 的 ControllerExpandVolume 接口。
      • external-snapshotter (可选): 处理 VolumeSnapshot 相关逻辑。
    • 职责: 与存储系统(如云提供商 API、存储阵列管理接口)交互,执行卷的创建、删除、附加、分离、快照等管理操作。
  3. CSI Node Plugin(数据平面)

    • 以 DaemonSet 形式部署到每个需要使用该存储的 Worker 节点上。
    • 包含 CSI 驱动的节点逻辑和一个 Sidecar 容器
      • node-driver-registrar: 向 kubelet 注册 CSI 驱动,让 kubelet 知道通过哪个 Unix Domain Socket 与该驱动通信。
    • 职责: 在节点上执行具体的卷操作,如格式化文件系统、挂载(Mount)卷到 Pod 的指定路径、卸载(Unmount)卷等。直接调用节点操作系统命令(如 mkfs, mount, umount)和系统调用。

2. 核心工作流程示例(动态创建并挂载卷)

  1. 用户创建 PVC,指定了某个 StorageClass。
  2. external-provisioner 监听到新的 PVC,调用 CSI Controller Plugin 的 CreateVolume gRPC 接口。
  3. CSI Controller Plugin 与底层存储系统交互,创建存储卷。成功后返回卷信息。
  4. external-provisioner 使用返回信息创建 PV 对象,并将其与 PVC 绑定。
  5. 用户创建 Pod,引用了该 PVC。
  6. 调度器将 Pod 调度到某个 Node。
  7. kube-controller-manager 中的 VolumeAttachment 控制器(通过 external-attacher)发现 Pod 需要挂载卷,调用 CSI Controller Plugin 的 ControllerPublishVolume,将卷附加到目标 Node(如果需要)。
  8. kubelet 在该 Node 上准备启动 Pod,发现需要挂载 CSI 卷。
  9. kubelet 调用 CSI Node Plugin 的 NodeStageVolume (如果需要,如全局挂载点准备)。
  10. kubelet 调用 CSI Node Plugin 的 NodePublishVolume,将卷挂载到 Pod 的目标路径(通常是 /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<volume-name>/mount)。
  11. Pod 启动,容器内的挂载点指向 kubelet 准备好的路径。

3. 关键源码与接口

  • Kubernetes 侧 CSI 逻辑主要在 pkg/volume/csi 包。
  • CSI 规范定义了 Identity, Controller, Node 三类 gRPC 服务及其接口,如 GetPluginInfo, CreateVolume, ControllerPublishVolume, NodeStageVolume, NodePublishVolume 等。

CSI 的设计极大地促进了 Kubernetes 存储生态的发展,使得各种存储解决方案能够无缝集成。


三、EmptyDir:Pod 内的临时高速公路

EmptyDir 是 Kubernetes 提供的一种最简单的卷类型,它为 Pod 内的容器提供一个临时的、生命周期与 Pod 绑定的空目录。

1. 生命周期与存储机制

  • 创建: 当 Pod 被分配到某个节点时,kubelet 会在宿主机的特定目录下(通常是 /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/<volume-name>)为该 Pod 创建一个实际的目录。这个目录最初是空的。
  • 共享: Pod 内的所有容器都可以挂载这个 EmptyDir 卷,并且都可以读写其中的内容。它是实现 Pod 内容器间数据共享(如通过 Unix Domain Socket 通信、共享配置文件或临时工作区)的常用方式。
  • 销毁: 当 Pod 因任何原因(完成、失败、被删除)从节点上移除时,kubelet 会清理该 Pod 相关的所有资源,包括其 EmptyDir 卷中的数据。EmptyDir 的数据不具备持久性,会随 Pod 的删除而永久丢失。
  • 节点重启:
    • 如果 EmptyDir 使用默认的宿主机磁盘 (medium: ""),节点重启后,若 Pod 被重新调度回该节点,理论上数据可能还在(取决于 kubelet 清理逻辑和磁盘状态),但不应依赖此行为。
    • 如果 EmptyDir 使用内存 (medium: "Memory"),节点重启后数据必定丢失。

2. 底层存储实现与 Linux 内核关联

1
2
3
4
5
6
7
8
# 典型配置示例
volumes:
- name: cache-volume
emptyDir: {} # 默认使用宿主机磁盘
- name: shared-socket
emptyDir:
medium: Memory # 使用内存 (tmpfs)
sizeLimit: 128Mi # 限制大小 (磁盘或内存)
  • 默认模式 (medium: ""): kubelet 直接在宿主机的某个分区(通常是 Kubelet Root Dir 所在分区)上创建一个普通目录。容器通过 绑定挂载 (Bind Mount) 的方式将这个宿主机目录挂载到自己的 Mount Namespace 中。性能受宿主机底层磁盘(HDD, SSD, NVMe)限制。
  • 内存模式 (medium: "Memory"): kubelet 在宿主机上创建一个 tmpfs (Temporary File System) 挂载点,然后将其绑定挂载到容器内。
    • tmpfs: 是 Linux 内核提供的一种基于内存的文件系统。读写操作直接在 RAM 中进行,速度极快,接近内存带宽。
    • sizeLimit: 当设置 medium: Memory 时,sizeLimit 参数会传递给 tmpfs 挂载选项,限制该内存文件系统的最大容量。这是通过 cgroups v1 (memory subsystem)cgroups v2 (memory controller) 来强制实施的。若不设置,tmpfs 默认大小通常是节点内存的一半。对于磁盘模式,sizeLimit 通过文件系统配额(quota)或定期检查实现(较新版本)。
  • 性能: 内存模式性能远超磁盘模式,适用于需要高速读写的临时数据,如缓存、进程间通信的 socket 文件等。

3. 内核级隔离机制

  • Mount Namespace: 每个容器都有独立的 Mount Namespace,EmptyDir 通过绑定挂载进入容器的视图,确保了文件系统视图的隔离。
  • Cgroups: sizeLimit 利用 cgroups 的内存或 I/O 控制能力来限制资源使用,防止某个 Pod 的 EmptyDir 耗尽节点资源。

4. 典型应用场景

  • Sidecar 模式: 主应用容器和 Sidecar 容器通过 EmptyDir 共享 Unix Domain Socket 或配置文件。
  • 多阶段构建/处理: Init 容器下载或生成数据,主容器使用这些数据。
  • Web 服务器缓存: Nginx 或 Apache 将静态文件缓存或 session 数据存放在内存型 EmptyDir 中加速访问。
  • 临时工作空间: 批处理任务或 CI/CD 流水线中的临时文件存储。

5. 安全与资源注意事项

  • 资源消耗: 内存型 EmptyDir 会消耗节点内存,磁盘型会消耗磁盘空间。必须通过 sizeLimitrequests/limits (Pod 级别) 进行约束,防止资源滥用。
  • 数据非持久: 强调其临时性,不适用于需要持久化的数据。

6. 与 CSI Ephemeral Volumes 对比

CSI Ephemeral Volumes 允许 CSI 驱动提供类似 EmptyDir 的内联临时卷,但可以利用更高级的本地存储(如 NVMe SSD),并由 CSI 驱动管理生命周期。对于需要高性能临时存储且希望利用 CSI 生态的场景,这是一个更现代的选择。


四、HostPath:直通宿主机文件系统

HostPath 卷允许将宿主机节点上的文件或目录直接挂载到 Pod 中。这是一种强大的机制,但同时也带来了显著的安全风险和使用限制。

1. 核心特性与内核交互

1
2
3
4
5
6
# 典型配置示例
volumes:
- name: host-logs
hostPath:
path: /var/log # 宿主机路径
type: DirectoryOrCreate # 类型:确保是目录,如果不存在则创建
  • 直接映射: HostPath 的核心是直接将宿主机的 path 通过 绑定挂载 (Bind Mount) 映射到容器的 Mount Namespace 中。容器内对该路径的读写直接作用于宿主机文件系统。
  • 类型 (type): 控制挂载行为:
    • "" (默认): 不进行检查,直接挂载。
    • DirectoryOrCreate: 路径必须是目录,若不存在则创建(权限 0755)。
    • Directory: 路径必须是目录。
    • FileOrCreate: 路径必须是文件,若不存在则创建空文件(权限 0644)。
    • File: 路径必须是文件。
    • Socket: 路径必须是 Unix Socket。
    • CharDevice: 路径必须是字符设备。
    • BlockDevice: 路径必须是块设备。
      指定 type 是一种安全措施,防止意外挂载错误类型的文件或目录。
  • 内核交互: 与 EmptyDir 类似,依赖 Linux 的 Mount Namespace 实现隔离,通过 VFS (Virtual File System) 层访问宿主机文件系统。容器内看到的 inode 与宿主机完全一致。

2. 生命周期与调度约束

  • 生命周期: HostPath 卷的内容独立于 Pod 生命周期。Pod 删除后,宿主机上的数据 依然存在
  • 节点绑定: 由于 HostPath 引用的是特定节点的文件系统路径,使用 HostPath 的 Pod 通常需要与特定节点绑定,否则 Pod 漂移到其他节点后将无法访问相同的数据(除非所有节点在该路径下都有相同内容,如通过外部同步机制保证)。这通常通过 nodeSelectornodeAffinity 实现。
  • 数据持久性: 持久性取决于宿主机上该路径的性质。如果是普通磁盘目录,数据是持久的;如果是挂载的 tmpfs,则随节点重启丢失。

3. 严重的安全风险与防护

HostPath 是 Kubernetes 中最危险的卷类型之一,因为它打破了容器与宿主机之间的隔离。

  • 风险:
    • 访问敏感文件: Pod 可能挂载 /etc, /var/lib/kubelet, /var/run/docker.sock 等敏感目录,读取或篡改宿主机配置、密钥,甚至控制 Docker 守护进程。
    • 控制宿主机设备: 挂载 /dev 下的设备文件可能允许容器直接操作硬件。
    • 文件系统破坏: 挂载根目录 / 并写入可能破坏宿主机系统。
  • 防护机制:
    • 最小权限原则: 仅在绝对必要时使用 HostPath,并挂载尽可能具体的路径,而非父目录。
    • 只读挂载: 在 volumeMounts 中设置 readOnly: true,限制容器只能读取。
    • Pod Security Admission (PSA) / PodSecurityPolicy (PSP - 已弃用): 集群管理员可以配置策略(如 baselinerestricted PSA 策略),限制或禁止使用 HostPath,或只允许挂载特定的、经过批准的路径前缀 (allowedHostPaths)。
    • Admission Webhooks: 可以实现自定义的准入控制器,对 HostPath 的使用进行更精细的校验。
    • SELinux/AppArmor: 在宿主机上启用这些强制访问控制系统,可以进一步限制容器进程即使通过 HostPath 也无法访问未授权的文件。

4. 性能特征

  • 性能基本等同于直接在宿主机上访问该路径的性能,受底层文件系统和存储介质影响。
  • 相比 OverlayFS 等容器文件系统层,访问 HostPath 路径通常开销更小,因为绕过了写时复制等机制。

5. 合理的应用场景

尽管风险高,HostPath 在某些特定场景下是必要的:

  • 节点级监控/日志代理: 如 Fluentd, Prometheus Node Exporter 需要读取宿主机的 /var/log, /proc, /sys 等。
  • 特定驱动/守护进程: 需要访问宿主机特定文件或 Socket 的系统组件,如 CNI 插件、设备插件(如 NVIDIA GPU 驱动需要访问 /dev/nvidia* 设备)。
  • 需要访问 Docker Socket: 用于构建镜像或管理其他容器的 Pod(需极度谨慎)。

6. 内核级问题排查

  • 挂载点检查: 在容器内使用 mountcat /proc/self/mountinfo 查看 HostPath 的挂载情况。
  • 权限问题: 使用 ls -l, ls -Z (如果启用 SELinux) 检查宿主机和容器内的权限和安全上下文。
  • 文件占用: 使用 lsoffuser 在宿主机上检查是哪个进程(可能是容器内的进程)占用了 HostPath 中的文件。

7. 与 EmptyDir 的关键区别

维度 HostPath EmptyDir
数据来源 宿主机现有文件/目录 Pod 创建时生成的空目录
生命周期 独立于 Pod,随节点 与 Pod 绑定,随 Pod 删除
跨 Pod 共享 同一节点上 Pod 可共享(若路径相同) 仅限同一 Pod 内的容器共享
节点依赖 强依赖特定节点 不依赖特定节点(数据随 Pod 迁移)
安全风险 高,可访问宿主机任意路径 低,局限于 Kubelet 管理的目录
主要用途 访问节点级资源、设备 Pod 内临时数据共享、缓存

五、总结与选择建议

卷类型 生命周期 数据源 性能 节点依赖 安全性 主要用途 推荐度
EmptyDir (Disk) Pod 新建空目录 中 (磁盘IO) Pod 内临时共享、工作区 高 (临时)
EmptyDir (Memory) Pod 新建 tmpfs 高 (内存速度) 高速缓存、Socket 通信 高 (临时)
HostPath 节点 宿主机现有路径 高 (直接访问) 访问节点日志/配置/设备、特定系统代理 低 (谨慎)
CSI (通用) PV/独立 外部存储系统 可变 (依赖驱动) 可配置 中/高 持久化存储、共享存储、特定存储特性 高 (持久化)
CSI Ephemeral Pod CSI 驱动提供 可变 (依赖驱动) 可配置 高性能临时卷、替代 EmptyDir/HostPath 部分场景 中 (特定)

选择建议:

  • 需要持久化存储: 优先选择 CSI 驱动配合 PV/PVC,这是 Kubernetes 标准且推荐的方式。
  • Pod 内临时数据共享或高速缓存: 使用 EmptyDir,根据性能需求选择磁盘或内存模式,并设置 sizeLimit
  • 必须访问宿主机特定文件/目录/设备: 谨慎使用 HostPath,严格限制路径,配置 readOnly,并配合 Pod 安全策略。考虑是否有 CSI 驱动(如本地存储 CSI 驱动)或 Projected Volume 等更安全的替代方案。
  • 需要比 EmptyDir 更高级的临时存储: 考虑 CSI Ephemeral Volumes

理解不同存储类型的原理、特性和风险,是构建健壮、安全的 Kubernetes 应用的基础。


Kubernetes 存储机制深度解析:CSI、EmptyDir 与 HostPath
https://mfzzf.github.io/2025/03/28/kubernetes-CSI/
作者
Mzzf
发布于
2025年3月28日
许可协议