一句话总结:cf Agents SDK
schedule(0, ...)self-renew 模式 +outbox.pushed_at无索引导致全表 DELETE,叠加多 DO instance 并发,4 天产生 10.97T row reads,账单 $12,578。已于 5/3 12:13 修复(commit6c95264/ ADR-0015)。
| 字段 | 内容 |
|---|---|
| 项目 | sandbox-agent-platform(基于 Cloudflare Workers + Durable Objects + Agents SDK 0.11.5) |
| 异常时段 | 2026-04-29 ~ 2026-05-03(北京时间) |
| 触发 commit | e6a9790 wire UserAgent/ProjectAgent/BranchAgent to outbox + ADR-0008(4/29) |
| 修复 commit | 6c95264 adopt cf native stability — scheduleEvery 替代 self-renew(5/3 10:44) |
| 修复部署 | 5/3 12:13 wrangler deploy (prod) |
| 真正归零 | 5/4 01:33 之后停用,加上 fix 生效,用量降至 0 |
| 总损失 | $12,578.22(DO Storage 占 99.95%) |
| 产品 | 总使用量 | 可计费使用量 | 使用成本 |
|---|---|---|---|
| Durable Objects Storage Rows Read(前 25B 免费) | 10.97T | 10.95T | $10,949.73 |
| Durable Objects Storage Rows Written(前 50M 免费) | 1.67B | 1.62B | $1,622.00 |
| Container Memory, per GiB-Second(前 25 GiB-hours 免费) | 2.69M | 2.69M | $6.49 |
关键指纹:读/写比 = 10.97T / 1.67B ≈ 6,569。健康 OLTP 系统这个比值通常 < 100;超过 1,000 基本就是「循环里反复扫一张没索引的表」。
4/29 e6a9790 部署 wire-d1-outbox
├── 每个 BranchAgent: onStart 中 schedule(0, sweepD1Outbox)
├── sweeper 内部 self-renew: schedule(result.nextDelaySec, sweepD1Outbox)
└── nextDelaySec = (batch.length === 100) ? 0 : 30
\\__ outbox 满 100 行 → 立即续 0 秒 → 死循环
4/30 你开始用 prod 跑 task
├── 04:17 PR flow / commit 自动化 bug 诊断(D1 远端查询)
├── 12:34 b8ead64 fix(pr-flow): no_diff 判据 → wrangler deploy
├── 21:04 c32be88 design docs 基建建立
└── 单日账单首次破 $1,000
5/1 零星 hotfix + 单测基建
└── 量级持续在 $1,000+/天
5/2 ★ 关键节点:dev/prod 环境拆分
├── 21:33 wrangler d1 create agent-platform-dev
├── 21:34 wrangler r2 bucket create agent-platform-{skills,workspaces}-dev
├── 21:34 vercel env add NEXT_PUBLIC_API_BASE_URL preview = <https://api-dev.douglas-agent.com>
├── 21:36 wrangler deploy --env dev
├── 21:44 git push origin main:dev → 触发 Vercel preview
├── 22:20 prod wipe(清 D1,但 DO 内 outbox 未清)
└── 单日仍 $1,000+ — outbox 表持续累积
5/3 ★ 修复日
├── 03:00+ 6 个 commit 改 cf-native stability(container 镜像、agent prompt 注入等)
├── 10:44 6c95264 feat(worker): adopt cf native stability
│ - scheduleEvery(60, sweepD1Outbox) 替代 self-renew
│ - keepAliveWhile 包 tickTask / handleTaskDone / triggerPrFlow
│ - createBackup/restoreBackup
├── 12:13 wrangler deploy (prod) ──→ 🛑 死循环熄火
├── 12:38 wrangler deploy --env dev
└── 之后多次 fix + dev 验证
5/4 00:30 ~ 01:33
├── milestone schema + Activity dedup + dev smoke fix
├── D1 migrations apply (dev + prod)
├── 01:33 最后一次 wrangler deploy (prod) + push main:dev
└── 项目停用至 5/7
5/4 之后:用量归零(fix 生效 + 项目停用,双重原因)
CF 的 row read 计费基于「SQLite 引擎扫描了多少行」,不是「SQL 返回多少行」。
| SQL | 5000 行表上的行为 | 计费 |
|---|---|---|
SELECT * WHERE seq = 100(seq 有索引) |
走 PK 索引直接定位 | 1 row read |
SELECT * WHERE seq > 0 ORDER BY seq LIMIT 100 |
走索引读 100 项 | 100 row read |
DELETE WHERE pushed_at < X(pushed_at 无索引) |
全表扫一遍才能判断 | 5000 row read |
单价 $1.00 / 10 亿 row reads,听起来很便宜——但没有索引的查询每次执行都按全表大小计费,循环里跑就会爆。
BranchAgent / ProjectAgent / UserAgent 实例 = 一个独立的 "员工",各自带一个独立的 SQLite。this.schedule(N, 'callback') = 在该实例的 SQLite 系统表(cf_agents_schedules)里写一条「N 秒后触发 callback」的记录。