第一次做测速系统时,我踩了一个特别典型的坑:在云主机上部署了个测速页面,用户点一下"开始测速",结果返回的是云机房到目标站的速度。用户直接说"我家宽带没有这么慢,你测的不是我这条线"。测速的本质是测量一条网络路径的传输表现,路径两端的位置决定一切,单点测速只能代表单个机房,没有参考价值。后来我把整套调度逻辑搬到了 Cloudflare Worker 上,让测速任务从全球边缘节点发起,才算是真正解决了"测哪条路"的问题。但这套分布式测速调度系统最让我头疼的其实不是 Worker 脚本本身,而是数据层:任务状态放哪、测速结果怎么落库、多个边缘节点如何避免重复执行同一个任务。KV 和 D1 这两类存储,配合好了就是调度系统的地基,配合不好就是事故现场。这篇文章就把我在数据层设计上的完整思路、可复用的代码和踩过的坑都写出来。
1. 先搞明白:分布式测速调度到底在调度什么
1.1 测速不是测服务器,而是测"网络路径"
很多第一次做测速产品的同学会默认"我有一台带宽很大的服务器,用户来测,我给它喂数据就行"。实际上用户感知到的速度取决于用户本地网络、运营商互联、骨干网路由、目标服务器带宽,这一整条链路里任何一段出现瓶颈,速度就上不去。单点服务器测到的数据,只代表该服务器所在机房到目标站点的路径状况,完全不能代表普通用户的访问体验。
所以一个正经的测速系统,必须把探测点布到不同地理位置。Cloudflare Worker 恰好是个很顺手的载体:脚本不需要自己管机器,平台会自动运行在全球边缘节点上,用户请求落到哪个城市,测速就从哪个城市的边缘节点发起。再加上 Worker 可以用 fetch 发起 HTTP 请求,天然适合做文件下载测速、接口延迟探测这一类任务。
1.2 调度系统拆开看,是三个环节的联动
整个分布式测速调度系统可以拆成三段:
- 派单:生成一批测速任务,明确每个任务要测哪个目标 URL,期望从哪个区域发起。
- 执行:边缘 Worker 收到任务后,发起真实的 HTTP 请求,测量 DNS 耗时、TTFB、下载速度等指标。
- 回收:把各地节点的测速结果归集起来,做汇总、去重、出报表。
这三段单独做都不难,难的是放在分布式环境里。派单阶段要避免同一个任务被多个节点重复执行;执行阶段要允许节点失败、超时、重试;回收阶段要处理结果重复上报、数据乱序、聚合查询慢。这些问题的根源都在数据层,而不是计算逻辑。
1.3 为什么说数据层是这种系统的分水岭
如果把任务状态、节点状态、测速结果全放进程内存里,Worker 一重启就丢了。如果全部放到一个中心化 MySQL,边缘节点回写延迟高,写入多了还会拖垮数据库。这个时候 Cloudflare 自带的 KV 和 D1 提供了一个很务实的组合:KV 做全球复制的状态存储,D1 做关系型结果落库。但这个组合并不是开箱即用的,需要非常明确地划分边界。接下来我会用实际项目中的设计,把这条边界一条条画清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. KV 与 D1 的分工:先分清"状态"和"记录"
2.1 两种存储的定位完全不同
很多刚接触 Cloudflare 生态的人会把 KV 和 D1 放在一起比较,觉得都是"数据库"。实际上它们的性格差异很大:
| 维度 | Cloudflare KV | Cloudflare D1 |
|---|---|---|
| 底层模型 | 全局复制的键值存储 | 基于 SQLite 的关系型数据库 |
| 一致性 | 最终一致,写入后读可能读到旧值 | 写入为主区域的 SQLite,读取可通过副本扩展 |
| 延迟 | 读极快,边缘就近返回 | 读也不差,但涉及主区域写时有一定延迟 |
| 查询能力 | 只能按 Key 读写,支持前缀列举但能力有限 | 完整 SQL,支持聚合、JOIN、索引 |
| 典型用途 | 配置、状态、缓存、会话 | 结构化业务数据、报表、长期存储 |
一句话总结:KV 擅长"读多写少、Key 明确、允许短暂不一致"的数据;D1 擅长"需要结构化查询、需要聚合统计、需要数据长期可靠"的数据。
2.2 数据层整体切分:状态进 KV,结果进 D1
我在项目里把数据分成了两大块。
控制面数据,包括任务状态、任务锁、节点心跳、调度配置。这类数据的特点是:每个 Worker 在执行前都要查一次,更新频率高,但单条数据小,丢了一两秒影响不大。我放 KV。
记录面数据,包括每次测速的结果明细、按地区聚合的统计报表、用户测试历史。这类数据需要持久化,要支持按时间、区域、任务维度查询。我放 D1。
这个切分听起来简单,但真正执行时容易混。比如我一开始把"测速结果"也写进 KV,想着 KV 写读都快,结果发现要根据某个时间段做统计时,KV 完全使不上劲,只能把所有 Key 拉出来遍历,效率很低,而且 KV 有 finally consistent,列 Key 时可能漏掉刚写入的数据。从那时起我就定了一条原则:只要是"将来需要再查出来算一遍"的数据,必须进 D1。
2.3 一个判断标准:这个数据下一秒变了会怎样
如果拿不准一条数据放 KV 还是 D1,就问自己:这个数据下一秒变了,对系统影响大不大?
- 任务状态从 PENDING 变成 RUNNING,晚几秒被其他节点看到,影响不大,可以直接读 KV。
- 测速结果如果丢失或者重复,报表就会错,影响很大,必须写入 D1 并用唯一键约束。
- 一个热门报告页面被请求一千次,每次都查 D1 太贵,可以把
