ITPub博客

首页 > 数据库 > 数据库开发技术 > 深入底层,避开热路径:D1 如何扩展至数百万个数据库

深入底层,避开热路径:D1 如何扩展至数百万个数据库

数据库开发技术 作者:ITPUB社群 时间:1970-01-01 08:33:46 0 删除 编辑

David Laban 和 Lambros Petrou

传统数据库架构假设每个应用程序拥有一到两个大型数据库集群,通常还需要搭配一个连接池解决方案。AI 辅助开发和 vibe coding 平台彻底颠覆了这一模式,正在推动应用程序走向大量小型数据库:每个用户、应用程序、预览环境或租户一个数据库。

这种模式特别适合 D1。D1 是 Cloudflare 基于 SQLite 构建的托管式无服务器数据库。它允许你创建数百万个数据库,并且只需为实际使用量付费。

问题随之而来:如何在不因集中式瓶颈导致每次查询变慢的情况下,为数百万个数据库扩展控制平面?

我们进行了大量架构调整,以横向扩展 D1,从而支持拥有数百万个已创建数据库的平台,其中任意时刻都有数十万个数据库处于活跃状态。

本文是一次幕后深度探索,介绍 D1 控制平面的工作原理。我们将描述的许多架构模式,任何在 Cloudflare 开发者平台上构建应用的人都可以采用。本文也让大家一窥我们是如何构建自己的控制平面的。

数据增长

在我们开始规划这些变更时,D1 的使用量已经在稳步增长,但我们知道,一旦大型平台开始基于 D1 构建,我们就需要支持更大的规模。

目前的发展情况。

在过去一年中,D1 的活跃(未删除)数据库数量增长了 15 倍,在过去两年中增长了 65 倍(截至 2026 年 10 月 8 日)。

我们还看到,过去一年中日活跃数据库数量增长了 18 倍(截至 2026 年 10 月 8 日)。如果一个数据库在当天至少执行了一次查询,则被视为活跃。

成熟且成功的平台已经在使用 D1 进行构建,使用的正是所有 Cloudflare 用户都可以使用的相同工具。

"ChatGPT Sites 让任何人都能将想法转化为可分享的体验。D1 为我们的用户提供了一种简单、可扩展的方式,为他们的仪表盘、游戏和工具添加自定义指标、排行榜和评论等功能。" - Jon Abrams, ChatGPT Sites @ OpenAI

在实施本博客文章中描述的变更时,我们定期针对远高于预期增长的目标进行负载测试。这指导了我们优化工作的重点方向。

指导性工程原则

构建在 Cloudflare Developer Platform 上的应用程序涵盖从全栈应用到服务 API(应用程序编程接口),从内部单用户门户到大规模多租户 vibe coding 平台,以及介于两者之间的各种形态。

在构建 D1 控制平面以支持数百万个数据库的过程中,以下信条和原则指导了我们的架构决策,对于任何在我们平台上构建控制平面和数据平面的人都有参考价值。

  • 所有进程的设计都要考虑故障和幂等性。故障必然会发生,因此系统需要尽可能自我纠正并继续推进。
  • 避免在数据平面操作(在我们的场景中是所有查询请求)的任何热路径中使用集中式单区域组件。
  • 使用 Workers KV(一个全球分布的 KV 存储)来存储关键路由元数据,使缓存未命中时仅在极少数情况下需要查询中央数据库。
  • 使用 Hyperdrive 访问中央 Postgres 数据库,以实现高效的连接池管理以及优化的路由和缓存,并尽可能使用只读副本。
  • 定期在数据库中预计算聚合结果,以避免在用户请求处理过程中进行扫描查询。
  • 在不需要强一致性的组件之间使用最终一致性和队列机制,以提高可靠性和可用性。
  • 使用真实工作负载对应用程序进行负载测试,以发现瓶颈和低效之处,并优先优化这些问题。

我们在内部构建系统时还遵循更多工程原则,但上述原则足够通用,适用于任何在 Cloudflare 上构建系统的人。

以下章节将深入探讨 D1 控制平面在 D1 数据库生命周期方面的主要系统组件和流程,并展示如何在一个真实系统中将这些原则付诸实践。

高层架构

让我们首先介绍 D1 的主要组件。

图中还缺少一些支持 D1 的辅助流程的箭头和方框,但以上展示的是提供 D1 公开功能集的组件。我们将在本博客文章的后续专门章节中探讨其余部分。

首先,每个 D1 数据库都由一个 SQLite Durable Object 提供支持,这使得创建数百万个数据库变得非常容易。

“HTTP API”形态代表我们所有的 API 接口,包括 Cloudflare dashboard、Wrangler 和 Cloudflare CLI。所有 API 请求都经过 Cloudflare 的边缘网关,并由 D1 Edge API Worker 提供服务。

图中展示了允许用户对其数据库执行 SQL 查询的 HTTP 端点,即“查询端点”。其余端点包括常规数据库操作,如创建、删除和列出数据库,以及导入/导出和时间旅行功能。这些被展示为“CRUD 端点”。

CRUD 端点使用 Hyperdrive 查询我们中央数据中心(称为“Core”)中的 Postgres 集群。有两种配置。一种配置启用了缓存并指向 Postgres 只读副本。另一种配置禁用了缓存并指向 Postgres 主库。

Edge API Worker 可能会调用 D1 Eyeball Worker(取决于请求类型)。Eyeball Worker 是我们所有需要由底层 D1 Durable Objects(DO)处理的请求的代理。这使我们能够将所有查询处理放在 Eyeball Worker 内部,包括可观测性、自动重试、错误处理和其他查询特定行为。

Workers KV 作为我们在边缘的缓存层,用于读密集型的元数据,并且可以被所有 D1 Worker 读取。

“Worker Binding”形态代表所有用户带有 D1 Worker binding 的 Worker。来自 D1 Binding 的请求直接发送到 D1 Eyeball Worker,后者再将请求转发到支撑数据库的 Durable Object,并将响应代理回 User Worker。

我们有两种方法让用户在其数据库中执行查询。一种是通过 HTTP API 查询端点,另一种是通过他们在其账户中部署的带有 D1 Binding 的 Worker。

在操作方面,我们将每个操作归入以下类别之一:

  • 数据平面:任何与数据库查询相关的请求;应尽可能快,并且始终位于边缘。
  • 控制平面:任何 HTTP API 操作或为响应非查询用户请求而运行的内部流程;它们可以依赖中心化的单区域组件。
  • 后台异步平面:任何与分析及计费相关的工作流;可以是最终一致的,并且可以在任何地方运行。

底层还有更多内容,所以让我们深入了解关键流程。

靠近用户的快速按需数据库创建

多租户平台和 AI 代理驱动的开发需要以快速、经济高效且可靠的方式创建大量数据库实例。

用于创建 D1 数据库的示例 Wrangler 命令:

%
time
wrangler
d1
create
"demo-01"
✅
Successfully
created
DB
'demo-01'
in
region
WEUR
...
0.56s
user
0.13s
system
42%
cpu
1.614
total

当用户创建 D1 数据库时,在数据库准备好接收查询之前,需要完成若干步骤。

  • 检查该账户是否可以在免费套餐限制或账户限制内创建更多数据库。
  • 生成唯一的 D1 数据库 UUID、唯一的 Durable Object ID(newUniqueId()),并使用账户和数据库信息初始化底层 Durable Object(DO)。
  • 将新创建数据库的信息持久化到 Postgres 中(每个数据库一行,包括元数据以及到已初始化 Durable Object ID 的映射)。
  • 将数据库 ID 到 Durable Object ID 的映射写入 Workers KV(后续由数据平面和查询端点使用)。

让我们来看看实际的流程,以及我们如何在每一步从故障中恢复。

一旦数据库创建请求到达,我们的 Edge API Worker 处理器会在最近的 Cloudflare 边缘位置被调用。Edge API 通过启用了缓存的 Hyperdrive(缓存 TTL 为几秒)查询我们的 Postgres 只读副本,以获取给定账户中未删除数据库的数量。

Edge API Worker 会将控制平面请求转发到靠近我们 Postgres 集群的数据中心,以处理那些需要对 Postgres 进行多次往返的端点(步骤 2),“创建数据库”端点就是其中之一。原因是,从远处进行往返可能需要数百毫秒,而靠近 Postgres 则可能只需几毫秒。对于最多只执行一次 Postgres 查询的端点,我们不需要做这种优化,因为 Hyperdrive 已经通过 Cloudflare 网络处理了高效路由。

在没有任何选项的情况下创建 D1 数据库时,默认会将数据库放置在请求位置附近。你可以通过提供显式的位置提示或管辖区来影响数据库的位置。位置提示和管辖区选项与 Durable Objects 支持的完全相同,因此当提供这些选项时,它们会直接传递给底层 Durable Object 初始化。

在默认创建情况下,我们希望 D1 数据库创建在请求的原始位置附近,靠近用户。为了实现这一点,在图中步骤 4 期间,我们会在内部将请求执行转发到靠近原始请求位置的数据中心,并从那里调用底层 Durable Object。这确保了底层 Durable Object 被放置在用户附近。

你可以通过使用显式的 Worker 放置提示,并在运行时通过 Service Bindings 调用正确的 Worker,来实现类似的路由技术。

就放置而言,创建端点是一个有点特殊的情况,因为尽管我们至少查询 Postgres 两次,但我们还会对底层 Durable Objects 进行长距离调用,而底层 Durable Objects 因请求而异,因此将请求运行到世界任何一侧都可能效率低下。这就是为什么我们倾向于将初始请求转发到 Postgres 附近。

下一步是生成数据库 UUID,并使用 newUniqueId() API 生成新的唯一 Durable Object ID,然后在新 Durable Object 的 stub 上调用 init(options) RPC 函数。Durable Object 会将信息存储在其持久化存储中,以便从那时起每个传入请求都拥有所关联数据库的所有元数据。

一旦 Edge API Worker 获得初始化响应,它就会通过 Hyperdrive 将数据库元数据持久化到我们的中央 Postgres 主数据库中,每个数据库一行元数据。

上述步骤被包裹在一个重试循环中(图中步骤 5-7)。当 DO 初始化或写入 Postgres 失败时,我们会使用不同的 ID 进行重试(这一点很重要,我们将在下一节中看到)。

写入 Postgres 是“创建检查点”,即使最终响应未能到达用户,数据库也被视为已创建。从那一刻起,该数据库被纳入所有控制平面操作,并且可以绑定到 Worker 以提供查询服务。

此时,D1 数据库已准备好接收查询。

向 Durable Object 发送的最终创建确认 RPC 在 waitUntil() 中执行,允许失败。此调用通知 DO 数据库现已激活,DO 更新 Workers KV 中的数据库元数据,以便任何后续查询可以立即从 Workers KV 缓存中读取信息,而无需查询 Postgres。

一个常见的问题是,为什么不能将创建流程完全放在一个 Postgres 事务中运行以简化流程?答案很简单:它无法扩展,因为这会导致在 Durable Object 初始化期间保持连接,而该 Durable Object 可能位于地球的另一端。

用于自校正数据库的心跳机制

每个 D1 Durable Object 定期向控制平面发送心跳,以获取最新的数据库状态(活跃或已删除)和账户限制(更多内容将在后面的执行部分介绍)。间隔为 30 秒,并在此基础上为每个数据库添加抖动。我们还提供了按数据库或账户更改间隔,或完全禁用心跳的调节选项。

心跳可以容忍略微过时的数据,这意味着它们可以由 Hyperdrive 的多层缓存或 Postgres 只读副本提供服务。此心跳流程使 Durable Object 能够自我校正,以反映关于数据库创建和删除的真相来源(Postgres)。

在继续之前,有一个需要澄清的细节。在上一节的创建流程中(步骤 5),我们为每次重试使用不同的数据库 UUID 和 Durable Object ID 的原因是,如果我们重用数据库 UUID 但使用新的 Durable Object ID,多个 Durable Object 会认为它们拥有同一个数据库,从而导致难以调试的情况。定期心跳也起到了清理孤立对象的作用。

此外,重要的是我们首先初始化 Durable Object,然后再将数据库映射持久化到 Postgres,否则查询请求可能会开始流向 Durable Object,并因未初始化状态而被拒绝。

例如,假设以下故障场景,并说明为什么它是自校正的:

  • 我们执行了上一节中的所有操作,直到初始化了 Durable Object A,然后写入 Postgres 失败。
  • Worker 重试循环,并使用不同的 ID B 初始化一个新的 DO,这次成功持久化到 Postgres。
  • 现在,作为此请求的一部分,我们有两个已初始化并运行的 Durable Object,但其中只有一个有效。
  • 用户只能查询 Durable Object B,因为这是 Postgres(我们的真相来源)中唯一可用的 ID。
  • 在 30 秒内,DO A 向控制平面发送下一次心跳,并收到其数据库不存在的响应。这将触发 DO A 将自身标记为已删除。

删除端点也遵循类似的流程。

心跳机制的另一个自我纠正行为是确保 Workers KV 数据最终反映真实状态:数据库是处于活跃状态还是已被删除。

创建流程中更新 Workers KV 的最后一步是“尽力而为”的,即使失败也不会造成问题。它是最终一致的,下次 Durable Object 心跳时,将确保 Workers KV 条目已写入正确的值。由于 Workers KV 是最终一致的,且无法进行一致性读取,Durable Object 充当了 Workers KV 条目写入的序列化点。

删除操作则以相反的方式运作:Durable Object 负责将其“已删除”状态写入 Workers KV,同样是在实际删除操作期间尽力而为,如果失败,心跳机制最终会纠正它。Durable Object 也会在其存储中保存活跃或已删除状态,因此它可以立即拒绝或接受查询请求,即使 Workers KV 数据已过时并允许请求通过。

总体而言,当操作需要写入外部数据存储并同时更新 Durable Object 本身时,数据库心跳是进入一致状态的回退机制。

解耦热路径:让查询远离中心数据库

查询端点的关键原则是避免与 Postgres 或任何其他集中式单区域组件通信。每个通过 HTTP 的查询请求会在靠近原始用户请求位置执行我们的 Edge API Worker,然后通过 Eyeball Worker 直接转发到 D1 Durable Object。

Edge API Worker 负责执行标准的 HTTP API 认证、基于资源的授权、账户和数据库级别的速率限制、请求验证、检查账户权限以及其他 API 相关功能,然后才执行实际的业务逻辑。

另一方面,Eyeball Worker 跳过这些额外检查(除了其自身的专用速率限制),因为在底层,D1 Binding 是指向 D1 Eyeball Worker 的 Service Binding。每个用户 Worker 的 D1 Binding 携带编码信息,授权访问一个特定的 D1 数据库,D1 Eyeball Worker 验证该信息并允许请求通过。

下图展示了通过 HTTP API 执行查询的过程。

  • 用户通过 HTTP API 发送查询请求。
  • Edge API Worker 首先尝试从 Workers KV 加载 D1 UUID 到数据库元数据的映射。
  • 在极少数情况下,Workers KV 中找不到该条目,可能是因为如前所述写入失败,或者可能是由于可用性问题。此时我们回退到通过 Hyperdrive 从 Postgres 副本读取映射。在查询实际副本之前,Hyperdrive 首先检查调用方位置的缓存,然后检查 Postgres 集群位置附近的缓存,只有当两者都未命中缓存时,才会查询 Postgres 副本获取映射。
  • 一旦获得映射,Edge API Worker 通过 Worker Service Bindings 调用 Eyeball Worker,传递相关 ID。
  • Eyeball Worker 执行验证并将查询请求转发给 D1 Durable Object。
  • D1 Durable Object 执行 SQL 查询,并通过 Eyeball Worker 将结果返回给 Edge API Worker,后者再将其返回给用户。
// Edge API Worker
const
parsed
=
parseQueries
(request);
const
cachedDB
=
await
withRetries
(
getCachedDatabase
(parsed.dbUuid));
const
response
=
await
env.
EYEBALL_WORKER
.
query
({
db: cachedDB,
queries: parsed.queries,
});
return
response;

如果你想知道为什么我们在数据库创建期间使用 newUniqueId() 而不是通过“名称”来寻址 Durable Objects 以消除上述步骤 2 和 3,答案是避免在查询请求期间对“名称”到“Durable Object ID”的中心映射进行查找往返。仅凭 Durable Object ID 是不够的,因为我们还需要其他元数据来帮助我们在 Eyeball Worker 中选择正确的 Durable Object Namespace。因此,我们会将一次查找替换为两次。

在大多数情况下,即使是我们自己的许多应用程序,通过名称来寻址 Durable Objects 也能很好地工作,而且它省去了从你自己的 ID 到 Durable Object ID 的查找需求,因此可以将其作为起点,只有在必要时才转向使用唯一的 DO ID。

你可能还会想,既然我们已经有 Hyperdrive Caching,为什么还要使用 Workers KV。

Workers KV 是一个持久化数据存储,而不仅仅是缓存:如果某个条目从 KV 的边缘缓存中淘汰,读取请求会由 KV 自身的存储来提供,而不是 Postgres,从而使 Postgres 远离数据平面。在灾难性的缓存清除场景中,一百万个数据库试图获取其路由信息将导致亚稳态故障。

提醒一下,来自带有 D1 Binding 的用户 Worker 的查询请求不需要解析 D1 UUID 映射元数据,因为绑定本身在内部持有该信息,因此请求会直接发送到 Eyeball Worker 和 Durable Object。

关于查询执行本身以及 Durable Objects 在底层是如何实现的,请阅读我们关于 D1 读取复制的博客文章,其中深入探讨了所有细节。

接下来,让我们看看支持 D1 的异步流程。

强制执行存储使用限制

所有达到一定规模的平台最终都需要执行计费和使用限制。在撰写本文时,D1 在 Free 计划下每个账户有 5 GB 的存储限制,在 Workers Paid 下为 1 TB(可根据请求提高)。

一般来说,使用限制执行和计费系统是最终一致性和异步处理的良好候选场景,在以下章节中我们将探讨 D1 如何实现这些限制的执行。

使用队列进行异步更新处理

这段旅程的第一步是每个 Durable Object 将存储使用更新发布到类似 Kafka 的队列中,以进行批处理和异步处理。控制平面不需要同步处理每一个更新:如果负载峰值没有立即反映出来,我们的使用信息是过时的,这也没关系。重要的是它不能使 Postgres 过载并影响 D1 的用户。我们通过限制队列消费者的数量,从而限制 Postgres 的写入负载来确保这一点。

队列消费者系统运行在 Cloudflare 的中央数据中心(“Core”)内,而 Durable Objects 本身则分布在全球各地的边缘。使用量在本地按边缘服务器聚合,然后发布到内部队列系统中。

这里的指导原则是,我们使用队列来平滑并批量写入到我们的中心区域 Postgres 数据存储。

批量处理用量增量和预聚合

队列消费者将每个数据库的最新报告大小写入 Postgres 表。但这并不是我们做执行决策所需要的。对于执行决策,我们需要整个账户所有存储用量的总和。当我们在执行时读取用量时,我们无法承受运行 SELECT sum(storage) FROM database_stats WHERE account_id = ? 。这个查询的成本随每个账户上的数据库数量线性增长。对于拥有数百万个 D1 数据库的账户,比如一些用 D1 构建的智能体编程平台,这无法扩展。

理想情况下,我们需要一个查找表,让我们可以直接从账户 ID 转到总数据库存储。解决方案是使用增量视图维护来预计算总量,并在数据库存储发生变化时保持其最新。

我们把这个表称为 account_database_stats 。

理论上,我们可以让队列消费者为每个传入的更新自行更新 database_stats 和 account_database_stats 。但在实践中,对于大型账户,这会导致对单行的大量写锁争用。相反,我们添加第三个表 account_database_stats_changes ,它就像一个变更队列。这是所有变更的日志,每个账户可以有多个条目。然后我们定期将该表中的更新聚合到 account_database_stats 中。流程如下:

  • 队列消费者将特定数据库的用量信息写入 database_stats 表。
  • 触发器计算该数据库的存储差异,并将增量放入 account_database_stats_changes 表(每个增量一行)。
  • 一个计划任务每秒运行一次,消费 account_database_stats_changes 中的所有内容,并用这些增量的总和更新 account_database_stats 。

具体来说,计划任务运行这个查询:

WITH
updates
AS
MATERIALIZED (
DELETE
FROM
account_database_stats_changes
RETURNING
*
),
changes
AS
MATERIALIZED (
SELECT
account_id,
SUM
(storage_change)
AS
storage
FROM
updates
GROUP BY
account_id
)
INSERT INTO
account_database_stats (account_id, storage)
SELECT
account_id,
storage
FROM
changes
ON
CONFLICT (account_id) DO
UPDATE
SET
storage
=
account_database_stats
.
storage
+
EXCLUDED
.
storage
;

它从 account_database_stats_changes 获取更新列表,按账户聚合,然后将增量写入每个账户的表。这避免了对任何特定行的写争用,因为我们在写入之前聚合了变更。

公共表表达式(CTE) DELETE ... RETURNING 原子地消费待处理的变更,并将删除的行传递给下一个 CTE。Postgres 会精确执行一次数据修改 CTE 并完成。然后查询按账户对这些变更进行分组,并为每个账户应用一次聚合更新,从而减少写争用。

这种模式设计是许多其他 Cloudflare 团队使用的模式,它让我们能够扩展到每个账户数百万个资源,而不会让每个额外资源使常见操作变得更昂贵。

值得注意的是,总量是最终一致的。它不像你在其他一些数据库中看到的一流增量视图维护。我们已经使用队列将更新从 Durable Objects 获取到 Postgres,所以我们已经可以接受最终一致性。

通过 Hyperdrive 进行缓存读取

现在我们已经为每个账户的总存储量建立了一个查找表。接下来呢?

每个 D1 Durable Object 会通过我们之前提到的周期性心跳定期获取账户的使用量。我们使用 SELECT storage FROM account_database_stats WHERE account_id = ? 查询 Postgres,并将其与账户的权益和层级限制进行比较。如果总量超过限制,则禁止所有会增加数据库大小的写入操作。

该查询通过 Hyperdrive 发送到 Postgres 副本,并使用 Hyperdrive 的内置缓存。Hyperdrive 的缓存键由查询语句和所有参数值组成,过期时间(time-to-live)为几秒。通过将账户 ID 作为唯一参数,该账户下的每个数据库都会共享相同的缓存值,这正是我们想要的效果。

接下来是一个巧妙的缓存技巧。通过 Hyperdrive 进行的缓存读取默认有 60 秒的过期时间,且 Hyperdrive 缓存配置适用于所有查询。对于特定查询,我们希望缓存值具有更短的过期时间。通过在查询中添加 dynamic_ttl 参数,我们每 X 秒生成一个不同的缓存键,从而更早地读取到最新值。

-- $2 = Math.floor(Date.now() / (X * 1000))
SELECT
storage, $
2
as
dynamic_ttl
FROM
account_database_stats
WHERE
account_id
=
$
1

可能会有人想构造一个以账户 ID 和数据库 ID 作为参数的查询。这样可以找到账户总量并减去当前数据库的总量。这样就能精确回答“在开始执行限制之前,这个数据库允许使用多少存储空间?”这个问题,从而让数据库更早地开始执行限制。

但这样做意味着数据库 ID 会被包含在 Hyperdrive 的缓存键中,从而导致缓存命中率非常低。对于拥有数百万活跃数据库的账户,基于账户的缓存键意味着大多数心跳都使用 Hyperdrive 中相同的缓存账户信息,而不会到达 Postgres。

在响应用量变化方面,限制执行的速度比理论上可能达到的稍慢,但这是可以接受的。

与上述流程分开,我们有一个进程会定期将 Postgres 中的存储使用信息快照到 ClickHouse 集群中,以便我们可以获得存储随时间变化的历史视图。这些 ClickHouse 事件也是存储使用量计费的权威数据来源。

在边缘执行行使用量限制

D1 还会对免费层客户执行每日执行使用量限制。我们在这里使用的模式有所不同,也值得提一下。

每当在 Durable Object 中执行 SQL 查询时,我们会跟踪该查询读取和写入了多少行(包括必要的索引更新)。当查询执行完成时,我们会发布一个使用事件,最终进入另一个 ClickHouse 集群。这些事件正是我们基于行的使用量计费的驱动来源。

一个核心组件,我们称之为“Limiter”,运行在我们的核心数据中心中,并定期从 ClickHouse 查询自当天开始以来的使用事件:

SELECT
account_id,
sum
(rows_read)
as
reads,
sum
(rows_written)
as
writes
FROM
requests
WHERE
timestamp
>
toStartOfDay(
now
())
GROUP BY
account_id
HAVING
reads
>
?
or
writes
>
?

ClickHouse 处理这类查询比 Postgres 更好,同时我们也受益于不会因分析查询而使 Postgres 集群过载。

对于每个超出限制的账户,我们会检查其权益和层级(此限制仅适用于免费层级用户)。如果超出限制,我们会向他们发送电子邮件(全天去重),并在 Quicksilver 中设置一个键,Quicksilver 是我们内部的全球分布式键值存储,具有快速的亚秒级复制,并设置当天结束时的过期时间。如果账户升级到 Workers Paid 层级,我们会在队列中收到一条消息并清除 Quicksilver 键。

每个 D1 Durable Object 会定期在边缘检查 Quicksilver,以确定其账户是否已被限制,如果已被限制,它们将拒绝任何未来的查询执行,直到限制解除。

在开发者平台上构建的人可以使用 Workers KV Instant,它构建在 Quicksilver 之上。

这种模式避免了整个流程对区域 Postgres 的任何负载,并为正确的工作使用了正确的工具。使用 ClickHouse 进行分析查询比在 Postgres 中执行更高效。

这种模式在整个公司中很常见,但与我们用于存储使用限制执行的方法不同。未来,我们可能会将其他限制执行迁移到使用这种模式。

结论

D1 架构的设计围绕将控制平面关注点与热路径查询执行分离。

每个决策都遵循我们的指导原则:减少中心区域瓶颈,在可能的情况下使用缓存和只读副本以及最终一致性来扩展读取吞吐量,并为每个工作选择正确的数据存储:Postgres 用于事务性元数据,ClickHouse 用于分析,Workers KV 用于全球复制的路由数据。

如果您正在构建一个 vibe coding 平台,并希望以快速、经济高效且可靠的方式为用户提供托管的 SQLite 数据库,请考虑使用 D1。

如果您正在 Workers 和开发者平台上构建应用程序,这里涵盖的模式和权衡反映了为 D1 做出的真实决策。将它们作为您自己应用程序的基础。

查看 D1 文档,并在 D1 Discord 频道中告诉我们你构建了什么!

致谢

非常感谢 Cloudflare 内部的 Databases 和 Hyperdrive 团队,他们帮助我们发现了有问题的使用模式并提出了改进建议。


转载声明:本文为转载整理,原文版权归原作者及原站点所有。

原文来源:Cloudflare Blog

原创地址:https://blog.cloudflare.com/d1-control-plane/

原文作者:Lambros Petrou;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-11-21

  • 博文量
    1148
  • 访问量
    548357