规模决定架构。当容量来到 EB 级、文件数以千亿计、几十万客户端接入,每周有 200PB 数据新增和删除时,开源文件系统的每一个"单点假设"都会被击穿。地平线 AIDI(智能驾驶研发数据平台)同时使用 JuiceFS 社区版与企业版承载 EB 级数据;本文将聚焦 JuiceFS 社区版的实践,分享我们四个工程命题的解决思路、实践效果与工程取舍。
从 PB 到 EB,存储面临哪些新问题
AIDI 平台支撑地平线智能驾驶研发的数据闭环:路采和量产车回传的数据经筛选、标注进入模型训练,训练产出经评测发布,新问题数据再回流。存储是这条链路的底座,数据在每个环节的就绪速度,直接决定算力是否空转。
过去几年,这条链路的规模持续跃升:数据容量从 PB 级增长到 EB 级,训练算力从千卡规模增长到万卡。随着模型迭代加快、数据回流更加密集,存储负载也不断超出原有架构的设计预期。当前的规模和负载表现为:
- 容量:EB 级(10¹⁸ B),且仍在增长,已超出社区版常规测试覆盖的规模;
- 文件数:千亿级(10¹¹),回收站等单个目录可达数百亿 entry;
- 数据周变更:每周约新增 200PB、删除 200PB,对象存储侧持续产生数百亿待删除对象;
- 客户端数量:几十万,通过 Kubernetes(K8s)训练集群、容器存储接口(CSI)、兼容 S3 对象存储协议的网关和软件开发工具包(SDK)直读等方式接入;
- 负载特征:数据集的顺序大 I/O 与训练检查点(checkpoint)的随机小 I/O 并存,冷热分布悬殊,多租户共享同一套元数据(Meta)集群。
由此,我们面对四个工程命题:
- 千亿文件下,元数据如何避免热点并保持稳定?
- 每周删除约 200PB,如何实现可恢复、可审计的数据治理?
- EB 级容量下,如何兼顾训练吞吐与存储成本?
- 几十万客户端如何接入、限流和观测?
自 2021 年起,我们在 AIDI 平台使用 JuiceFS 社区版,并围绕上述命题持续开展定制。随着数据规模和使用场景扩大,我们也逐步引入企业版。目前两种版本均在平台中使用;下文以社区版实践为主线,介绍具体的改造与取舍。
命题一:千亿文件元数据优化
缓解元数据热点与大目录压力
文件数达到千亿级后,TiKV 元数据访问出现三个瓶颈:
- 训练高峰期,主副本(leader)所在节点的处理器(CPU)打满,从副本(follower)仍有余量;
- 事务向集群调度组件 PD(Placement Driver)的时间戳服务 TSO(Timestamp Oracle)申请全局时间戳,增加网络往返;
- 挂载配置、容量信息等热点元数据被反复查询。单纯调参已难以缓解这些压力,需要改造访问路径。
针对这些瓶颈,我们改造 TiKV Go 客户端的数据分片(Region)路由缓存,让 follower 分担读请求;减少高频点查的一次 TSO 往返;并以读缓存处理热点元数据。
相关改动在新旧版本上分别通过持续集成测试验证,并以构建时注入补丁的方式交付,避免长期维护上游依赖的独立分支。此外,缓存集群扩缩容时,我们尽量减少缓存键的大面积重新映射,降低缓存击穿风险。

此外,数十亿 entry 的大目录也会给客户端带来内存压力。我们采用流式按批扫描,避免 readdir 一次性构造完整数组导致内存耗尽,并支持内核 readdir 缓存。
限流保障集群稳定
几十万客户端共享一套 Meta 集群,任何客户端都能打爆它。为避免单个租户的流量影响整个集群,我们分别约束元数据和数据面的资源消耗:元数据侧基于 PD Resource Group(资源组)令牌桶,以 RU(请求单元)衡量并限制各租户的资源消耗;数据面分别限制缓存集群流量和对象存储回源流量。超出预算的客户端会被限速,避免压力扩散。限流预算采用声明式配置:运维在 YAML 格式的配置文件中维护目标状态,manager 对比当前配置并自动生成数据库建表、修改、删除等 SQL 语句,使配置收敛到目标状态。

元数据容灾:冷备兜底
元数据是整个系统的命门,千亿文件的归属关系保存在 TiKV 中,如果元数据无法恢复,即使文件内容仍在对象存储中,也难以确定归属。为应对不同风险,我们设置三层保障:
- TiKV 的 Raft 多副本应对硬件故障;
- 定期将 TiKV 全量快照备份到异地对象存储,为误删 keyspace 中的元数据、元数据逻辑损坏和地域级故障提供恢复手段;
- 对象存储保留文件内容,使元数据恢复后数据集可以重新使用。需要恢复时,我们重建 TiKV 集群并回放最近一份快照。
冷备的代价是元数据只能恢复到最近一次备份;此后发生的变更可能丢失,回退范围取决于备份间隔。在当前训练场景下,我们可以接受这一取舍。
命题二:数据洞察与数据治理---每周删 200PB 的降本工程
每周约 200PB 数据被删除时,逻辑删除只是起点:对象存储侧持续产生数十亿待回收对象,单机扫描难以及时清理;异步删除失败还可能留下难以从对象 key 追溯文件归属的遗漏对象。我们将删除决策、误删恢复、空间回收和泄漏审计串成一条治理链路。
删除决策:看板与业务确认
哪些数据该删、何时删除,需要业务先作判断,回收站无法替代这一环节。我们通过看板展示数据集的冷热分布、访问衰减和容量增长趋势,供业务制定删除与扩容计划。确认后,业务可自助发起删除或恢复;删除的数据先进入回收站。
看板依赖次日可查询(T+1)的元数据统计链路:我们先将 Meta 集群的文件信息全量导入湖仓建立基线,此后每天通过 changelog 增量同步。业务可直接查询容量、冷热分布和生命周期数据,无需存储团队代查。由于每周约有 200 PB 的新增和删除,这条链路一旦中断或发生数据漂移,就会影响容量及回收决策,因此必须保持稳定。

误删恢复:7 天回收站
平台删除的数据保留在回收站 7 天。这个窗口既给误删留下恢复时间,也缓冲每日 PB 级的删除量,避免回收压力直接冲击元数据引擎。
回收站针对不同路径采用以下机制:
| 机制 | 设计 | 解决什么 |
|---|---|---|
| 免回收站路径(挂载参数) | 训练临时目录等明确免恢复路径直接跳过回收站 | 防止回收站被海量删除垃圾撑爆 |
| 定时批量清理 | 按时间桶批量清理 7 天前数据,全程可观测,支持恢复 | 回收站容量治理与误删恢复 |
| 流式删除定位 | 流式扫描定位已删除文件 | 误删秒级恢复与删除审计 |
保留期内,业务可通过恢复平台或命令行工具自助找回数据,无需运维工单。恢复通过元数据回滚和对象引用重建完成,无需复制数据本体;平台支持每日 PB 级恢复,单次 PB 级恢复可在小时级完成。删除与恢复均通过访问控制列表(ACL)校验权限,并记录审计日志。
空间回收:分布式垃圾回收(GC)
社区版 GC 依赖单机扫描,难以跟上每周约 200PB 的删除量。我们将待删除的文件和数据片段(slice)记录在 TiKV 中,再把回收任务拆成多个分片,交给多个工作节点(worker)并行处理。
worker 通过租约领取任务,处理完成后删除对应的任务记录,再领取下一批。任务在完成前始终保存在 TiKV 中;即使某个 worker 故障,未完成的任务也能由其他 worker 接手。这样既分散了回收压力,也无需另建消息队列。
租约决定由谁处理任务;对象能否删除,则由删除事务中的引用计数仲裁。只有引用计数达到可删除条件(当前实现为负值)时,才执行物理删除,避免并发处理或重试导致重复删除。对象存储侧通过批量删除、退避重试和审计记录完成回收。
同样的分布式处理思路也用于大文件整理。长期追加的大文件可能累积多个 slice,增加读取开销。我们通过另一套 gc_cluster,由 Manager 向多台 worker 分发 compaction 任务,定期在线合并这些 slice,以控制读放大。

泄漏审计:leak-check
除按元数据记录执行物理回收外,我们还用 leak-check 审计遗漏对象。社区 fsck 检查"元数据有引用、对象却缺失"的情况;leak-check 则反向查找"对象存在、元数据已无引用"的情况。对象存储侧按 chunks/ 子前缀 32 路并发列举,缓解单连接扫描的瓶颈。我们分别取得对象清单和元数据引用清单,经两路 extsort 外部排序后流式归并,生成候选清单,无需在内存中构建千亿级全量 map。

比对结果只是候选清单,不能直接删除:在途写入的对象可能暂时没有元数据引用。审计默认只生成清单,并过滤近期对象;确认后才执行删除。这样既能处理遗漏对象,也能降低误删在途写入的风险。
命题三:如何兼顾训练性能与 EB 级存储成本?
训练需要百 GB/s 量级的吞吐,还要处理数据集的顺序大 I/O 与 checkpoint 的随机小 I/O。在 EB 级容量下,数据不能全部放在热层;算力跨云部署也要求数据尽量就近访问。
三级缓存:从客户端到集中式集群
三级缓存按共享范围逐级扩大:一级缓存(L1)复用当前客户端的数据块,二级缓存(L2)通过客户端间的点对点(P2P)方式共享数据块,三级缓存(L3)集中承载训练热数据的读写。读取时依次查找 L1、L2、L3,三层都未命中才访问对象存储。
多个训练任务读取同一数据集时,L1 缓存无法在客户端间共享,仍可能重复回源。L2 让客户端互为缓存节点:一个客户端已有的数据块,可供其他客户端通过按范围读取接口(getrange)复用。数十个任务并发读取相同数据时,回源量可接近一份,从而利用已有的本地盘和内存节省带宽。
L1 和 L2 都未命中的请求进入 PB 级 L3 集中式缓存集群。L3 承接跨客户端共享的热工作集和主要读吞吐,并在同一集群中提供读缓存与写聚合,分别服务数据集的顺序大 I/O 和 checkpoint 的随机小 I/O;集群可通过水平扩展增加热数据容量。
缓存故障可能使几十万客户端集中回源,放大对象存储压力。我们通过健康检查摘除异常节点,恢复后重新加入;若读缓存整体不可用,则降级为直读对象存储,以更高延迟换取访问连续性。同时记录各层命中量、字节数、延迟和慢请求,以识别缓存退化及回源变化。
元数据缓存:减少重复查询
训练任务读取文件时,不仅要获取数据块,还会反复查找文件、读取文件属性和打开文件(lookup、getattr、open)。这些操作都需要查询元数据。为减少对 TiKV 的重复访问,客户端用 open cache 缓存查询结果。
难点是文件变化速度不同:数据集图片、打包文件通常长期不变,适合缓存得久一些;索引、日志等文件可能持续更新,需要尽快看到变化。如果所有文件使用相同的缓存过期时间(TTL),设得太长可能读到旧信息,设得太短又会让大量不变的文件反复回源。
因此,我们按文件类型设置三档 TTL:静态文件使用较长的过期时间,动态文件使用秒级过期时间,其余文件使用默认值,配置支持 YAML 热更新。对静态文件,命中续租(expire_lease)会重置过期时间,让常用文件持续留在缓存中、冷文件自然过期;修改时间过滤(filter_mtime)则让最近修改、可能仍在写入的文件不进入缓存,避免缓存尚不稳定的元数据。

控制读放大:预取与缓存协同
预取能加快顺序读取,但预取过多也会放大回源压力,因此我们在缓存层和内核层分别控制。缓存层根据对象 key 中的块序号提前加载后续数据块,通过去重表和有界队列限制并发;队列满时丢弃预取,不阻塞正常读取。内核层的动态 readahead 根据顺序或随机访问调整窗口,受全局水位约束,配置可热更新。

数据就近与冷热分层
前述改造提高了热数据的访问效率;要控制 EB 级容量成本,还需要决定数据放在哪里。当训练算力跨云部署时,我们让多个区域的桶共享同一文件系统命名空间,元数据记录对象的归属桶与存储层级。读请求据此路由到目标桶,因此数据迁移后业务访问路径无需改变。
数据通过服务端桶间直连迁移,不占用客户端带宽;迁移期间,读路径由热层、冷层和全池遍历兜底。数据集或目录的归属桶及迁移策略由声明式配置管理。
训练算力迁往其他云时,可提前将数据移至相应区域,降低跨云读取对专线带宽的依赖;低频数据则下沉到低成本桶或磁带库等冷数据池,并按需取回。这样既让热数据靠近计算,也控制 EB 级容量成本,容量扩展不再只依赖单一云厂商。此外,关键数据集还可保留跨云备份桶,作为云故障时的读取兜底。

命题四:几十万客户端,如何接入和管理?
几十万客户端同时接入时,多租户权限、接入方式和客户端观测都需要重新设计。我们分别从权限控制、训练任务接入和客户端可观测性三个方面进行处理。
多租户:权限控制
由于生产环境的内核版本无法在 FUSE(用户态文件系统)挂载路径启用符合 POSIX(类 Unix 系统接口标准)的 ACL,我们将租户鉴权放到用户态的 ACL Manager。它以 Bucket 为授权单元,通过 READER、MAINTAINER 角色组管理权限,借助 guid 文件补充 LDAP(轻量级目录访问协议)组信息,并记录审计日志。

这样,LDAP 侧只需维护组员关系;ACL 语义在用户态实现,绕开内核版本限制,共享文件系统中的越权访问在鉴权阶段被拒绝。代价是用户态鉴权增加开销,我们用三层 inode 缓存缓解。
接入:S3 网关与免挂载 SDK
POSIX 挂载之外,我们提供基于 MinIO 插件层的 S3 Gateway 和进程内直读 SDK 两条接入路径。
部分应用已使用 S3 协议,需要沿用现有接口访问同一份数据。S3 Gateway 将对象协议请求转换为文件系统操作,与 POSIX 挂载和 SDK 共享同一文件系统命名空间,并复用权限、缓存和底层数据。插件层实现目录级租户隔离和用户 token 校验,修复 ListObjectsV2 分页慢查询;我们还裁剪了非 S3 后端,形成轻量镜像。此外,针对跨云专线场景,我们提供 S3 协议的轻量读侧 SDK。
另一条路径面向高并发训练。随着任务和 dataloader 进程增多,客户端同时活跃的数据连接达到百万级;FUSE 访问需经过内核态转发,短生命周期任务还要反复挂载和卸载,连接与挂载开销不断累积。为此,我们自研进程内直读的 Python SDK,让训练代码免挂载访问数据,连接随进程创建和退出。免挂载的代价是 SDK 需自行管理进程连接、fork 后的 session 和心跳。
SDK 随容器启动,通过 fsspec、ffrecord 接入现有数据加载流程。针对 dataloader 的 fork 场景,子进程重建 session 和心跳线程并独立上报;运行期间,SDK 定期上报指标,接收服务端下发的心跳间隔与限速策略。训练侧采用只读访问,多层拦截写操作;路径校验和跨桶隔离限制越权访问。

指标观测与客户端升级
几十万客户端各自产生数百项指标。如果由 Prometheus 逐个抓取,时序规模会迅速增长。我们让客户端按分片向 MetricManager 上报指标,再由 Prometheus 抓取聚合结果。MetricManager 通过合并或舍弃部分细粒度标签控制时序规模,代价是部分观测维度无法继续保留。聚合服务还需支持配置热更新、异常恢复、过载丢弃和过期清理,避免自身成为新的瓶颈。
在几十万客户端规模下,发布升级也需要尽量降低对训练任务的影响。Gateway 和文件系统客户端(FS)镜像由同一构建链路产出;客户端无状态化配合 K8s 滚动重启,支持逐批升级:SDK 直读和 S3 网关路径对训练任务无感,POSIX 挂载路径的影响窗口可控。
上述改造中,批量缓存接口、ReaddirFrom 流式扫描和动态 readahead 等通用能力,正在评估,整理后回馈社区;ACL、治理闭环等与业务耦合较深的能力,则继续在内部演进。
小结
回顾上述实践,我们将应对 EB 级容量、千亿文件和几十万客户端规模的方法归纳为四点:
-
以分散化设计消除集中瓶颈。我们让元数据读请求由 follower 分担,减少高频点查的 TSO 往返;优化热点 key,避免单节点被打爆;同时深入定制化元数据,提升整体能力。架构设计还需评估关键组件承担全局负载时的失效影响。
-
以闭环机制治理数据生命周期。看板支撑业务删除决策,7 天回收站与 PB 级恢复能力提供误删保护,GC 负责空间回收,leak-check 审计遗漏。恢复吞吐与删除规模相匹配,才能尽量把回收异常控制在空间收敛延迟范围内,降低误删损失与永久泄漏风险。
-
以分层缓存和有界预取保障训练性能。L1 客户端缓存、L2 客户端 P2P 缓存和 L3 集中式集群逐级吸收访问压力;预取则设置总量限制和水位约束,并在队列满时丢弃任务,避免额外读放大拖慢正常请求;同时通过数据就近和冷热分层降低跨云访问与 EB 级存储成本。
-
以权限和资源预算约束多租户影响。ACL 明确访问范围,令牌桶限制各租户的元数据与数据面资源消耗;基于 RU(请求单元)的资源预算通过声明式配置管理,避免单个客户端影响共享集群。
随着数据、算力和业务复杂度持续增长,我们将继续完善元数据引擎、数据面、性能工程、治理机制与可观测性,并在下一次数量级跃迁到来前完成必要的架构准备。持续识别并提前解决潜在瓶颈,是我们后续演进的重点。
我们希望本文中的一些实践经验,能为正在面临类似问题的开发者提供参考,如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。