
把 AI 训练集、模型权重往 RustFS 这类对象存储里一丢,爽的是容量近乎无限、多副本持久化、S3 接口谁都能读。别扭的也是接口:训练框架要的是 POSIX 文件语义,也就是能 open/read/seek、能重命名、能列目录,还要 GPU 跑着跑着就近读到热数据,而不是每次都走网络把对象整块拖回来。对象存储原生没有 POSIX,热读反复拉取也慢,随机 seek 和按 offset 读一块这种操作更是它的弱项,这层 gap 得有人补。典型的训练循环里,每个 epoch 都会把整个数据集从头读一遍;如果每次读都回源到对象存储,GPU 大部分时间在等数据而不是在算。把热数据缓到离计算更近的地方,等于把这份重复读的成本摊掉。
Curvine 就是补这块的。它是用 Rust 写的分布式缓存文件系统,在对象存储之上叠了一层 POSIX 文件语义,再挂上多级缓存(内存 → SSD → HDD)。通过 FUSE 挂载后,应用把它当成一块本地盘来读写,训练脚本一行都不用改。对 RustFS 来说,Curvine 是站在它和训练任务之间的加速层:RustFS 继续管持久化和多副本,Curvine 管怎么让上层像读本地盘。

Curvine 把 RustFS 当脚下的持久化底座
Curvine 的存储层设计就是接任意 S3 兼容后端,官方文档明确列出 AWS S3、Azure Blob、GCS、OSS,以及 MinIO 这类。RustFS 100% 兼容 S3,所以天然在这个名单里,不用做任何适配。
这里有个对 RustFS 用户特别实在的点:元数据独立性。Curvine 的文件元数据路径和底层 S3 对象路径是一一对应的,哪怕 Curvine 整个集群挂了,RustFS 桶里的对象还是原本的结构,拿 S3 客户端就能直接读出来。换句话说,加速层是可插拔的,没把你的数据锁进另一套私有格式里。把 RustFS 当底座,加 Curvine 是锦上添花,不是把命交给它。
另外存储层兼容多云对象存储,RustFS 之外的桶也能一起挂进来,做成跨存储的统一命名空间。这点在多团队共用、又不想把数据都搬进同一个桶时挺有用。Curvine 自己还分了协议层:除了 FUSE,它带 S3 网关、HDFS 适配器、Java/Python/Rust SDK,以及 Kubernetes 的 CSI 驱动。Master 节点的 Web UI 在 9000、Worker 在 9001,集群状态一眼能看。这组默认端口和 RustFS 的 S3(9000)、控制台(9001)正好重合,同一台主机同时跑两者会直接撞端口,要么分开机器部署,要么把 Curvine 的 Web 端口改到别的值。
多级缓存怎么配:内存、SSD、HDD 三层
Curvine 的缓存不是单块盘,而是按介质分层的。Worker 节点的 data_dir 用方括号前缀标出介质类型,框架会按访问热度在三层之间自动晋升和降级:
toml
[worker]
data_dir = [
"[MEM]/data/mem", # 最热数据,内存层,纳秒级
"[SSD]/data/ssd", # 次热数据,SSD 层
"[DISK]/data/disk", # 冷数据兜底,HDD 层
]
[MEM]、[SSD]、[DISK] 这三种前缀就是缓存层的介质标记,框架认前缀不认书写顺序。热数据被反复读时自动往 [MEM]/[SSD] 提,冷下来再落回 [DISK],不用人工迁。生产上一般内存给小、SSD 给中、HDD 给大,三层容量加起来就是这台 Worker 能缓存的总量;想多挂几块盘,就多写几个 [DISK] 路径,Worker 各自管自己的缓存盘。
缓存的淘汰和容量也在这层管:dir_reserved 给每块盘留底空间,避免把宿主机的其他应用挤爆;缓存命中不上 RustFS,miss 才回源拉对象再塞进对应介质。Master 走 Raft 只管元数据一致性,不碰缓存数据,所以加 Worker 就是横向扩缓存容量,扩起来不牵连元数据层。
Worker 是缓存的持有者,缓存是本地状态,某台 Worker 重启,它那部分的缓存就清空、从头回 RustFS 预热;[MEM] 层的数据只驻留在进程内存,Curvine 进程一重启就全部失效,别把内存层当持久层用。规划容量时别把单台 Worker 的缓存当成永久层,热数据要靠多 Worker 分摊。三层之间也不是严格温度计:一个对象落进哪层由访问频率决定,冷太久会从 [MEM] 退到 [DISK],长时间不读就按 TTL 过期回源。所以缓存是加速用的,真相始终在 RustFS 那侧,这一点设计上就定死了。
容量规划上,内存层不必等于整个数据集,只要 cover 住最热的索引和小文件;SSD 层放当前在训的那几轮数据;HDD 层当兜底,容量接近数据集大小即可,毕竟冷数据回源也不慢。三层都不必照搬整份数据,按访问频度分层才发挥得出多级缓存的价值。单台 Worker 的盘不够,就多挂几个 [DISK] 路径或者多加 Worker,缓存总量是横向拼出来的。
接进来只要一条 mount 命令
Curvine 把底层存储统称 UFS(Unified File System,对象存储、HDFS 都算 UFS)。因为 RustFS 兼容 S3,接进去就是标准的 S3 挂载方式,把 endpoint 指过去:
bash
bin/cv mount \
s3://<bucket>/<prefix> /s3/train-set \
--write-type cache_mode \
-c s3.endpoint_url=http://<rustfs-host>:9000 \
-c s3.credentials.access=<access-key> \
-c s3.credentials.secret=<secret-key> \
-c s3.region_name=cn
上面几个参数都是 Curvine UFS 挂载 的真实配置项:
s3.endpoint_url填 RustFS 的 S3 地址,默认端口 9000;s3.credentials.access/secret是 RustFS 的 access key 和 secret key;s3.region_name没特殊要求填cn即可;s3.force.path.style默认true,走标准 path-style 寻址,RustFS 也是这个方式,不用改;s3.list_objects_version默认v2,RustFS 兼容,也不用改。
挂的时候还能用 -s ssd 指定 Curvine 本地缓存介质(可选 mem/ssd/hdd),--ttl-ms 7d 控制缓存过期,默认七天。挂载表存在 Curvine 的元数据里,集群重启不用重新挂。跑 bin/cv mount 能列出全部挂载点,包括写模式、TTL 策略和 provider。如果 RustFS 那侧会被别的工具改,挂的时候加 --read-verify-ufs=true,让每次读先比一下 mtime 和大小,避免读到过期缓存。代价是每次读多一次 UFS 元数据请求,高 QPS 热读场景要权衡这个值不值。挂完用 bin/cv report 看一眼挂载点状态,确认 write-type 和 endpoint 对得上,再让训练脚本试读一个文件,比直接开训更稳。
cache_mode 还是 fs_mode,得看 workload
--write-type 决定 Curvine 怎么对待 RustFS 这层后端,两个模式差别很大:
cache_mode 把 RustFS 当只读源加缓存后端,目录结构以 RustFS 为准,Curvine 只缓存热读。训练集加载、模型分发这种读多写少的场景最对路,热数据就近命中,冷数据回 RustFS 拉。要注意两点:一是 cache_mode 下删文件会直接删掉 RustFS 后端的原始数据 ,不是只清本地缓存,这是最容易凭直觉踩的坑,动手前一定想清楚;二是缓存窗口内如果 RustFS 被别的工具改了,Curvine 不会自动感知,挂的时候加 --read-verify-ufs=true 让它每次读先比一下 mtime 和大小。
fs_mode 给的是完整 POSIX 语义,写先落 Curvine、再异步回 RustFS,适合当通用工作区。代价也明显:对象存储不支持 rename ,在 RustFS 上 rename 一个大目录会触发整目录拷贝,又慢又费带宽,别这么干;而且 RustFS 里被直接改的数据不会自动回灌 Curvine,得定期跑 bin/cv mount resync /path 对齐元数据。
加速能到什么程度,代价是什么
Curvine 官方标称约 100μs 级访问延迟、100K+ 稳定 QPS,单集群能撑 50 亿个小文件。多级缓存不是静态分层,Worker 会按访问热度把热数据自动晋升到更快的介质,冷数据再降回去,不用手动迁。它还在 LTP 测试里过了 1129 个用例,POSIX 兼容性不是纸面说法。
官方基准里有一组同硬件对比开源 Alluxio 的 256K 顺序读(GiB/s):单线程 Curvine 2.2 对 Alluxio 0.6,16 线程 9.2 对 7.9;元数据操作在并发 40 下,create 跑到 19985 QPS,对比 JuiceFS 16000、直连 OSS 2000。资源层面,对比 Alluxio 线上部署内存省 90% 以上、CPU 省 50% 以上,这是 Rust 无 GC 内存模型带来的直接好处。数字都来自 Curvine 官方基准页,要上生产自己照着压一遍最稳。
代价要摊开说:多引入一层 Curvine 集群得自己运维,Master 走 Raft 做高可用、Worker 管缓存(但要起多个 master 实例 Raft 才生效,单 master 就是单点,生产至少三个);缓存和 RustFS 之间谁是真相源要想明白,cache_mode 真相在 RustFS,fs_mode 写异步回 RustFS 有最终一致窗口;rename 大目录是对象存储的通病,不是 Curvine 的锅,但踩了就是你的线上事故。实际落地最容易踩坑的就是混淆真相源,上线前建议拿小数据集完整跑一遍增、删、改的验证,确认缓存和底层 RustFS 行为符合预期。
缓存层之上 Curvine 还暴露了多种访问协议,这点和纯 FUSE 方案不一样:S3 网关让原来就用 S3 SDK 的工具也能打到缓存上,K8s 侧有 CSI 驱动能把挂载直接变成 Pod 里的卷,不用每台机器手动挂 FUSE。也就是说同一份 RustFS 数据,既能当本地盘给训练读,也能当 S3 端点给推理服务读,缓存是同一份。对已经用 RustFS 存训练集、又想把同一份数据喂给推理服务的团队,这套多协议出口省得维护两套存储。
直接用 RustFS 够不够,什么时候才需要 Curvine
不是所有场景都要加这一层。如果你的训练脚本本身就支持 S3 SDK 直接读 RustFS,或者只是偶尔 aws s3 cp 拉一次训练集、跑完就扔,那 Curvine 是多余的开销,反倒是多养一个集群。
Curvine 真正划算的场景有三个信号同时出现:训练框架只认 POSIX 文件接口(很多老训练代码、数据加载器不改就跑不起来);同一份训练集或模型权重会被反复热读(多卡、多机、多轮 epoch 都读同一批);你又不想把上百 GB 的数据全塞进本地 NVMe。这时候 Curvine 把 RustFS 当远端真相源、本地做热缓存,就近读省下的网络时间比加一层集群的运维成本值。判断很简单:只读一遍就完事,用 RustFS 直连;要反复当本地盘读,上 Curvine。
还有一个隐性收益:RustFS 桶通过 Curvine 暴露成 POSIX 后,原来只能 S3 读的数据也能被老工具当文件扫,迁移老训练代码时不用重写数据读取层,只把路径从对象 URL 换成挂载点就行。这比为了兼容老代码把数据整体搬进本地 NVMe 轻得多。
上手先跑通这三步
- 起集群:在 Curvine 安装目录跑
./bin/restart-all.sh(顺带拉起 master、worker、fuse),bin/cv report看集群状态; - 挂 RustFS:上面那条
bin/cv mount ... --write-type cache_mode -c s3.endpoint_url=http://<rustfs-host>:9000 ...; - 用 FUSE 暴露给训练:
./bin/curvine-fuse.sh start --mnt-path /mnt/curvine,训练脚本直接读/mnt/curvine下的路径。
选模式的速记:读多写少、数据以 RustFS 为真相源,用 cache_mode;要当通用 POSIX 工作区、写多,用 fs_mode,但避开大目录 rename、记得 resync。具体配置项看 Curvine UFS 挂载文档。
Curvine 本身是个 Apache 2.0、CNCF Sandbox 的 Rust 项目(Sandbox 是孵化阶段、不等于毕业项目,意味着还在早期快速迭代,选型时要评估版本节奏和 API 稳定性),仓库在 GitHub;想把它接进你自己的 RustFS,S3 兼容与部署细节看 RustFS 文档。前提是你这边 RustFS 已经在跑,Curvine 只是叠上去;存储本身还没定的话,先把 RustFS 起好再接 Curvine 是最顺的路径。