收到一张 Cloudflare 的 $9 账单。一个跑着个人 API 平台的 DigitalOcean droplet,平时几乎零成本,突然冒出来九块钱。查下去发现是 Litestream 的默认配置在闷声刷操作数。
账单长什么样
只有一行非零:
R2 Storage Class A Operations (First 1M included)Qty: 1,640,355 Unit: $4.50 / 1,000,000 Amount: $9.00存储、Class B、data retrieval 全是 9.00。一个备份桶,一个月刷了 164 万次 Class A 操作。
为什么是 4.50
第一反应:Class A 标准价 4.50。
但账单是 $9,而且没有任何抵扣行。
原因是 R2 的计费取整规则。官方文档写得很直白:用量向上取整到下一个计费单位,文档原话的例子是「执行了一百万零一次操作,会按两百万次计费」。
套到这张账单上:1,640,355 → 向上取整到 2,000,000(2 个百万单位)→ 2 × 9.00**。那 100 万免费额度(line item 里写着 “First 1M included”)在这张发票上没有从计费量里扣减,而是把毛量整体取整后直接收了。
也就是说,「先扣免费再取整」会是 9.00,这张账单走的是后者。这点和取整文档一致,但比直觉狠了一档 —— 值得对着账单确认一下有没有该出现的抵扣行。
哪个桶干的
平台有两个 R2 桶:
| 桶 | 大小 | 对象数 | Class A(快照) |
|---|---|---|---|
platform-backups | 568.5 MB | 3.44k | 339.52k |
oss-platform | 6.38 GB | 4 | 11 |
对比一眼就清楚:oss-platform 存了 6.38 GB 实际文件,整个周期才 11 次 Class A;而 backups 桶 3.44k 个对象,却产生了几十万次 Class A,约 99 次/对象。
存储不是问题,操作模式才是。backups 桶里是 auth/ deployer/ registry/ 三个组件的 SQLite 数据库,用 Litestream 持续备份到 R2。
根因:Litestream 默认 1s sync
Litestream 把 SQLite 的 WAL 变化持续复制到对象存储。它的工作方式是持有一个长读事务阻止别的进程 checkpoint,自己把 WAL 页复制到 shadow WAL 再手动触发 checkpoint。每次 sync 把脏页打包上传 —— 一次上传就是一个 PUT,在 R2 上就是一次 Class A。
而 sync-interval 默认就是 1s。官方文档直接给了换算(按 S3 $0.000005/请求):
| sync-interval | 满写理论月请求 | 成本(S3) |
|---|---|---|
| 1s | ~259.2 万 | $12.96 |
| 10s | ~25.92 万 | $1.30 |
| 1m | ~4.32 万 | $0.22 |
R2 Class A 是 $4.50/百万,数量级一样。我这个备份是间歇写,没顶到 259 万的理论上限,但 164 万也轻松破了 100 万免费线。
这坑很有名。有人同时跑了好几个 Litestream 进程忘了设 sync-interval,默认 1s,在 R2 上刷了 2000 多万次 Class A,账单逼近 $100,最后的修复就是加一行 sync-interval。
修复
一行配置。但有个坑:sync-interval 必须放在 replica 级别(- type: s3 下面),放在 db 级别在 Litestream v0.5.x 上不生效。
dbs: - path: /var/lib/platform/registry.db replicas: - type: s3 bucket: platform-backups path: registry endpoint: <R2_ENDPOINT> sync-interval: 1h # 默认 1s → 1h retention: 168h # 默认 24h → 7 天改完服务器 /etc/litestream.yml,同步更新生成这份配置的 bootstrap 脚本(避免下次重装漂移),重启,日志确认 sync-interval=1h0m0s 生效。Class A 月请求量从 1s 的量级砍到 ~720 次的量级,稳稳趴在免费额度内。
一个容易混的点
重启后日志里会出现一行 retention=5m0s,看着像我设的 retention: 168h 没生效。两回事。
Litestream v0.5.x 用分层压缩(L0–L9),日志里那个 retention=5m0s 是内部 L0 WAL 段的保留时间,和 replica 级别的 retention: 168h(快照保留多久)不是一个东西。别被吓到去改。
还有个常见误解:checkpoint 和 sync 是两回事。WAL 体积由 checkpoint 阈值控制(默认 WAL 到 ~10MB / checkpoint-interval 默认 1m),不是 sync-interval。所以把 sync 拉到 1h,本地 WAL 仍按自己的阈值正常截断,不会无限涨;只是这一小时积累的 WAL 段被合并成更少的 PUT,反而更省。
怎么选 sync-interval
就是 RPO(最多丢多少数据)和操作量的权衡:
- 热备 / 需要低 RPO failover:30s–1m
- 冷备 / 变化少 / 没 SLA 要求:1h 甚至 4h 都行
我这个平台变化很少、几乎不会崩、也不需要 SLA,1h 对它已经偏保守 —— 最坏丢 1 小时写入,对一个备份用途的库没有实际代价。
经验
- 托管型工具的默认值要当成「未配置」看待。Litestream 的 1s 默认是为了 Getting Started 体验好,不是为生产成本优化的。装上就跑 = 默认给你刷满。
- 按操作计费的对象存储,关注操作数比关注存储费重要。我 6.38 GB 的真实文件桶一个月 11 次 Class A,几乎免费;500 MB 的备份桶因为 sync 太勤反而吃了钱。
- R2 的取整计费会放大小用量。一旦某月 Class A 越过 100 万,这张账单的行为直接跳到 $9,不是按溢出量线性涨。护城河就是把月度 Class A 压在免费线以内。
(同一个平台之前还踩过一次生产 503 的坑,五个问题叠在一起,记录在 posts/api-platform-503-debugging.md。)