LinkForge 總回顧與 production checklist
四十二章,四十二個範例,全部在本機跑過。這一章不是摘要,是把散在各章的東西收成兩份可以直接用的清單:一份是「這些坑我們踩過了」,一份是「上線前照著檢查」。
43.1 LinkForge 的最終架構,以及每個決策的理由
Section titled “43.1 LinkForge 的最終架構,以及每個決策的理由” ┌──────────────────────────────────────┐ 使用者請求 ─────▶ │ Worker(Hono) │ │ ├ 靜態資源(run_worker_first 例外) │ │ └ 路由 │ └───────┬──────────────────────────────┘ │ ┌───────────────────┼────────────────────┬─────────────────┐ ▼ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌────────────┐ │ KV │ │ D1 │ │ Queue │ │ R2 │ │ slug快取 │ │ 事實來源 │ │ 點擊事件 │ │ 匯出檔案 │ └─────────┘ └──────────┘ └─────┬─────┘ └────────────┘ ▼ ┌───────────────┐ │ DO: LinkCounter│ │ per-slug 計數 │ │ + alarm 落盤 │ └───────────────┘| 決策 | 選了什麼 | 為什麼不是別的 |
|---|---|---|
| slug → URL 查詢 | KV(前面再擋一層 Cache API) | D1 每次查詢都要跑 SQL,KV 的讀是 $0.50/M 且全球複製。代價是 60 秒的最終一致(第 8 章) |
| 事實來源 | D1 | KV 沒有查詢能力;要「這個租戶的所有連結按時間排序」就必須有 SQL(第 9、13 章) |
| 點擊計數 | DO | KV 每個 key 每秒 1 次寫入的限制直接判死;需要原子遞增(第 14 章) |
| 計數落盤 | DO alarm | 不要每次點擊都寫 D1。alarm 批次落盤把 rows written 降兩個數量級(第 17、42 章) |
| 點擊事件傳遞 | Queue | 讓 redirect 路徑不等待。但每則訊息 3 次 operation,在第 42 章是第二大開銷 |
| 匯出 / 大檔 | R2 | egress 免費是決定性的(第 12、42 章) |
| 分析查詢 | Analytics Engine | 高基數維度免費,且目前完全不計費(第 22、42 章) |
三個「當初想錯了」的地方,值得寫下來:
- 一開始想把計數直接寫進 D1。 第 42 章的實測說明為什麼不行:rows written 比 rows read 貴 1000 倍,而且有 index 的欄位每次寫兩筆。
- 一開始想用 Cache API 取代 KV。 第 6 章實測 Cache API 在本機是 stub、
match()永遠 miss,而且它是 per-colo 的 —— 不是全球共享。它是 KV 前面的一層,不是替代品。 - 一開始想每個租戶一個 DO 當作安全邊界。 第 41 章的結論是:官方從未把 DO 描述成安全隔離邊界。它給的是狀態隔離。租戶邊界還是得靠「tenantId 只能來自驗證過的 session」。
43.2 全系列的六個反覆出現的教訓
Section titled “43.2 全系列的六個反覆出現的教訓”這是本系列真正的產出。不是「Cloudflare 有什麼功能」,而是「這個平台會用什麼方式騙你」。
教訓 1:typeof x === "function" 在 Workers 上什麼都證明不了
Section titled “教訓 1:typeof x === "function" 在 Workers 上什麼都證明不了”七次遇到「API 存在但一呼叫就 throw」:
| 章 | 東西 | 症狀 |
|---|---|---|
| 6 | Cache API | 本機是 stub,match() 永遠 miss |
| 23 | Pipelines binding | RPC proxy,本機呼叫即 throw |
| 25 | Astro.locals.runtime | 存在但沒有內容 |
| 27 | WebAssembly.compile | 存在但 EvalError |
| 30 | Browser Run quickAction | RPC proxy |
| 32 | Email send | 是 RPC proxy 但本機真的能用 —— 反例 |
| 37 | getMcpAuthContext() | 乾淨地回 undefined,不是 throwing stub —— 反例 |
規則:不要 feature detect,去讀 node_modules 裡的 .d.ts 與實作。而且**「是 RPC proxy」不等於「不能用」**(第 32、37 章是反例)。
教訓 2:型別不會流動
Section titled “教訓 2:型別不會流動”三次:第 18 章(service binding 的 class 名稱只出現在註解裡)、第 26 章(monorepo 跨套件的 ambient type)、第 29 章(getContainer() 拿到的是 binding name 不是 class name)。
規則:wrangler types 產生的是設定的型別,不是程式碼的型別。跨 Worker、跨套件的地方一律自己手寫一層結構型別。
第 40 章補了一個正面用法:wrangler types 對所有 environment 取聯集,任何 environment 缺 binding 就標成 optional(?)。那個問號是免費的 CI 警報。
教訓 3:真相有三個版本,而且互相矛盾
Section titled “教訓 3:真相有三個版本,而且互相矛盾”| 章 | 誰比誰寬 |
|---|---|
| 15 | 型別比 runtime 窄 |
| 21 | 文件比型別 窄 |
| 23 | JSON schema 比 validator 寬 |
| 37 | 文件說「移除」,型別說 @deprecated(38 章同款) |
| 38 | JSDoc 指向一個已經不存在的 package 子路徑 |
| 39 | schema 有 logs.head_sampling_rate,文件一頁都沒提 |
| 39 | 文件說「不能手動結束 span」,但 API 三天前就上了 |
| 40 | --strict 在同一頁有兩種說法 |
| 41 | Vectorize namespace 上限兩頁差 50 倍 |
| 42 | Analytics Engine 有價目表但「目前不收費」 |
規則:排序是「實測 > 型別 > schema > 文件」。 而且要標註你用的是哪一層。
教訓 4:本機比 production 寬鬆 —— 除了那次不是
Section titled “教訓 4:本機比 production 寬鬆 —— 除了那次不是”大部分時候本機比較寬鬆(第 27 章 PBKDF2 迭代上限本機不套用、第 33 章 cpuMs 不強制、第 38 章 new Function 本機能跑)。
🔴 第 37 章是唯一方向相反的一次:Miniflare 把每個 DO class 包一層 Wrapper extends UserClass,所以 hasOwnProperty(Object.getPrototypeOf(this), …) 在本機永遠看不到你的方法 —— 函式庫自己寫的 runtime assertion 在本機被關掉了。錯誤設定會順利通過 wrangler dev,然後在 deploy 之後才 throw。
規則:「本機綠燈」永遠只是必要條件。CI 一定要有一個真的把 Worker 起起來打一發請求的 smoke step(第 40 章)。
教訓 5:模組層變數是跨租戶洩漏
Section titled “教訓 5:模組層變數是跨租戶洩漏”第 34 章(AI client)、第 37 章(McpServer singleton 洩漏 MCP session)、第 41 章(官方 best practice:“Do not store request-scoped state in global scope”)。
規則:任何在模組層被賦值、而值來自請求的東西,都是資料外洩。 isolate 會跨請求重用,這不是可能,是預設。
教訓 6:便宜的東西一定要量,不能假設
Section titled “教訓 6:便宜的東西一定要量,不能假設”第 42 章:ORDER BY … LIMIT 10 在沒有 index 時讀了兩倍表大小。head() 不免費。KV 的 miss 是計費讀取。Queues 的 batch 不省錢。
規則:meta.rows_read 在本機就看得到。 計費單位大多可以在本機數出來 —— 數它,不要猜它。
43.3 備份與復原:三種資料,三種機制
Section titled “43.3 備份與復原:三種資料,三種機制”這是最容易被跳過的一節,也是出事時唯一有用的一節。
D1:Time Travel
Section titled “D1:Time Travel”wrangler d1 time-travel info <database> --timestamp 2026-07-30T12:00:00Zwrangler d1 time-travel restore <database> --bookmark <bookmark>wrangler d1 time-travel restore <database> --timestamp 2026-07-30T12:00:00Z--timestamp 的說明文字就寫著範圍:「within the last 30 days」。
兩件要知道的:
- restore 是就地覆蓋。 沒有「還原到另一個資料庫」的選項。演練一次,不要第一次用是在出事的時候。
- 第 40 章那條規則在這裡收成:回滾程式碼不會回滾資料。 Time Travel 是分開的動作,而且它會把整個資料庫拉回過去,包含你想留下的那些寫入。
Durable Object:bookmark(PITR)
Section titled “Durable Object:bookmark(PITR)”DO 的 PITR 不是 CLI 指令,是 runtime API(第 15 章):
const bookmark = await this.ctx.storage.getCurrentBookmark();const past = await this.ctx.storage.getBookmarkForTime(Date.now() - 3600_000);await this.ctx.storage.onNextSessionRestoreBookmark(past);// 然後 abort 這個 DO,下一次啟動就會從那個 bookmark 開始注意 onNextSessionRestoreBookmark 的名字:它排程的是「下一次 session 啟動時還原」,不是立刻還原。你還要主動終止當前 session。
實務建議:在每次重要的批次寫入之前 getCurrentBookmark() 並把它記進 log(結構化的,第 39 章)。出事時你需要的是一個具體的 bookmark 字串,不是一個大概的時間。
R2:沒有 S3 式的 versioning
Section titled “R2:沒有 S3 式的 versioning”wrangler 的 r2 bucket 子指令實測有:lifecycle、lock、cors、notification、catalog、sippy、domain、dev-url、local-uploads。
沒有 versioning。 R2 提供的是 object lock rules(防刪改)與 lifecycle,不是版本歷史。
所以 R2 的「備份」要自己設計:寫入時就用帶版本的 key(
docs/<id>/<timestamp>.pdf),加 lifecycle 規則清理舊的。覆寫式的 key 一旦被覆蓋就沒了。
wrangler r2 bucket lock add可以擋掉誤刪,是防呆的第一道。
43.4 Production checklist
Section titled “43.4 Production checklist”照著這份跑一次。每一條後面標了它從哪一章來。
-
compatibility_date明確寫死,不用--latest(第 2 章) -
observability.enabled: true(第 39 章) -
upload_source_maps: true—— 上限 15 MB gzip 後,不影響 CPU(第 39 章) -
preview_urls明寫在設定檔裡,不要只在 dashboard 切(第 40 章) - 每個 named environment 都宣告了所有 binding —— 用
wrangler types產生的?檢查(第 40 章) - CI 有
wrangler types --check,而且沒有 pipe(第 40 章) - CI 有
deploy --dry-run --outdir的 bundle 預算(第 40 章) - CI 有部署後的 smoke test(第 38、40 章)
-
concurrency群組防止部署賽跑(第 40 章) - rollback 是一個 workflow_dispatch 按鈕,而且回滾後也跑 smoke(第 40 章)
- 改 DO class 生命週期的變更單獨部署,用
wrangler deploy(第 40 章) - 知道
versions upload不會套用observability改動(第 40 章)
Observability 與告警
Section titled “Observability 與告警”- log 一律傳物件,不要
JSON.stringify(第 39 章) - 告警條件是
http.response.status_code >= 500,不是outcome != "ok"(第 39 章) - 篩 Worker 用
$metadata.service,不是service.name(第 39 章) - alert 規則裡沒有寫死 span 名稱(官方說會改)(第 39 章)
- trace 抽樣率設過了,而且知道 log 抽樣會讓 debug 變難(第 39、42 章)
- 成本告警:把第 42 章那個模型的輸出設成月度預期,超過就通知
- 沒有任何模組層變數存放請求相關的狀態(第 41 章)
- 所有 D1 存取走 tenant-scoped 包裝,
env.DB只在一個檔案出現(第 41 章) -
tenantId只從驗證過的 session / JWT claim 讀(第 27、41 章) - 每張表的主鍵開頭是
tenant_id(第 41 章) - rate limit 的 key 保證是非空字串,否則 fail closed(第 41 章)
- 精確配額用 DO,不是 rate limit binding(第 41 章)
- 密鑰比對用 先 SHA-256 再
crypto.subtle.timingSafeEqual(第 41 章) - cookie 全部有
__Host-前綴(第 41 章) - SSR 回應的安全 header 在 Worker 裡加,不是
_headers(第 41 章) - Access 的 JWT 驗了
audience(第 41 章) - 跑使用者程式碼時
globalOutbound: null(第 33、41 章) - 會 fetch 使用者提供 URL 的地方開了
global_fetch_strictly_public(第 41 章) - CI 有
npm audit,用 lockfile(第 40、41 章)
- 每一個熱查詢都量過
meta.rows_read,而且 rows_read ≈ 回傳列數(第 42 章) - 沒有熱路徑上的
R2.list()(第 42 章) - 所有 WebSocket 都用 hibernation API(第 16、42 章)
- queue 有 retry 上限與 DLQ(第 19、42 章)
- queue 訊息 小於 64 KB(第 42 章)
- D1 migration 向後相容(因為回滾不會回滾資料)(第 12、40 章)
- Time Travel 演練過一次(第 43 章)
- R2 重要物件用帶版本的 key,加 lock rule(第 43 章)
- DO 在重要批次前記錄 bookmark(第 15、43 章)
-
compatibility_date每季往前推一次,看一次 changelog 的 flag 清單(第 2 章) - wrangler、
@cloudflare/vitest-pool-workers、vitest 釘死版本(第 38 章) - 0.x 的套件(
agents、@cloudflare/containers、@cloudflare/codemode)釘到 patch(第 29、37 章) - 升級 wrangler 之後重跑
wrangler types,看 diff(第 40 章)
43.5 這個系列沒有涵蓋的東西
Section titled “43.5 這個系列沒有涵蓋的東西”實測 wrangler 4.118.0 的頂層指令清單,這些是本系列完全沒碰的:
| 指令 / 產品 | 狀態 | 大概是什麼 |
|---|---|---|
wrangler vpc | open beta | 連到你自己的私有網路 |
wrangler tunnel | experimental | Cloudflare Tunnel |
wrangler flagship | open beta | feature flag |
wrangler websearch | experimental | Cloudflare Web Search |
wrangler artifacts | private beta | Artifacts namespace |
wrangler agent-memory | private beta | Agent 記憶體 namespace(第 37 章的延伸) |
wrangler ai-search | open beta | AI Search(第 36 章的託管版) |
wrangler cert / mtls-certificate | open beta / GA | client mTLS |
wrangler turnstile | alpha | Turnstile widget 管理(第 41 章用了產品但沒用 CLI) |
wrangler preview | private beta | 另一種 preview deployment |
以及不在 wrangler 裡的:Zaraz(第三方腳本管理)、Stream(影片)、Snippets(輕量版 Workers)、Zero Trust 的完整產品線、Terraform provider。
⚠️ 注意
private beta與alpha那幾個。 它們出現在 CLI 的 help 裡,但那不代表你申請得到、也不代表 API 穩定。本系列的原則從第 1 章就是「不要宣稱你沒驗證過的東西」 —— 這張表是誠實的邊界,不是待辦清單。
43.6 最後一件事
Section titled “43.6 最後一件事”這個系列有 42 個「實測結論彙整」表格,加起來幾百條。如果只能記三條:
-
去讀
node_modules。 型別檔、bundle、config-schema.json都在你的硬碟上,而且它們比文件新。本系列最有價值的十幾個發現全部來自這裡。 -
本機綠燈只是必要條件。 尤其記得第 37 章那個反方向的例子 —— Miniflare 會關掉函式庫自己的 assertion。CI 一定要有真的打一發請求的 smoke step。
-
計費單位可以在本機數出來。
meta.rows_read那一行就是帳單那一行。一個 index 的差別是 $0 和 $4,975。
其他的都會過期。這三條不會。
- D1 Time Travel · Durable Objects storage API · R2 bucket lock
- Compatibility dates · Wrangler commands
- 指令清單、Time Travel 的 30 天範圍、DO bookmark API 皆取自本機
wrangler 4.118.0(--help與wrangler types產生的worker-configuration.d.ts),2026-08-01 - 各章的實測來源見該章的「參考資料」段落
全系列完。 四十三章,四十二個可執行的範例,每一個結論都標了它是實測、推論,還是引用。歡迎在你自己的環境重跑一遍 —— 如果有哪一條在你那裡結果不同,那本身就是最有價值的回饋。