大模型训练:K8s 环境中数千节点存储最佳实践

今天这篇博客来自全栈工程师朱唯唯,她在前不久举办的 KubeCon 中国大会上进行了该主题分享。

Kubernetes 已经成为事实的应用编排标准,越来越多的应用在不断的向云原生靠拢。与此同时,人工智能技术的迅速发展,尤其是大型语言模型(LLM)的推进,导致企业需要处理的数据量急剧增加,例如,Llama 3.1 模型拥有 4050 亿参数,其模型文件的大小达到了 231GB。随着模型参数的增长,模型文件体积也随之增大。

01 Kuberenetes 中大模型训练的存储挑战

随着数据集群规模不断扩大,在 Kubernetes 环境中管理大规模数据集群面临多重挑战:

  1. 复杂权限管理:大规模 AI 训练往往涉及到上百人算法工程师,对文件系统的权限管理提出了复杂的需求。在 Kubernetes 环境中,这种规模的权限管理尤其困难,因为必须细粒度地控制对高度动态和分布式资源的访问,同时确保不影响开发和运维的效率。
  2. 稳定性挑战:在极致弹性的云原生环境中,文件系统的稳定性也面临了极大的挑战。如何保证重启或升级文件系统服务时,不影响业务?
  3. 系统可观测性:在复杂的 Kubernetes 系统下,如何增加系统可观测性,简化运维与问题排查的难度?

除了在 Kubernetes 环境中的挑战之外,存储系统还面临高并发、高吞吐量和低延迟的性能要求,以及在多云架构中维持数据一致性的挑战。

02 JuiceFS 的架构设计如何应对这些挑战?

JuiceFS 将元数据与数据分开存储。元数据存储在包括 Redis、MySQL 以及自研高性能云数据引擎等数据库中;而数据则被切分成块存储在对象存储中,支持市面上几乎所有类型的对象存储。这种将文件分块存储的方法使得所有文件的 I/O 请求都可以通过偏移量精确锁定到特定的块,特别适合大文件的读写操作,并确保数据一致性。

如下图所示,JuiceFS 客户端位于系统的上方,处理所有文件 I/O 请求,并向上层应用提供多种访问方式,包括 POSIX 接口、JuiceFS CSI Driver 和 S3 Gateway。

Kubernets 环境中的 JuiceFS

JuiceFS 提供了 CSI Driver,使用户可以在 Kubernetes 环境中通过原生的 Persistent Volume Claim (PVC) 方式使用文件系统,支持静态和动态配置。

在静态配置中,管理员为应用 Pod 创建一个单独的 Persistent Volume (PV)。用户只需创建一个 PVC,并在 Pod 中声明这个 PVC,即可在 Pod 中使用 JuiceFS。

动态配置则简化了管理员的工作。管理员无需为每个 Pod 单独创建 PV,而是创建一个 PV 的模板即 StorageClass。用户的操作与静态配置相同,仍需创建 PVC,系统将自动基于 StorageClass 动态生成所需的 PV。然后在运行当中系统会自动创建对应的 PV。

以下图解演示了当 JuiceFS CSI Driver 收到 Kubernetes 的挂载请求后的操作流程。系统会创建一个独立的 Pod 来运行 JuiceFS 客户端。这种设计带来了几个显著的好处:

  1. 增加系统稳定性和可扩展性:FUSE 客户端与 CSI Driver 组件进行了彻底的解耦,使得 CSI Driver 的重启和升级不会影响到 FUSE 客户端的运行。
  2. 便于管理 :此架构允许以 Kubernetes 的方式直观管理 FUSE 守护进程(daemon),增强了过程的透明性和管理效率。

Serverless 环境中如何运行 CSI Driver

在 Serverless 环境中,服务通常不与特定节点关联,这意味着无法在节点上运行守护进程(daemon site),从而使得 CSI node 组件无法正常工作。为解决这一问题,我们采用了创新性的解决方案,使用 Sidecar 模式,以支持 JuiceFS 在 Serverless 弹性环境中的运行,确保存储客户端的高可用性和灵活性。

具体操作中,我们在 CSI controller 中向 API server 注册了一个 webhook。当 API server 需要创建 Pod 时,它会向此 webhook 发起请求。通过这一机制,我们会向应用 Pod 注入一个 Sidecar 容器,在该容器中运行 JuiceFS 客户端。这种配置使得 JuiceFS 客户端能以 sidecar 的形式与应用容器共存于同一个 Pod 中,共享相同的生命周期,从而提高了整体的运行效率和稳定性。关于 Sidecar 模式,点击此处了解更多详情。

多租户环境中实现数据安全性

在多租户环境下,确保数据安全是一个重大挑战。JuiceFS 采用了多种安全机制来应对这一挑战:

数据隔离:通过为 StorageClass 动态声明的 PVC 分配不同的存储目录,JuiceFS 实现了不同业务间的数据隔离。

数据加密:在文件系统启动静态数据加密功能后,用户可以在 Secret 中设置密钥口令和密钥文件,从而启用 JuiceFS 的数据加密功能。

权限控制:用户可使用类 Unix 系统的 UID 和 GID 来管理文件权限,并可直接在 pod 中设置 uid 和 gid。此外,JuiceFS 还支持设置 POSIX ACL,以实现更细粒度的权限控制。

可无限扩展的存储空间

JuiceFS 是基于对象存储构建的,因此其实际上没有存储容量的上限。我们在对象存储的基础上实现了一套逻辑数据管理系统。

用户在使用时,可以在 PersistentVolumeClaim (PVC) 中通过指定 StorageClass 的属性值来设定 JuiceFS 的配额。这一过程类似于为 JuiceFS 设置一个容量配额。当需要进行数据扩容时,操作也非常简单:用户只需使用 kubectl 命令修改 PVC 中的 StorageClass 属性中的存储容量值,即可轻松实现数据的扩容。

如何实现高性能?

当大量客户端需要频繁访问相同的数据集时,分布式缓存能让多个客户端共享相同的缓存数据,显著提升性能。这种机制特别适用于使用 GPU 集群进行深度学习模型训练的场景。

以下是分布式缓存集群的一个典型部署方式。在 GPU 计算节点中,JuiceFS 客户端会运行,并使用本地的 NVMe 作为本地缓存。分布式文件缓存集群通常部署在近端,并通过预热方式从远端的对象存储中拉取数据到近端,供 GPU 节点上的客户端使用。

这里分享一项针对大文件顺序读和随机读的性能测试,以帮助大家理解分布式缓存的效果。如下图所示,在顺序读大文件的测试中,未启用缓存时带宽为 4.7GB,而启用缓存后带宽提升至 13.2GB。在随机读大文件的测试中,未使用缓存时的延迟为 29 毫秒,启用缓存后,延迟显著降低至 0.3毫秒 ,性能得到显著提升。

多云环境如何保持数据一致性?

随着模型参数和数据集规模的不断增大,公有云的 GPU 资源因其充足数量和高度灵活性而成为更合适的选择。为了降低成本并满足多云架构的需求,越来越多的公司选择在不同的云平台之间分配 GPU 资源。

我们引入了镜像文件系统功能,使用户能够在不同的云平台中访问 JuiceFS 数据,并保持数据的一致性。该系统通过异步方式将数据从原始文件系统同步到对象存储。

此外,我们通过元数据引擎定期同步 Raft 的 changelog 来增强数据一致性。在镜像文件系统中,客户端可对原始文件系统发起写请求,而读请求可从任一端发起------无论是原始文件系统还是镜像文件系统,均能确保数据的一致性。这种设计有效确保了多云架构中数据一致性的稳定性。

03 针对数千节点集群的实践与优化

在一个包含数千节点的集群中,最大的挑战之一是管理大量节点及其关联的资源,如Deployment、DaemonSet 等。随着这些资源请求的增多,APIServer 将面临极大的压力。

优化1:可视化监控

当集群中的资源众多时,故障排查往往变得繁琐且困难。为此,我们提供了一套可视化的dashboard。在这个 dashboard 中,我们可以列出所有使用 JuiceFS PVC 的应用 Pod,并显示每个 Pod 对应的挂载 Pod 以及它们的运行状态。

此外,如果应用 Pod 出现异常,dashboard 还会展示一些 tips,说明可能的具体原因,以便为用户提供进一步的排查方向。这套 dashboard 极大地简化了用户的故障排查过程,点击此处了解该功能详情。

优化2:资源 & 性能

对于应用 Pod,为每个 Pod 单独创建挂载点是不现实且浪费资源的。因此,所有使用同一 PersistentVolumeClaim (PVC) 的应用 Pods 默认会共享一个挂载 Pod,并且在某些配置下,所有使用同一 StorageClass 的应用 Pods 也将共享一个挂载 Pod,以进一步优化资源使用。

另一方面,CSI 管理挂载 Pod 生命周期时采用了 list-watch 的方法。在大规模集群中,CSI 组件启动时的全量 list 请求会对 API server 造成极大压力,甚至有可能导致其宕机。因此,我们采取的策略是每个节点上的 CSI 组件会单独轮询对应节点的 Kubelet,从而减轻对 API server 的压力。

优化3:稳定性

由于 FUSE 客户端的特殊性,在重启后其挂载点可能仍然无法使用,这会影响所有应用端的数据请求。为此,我们之前进行了一项优化:当 Mount Pod 因 OOM 或其他原因重启时,我们会在 CSI 中为应用 Pod 执行重新挂载。虽然这种方式能够恢复挂载点,但文件请求在当时仍可能受到影响。

为了进一步优化,我们在 Mount Pod 启动时从 /devfuse 中获取它使用的 fuse 文件描述符(fd),并将此 fd 通过进程间通信(IPC)直接传递给 CSI Driver。CSI Driver 会在内存中维护 Mount Pod 和它所使用的 fuse fd 的映射关系。如果 Mount Pod 因 OOM 重启,CSI 会立即将其删除,并启动一个新的 Pod 来替代它。

这个新的 Pod 启动后,会通过 IPC 从 CSI 获取之前使用的 fuse fd,并重新处理业务请求。这种方式对用户端的影响相对较小,在读文件操作中只会出现短暂的卡顿,不会影响后续的处理。

优化4:平滑升级

Mount Pod 的平滑升级与之前提到的故障恢复相似,但有一个显著区别,在原有的升级过程中,旧的客户端会将其当前处理的所有数据请求保存到一个临时文件中。实现平滑升级功能后,新的客户端将执行两个操作:一是从 CSI 获取其 fuse 文件描述符(fd),二是在启动后立即从该临时文件中读取升级前的数据请求。这样做能确保在升级过程中不会遗漏任何业务请求,从而实现真正的平滑升级功能。

04 总结

自 2021 年 7 月 JuiceFS CSI Driver 首次推出以来,随着 Kubernetes 用户数量的增长和集群规模的不断扩大,面对的应用场景也愈发复杂。过去三年多的时间里,我们对 JuiceFS CSI Driver 在稳定性、管理权限等关键领域进行了持续的优化和改进,这使得 JuiceFS 能够有效地适应各种复杂的需求,成为 Kubernetes 环境中数据持久化的理想选择。

最后,我们将再次概述 JuiceFS 的核心特性以及关键优化,以帮助用户在 Kubernetes 环境中更好地进行存储选型。

  • 数据安全:JuiceFS 通过实行数据隔离、加密和权限控制来保障数据安全。此外,其分布式缓存技术不仅提升了系统性能,还有效地控制了成本。
  • 数据弹性:在 Serverless 环境中,JuiceFS 采用 sidecar 设计模式和动态数据扩容技术,以增强数据弹性。
  • 数据一致性:JuiceFS 的镜像文件系统功能保证了多云架构下的数据一致性,确保了数据在不同云平台间的稳定性和可靠性。
  • 高性能与成本控制:JuiceFS 支持从对象存储中快速将必要文件数据拉取到本地缓存,这一过程通常在 10 到 20 秒内完成,大幅缩短了数据获取时间,相较于无缓存状态下的四至五百秒。预热过程采用并行处理方式,能在模型加载前完成,有效减少了启动时间。
  • POSIX 兼容性:JuiceFS 提供全面的 POSIX 兼容性,确保与多种应用和系统的高度兼容性。