基于 JuiceFS 构建高校 AI 存储方案:高并发、系统稳定、运维简单

中山大学的 iSEE 实验室(Intelligence Science and System) Lab)在进行深度学习任务时,需要处理大量小文件读取。在高并发读写场景下,原先使用的 NFS 性能较低,常在高峰期导致数据节点卡死。此外,NFS 系统的单点故障问题也导致一旦数据节点宕机,该机器上的数据将完全不可用。扩容问题同样棘手,每增加一台数据节点,就需要在所有计算节点上进行多次挂载。而新增的数据节点由于数据量较小,并不能有效分担读写压力。

为解决这些问题,经过初步评估,实验室选择了JuiceFS 作为替代的存储方案。当前,结合TiKV 的 JuiceFS 已成功管理超过 5 亿个文件。新方案显著提升了在高并发场景下的性能和系统稳定性,确保了深度学习训练过程中计算节点的连续运行,同时基本解决了单点故障的问题。

此外,JuiceFS 的操作简便易学,甚至不需要专职的存储管理人员来维护,这对于主要由 AI 领域学生组成的实验室集群管理团队来说,极大减轻了他们的运维负担。本文将详细介绍新方案的选型与实践历程。

01 深度学习场景存储需求

实验室集群主要用于深度学习的训练和方法验证。在训练过程中,我们面临四种主要的读写需求。

  1. 训练过程中需要读取大量的数据集文件。

  2. 在模型训练的初始阶段,我们需要加载一个 Conda 的环境,这涉及读取众多不同大小的库文件。

  3. 在训练过程中,随着模型参数的调整,我们需要频繁地写入不同大小的模型参数切换文件,这些文件的大小可能从几十兆字节到数几字节不等,具体取决于模型的大小。

  4. 我们还需要记录(每次训练的)训练日志,这主要涉及到少量数据的频繁写入。

基于上述需求,我们对存储系统有以下四个主要期望:

  • **首要的是,系统在高并发的读写环境下应具备出色的稳定性。**在确保稳定性的基础上,我们再追求性能的提升。

  • 其次,我们希望系统能够消除单点故障,即任何节点的故障都不应影响整个集群的训练进程。

  • 第三,我们期望系统具有友好的运维特性。考虑到我们实验室团队成员主要专注于 AI 深度学习,对存储系统的专业知识了解有限,因此我们需要一个运维简便、维护频率较低的存储系统。

  • 最后,我们希望系统能够提供一个 POSIX 接口。由于我们使用 PyTorch 框架进行模型训练,如果系统支持 POSIX 接口,将极大地降低用户的学习成本,同时减少对现有代码的改动。

实验室现有的硬件配置

首先,我们的硬件系统主要包括三类设备。

第一类是数据节点,我们目前拥有大约三四台这样的数据节点。这些节点主要由大量机械硬盘构成,每个节点的机械硬盘都搭建RAID6阵列,所有节点加起来的总存储容量近 700TB。

第二类是我们的计算节点,这些节点主要搭载了 GPU,用于处理计算密集型任务。在存储方面,由于 JuiceFS 具有缓存功能,我们为这些节点配备了 1 至 3 个 SSD 缓存盘,每个盘的大小约为 800GB。尽管这些缓存盘的大小看起来并不庞大,但它们通常能够缓存多个完整的数据集,前提是数据集的大小适中。

第三类是基于我们使用 TiKV 作为元数据引擎的考量,我们配置了三个 TiKV 节点,专门用于作为元数据引擎运行。这些节点的数据盘容量大约为 2TB,内存则主要配置了 512GB。根据目前的使用情况,当处理大约 5 至 6 亿的文件量时,每台节点大约使用了 300 多 GB 的内存。

NFS 的存储解决方案所面临的挑战

在初期,当计算节点数量较少时,我们采用了在数据节点上构建单机文件系统的策略,并通过简单的 NFS 挂载方式,将这些文件系统挂载到各个计算节点的目录上。这样,当需要添加第二台数据节点时,我们只需将其挂载到新的目录上,即可实现数据在所有节点间的共享。此方案在运维上相对简单,但随着节点数量的增加,我们逐渐发现基于 NFS 的存储系统性能显著下降。

在高并发训练高峰期,系统常出现明显的卡顿甚至卡死现象,这极大地影响了我们的工作效率。为了缓解这一问题,我们曾尝试将集群划分为两个小集群,以减少每个节点的数据压力。然而,这一临时措施并未带来显著的改进,反而带来了新的问题,如不同集群间数据的互通性受限。

总的来说,我们在处理大量小文件读取的场景下,原有的 NFS 存储方案表现出了较低的读取性能和较高的宕机风险。此外,整个系统缺乏缓存机制,而深度学习数据集在训练过程中需要频繁读取,这无疑增加了读写压力。因此,我们需要寻找一种更为高效、稳定的存储解决方案,以应对这些挑战。

02 为何选择 JuiceFS

以下是我们选择 JuiceFS 的主要原因:

  • JuiceFS 支持 POSIX 接口,这意味着在迁移系统时,用户几乎不会感受到任何影响,实现了无感的迁移体验。

  • JuiceFS 的缓存功能对于深度学习模型的训练尤为重要。尽管在首轮数据加载时可能无法直接命中缓存,但随着训练的进行,后续轮次几乎能够百分之百地利用缓存,从而显著提升训练过程中的数据读取速度。

  • JuiceFS 的回收站功能为用户提供了数据恢复的可能性,我们设置的为期一天的回收站使得误删的文件在一天内得以恢复,这在实际应用中已帮助用户及时恢复了误删的数据。

  • JuiceFS 还配备了一系列特有的文件管理命令,这些命令不仅方便用户使用,而且相较于传统文件系统,其功能更为快速和高效。

  • 值得一提的是,在运维层面,JuiceFS 的表现格外出色。由于我们实验室没有专职的人员负责存储,我们期望的存储系统应具备简单性和低运维频率的特点,而 JuiceFS 恰好满足了这些需求。它提供了丰富的文档资源,使得我们在上手和解决问题时能够迅速找到解决方案。JuiceFS 能够自动备份元数据,为数据提供了额外的保护层,增强了我们的安全感。JuiceFS 自带的 Prometheus 和 Grafana 监控功能使得我们能够方便地在网页端查看整个系统的状态,包括文件数量的增长情况、文件使用量以及整个系统的大小等关键信息,这为我们提供了及时的系统监控和管理的便利。

03 搭建基于 JuiceFS 的存储系统

首先,我们的 JuiceFS 采用了 TiKV 作为元数据引擎,同时利用 SeaweedFS 作为对象存储。

总体来看,JuiceFS 分为两个目录进行挂载。第一个目录用于存储用户文件,采用独立挂载的方式使我们能够灵活调整挂载参数,以满足不同目录的特定需求。在用户文件目录方面,我们主要沿用了默认的挂载参数,以确保稳定性和兼容性。

第二个在数据集目录方面,鉴于其几乎只读的特性,我们特别增加了元数据缓存的失效时间。这样做可以在多次读取过程中多次命中元数据缓存,从而避免重复访问原始数据,显著提升数据访问的速度。正是基于这一改动,我们选择了将数据集和用户文件分别挂载于两个不同的目录。

此外,我们还进行了一项实践上的优化。考虑到计算节点的主要任务是处理计算任务而非后台任务,我们在一台相对空闲的节点上配备了较大的内存,专门用于处理后台任务,如备份元数据。随着文件数量的增加,备份元数据对内存的需求也逐渐增大,因此这种分配方式既确保了计算节点的性能,又满足了后台任务对资源的需求。

元数据引擎选型:Redis vs TiKV

起初,我们使用了两个数据引擎:Redis 和 TiKV。在数据量相对较小,即上千万级别的阶段,我们选择了 Redis。当时,由于我们对这些软件并不十分熟悉,我们参考了相关文档,并考虑到 Redis 的上手难度较低、性能较高,且相关资料丰富,因此决定采用它。然而,随着文件数量的迅速增长,Redis 的性能出现了显著的下降。

具体来说,由于我们为 Redis设 置了 RDB 持久化,当内存占用量增加时,Redis 几乎持续处于 RDB 备份状态,这导致了性能的明显下滑。另外,我们当时采用了哨兵模式,并启用了主从复制以增加可用性,但这也带来了问题。由于是异步复制,主节点宕机后,从节点的数据一致性难以保证。

此外,我们了解到客户端并不会从 Redis 的从节点上读取元数据,而是主要依赖主节点进行读写操作。因此,随着文件数量的增加,Redis 的性能进一步下降。

随后,我们考虑并测试了 TiKV 作为元数据引擎的替代方案。从官方文档来看,TiKV 的性能仅次于 Redis,并且在实际使用中,用户层面的体验与 Redis 相差不大。在数据量达到 5 至 6 亿文件的情况下,TiKV 的表现相当稳定。

TiKV 的另一个优势在于其负载均衡和冗余存储的能力。我们采用了三台节点的配置,每个节点都具备多个副本,确保了数据的安全性和可用性。对于运维团队来说,这些特性极大地减轻了工作负担,提高了系统的稳定性和可靠性。

元数据迁移 :从 Redis 到 TiKV

我们于今年一月份左右进行了系统迁移,并已经稳定使用 TiKV 近半年,期间未出现宕机或任何重大问题。

在迁移初期,由于 Redis 难以支撑高达 5 至 6 亿文件的元数据负载,我们决定采用导出与导入的方法来实现迁移。具体地,我们使用了特定的命令将 Redis 中的元数据导出为统一的 JSON 文件,并计划通过 load 命令将其加载到全新的 TiKV 系统中。

然而,在迁移过程中,我们遇到了一个挑战。我们注意到,某个用户的目录由于文件深度过深或其他原因,在导出时遇到了失败。为了解决这个问题,我们采取了一种创新的方法。我们将除问题目录外的其他所有目录分别导出,并手动打开和拼接这些 JSON 文件,以重新构建完整的元数据结构。

在手动处理这些文件时,我们发现元数据的 JSON 文件结构清晰,拼接操作相对简便。其嵌套结构与目录结构基本一致,这使得我们能够高效地处理元数据。最终,我们成功地将这些文件导入到 TiKV 中,完成了迁移过程。

04 为什么使用对象存储 SeaweedFS?

总体而言,我们遵循了 SeaweedFS 官方文档中推荐的主要组件和基本功能,并未涉及过多新颖或高级特性。在具体部署上,我们采用了 CPU 节点作为核心,并在其上运行了 master 服务器。而数据节点则分布运行在不同的物理节点上,每个节点均运行 volume 服务以处理数据存储。此外,我们在相同的 CPU 节点上运行了 filer 服务器,该服务器为 JuiceFS 提供 S3 服务接口,负责与 JuiceFS 进行对接。从目前的运行情况来看,CPU 节点的负载并不重,主要的数据读写和处理任务均分散在各个 worker 节点上。

在数据冗余和备份方面,我们充分利用了 SeaweedFS 的冗余特性。逻辑上,我们将数据节点划分为两个 Rack。当数据写入时,它会在两个 Rack 中都进行写入,确保在 Rack 0 和 Rack 1 中各有一份备份。只有当两个 Rack 都成功写入数据时,才视为写入成功。虽然这种策略使得我们的硬盘容量在逻辑上几乎减半(因为每份数据都要写两份),但它确保了系统的高可用性和数据安全性。即使其中一个 Rack 中的某个节点出现故障或宕机,也不会影响整体的读写操作和数据安全。

SeaweedFS 的优缺点

首先,从上手难度来看,SeaweedFS 相较于工业界广泛使用的 Ceph,显得更为容易操作。其 master、volume 和 filer 等核心组件均可通过编写脚本来轻松启动。用户只需浏览对应的命令参数,并根据实际需求进行配置,随后通过脚本即可完成启动过程。尽管 SeaweedFS 的文档资料相对较少,但其易用性依然值得肯定。

此外,与另一款常用的对象存储系统 Minio 相比,尽管我们未曾直接使用过 Minio,但根据以往案例和介绍,Minio 在处理大量小文件时可能存在一定的不足。因此,我们在选择时并未考虑 Minio。

SeaweedFS 的另一个显著优点在于其安全性和可用性。通过将数据节点划分为两个 Rack,系统实现了数据的冗余备份,从而提高了数据安全性。同时,其master服务器具备自动调度数据写入的功能,能够自动将数据分配到各个 worker 节点上,实现了负载均衡。此外,SeaweedFS 的扩容过程也相对简单,只需新增机器并连接到 master 节点,系统即可自动进行扩容。

此外,SeaweedFS 官方还推荐了一款 master 管理脚本,该脚本配置简单,仅需编写少量配置即可实现定期自动修复数据冗余和平衡数据分布的功能,极大地提高了系统的维护效率。然而,SeaweedFS 的缺点也不容忽视。其文档资料相对较少且较为陈旧,这可能会给新用户带来一定的学习难度。尽管我们在使用过程中并未遇到其他明显的缺点,但这也是未来需要改进的地方。

05 方案实践中遇到的挑战

使用过程中客户端会异常退出

首先,我们曾遭遇客户端异常退出的情况,经过分析发现这是由于内存溢出(OOM)导致的。随着原数据的不断增长,当计算节点未启用 --no-bgjob 选项时,特定节点因执行高内存消耗任务(主要是自动元数据备份)而导致剩余内存不足,进而无法备份原数据,最终引发 OOM 并导致客户端退出。为了解决这个问题,我们在所有计算节点上添加了--no-bgjob 选项,并利用闲置的数据节点作为专门的后台任务处理节点。

首次大文件的读取较慢

其次,我们在使用初期发现了一个性能问题。特别是在首次读取大文件时,发现读取速度远低于千兆网络带宽的极限。经过深入调查,我们发现这是由于在测试对象存储性能时,未正确配置 JuiceFS 命令的参数。我们没有指定 --storage s3 选项,导致测试默认为本地硬盘性能,而非实际的对象存储性能。这一误解导致了我们对对象存储性能的误判。通过进一步检查,我们发现 SeaweedFS Filer 元数据引擎的性能瓶颈,这主要是因为底层使用的是单个机械硬盘。因此,我们将考虑优化这一环节以提升性能。

解压数据集较慢(大量小文件写入)

最后,我们在日常使用中偶尔需要解压数据集,这涉及到大量小文件的写入操作。我们发现这一过程相较于本地解压明显较慢。在与 JuiceFS 官方沟通后,他们建议我们尝试使用 write-back 加速功能。这一功能允许在文件写入后立即返回,而后台则负责将数据上传到对象存储。我们计划在未来实施这一建议以优化解压性能。

希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。