1299 字
6 分钟
Litestream 默认 1s sync 把 R2 Class A 刷到 164 万次,吃了张 $9 账单

收到一张 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 全是 0Subtotal/Total/Amountdue都是0,Subtotal / Total / Amount due 都是 9.00。一个备份桶,一个月刷了 164 万次 Class A 操作。

为什么是 9而不是9 而不是 4.50#

第一反应:Class A 标准价 4.50/百万,164万次扣掉100万免费额度,溢出64取整到100应该是4.50/百万,164 万次扣掉 100 万免费额度,溢出 64 万 → 取整到 100 万 → 应该是 4.50。

但账单是 $9,而且没有任何抵扣行。

原因是 R2 的计费取整规则。官方文档写得很直白:用量向上取整到下一个计费单位,文档原话的例子是「执行了一百万零一次操作,会按两百万次计费」。

套到这张账单上:1,640,355 → 向上取整到 2,000,000(2 个百万单位)→ 2 × 4.50=4.50 = **9.00**。那 100 万免费额度(line item 里写着 “First 1M included”)在这张发票上没有从计费量里扣减,而是把毛量整体取整后直接收了。

也就是说,「先扣免费再取整」会是 4.50,「先把毛量取整再收」是4.50,「先把毛量取整再收」是 9.00,这张账单走的是后者。这点和取整文档一致,但比直觉狠了一档 —— 值得对着账单确认一下有没有该出现的抵扣行。

哪个桶干的#

平台有两个 R2 桶:

大小对象数Class A(快照)
platform-backups568.5 MB3.44k339.52k
oss-platform6.38 GB411

对比一眼就清楚: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。)

Litestream 默认 1s sync 把 R2 Class A 刷到 164 万次,吃了张 $9 账单
https://blog.lishuyu.top/posts/2026-06-05-litestream-default-1s-sync-r2-class-a-bill/
作者
猫猫魔女
发布于
2026-06-05
许可协议
CC BY-NC-SA 4.0