本案例来自于自动驾驶行业的用户的分享。自动驾驶行业面临的数据量规模是前所未有的,从 2021 年开始,团队决定自建云原生存储平台,在综合考虑成本、容量弹性和性能等因素后,最终选择了基于 JuiceFS 自建存储底座。目前,存储平台已成功管理百亿文件,IOPS 达到 40 万,吞吐量近 40GB/s,性能水平与原商用存储相当,同时大幅减少了存储成本。
01 自动驾驶行业的存储挑战
自动驾驶研发每天都将产生海量的数据,每一辆测试车每天将收集大约 60 TB 的数据。业界 Mobileye 公司的数据量已经在 200 PB 以上,通用旗下的自动驾驶业务部门 Cruise 的数据量甚至达到了 EB 级别。
与此同时,这些数据经过提炼后,也并不是静态存储的「冷数据」,还需要在模型训练等环节被高频使用。因此,海量数据存储和使用对于自动驾驶行业来说是一个前所未有的难题,面临多重技术挑战:
- 商用存储成本高。高性能商用存储的服务器和硬盘的采购周期以及价格都随市场有较大波动。
- 高吞吐的数据访问挑战。在 AI 场景中,随着企业使用 GPU 越来越多,底层存储的 I/O 已经跟不上计算能力。企业希望存储系统能提供高吞吐的数据访问能力,充分发挥 GPU 的计算性能。
- AI 场景对于百亿以上文件规模的存储管理需求越来越强。在自动驾驶领域,用于模型训练的是百 KB 的小图片,一个训练集由数千万张图片组成,一张图片就是一个文件,总的训练数据多达几十亿、甚至几百亿文件。海量小文件管理一直是文件存储领域的难题。
- 为热点数据提供吞吐扩展能力。在自动驾驶领域,用于模型训练的数据需要被多个研发团队共享读写训练,这会带来数据热点问题,即使数据存储的磁盘吞吐已经跑满,仍然不能满足应用端的需求。
- 混合云中的数据高性能访问。在混合云环境中,数据可能分布在本地、公有云和私有云之间。确保对这些分布在不同环境中的数据进行高性能访问是一项挑战。需要考虑数据的快速传输、访问延迟以及混合云环境下的数据一致性。
文件系统选型
基于上文提到的挑战,团队着手自建存储平台,首先是文件系统的选型。据市场不完全统计,行业内主流存储系统分为以下三类:云厂商文件存储、对象存储以及云原生文件系统。
- 云厂商文件存储:硬件配置通常都是全闪存,性能卓越,但价格高昂;
- 对象存储:成本都相对较低,训练速度太慢,不能满足训练场景的需求;
- 云原生文件系统:我们考察了多种系统,如 Alluxio、RapidFS、JindoFS、JuiceFS。
最终基于成本、容量、协议以及性能方面的综合考量选择了 JuiceFS。经过测试后发现 JuiceFS 在大文件顺序读写的场景中表现出色,但是在随机读写和小文件读写方面并不能完全匹配我们的需求。不过由于 JuiceFS 可以满足企业完全自主定制化的需求,以及在成本方面具备的优势,我们最终还是选择使用 JuiceFS。针对这些性能表现不够好的场景,我们持续进行了一系列的优化工作,将在下文详细说明。
02 JuiceFS 应用场景
场景 1:数据处理
在 IDC 中进行一些数据处理工作(例如抽帧)用以生成训练数据。最初, IDC 和云端对象存储之间的专线带宽是 40 Gbps,但随着数据处理需求的增加,专线带宽的压力显著增大,40 Gbps 的专线也迅速达到饱和。因此,我们将专线带宽从 40 Gbps 升级到了 100 Gbps。升级以后暂时缓解了一些带宽压力,但是在某些高峰期,仅仅一两个用户的数据处理需求可能就足以再次使得专线饱和。
为了应对对象存储专线带宽不足的问题,我们采取了临时措施:将商用存储用作 JuiceFS 的缓存盘,以降低数据处理过程中对于专线带宽的需求。
这个方案上线以后遇到了以下一些问题:
- 缓存共享的时效及性能问题:JuiceFS 会定期扫描缓存盘,扫描完成后文件索引会储存在内存中。这导致在用户 A 写入新的缓存数据后,用户 B 不能即时感知到这些更改。同时,如果存在大量的缓存数据,也会给索引扫描和更新带来压力。
- 缓存重建性能问题:鉴于缓存数据量很大,需要添加多个缓存目录,以便通过多线程的方式加快索引重建。不过,即便如此,缓存盘的命中率也不理想。
- TiKV的稳定性及性能问题:由于数据量巨大,磁盘 I/O 压力比较高,我们优化了 TiKV 的相关参数,包含锁、缓存、线程等,以及优化了故障域级别,解决了一系列稳定性和性能问题。
- 上传速度:某些机器的上传速度明显较慢。经过排查,发现这是因为某些机器的 CPU 设置在节能模式,于是我们将所有机器设置为高性能模式。
- 客户端读写慢:在上传过程中,初期的读写性能不佳。为此,我们优化了一系列参数,如调整 FUSE 队列大小和增加并发数,从而提高了上传和读取的性能。
经过优化后的系统已经能够有效地完成数据处理和抽帧入库的过程。在这个临时方案中,需要借助商用存储集群做为 JuiceFS 的本地读缓存盘。然而,随着后续建设的进行,我们将引入一个读写缓存集群,以完全替代基于 商用存储的临时解决方案,更详细的说明将在下文提供。
场景 2:模型训练
模型训练场景是众多场景中,对数据容量、性能要求最为复杂的一个场景。这个场景的挑战主要来自于:多云环境和数据访问性能这两个方面。
由于自建 IDC 的 GPU 资源有限。因此,我们与多家云厂商合作,实现了资源的弹性调度,同时达到了容灾和高可用性的要求,还降低了整体成本。
为了降本,我们在一些 GPU 价格相对低的地区做了更多的采购,并在多地部署。IDC 机房专线延迟为1-2毫秒,但从外地到 IDC 机房的专线延迟则为 4-6 毫秒,这样的网络延迟在使用 JuiceFS 进行亿级图片数据的模型训练时产生了不小的影响。
为此,我们考虑自建独立的缓存系统来缩短 I/O 访问时间,旨在保证每次训练都能在同一机房内高速读取数据,从而优化数据访问性能,减少网络延迟的开销。
读缓存集群建设
我们的业务主要侧重于读操作,例如某个场景其读写比例为 4:1 以上,优化读取性能就显得尤为关键。 由于 JuiceFS 社区版不支持分布式缓存,因此我们计划自己建立一个独立的缓存集群。与写缓存相比,读缓存不仅更易优化,而且不会导致数据丢失,只会影响读取速度。
读放大造成缓存集群负载高 我们在初期建设完读缓存集群后,又遇到了一些与读放大有关的问题。 这里要先解释一下 JuiceFS 的「预读」机制,根据不同的文件读取方式,JuiceFS 客户端会有不同的预读模式: 第一种叫做「prefetch(预取)」, 当随机读取文件的某个数据块(Block)的一小段数据时,客户端会异步将整个对象存储块下载下来(即默认情况下会下载 4 MB 数据)。 第二种叫做「readahead(预读)」,当顺序读取文件时,客户端会提前下载文件后续的数据块,对于单个文件来说最多会预读 8 个数据块(即默认情况下最多会预读 32 MB 数据)。 举个例子,当我们尝试从 JuiceFS 读取 4 KB 数据时,首先需要请求元数据引擎,随后从对象存储下载数据。当从对象存储读取数据时,由于 JuiceFS 的预读机制(不管是 prefetch 还是 readahead),即使我们只需读取 4 KB 或 128 KB 的数据,都可能会扩大到 4 MB 或者 32 MB,造成了读放大(特别是 readahead 带来的读放大),从而使得我们的缓存集群网卡承受巨大压力。
对于 prefetch 来说,可以通过 --prefetch
挂载选项进行调整,默认值为 1,如果设为 0 即表示关闭预取功能。而 readahead 目前没有选项可以控制,因此为了优化这一问题,我们对 JuiceFS 社区版进行了定制化开发,将 readahead 预读功能改进为可开关的模式(默认关闭)。新方案将 readahead 机制下沉到独立的缓存系统端,数据从对象存储里加载到缓存中,有效地缓解了缓存集群的压力。
另外,由于 readCache KV 接口不支持随机读,当我们想从中读取某一部分数据时需要下载完整的数据块,这也会造成一定程度的读放大。
例如,当我们需要读取 1 KB 的数据时,由于缓存系统的 KV 接口机制,需要从缓存集群中获取 4 MB 的数据。这明显放大了数据读取量,也增加了网络负担。为了解决这个问题,我们调整了默认的缓存策略,将缓存的 key 大小从 4 MB 改为可配置的 128 KB(对象存储中的数据块大小依然是 4 MB)。
综上,我们通过改进客户端预读策略,以及调整默认的读缓存策略,有效地降低了缓存集群的压力,显著提高了缓存数据的读取性能,基本控制在 1 ms 以内。
写缓存集群建设
当用户首次读取数据时,由于数据尚未被缓存在读缓存集群中,这可能导致读取速度较慢。为了解决这个问题,我们引入了写缓存集群的概念。
当写操作发生,数据首先被写入基于闪存自建的 Write Cache 集群。虽然我们曾考虑改造 readCache 支持写缓存和双写,但由于其实现机制可能在高负载下丢失写请求且改造难度高,最终我们还是选择使用 Write Cache 作为写缓存。
为了确保数据的可靠性,Write Cache 集群支持多种类型的冗余配置。数据首次写入后,小 I/O 根据需求选择双副本或三副本,而大 I/O 直接写入 EC 对象存储中。数据成功写入 Write Cache 集群后,会立即响应给客户端,同时异步拷贝至读缓存集群,确保用户下次能快速读取。
除了异步拷贝至读缓存集群,数据还需要异步上传到对象存储中。如果写入速度快而消费速度慢,可能导致上传延迟较高。因此我们采用多线程和多客户端上传的方式来加速上传,确保待上传的数据始终在可控范围内。同时为了避免多线程或者多客户端间的数据争抢导致的流量放大,我们引入了分布式锁机制。锁定的线程完成数据上传后会与对象存储中的数据对比,并在匹配时清除写缓存数据。
通过建立专用的写缓存集群,我们进一步优化了首次读取速度。现在,读写缓存都可根据需求动态调整。我们还引入了降级策略,如在达到某个水位线时改为同步写。
元数据缓存优化
目前,我们的 GPU 训练资源所在机房与元数据存储位于不同城市。由于专线延迟为 4~6 毫秒,每次元数据请求都需向元数据集群发起,导致整体性能难以满足 GPU 训练任务的需求。
尽管 JuiceFS 社区版支持使用内核和进程内存缓存元数据,但这种方法存在数据不一致的风险。 例如,当用户 A 更改数据时,用户 B 可能不会立即察觉这些更改(只有当重新打开文件后才能感知到)。特别是在标注场景生成 JSON 文件时,同一个文件可能会被频繁覆盖或追加写入。
为了解决这些问题,我们对 JuiceFS 社区版的元数据缓存进行了优化,具体方案为:
- 文件类型智能缓存:根据文件类型智能选择缓存策略。将动态文件和静态文件分类,然后采取不同的缓存机制,有助于提高缓存的效率和一致性。
- 元数据缓存在客户端或同一机房的缓存集群中:将元数据缓存在客户端内存或同一机房的缓存集群中是一种有效的优化策略,可以减少跨机房的元数据请求,提高元数据的访问速度。
场景 3:支持 HDFS 场景
为了统一存储底座技术栈和优化性能,我们决定将语音场景的存储集群从 HDFS 迁移到 JuiceFS + Ceph RADOS。尽管迁移过程中遇到了一些问题,但目前的解决方案已经能够满足业务的需求。
我们面临的主要问题是,JuiceFS 社区版支持 Java SDK,但不支持 C++ 和 Python 的 SDK。为了解决这个问题,我们与业务方和社区进行了讨论。考虑到自行开发的成本过高,我们做了一个折衷的决定,让业务方改变了 Python 和 C++ 程序访问存储集群的方式,从 SDK 转为采用 POSIX 接口,使数据访问就像读取本地硬盘一样。
03 针对 JuiceFS 的更多优化
支持 POSIX ACL 权限
在使用 JuiceFS 时,我们将不同的目录虚拟成一个 bucket,并分配给不同的业务使用。我们目前有数十个这样的 buckets,并且每个 bucket 都有多个业务组在使用。然而,关于权限管理的问题,社区版的 JuiceFS 默认仅支持简单的权限设置,无法满足复杂的权限需求,例如为特定的业务组或用户分配特定的 bucket。
为了解决这一问题,我们研究了 Linux 内核系统关于 FUSE 的文档,特别是 Linux 内核版本 4.9 中的改进,它引入了对 POSIX ACL(Access Control Lists)的支持。这意味着我们可以将权限管理的任务交给 Linux 文件系统内核来处理。然而,由于我们生产环境中的机器内核都是 3.10.x 版本,无法直接受益于这一功能。
因此,我们在用户态实现了 ACL 的权限管理功能,以解决权限控制的问题。现在,我们为用户提供了两个权限组选项:只读和读写模式。用户在申请权限时可以从这两个组中选择一个,以满足其特定的业务需求。通过这种方式,我们成功地解决了数据安全和合规性的问题,确保数据不会被未经授权的访问或泄露。
值得一提的是,这个新的权限管理方案已经在我们的生产环境中得到了成功的部署和应用,为我们的业务提供了更高级的权限控制,同时保护了数据的完整性和安全性。
自助删除服务
随着业务数据量的增长,业务面临巨大的成本压力。但是,JuiceFS 的删除速度慢。为避免长时间的积压,我们开发了一种自助删除服务,允许用户自行提交删除列表,并通过多任务方式执行。这不仅加快了删除速度,而且有助于降低业务存储成本。
收益:
- 降本:加快删除速度,帮助用户快速降本。
- 统一管理入口:所有的删除/迁移都有审计日志收集,方便进行历史追溯。
- 用户能自主查询进度和容量,方便用户提前规划和减少存储成本。
支持热升级
目前 JuiceFS 社区版不支持热升级功能,每次升级只能等应用停止访问以后滚动式升级,升级周期非常长,用户体验差且成本巨大。为了满足业务的需求,我们针对 JuiceFS 社区版定制化开发了热升级功能。
热升级流程如下:
- 管理员执行热升级动作,新进程向旧进程发起「挂起」请求;
- 旧进程接受挂起,停止接受新请求,并完成已接收的请求;
- 旧进程保存中间数据并及时通知新进程;
- 新进程根据接收到的数据恢复现场;
- 新进程开始处理用户请求,旧进程退出。
支持生命周期自动删除
在数据处理过程中,需要经过抽帧、标注和训练等步骤,以得到最终所需的数据。在这个过程中,会产生大量的中间数据,它们的成本控制和管理都相当不便。因此,用户需要针对不同的文件夹实施不同的生命周期自动回收策略,以解决这个问题。目前我们已经定制化开发了基于目录或者文件的生命周期配置功能。用户可以为指定的 bucket(相当于一个文件夹)设置 atime 或者 ctime 等规则,对符合规则的数据进行自动清理。
生命周期删除流程如下:
- 管理员在 web bucket 管理平台配置 bucket 生命周期策略;
- 生命周期策略服务根据上一步配置的策略,将目录拆解成多个子任务;
- 分布式调度服务执行子任务,生成符合策略的文件列表;
- 将最终生产的文件列表交给自助删除服务,对文件进行删除。
支持通过对象查询文件 inode
在使用云上的对象存储时,如果对象(object)意外损坏,通常需要根据对象来查找相应的文件。如果没有对象与文件之间的映射关系,可能需要对整个文件系统的元数据进行全面扫描,但是全量扫描元数据对于具有海量数据的文件系统来说成本很高,一般不太可能在生产环境中实施。
因此,我们提出了一个解决方案,通过设置对象存储自定义元信息,在每个对象的元信息字段中存储对应的文件 inode,来解决通过对象反查对应文件的需求。