WAL 是写前日志。数据库里的老概念:任何改动先落一条日志,落稳了才动真正的数据页,掉电之后靠回放把没写完的补齐。传统做法是给它挂一块专用磁盘。这两年有人把它挪进了对象存储,不买盘、不起共识节点,直接用 S3 的条件写把每次提交串成一条链,就得到一条持久、无主、还能无限堆积的日志。
这套做法最完整的一次公开表达,是 Cursor 那篇「Git at Any Scale」。他们内部的 Git 存储系统叫 Continuity,动机跟性能无关,是不想再养一套要人伺候的基础设施。GitHub 那套 Spokes 集群难运维的地方在于:得记住每个仓库落在哪台机器上,得跑共识选主,还得配一份关系型数据库来记引用。Cursor 把日志本身搬进了对象存储,机器上的仓库退化成一层热缓存,路由表、共识、数据库三样一起消失,S3 里那条链子成了唯一真相。他们的取舍写得很直白:Git 数据的一致性压过其他一切考虑。
这条路径上已经有东西落了地。walgit 是 Tobias Lütke 一个周末写出来的 Rust 单二进制 Git 服务,跑在对象存储前面,同样没有数据库、没有主节点;发布一天内在 Hacker News 上了首页(报道见这里)。Ryan Dahl 在下面留的评论说得更实在:walgit 只依赖 S3 做存储和协调,这事儿之所以成立是因为 S3 从 2024 年起支持条件写,他管这套叫关于可靠性和成本的实用模式。
把它认真量了一遍的是 Trylle,一个独立的技术 journal,不是任何一家的市场部。作者 Max 在 2026-08-24 发的那篇长文,起点是 Dan Goodman 的 Waltier——最早照着 Cursor 那套思路做出来的实现之一。他们 fork 了它,补上一套像样的基准 harness、修掉其中几个 bug,然后拿完全相同的负载打到 AWS S3、S3 Express One Zone、Tigris、Cloudflare R2 四家上,反复跑了很多小时。四家全部通过正确性门,差距全部出现在通过之后那一段。
如果这套负载换成你自己在运维的存储呢。下面这三条线就是这么来的,它不排谁的名次,只划三条线,量完就知道该在哪个环节补。
过了门才见真章
这份负载是持久性优先的:每个跑次推同一份约 200 GiB 的负载矩阵,4 KiB / 1 MiB / 16 MiB 三种对象,同一份 benchmark 代码、16 路写入、15 秒预热加 60 秒测量窗口,一台主机一条 3 Gbit/s 上行,每个对象存储只跑一轮,脚注原话是 one run per provider,跑与跑之间的方差没有量化。
四家全部通过了正确性门:每一跑次 12,044 个已确认的 payload,全部满足「恰好一个 CAS-create 胜出、恰好一个 CAS-update 胜出、follower 收敛、零孤儿对象」。所以排名只反映「满足契约时的延迟和成本」,不反映「契约能否被满足」。差距就出在能被满足之后的那一段,而这一段恰好是那套自建存储最容易含糊过去、也最该先补测的部分。
四家的最终数字是:S3 Express One Zone 综合指数 92.5,AWS S3 37.2,Tigris 13.3,Cloudflare R2 10.0。这四家里没有自建方案的一席之地,公开基准里没有 RustFS 的数字,要自己测。
顺带说清这个指数能怎么用、不能怎么用。四家能放在一张表上比,是因为负载统一成了一件很具体的事:持久性优先的串行 CAS。换成常见的读写混合业务,这个排序未必还长这样。它是 WAL 这一种负载下的相对量,不是产品能力的总分,别拿它去做通用选型对比。
WAL 为什么是对象存储的病理负载
WAL-on-S3 的精髓是一条 CAS 链:每次提交先读再改再写同一个对象,下一笔必须等上一笔的 ETag 确认后才能开始。吞吐因此被钉死在单条串行链路上,而每一跳都是小对象、受延迟约束、强一致的条件写。
这恰恰是各家差异最大的那根轴。Express One Zone 单条串行 WAL 约 98 op/s、控制面 p50 约 10 ms,代价是它显式单可用区;AWS S3 单条慢一些,却能靠 64 个 WAL 分片跑到 1,109 op/s;Tigris 约为 S3 吞吐的六到七成、尾更长,但正确、可用、零出口费;R2 单分片约 5 op/s、小对象持久写慢、follower 尾延迟约 1 秒。
这里有个几何事实值得自建存储的人记住:只要提交链严格串行,单条 WAL 的吞吐上限就约等于「1 除以单跳延迟」。对象存储的单跳延迟动辄十几到几十毫秒,单链理论上限就只有几十到一百出头的 op/s。想突破只能把一条大 WAL 拆成多条独立 shard。代价是分片之间不存在任何全局事务:跨分片的一次提交要么彻底没原子性,要么得由你的业务代码自己拼起来:先写哪几个分片、写到一半崩了怎么回滚、分片之间的先后怎么定,对象存储这边一律不管,一致性边界就这么从存储层推回到了应用层。基准里 AWS S3 从单条到 64 分片吞吐涨了约 30 倍,正是这个道理。
Cursor 在生产里也没硬扛这条链。Continuity 把仓库本体放在本地 NVMe 上,WAL 只当同步凭据,并且刻意不做「一次 push 一次对象写」,配上批量化之后 push 吞吐由磁盘决定,不由 S3 的 PUT 延迟决定。这反过来说明那条串行链是这套方案的默认形态和默认瓶颈,想绕开得额外加组件,不是白送的。
换句话说,WAL 不是难优化的边缘场景,它把对象存储最不擅长的小对象串行写放到了负载主干上。谁在这个环节吃亏,谁的综合指数就掉。自建一套 S3 兼容存储,这一项同样躲不掉,差别只在单跳延迟压得有多低,RustFS 也在这条线上,没有谁能靠换实现绕过那点物理延迟。
拿这三条线验收自建的那套
先泼一盆冷水。这套模式的门槛主要不在存储那一侧,而在它把整个系统的正确性压在了存储的条件写语义上:存储实现里一个边缘 bug,该冲突的时候没冲突,或者该放行的时候误判成冲突,会直接变成日志损坏,而且往往要等到回放那一遍才暴露。再叠上串行等待这个结构,一次 p99 长尾就能把整条 WAL 的吞吐拖下去,重试、超时、部分失败的边界情况又极难测全。这一段是 WAL-on-S3 当前阶段的真实状态,不是存储选得对就自动绕过去的。
这三条线的价值不在排名,而在它划出了三个通常不会被公开、却必须自己测的指标:单对象条件写的 p50 与 p99、跨可用区的尾延迟、以及分片扩展之后尾延迟还能不能收住。对一套自己运维、自己排障的存储来说,这三条就是验收清单。
落到动作上,先确认协议层能不能跑起来,再谈性能。RustFS 官方的 S3 兼容性矩阵里条件写是已测项,但这句话得读准:矩阵每一行都带 scope 一栏,已测那批是从 Ceph s3tests 里挑出来过门禁的用例,不是条件写这个 API 只做了一半。真正的缺口在另一头,挑中的那批用例里并没有你这条链。WAL 跑的是纯串行、每一跳都带 ETag 更新的写法,未必落在其中,所以拿自己的端点验一遍最稳:
aws s3api put-object\--bucketwal-demo--keylog/head\--bodyhead.bin --if-none-match"*"\--endpoint-url http://127.0.0.1:9000 aws s3api put-object\--bucketwal-demo--keylog/head\--bodyhead.bin --if-match"\"<上一步返回的 ETag>\""\--endpoint-url http://127.0.0.1:9000两条都返回 200,才算这条链真正通了。只跑第一条就下结论,等于把最要紧的那半条没测。注意 S3 返回的 ETag 值自带引号,填进--if-match时要连引号一起转义。
这条命令过完之后,接着做两件事:一是拿你真实的提交大小分布压一遍,而不是拿厂商的吞吐数字反推小对象串行写的体验;二是先单条跑,再拆 8 分片、32 分片跑,看尾延迟是不是跟着分片一起降下去。这两步做完,你对那套自建存储在 WAL 这项负载上的位置就有数了,比看任何综合指数都可靠。
但延迟数字全绿也不等于数据正确。即便存储侧条件写完全合规,上层那个 WAL 客户端自己写错照样毁数据:重试次数算错、超时之后误判成提交成功、follower 收敛顺序乱了,回放的时候就会对不上。所以压测收尾要跑端到端校验,拿 WAL 回放出来的状态和预期逐条比,而不是只看延迟和吞吐这两个数。
上面这些数字都出自别人的硬件,搬到你那套上未必成立。所以这篇给的不是名次,是一份验收动作,数字得你自己在自己的环境里跑出来。
成本这笔账怎么摆
性能之外,账本也得算清楚,各家走的是完全不同的路子。单可用区那家用最低的每操作单价和最紧的延迟,换来数据只落在一个 AZ,要跨区容灾得自己复制,成本模型更接近「本地盘加对象接口」;AWS S3 是多 AZ 全场景选手,单价里已经含了那份冗余;Tigris 和 R2 打零出口费牌,对读多、要对外分发的负载更友好,代价是各自在延迟或吞吐上各有取舍。
自建存储的成本结构又不一样,压测前就该想清楚:机器、网络、磁盘冗余是固定成本,而 WAL 负载的写入量往往远高于直觉里的「业务数据量」,因为每次提交、每个 follower 收敛都要落一笔。条件写还会再放大一层:每次冲突都换回一个 412 PreconditionFailed,客户端重试又是一整个请求,冲突越密集,实际请求数可能是有效写入的好几倍。机器和网络配额得按放大后的数字预留,不是按写入量估。选错账本的差价可能比延迟差距更肉疼,而这套账在自建的场景里尤其得自己算,因为固定成本那部分压不掉。
边界:这三条线不能当结论
WAL-on-S3 仍是一个相当新的用法,社区里第一批实现还在快速修 bug,拿它扛生产流量之前,先在 staging 把正确性门和压力窗口都跑满。
还有一条方法论上的边界:这套基准每个对象存储只跑了一轮,原文明确写了 run-to-run 方差未量化。所以它给的是量级,不是能写进 SLA 的数字。真要拿它做容量决策,得在自己的负载上重复跑,把方差算出来。
回到 RustFS 自己。它在 2026-09-16 走到 1.0.0 GA,官方 benchmark 里 4 KiB 小对象 PUT 约 MinIO 的 2.3 倍,这个数字出自官方仓库的性能对比章节,引用时别挪作他用。
但要分清两件事:小对象 PUT 快,不等于 WAL 这类 CAS 链就快,因为 CAS 链每一跳都要先读再改再写,瓶颈在单跳延迟和串行等待上,不在单次写的吞吐里。真正决定这一项上限的,是前面那几组实测。
所以判断顺序是:先看条件写的协议层是否完整,再看单跳 p50 和 p99,最后才算账。按顺序量完,自建存储在这三条线上的位置,就不需要靠推理了。项目仓库见 https://github.com/rustfs/rustfs。