D1 如何在雲端邊緣架構下擴充套件至數百萬資料庫
Under the hood, off the hot path: How D1 scales to millions of databases
D1 是 Cloudflare 提供的無伺服器 SQLite 資料庫,本文深入說明其控制面如何支援數百萬個資料庫的即時擴充套件。
- - 成長資料:過去一年活躍資料庫數量增長 15 倍,兩年增長 65 倍。
- 日活資料庫增長 18 倍。
本文揭示 D1 控制面如何在雲端邊緣架構下實現數百萬資料庫的即時擴充套件,對於構建多租戶或 AI 平臺的開發者提供可直接套用的設計原則。
傳統資料庫架構假設每個應用程式有一到兩個大型資料庫叢集,通常會搭配連線池解決方案。AI輔助開發與 vibe coding 平臺將此模式顛倒,並推動應用程式走向許多小型資料庫:每位使用者、每個應用程式、預覽環境或租戶各一個。
這種模式特別適合 D1。D1 是 Cloudflare 的管理式無伺服器資料庫,基於 SQLite。它允許你建立數以百萬計的資料庫,且只需支付實際使用量。
問題是:如何在不透過集中式瓶頸使每個查詢變慢的情況下,擴充數以百萬計資料庫的控制平面?
我們進行了大量架構變更,以擴充 D1 以支援擁有數以百萬計已建立資料庫的平臺,且在任何時點都有數十萬個活躍資料庫。
本篇文章深入探討 D1 控制平面的運作原理。 我們將描述的許多架構模式可供任何在 Cloudflare 開發者平臺上構建的開發者使用。文章亦提供了我們如何構建自有控制平面的洞見。
數字成長
在我們開始規劃這些變更時,D1 的使用量已經呈現穩定成長,但我們知道一旦大型平臺開始使用 D1,將需要支援更大的規模。
這是目前為止的發展情況。
D1 已經看到一個 15x 成長於過去一年活躍且未刪除 資料庫數量,以及一個 65x 成長於過去 2 年(截至 2026 年 10 月 8 日)。
我們也看到一個 18x 成長於過去一年每日活躍資料庫數量(截至 2026 年 10 月 8 日)。若資料庫在該日執行至少一次查詢,即視為活躍。
已建立且成功的平臺 已經在使用 D1,並使用所有 Cloudflare 使用者可用的相同工具。
"ChatGPT Sites 讓任何人都能將想法轉化為可分享的體驗。D1 為我們的使用者提供一種簡單、可擴充的方式,將自訂指標、排行榜和評論等功能加入他們的儀錶板、遊戲和工具。"
- Jon Abrams, ChatGPT Sites @ OpenAI
在我們進行本篇文章所描述的變更時,我們會定期進行負載測試,目標遠高於預期成長。這幫助我們確定最佳化重點。
指導工程原則
在 Cloudflare 開發者平臺上構建的應用程式涵蓋從全端應用程式到服務 API,從內部單使用者入口到大型多租戶 vibe coding 平臺,以及介於兩者之間的所有範疇。
在構建 D1 控制平面以支援數以百萬計的資料庫時,以下原則和信條指導了我們的架構決策,並對任何在我們平臺上構建控制與資料平面的人都有用。
- 設計所有流程時都要考慮失敗與冪等性。失敗會發生,因此係統必須在可能的情況下自我修正並繼續進行。
- 避免在資料平面操作(在我們的情況下是所有查詢請求)的任何熱路徑中使用集中式單區域元件。
- 使用 Workers KV,一個全球分散式 KV 儲存,為關鍵路由元資料,這樣快取失效時才很少需要查詢中央資料庫。
- 使用 Hyperdrive 存取中央 Postgres 資料庫,以實現 高效連線池化 並最佳化路由與快取,盡可能使用讀取複本。
- 定期在資料庫中預先計算聚合,以避免在使用者請求處理時進行掃描查詢。
- 對於不需要強一致性的元件,使用最終一致性與佇列化,以提升可靠性與可用性。
- 使用真實工作負載進行負載測試,找出瓶頸與低效處,並優先最佳化這些部分。
我們內部還遵循更多工程原則來構建系統,但上述原則已足夠通用,適用於任何在 Cloudflare 上開發的人。
以下各節將深入探討 D1 控制平面關於 D1 資料庫生命週期的主要系統元件與流程,並展示如何在實際系統中落實這些原則。
高階架構
讓我們先介紹 D1 的主要元件。
圖中缺少一些箭頭與方框,代表支援 D1 的次要流程,但這些即為 D1 的公開功能集合。我們將在後續專門章節中探討其餘部分。
首先,每個 D1 資料庫都是由一個 SQLite Durable Object 支援,這使得建立數以百萬計的資料庫變得非常容易。
「HTTP API」圖形代表我們所有的 API 介面,包括 Cloudflare 儀錶板、Wrangler 與 Cloudflare CLI。所有 API 請求都經由 Cloudflare 的邊緣閘道處理,並由 D1 Edge API Worker 服務。
圖表顯示允許使用者對其資料庫執行 SQL 查詢的 HTTP 端點,即「Query endpoints」。其餘端點則包含常見的資料庫操作,如建立、刪除與列出資料庫,以及匯入/匯出與時間旅行功能。這些被標示為「CRUD endpoints」。
CRUD 端點使用 Hyperdrive 來查詢我們中央資料中心(稱為「Core」)中的 Postgres 群集。有兩種設定。其一 啟用快取 並針對 Postgres 讀取複本。另一種設定則停用快取,並針對 Postgres 主節點。
Edge API Worker 可能會呼叫 D1 Eyeball Worker(視請求型別而定)。Eyeball Worker 是我們對所有需要由底層 D1 Durable Objects (DO) 處理的請求的代理。這使我們能將所有查詢處理放在 Eyeball Worker 中,包括可觀測性、自動重試、錯誤處理以及其他查詢特定行為。
Workers KV 作為邊緣快取層,用於讀取頻繁的元資料,並可被所有 D1 Workers 讀取。
「Worker Binding」圖形代表所有使用者的 Workers,並附帶 D1 Worker binding。來自 D1 Binding 的請求直接送至 D1 Eyeball Worker,該 Worker 再將請求轉發至支援資料庫的 Durable Object,並將回應代理回使用者 Worker。
使用者執行資料庫查詢有兩種方式:一是透過 HTTP API query 端點;另一是透過在其帳戶中部署的帶有 D1 Binding 的 Worker。
在操作層面,我們將每項操作歸類為以下幾類:
- 資料平面: 任何與資料庫查詢相關的請求;必須盡可能快速,並永遠在邊緣。
- 控制平面: 任何 HTTP API 操作或在回應非查詢使用者請求時執行的內部流程;它們可以依賴中央單區域元件。
- 背景非同步平面: 任何分析與帳單相關的工作流程;可最終一致,並可在任何地方執行。
實際上還有許多細節,讓我們深入探討關鍵流程。
快速即時資料庫建立,靠近使用者
多租戶平臺 及 AI 代理驅動開發需要以快速、成本效益高且可靠的方式建立大量資料庫例項。
以下是一個範例 Wrangler command 以建立 D1 資料庫:
% 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,這些物件因請求而異,因此將請求送往世界任何一端可能效率低下。這就是我們傾向於將初始請求轉發至靠近 Postgres 的原因。
下一步是使用 newUniqueId() API 產生資料庫 UUID 以及 新的唯一 Durable Object ID,並在新的 Durable Object 的代理 上呼叫 init(options) RPC 函式。Durable Object 將資訊存入其耐久儲存,確保從此之後每個進來的請求都擁有連結資料庫的所有元資料。
當 Edge API Worker 收到初始化回應後,會將資料庫元資料寫入我們的中央 Postgres 主資料庫(透過 Hyperdrive),每個資料庫一筆元資料列。
上述步驟被包裝在重試迴圈中(圖中的步驟 5-7)。若 DO 初始化或寫入 Postgres 失敗,我們會使用不同的 ID 重新嘗試(這點很重要,稍後會說明)。
寫入 Postgres 是「建立檢查點」,即使最終回應未能送達使用者,資料庫仍被視為已建立。從那時起,該資料庫會被納入所有控制平面操作,並可繫結至 Worker 以提供查詢服務。
此時 D1 資料庫已準備好接收查詢。
最後的建立確認 RPC 呼叫至 Durable Object 發生於 waitUntil(),失敗也無妨。此呼叫通知 DO 資料庫已啟動,DO 會更新 Workers KV 的資料庫元資料,讓後續任何查詢都能立即從 Workers KV 快取讀取資訊,而不必查詢 Postgres。
常見的問題是為什麼建立流程不能完全在 Postgres 交易內執行以簡化流程?答案很簡單,因為這樣無法擴充套件,會導致連線在 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,Postgres 為我們的真實資料來源。
- 在 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 再將結果回傳給使用者。
// 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() 而不是以「name」來定位 Durable Objects,以消除上述步驟 2 和 3,答案是為了 避免在查詢請求時進行一次中央對映的查詢往返 將「name」對應到「Durable Object ID」。僅靠 Durable Object ID 仍不足夠,因為我們還需要其他元資料來協助選擇正確的 Durable Object Namespace 供 Eyeball Worker 使用。因此,我們將一次查詢換成兩次。
在大多數情況下,即使是我們自己的許多應用程式,使用名稱定位 Durable Objects 也相當有效,並且省去從自己的 ID 轉到 Durable Object ID 的查詢需求,因此先採用此方式,只有必要時才改用唯一的 DO ID。
你也許會想知道為什麼在已經有 Hyperdrive 快取的情況下仍使用 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 Object 本身則分佈在全球各邊緣節點。使用量會在每個邊緣伺服器本地聚合,然後發布到內部佇列系統。
此處的核心原則是使用佇列來平滑並批次化寫入我們的中央區域 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 的快取鍵由查詢及所有引數值組成,存活時間(TTL)僅為幾秒。只以帳號 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(我們內部全球分散式的鍵值儲存,具備秒以下快速複製)設定一個鍵,過期時間為當天結束。如果帳號升級至 Workers Paid 層級,我們會收到佇列訊息並清除 Quicksilver 鍵。
每個 D1 Durable Object 會定期在邊緣節點檢查 Quicksilver,以判斷其帳號是否被限制;若被限制,則拒絕任何後續查詢執行,直至限制解除。
在 Developer Platform 上開發的開發者可以使用 Workers KV Instant,該產品建立於 Quicksilver 之上。
此模式避免將整個流程的負載傳遞至區域 Postgres,並為每項工作選擇最適合的工具。使用 ClickHouse 執行分析查詢比在 Postgres 中執行更為高效。
此模式在公司內部相當普遍,但與我們用於儲存使用量限制執行的做法不同。未來我們可能將其他限制執行方式遷移至此模式。
結論
D1 架構的設計核心在於將控制平面關注點與熱路徑查詢執行分離。
每項決策皆遵循我們的指導原則:降低中央區域瓶頸、盡可能使用快取與讀取複本並結合最終一致性以擴充讀取吞吐量,以及為每項工作挑選合適的資料庫:Postgres 用於交易性元資料、ClickHouse 用於分析、Workers KV 用於全球複製的路由資料。
若您正在打造一個 vibe 程式碼平臺,並希望以快速、成本效益高且可靠的方式為使用者提供管理式 SQLite 資料庫,請考慮使用 D1。
若您正在於 Workers 與 Developer Platform 上開發應用程式,本文所述的模式與折衷反映了 D1 的實際決策。請以此為基礎,應用於您自己的應用程式。
請檢視 D1 檔案,並在 D1 Discord 頻道告訴我們您的成果!
致謝
衷心感謝 Cloudflare 內部的 Databases 與 Hyperdrive 團隊,他們協助我們發現問題使用模式並提出改進建議。
來源:cloudflare blog · blog.cloudflare.com