全部文章
开发2026/06/15sylwair12 分钟阅读

一个小项目把 D1 读到 330 亿行:Cloudflare Workers 付费后也要盯账单

这篇文章复盘了一个小项目在 Cloudflare D1 上读到 330 亿行的账单排查案例。问题核心是搜索页、动态 sitemap、SEO 提交队列和统计查询反复触发 D1 读取,导致 rows read 被放大。文章解释了 D1 按读取或扫描行数计费的逻辑,并总结了用汇总表、复用查询结果、sitemap 快照、降低 cron 频率、补充索引等方式降低读取量。最后给出了一份面向 Workers 和 D1 项目的发版前成本检查清单。

#cloudflare#deployment

一个小项目触发 Cloudflare D1 读取量账单风险的示意图

先说结论

我原来以为,开了 Cloudflare Workers 付费订阅之后,只要项目流量不大,账单就不会有什么问题。

这次被 D1 教育了一次。

账单页里最刺眼的一行是:

  1. D1 已读取行:33B

  2. Workers Paid 已包含额度:25B rows read

  3. D1 Rows Read 费用:8.22 美元

  4. 按每 100 万行 0.001 美元倒推,约等于 8.22B 超额读取行

Cloudflare 账单费用拆分截图,D1 Rows Read 为 8.22 美元

这张费用拆分截图里,D1 Rows Read 是 8.22 美元。同一组截图还显示容器内存 13.27 美元、容器 vCPU 4.08 美元、容器磁盘 0.58 美元。本文聚焦 D1 Rows Read 这条费用线,其他容器费用暂不展开。

Cloudflare D1 Metrics 截图,已读取行显示为 33B

单看 8.22 美元不算吓人。真正的问题在于:这笔费用来自查询方式放大 D1 读取量,并非用户爆发增长。

如果项目继续增长,或者类似逻辑出现在更多接口里,账单会跟着放大。

这篇文章想讲清楚四件事:

  1. Cloudflare D1 到底怎么计费。

  2. 为什么一个小项目也会产生额外账单。

  3. 我这次具体踩到了什么坑。

  4. 后续做项目时怎么避免账单悄悄涨起来。

这次问题的本质

这次问题的本质很简单:

Cloudflare D1 计费关注数据库为了完成查询实际读取或者扫描了多少行,页面返回数据量只是结果的一部分。

你看到的是一个搜索页、一个 sitemap、一个后台提交任务。

D1 看到的是一堆持续运行的查询。

如果这些查询每次都要扫描很多历史记录,账单就会被放大。

可以用一个粗略公式理解:

总读取量 = 请求次数 × 每次查询扫描行数 × 每个请求触发的查询次数

这里最容易被忽略的是机器流量。

用户访问可能不多,但 sitemap 会被搜索引擎抓取,URL 提交队列会被定时任务触发,后台统计也会定期跑。它们不会因为没人打开网站就停下来。

Cloudflare D1 的计费重点

根据 Cloudflare 官方 D1 Pricing 文档,截至 2026 年 6 月 15 日,Workers Paid 计划包含:

  1. Rows read:每月前 250 亿行包含在计划内,超出后每 100 万行 0.001 美元。

  2. Rows written:每月前 5000 万行包含在计划内,超出后每 100 万行 1 美元。

  3. Storage:前 5GB 包含在计划内,超出后每 GB 每月 0.75 美元。

官方文档还说明,rows read 统计的是查询读取或扫描的行数,和每行大小无关,也和最终返回多少行无关。

举个通俗例子:

  1. 页面最后只展示 10 条结果。

  2. 数据库为了找出这 10 条结果,扫描了 10 万行。

  3. D1 账单看的是 10 万行。

这就是很多人容易误判的地方。

你以为自己只是查了一点点数据,数据库实际可能已经翻了很多页。

查询返回结果很少,数据库实际扫描很多行的 rows read 示意图

参考资料:

  1. Cloudflare D1 Pricing:https://developers.cloudflare.com/d1/platform/pricing/

  2. Cloudflare D1 Metrics and analytics:https://developers.cloudflare.com/d1/observability/metrics-analytics/

  3. Cloudflare D1 Use indexes:https://developers.cloudflare.com/d1/best-practices/use-indexes/

我的项目里发生了什么

这个项目可以理解成一个搜索型内容站。

它有几类功能:

  1. 用户访问搜索页。

  2. 项目记录每个关键词的搜索次数。

  3. 搜索页会判断这个关键词是否值得被搜索引擎收录。

  4. 系统会生成动态 sitemap。

  5. 后台会把合适的 URL 提交给搜索引擎。

这些功能单独看都合理。

问题出在它们被放在了一起,而且都在频繁读取 D1。

问题一:搜索页每次都查历史统计

搜索页需要判断一个关键词有没有搜索价值。

旧逻辑会去历史统计表里找这个关键词的累计搜索次数、最近搜索时间、结果数量。

对人来说,这只是一个小判断。

对数据库来说,它可能要从历史记录里做汇总。

如果这个动作发生在每一次搜索页访问里,读取量就会被放大。

问题二:同一次请求里重复查同一份数据

旧链路里还有一个浪费:

  1. 页面渲染时查一次关键词统计。

  2. 后台判断是否提交给搜索引擎时又查一次关键词统计。

同一个请求里,同一份数据被查了两遍。

如果一次查询本来就贵,重复查询会直接把成本乘上去。

问题三:动态 sitemap 每次都实时计算

sitemap 看起来像一个静态文件。

很多项目里,它其实是动态生成的。

我的旧逻辑会在访问 sitemap 时实时计算哪些搜索页应该出现在 sitemap 里。

这类计算通常涉及筛选、排序、聚合。

如果搜索引擎频繁抓取 sitemap,D1 会一直重复做这些计算。

问题四:SEO 提交队列跑得太勤

项目里还有 URL 自动提交队列。

它会定时扫描待提交 URL,然后提交给搜索引擎。

旧策略跑得比较频繁,单次处理批量也偏大。

这类任务有一个特点:它不依赖真实用户访问。

哪怕没人打开网站,它也会按时间运行。

为什么账单会涨

账单上涨的关键原因,不在某个 SQL 写得多复杂。

真正的原因是三个因素叠加:

  1. 高频入口:搜索页、sitemap、定时任务都会触发查询。

  2. 扫描范围大:查询返回结果很少,但底层读取了很多行。

  3. 重复执行:同一份数据在同一次链路里被多次读取。

所以这次问题的重点不在数据库存储,也不在 Cloudflare 空闲计费。

Cloudflare 官方文档说明,D1 不按运行小时或容量单元收费。没有查询时,不产生 D1 compute 费用。D1 的关键账单线来自查询读取、查询写入和超额存储。

我的问题就是查询一直在读,而且读得比我想象中多。

我是怎么发现问题的

这次排查最有用的入口是 Cloudflare D1 的 Metrics 和 Query Insights。

不要只看查询次数。

要重点看 rows read。

Cloudflare 官方观测文档里也把这几个指标分开了:

  1. readQueries:读查询次数,不用于计费。

  2. rowsRead:查询扫描的行数,这是读费用核心指标。

  3. rowsWritten:写入行数,这是写费用核心指标。

  4. queryBatchTimeMs:查询耗时。

  5. databaseSizeBytes:数据库大小。

我的经验是,费用异常时优先看两件事:

  1. 按 rows read 排名前十的查询。

  2. 这些查询分别由用户访问、sitemap、cron 还是后台任务触发。

很多时候,最贵的查询可能并不慢,真正危险的是高频扫描很多行。

我做了哪些优化

这次优化的核心思路是:

把高频页面里的实时计算,改成提前算好结果。

具体做了几件事。

1. 建汇总表

旧逻辑是每次请求都去历史明细里算关键词统计。

新逻辑是提前维护一张汇总表。

每个关键词只保留一条当前汇总结果。

这样搜索页读取统计时,只需要按关键词读取一行。

2. 复用已经查过的数据

页面渲染时已经查过关键词统计,后面的 SEO 提交判断就直接复用这份数据。

同一次请求里,不再重复向 D1 要同一份结果。

3. sitemap 改成快照

sitemap index 和热门搜索 sitemap 不再每次访问都实时计算。

现在会定期生成快照。

访问 sitemap 时,优先读取快照。

快照过期后再刷新。

搜索引擎收录通常不需要秒级实时性,小时级刷新已经足够。

4. 降低定时任务频率

URL 提交队列从更高频的运行方式,改成更保守的周期。

单次处理数量也下调。

SEO 自动提交是异步任务,延迟几个小时通常可以接受。

5. 给高频查询补索引

Cloudflare D1 使用 SQLite。索引能减少查询需要扫描的行数。

这次我给高频过滤字段、排序字段、队列状态字段补了索引,并在创建索引后执行优化。

索引会增加一点写入和存储成本,但对读多写少的项目来说,减少 rows read 通常更重要。

优化效果

从优化后的 Cloudflare 面板截图看,已读取行在右侧红框区域明显下降。

Cloudflare D1 Metrics 优化后趋势截图,右侧红框区域的已读取行明显下降

上面这张是实际面板截图。为了更直观解释优化前后的变化,我也做了一张示意图:

优化前后 D1 读取量从高扫描路径下降到低量级读取的示意图

优化前,每个时间段的柱子仍然能达到很高的位置。

优化后,柱子明显贴近底部。

这说明 D1 的读取量已经从原来的高扫描路径,降到了更低量级。

需要说明的是:

  1. 截图能证明趋势明显下降。

  2. 精确降幅需要导出同周期后台数据后计算。

  3. 最终账单仍要看完整账单周期。

所以我不会把截图直接写成精确节省了多少美元。

但从趋势上看,这次优化有效,而且下降幅度非常明显。

后续做项目时,我会检查这些事

1. 所有公开页面都要问一句

这个页面会不会被搜索引擎抓?

如果答案是会,就不能只按真人访问量估算成本。

2. 所有 sitemap 都按机器流量设计

sitemap 默认会被爬虫访问。

适合用静态文件、缓存或快照。

不适合每次请求都实时聚合大量数据。

3. 所有 cron 都要有预算

每个定时任务至少要写清楚:

  1. 多久跑一次。

  2. 每次最多处理多少条。

  3. 大概会扫多少行。

  4. 异常时怎么暂停。

4. 所有统计功能都要区分明细和汇总

明细表适合记录历史。

汇总表适合给页面高频读取。

不要让每个用户请求都去历史明细里重新算一遍。

5. 每周看一次 Query Insights

我后续会固定看:

  1. rows read 排名前十的查询。

  2. 查询次数排名前十的查询。

  3. rows written 排名前十的查询。

  4. sitemap 和 cron 相关查询是否异常。

  5. 本月可计费使用量是否开始增长。

给非后端读者的简单理解

可以把 D1 想象成一个仓库。

你问它:“帮我找出这 10 件东西。”

如果仓库有清晰的货架编号,它可能只走几步就拿到。

如果没有编号,它可能要从头翻到尾。

Cloudflare 账单关注仓库为了帮你找东西,翻了多少格货架。

我的项目旧逻辑,就是让仓库反复翻货架。

修复方式,就是把常用结果提前整理好,把货架编号补上,把不需要实时翻仓库的任务改成定期快照。

最后提醒

如果你也在用 Cloudflare Workers 和 D1,建议现在就去看一下账单页和 D1 Metrics。

重点看这几个位置:

  1. Billing 里的可计费使用量。

  2. D1 Metrics 里的 rowsRead 趋势。

  3. Query Insights 里按 reads 排名最高的查询。

  4. sitemap、搜索页、cron、后台队列这些机器流量入口。

开了 Workers 付费订阅,不代表所有使用量都不用管。

真正要盯的是可计费使用量。

账单不会主动告诉你是哪条业务逻辑在浪费钱。

你要自己把 rows read 排名拉出来看。

可复用检查清单

发版前可以问自己这些问题:

  1. 这个接口会不会被爬虫访问?

  2. 这个接口会不会被 cron 触发?

  3. 这个查询返回几行?

  4. 这个查询实际扫描几行?

  5. 这个查询有没有合适索引?

  6. 同一次请求里有没有重复查同一份数据?

  7. sitemap 是否可以用快照?

  8. 热门榜、统计页、搜索页是否可以用汇总表?

  9. URL 提交队列是否跑得太勤?

  10. 本月可计费使用量有没有持续上升?

如果这些问题你答不上来,账单就有可能在后台悄悄变大。