尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI 训练大文件 Range 读爆炸?RustFS 前面挂一层 Cachey

AI 训练大文件 Range 读爆炸?RustFS 前面挂一层 Cachey 多机训练那头最舒服的往往是写数据几十个 worker 把权重和 checkpoint 写进去落盘、返回 200吞吐曲线漂亮得能当壁纸。真正卡住训练节奏的常常是读的这一头同一个 4 GiB 的模型权重八个 worker 各读各的段回源次数不是按 worker 数算的而是按「每个 worker 每碰一次冷页算一次」往上翻。单次延迟都不高可尾延迟会被拉得不讲道理。这篇文章记的是一条具体路径把 Cachey 挂到 RustFS 前面让重复 Range 读不再打到后端。Cachey 是个开源读穿透缓存后端走标准 S3 接口。作者 Shikhar 所在的 s2.dev 做流式日志存储最近写的那些记录由指定的进程持有读延迟几乎为零代价是并发读者数和吞吐都被这个进程框住真正持久化的一头又是 S3而 S3 自己有请求速率上限网关和持有进程还不总落在同一个可用区跨区网络成本跟着来。缓存是那块还能再挤的地方降操作成本、改善延迟分布、抬高扩展性上限。作者列这门心思的理由时也点过名ClickHouse、Turbopuffer、WarpStream、RisingWave 都公开讲过自己在 S3 上做分布式缓存的路数这不是某一家临时想到的。自建后端的读路径卡在哪一个典型的对象存储读路径大致是客户端带Range头打过来网关解析范围到 EC 编解码层取数据块拼好返回。单次 GET 的延迟本身不高问题出在重复读和并发读。第一个问题是尾延迟。多个 worker 同时读同一段冷数据后端会同时处理好几条回源队列一深谁都不快。第二个问题是回源放大。同一个页被八个请求经过如果没有合并后端就要做八次昂贵的取块而这八次的结果完全一样。第三个问题是小随机读。列举和单对象 GET 在「小范围随机读」上并不便宜元数据检索的占比会突然变得很难看。这三个问题都不是 RustFS 独有是 POSIX 式语义摊到分布式对象存储上的通病。解法得在前面加一层「存储感知」的缓存理解 Range、理解页对齐、能把同页并发请求合并。Cachey 是一个用 Rust 写的对象存储读穿透缓存MIT 协议。它的核心假设很克制缓存的对象是不可变 blob所以某段一旦被拉过就可以放心复用这一条也决定了它的适用面。它的接口面极小。客户端打GET /fetch/{kind}/{object}带上Range: bytes...Cachey 把请求的字节范围对齐到固定的 16 MiB 页先在混合缓存里找miss 才回源到后端。同页的并发请求会被合并成一次回源多个 bucket 之间还能做 hedged request 来压住尾延迟。它连鉴权都不自己做认证和签名都留在你的反向代理或网关上。「只做缓存、不做其他」是它能插到任何 S3 兼容后端前面的原因接 RustFS 也不例外。但「不做其他」有反作用的一面既然没有鉴权请求里由客户端填写的字段也就没人拦。C0-Bucket让请求方自己指名桶可以一次填多个当副本C0-Config让请求方逐页覆盖回源的连接超时、首字节时间、重试次数和路径风格。换句话说打到 Cachey 的任何请求都能让它去后端任意有权限的桶上取数据还能顺手把超时调紧、把重试次数调低制造大量失败回源。所以前面那层反向代理要多做两件小事剥离客户端传来的C0-Bucket与C0-Config改由代理注入可信的桶名。这步省不得省了就等于是把后端桶列表交给了每个下游。16 MiB 页对齐和它划出的甜区页大小是这套东西里最值得先想清楚的一个数。Cachey 固定按 16 MiB 切页把任意 Range 请求映射到页对齐的查找这个数是写死的命令行里没有对应的参数可调。这个思路是从操作系统的页缓存借来的代价也一并跟过来通常可以省掉的Range头在这里变成了必填而且必须给出精确的字节范围。页大小选 16 MiB是因为它落在 S3 建议区间的上半段越大越省回源次数也越浪费。落到开篇那份 4 GiB 权重上按 16 MiB 切正好是 256 个对齐页。八个 worker 各读各的段只要落在同一页第一跳回源、后续全命中回源次数从「按请求数算」变成「按用到的页数算」。训练、权重分发、视频切片这类大文件配按段随机读的负载命中率天然就高。一次很小的 Range 也可能触发一整页的回源与缓存占用小对象居多的负载并不划算。服务端对每次读都按整页预留缓冲哪怕你只要最后 1 KiB 也照样占满 16 MiB默认预算下并发副本数因此只有 64 个。你的文件大小分布离 16 MiB 越远浪费掉的带宽和缓冲越多这一层就越不划算。更要紧的是另一头。缓存键由kind和object拼成ETag 和版本号都不参与所以它锁死在「对象不可变」这个假设上。后端对象一旦被覆盖、新版本传上去旧页会被原样返回业务读到的还是上一次的内容而且不会有任何提示。想避开只能把kind当命名空间用发新版就换一个kind等于新开一块缓存区旧页只能等容量压力慢慢淘汰。频繁覆盖的对象本来就不该放在这一层后面。命令行的旋钮在C0-Config头里覆盖ct连接超时、rt首字节时间、ot操作超时、ma每桶最大尝试数、ib/mb退避上下限、fps强制 path-style 寻址。rt调小能让慢回源更快失败并转去冗余桶ma调大能在后端波动时多扛几次ib/mb决定重试之间退避多久避免雪崩式回源。要看缓存有没有生效把/metrics直接接进 Prometheus 就行。接 RustFS 的最小步骤接 RustFS 的完整动作只有三步没有额外的适配层。这件事在测试里已经发生过一次PR #106 把集成测试中的 MinIO 容器换成了rustfs/rustfs当时拉的还是 1.0.0-alpha.98 那版。它说明在第三方项目的自动化测试这一层RustFS 已经当 S3 后端跑起来了。第一步起服务。把 RustFS 的端点当成源配进去cachey server\--memory4GiB\--disk-path /data/cachey\--disk-capacity 100GiB\--iouring\--bucket-timeout-ms5000\--page-timeout-ms10000\--max-download-memory 1GiB\--hedge-budget-percent5这一串参数同时也是它的全部配置面官方文档里只有server [OPTIONS]这一条路径没有配置文件也没有环境变量。所有旋钮都在命令行上想改就得重启进程所以参数一次给足比事后调划算。第二步告诉它从哪个桶回源。每次请求带C0-Bucket: your-bucket即可想给 RustFS 开 path-style 寻址比如端点后面带桶名、不走虚拟主机在配置里加fps。第三步把应用侧的上游从 RustFS 改到 Cachey。这一步之所以只是改一个地址是因为/fetch就是标准 HTTP Range 读客户端不需要引入任何 SDK。原直连 RustFS 的 Range 读改成打到 Cachey 的/fetch迁移和回滚都不碰对象存储本身的数据。比较省事的一种部署是 Cachey 与 RustFS 同机或同子网部署前端由反向代理做 TLS 终止缓存层不对外暴露签名校验。因为协议就是普通 HTTP哪天要下线缓存把上游指回 RustFS 即可数据零迁移。真要上线还有四件事得先想明白。一是上面提过的请求头。代理侧必须把客户端传来的C0-Bucket和C0-Config全部剥掉换成自己注入的固定值否则等于把后端的桶列表和超时旋钮交给了每个下游。二是对象更新。缓存不认版本号后端覆盖之后旧页照样命中得靠换kind启新缓存区来切不要指望它自己感知。三是后端故障时的行为。Cachey 不会拿缓存里的旧数据兜底副本读取失败回 503超时回 504响应已经开始流式传输之后再出错就直接中断连接。它是加速层不是容灾层缓存命中省下的是延迟省不下后端的可用性。四是多实例命中率摊薄那部分放到部署形态那一节说。三条路的差别不在「谁更快」而在理解不理解对象存储。CDN 擅长把内容推到离用户近的边缘做全局分发但它不理解 Range 语义和页结构热数据 miss 之后还是要回到源站源站承受的还是那一份重复读。Varnish 这类通用 HTTP 缓存能缓存 HTTP 响应但默认不按「大块对齐加并发合并」做优化小随机读照样把回源打穿。Cachey 的差别在于它原生就是 object-storage-aware16 MiB 页对齐、同页并发合并、hedged 回源这些恰好对准对象存储的弱点。如果你的瓶颈是边缘分发CDN 更合适如果是回源的存储被重复 Range 读打爆就该考虑存储感知缓存。自建 RustFS 的场景里第二种更常见因为后端是自己运维的资源有限尤其不该为同一段冷数据做十几次重复取块。缓存底座、命中边界与替代路线Cachey 的命中层是 foyer一个把内存和磁盘合在一起的混合缓存热页留在内存、冷页落盘按访问热度自动在两级边界上滑动。这意味着缓存容量可以远超物理内存你给的 100 GiB 磁盘容量不是「二级缓存」而是和内存一起参与同一套淘汰的存储面。对 RustFS 这种可能承几 TB 权重文件的后端来说这一条决定了缓存层能不能真的挡住大部分回源而不是只挡住头几百兆的热数据。部署形态上还有一点容易被忽略。内存和磁盘缓存都长在单个进程上作者在发布帖里也写得很直接它就是一个自包含的单节点二进制亲和与负载均衡指望客户端那侧的逻辑。所以后面多实例各自一份缓存不是配错了是设计如此。C0-Bucket那套「一次给多个桶当副本」的设置也是同一套思路的延伸服务端不做选主把判断交给调用方。摊开有它的好处单实例挂掉不影响整体但代价同样直接每个实例的缓存彼此独立一份 4 GiB 权重在四个实例上会被各存一份有效容量仍是单实例的那些整体命中率跟着实例数摊薄实例一重启本地磁盘上那份也一起没。所以扩实例能扛住更多并发回源但不等于命中率会一起涨上去。少数大文件反复按段读命中率会很快爬到很高海量小对象每个只读一次缓存全程 miss不如不要。上线前先把/stats的命中率和回源延迟盯一段时间确认你的负载真落在甜区里。边界与判断Cachey 仍是相当早期的项目单仓库、MIT、文档还在长。它能解决的那部分尾部问题很明确但它自己能撑到什么程度得看一段时间才知道。前面讲过的还有一条后果灾备切换的时候指望缓存层跟着一起扛这层给不了。RustFS 在 2026-09-16 走到 1.0.0官方口径下 4 KiB 小对象 PUT 约 MinIO 的 2.3 倍读路径同样走高性能实现正好适合当这类缓存层的回源后端尤其当热点 miss 需要快速回填时。缓存层与后端是各自独立的取舍后端负责持久与一致性缓存层负责把重复读挡在前面。搞清楚这个分工这一层加不加、加多大就不难判断了。项目仓库见 https://github.com/rustfs/rustfsCachey 本身在 https://github.com/s2-streamstore/cachey。
返回列表