架構模式與反模式:Workers 應用怎麼設計才穩 | Cloudflare 完整教學
這一篇要拉高視角,談 Cloudflare Workers 應用的架構模式與反模式。前面 45 篇我們學會了各種零件——KV、D1、Durable Objects、R2、Queues、Service Bindings、框架整合;這一篇要把它們組裝起來,看看業界常見的架構模式(BFF、API Gateway、fan-out/fan-in、快取層、微服務組合、edge-first 資料就近),教你怎麼選對儲存與運算,並點名那些看似方便、實則會踩雷的反模式(單體巨 Worker、把 Worker 當常駐伺服器、跨請求共享狀態、過度微服務)。目標只有一個:讓你的 Workers 應用不只跑得起來,還跑得穩、擴得動。
前言
上一篇《框架整合》,我們把 Next.js、React、Astro、Remix、Hono 部署上 Workers,解決了「怎麼把應用放上去」。但「放得上去」不等於「設計得好」。當你的應用開始長大——多了驗證、多了資料庫、多了背景任務、多了第三方 API——你就會面臨一連串架構決策:這段邏輯該放同一個 Worker 還是拆出去?這份資料該存 KV 還是 D1?這個計數器為什麼在生產環境會忽大忽小?這些問題,正是這一篇要回答的。
先給一句話定義:
架構模式(Architecture Pattern)是一套「在特定情境下反覆驗證有效」的元件組織方式——它告訴你 Worker、bindings、資料流該怎麼擺放;而反模式(Anti-pattern)則是那些「看起來合理、實際上會在規模或並行下崩壞」的做法。在 Workers 這種 edge-first、isolate-based 的執行環境上,好模式與反模式的界線,常常和傳統伺服器開發的直覺相反。
打個比方會更好懂。蓋房子時,「承重牆該放哪、水電怎麼走、房間怎麼隔」有一套建築師反覆驗證的標準格局(模式);而「把承重牆打掉做開放空間」「把插座裝在浴缸旁」則是看似方便、實則危險的反模式。Workers 的架構也一樣:BFF、API Gateway 這些是被驗證過的格局;而「把所有邏輯塞進一個巨大 Worker」「在模組層級放一個累加計數器」則是遲早會塌的設計。這一篇,就是幫你認得哪些是承重牆、哪些是不能打的牆。
讀完這篇你會掌握:
- 常見架構模式——BFF、API Gateway、fan-out/fan-in、快取層、Service Bindings 微服務組合、edge-first 資料就近,各自解決什麼問題。
- 儲存與運算選型——KV、D1、Durable Objects、R2 的決策框架:用資料形狀與一致性需求來選。
- 四大反模式——單體巨 Worker、把 Worker 當常駐伺服器、跨請求共享狀態、過度微服務,為什麼會踩雷、怎麼修。
- 整合前面所學——把 45 篇學到的零件,組裝成一個能穩定擴展的架構觀。
核心概念
一、六個常見架構模式
Workers 部署在全球邊緣節點,天然適合幾種架構模式。先用一張表建立全貌,再逐一展開:
| 模式 | 一句話說明 | 解決的問題 | 關鍵技術 |
|---|---|---|---|
| API Gateway | 統一入口,做路由、認證、限流 | 多後端的統一門面 | Service Bindings、CORS |
| BFF | 為前端量身聚合多個後端回應 | 減少客戶端往返 | Promise.allSettled、邊緣聚合 |
| Fan-out / Fan-in | 一次請求並行打多個來源再合併 | 降低總延遲 | Promise.all、並行 I/O |
| 快取層(Cache-aside) | 先查快取,未命中再回源 | 降低後端負擔與延遲 | KV / Cache API |
| 微服務組合 | 多個 Worker 用 Service Bindings 串接 | 獨立部署、職責分離 | WorkerEntrypoint RPC |
| Edge-first 資料就近 | 資料/運算擺在離使用者最近處 | 消除跨區延遲 | KV、DO location hint、Smart Placement |
1. API Gateway(API 閘道): Worker 作為所有流量的統一入口,在請求進入後端前完成 CORS、認證、路由、限流。因為 Worker 跑在邊緣,這些「守門」工作零額外基礎設施就能完成。
2. BFF(Backend For Frontend): 為不同客戶端(Web、行動)在邊緣聚合多個後端回應,讓客戶端「一次請求拿到整頁所需資料」,而不用自己打三四個 API。BFF 的靈魂是 Promise.allSettled——單一後端掛掉時優雅降級,而不是整頁崩潰。
3. Fan-out / Fan-in(扇出/扇入): 這是 BFF 的底層技術。「扇出」指把一個請求並行分派給多個資料來源(fetch A、B、C 同時發),「扇入」指等它們全部回來後合併結果。關鍵在於用 Promise.all 而非序列 await——序列的總延遲是 A+B+C,並行則是 max(A, B, C)。
4. 快取層(Cache-aside): 讀取優先查快取(KV 或 Cache API),命中就直接回、未命中才回源並寫入快取。約三成的讀取可由快取層直接服務,大幅降低後端壓力與延遲。
5. 微服務組合: 用 Service Bindings 把多個 Worker 串成一個系統——閘道 Worker、認證 Worker、使用者 Worker……彼此用 RPC 直接呼叫,延遲近乎為零。這讓「拆分」的效能成本幾乎消失(詳見反模式一節)。
6. Edge-first 資料就近: Workers 的核心哲學是「把運算與資料推到離使用者最近的地方」。讀多寫少的資料放 KV(享全球邊緣快取);需要靠近資料庫的運算搭 Smart Placement;Durable Objects 可用 location hint 指定地理位置。
二、儲存與運算選型:KV / D1 / DO / R2 決策框架
架構設計裡最貴、最難回頭的決策,就是「資料該存哪」。選錯儲存是 Workers 應用最常見的架構債。用資料的形狀加上一致性需求來選,而不是憑感覺:
| 儲存 | 資料形狀 | 一致性 | 最適合 | 別拿來做 |
|---|---|---|---|---|
| KV | 鍵值 | 最終一致(寫後約 60s 全球可見) | 設定、功能旗標、快取、session | 強一致計數器、需即時的資料 |
| D1 | 關聯式(SQL) | ACID 交易 | 使用者表、訂單、需 JOIN/WHERE | 大型二進位檔案 |
| Durable Objects | 單點狀態物件 | 強一致、序列化 | 計數器、限流、聊天室、鎖、斷路器 | 海量獨立鍵值(該用 KV) |
| R2 | 大型二進位物件 | 物件層一致 | 圖片、影片、備份、檔案 | 需查詢的結構化資料 |
決策時問自己三個問題:
- 這份資料要不要用 SQL 查(JOIN、WHERE、聚合)? 要 → D1。
- 要不要「跨請求強一致地協調」(計數、排隊、鎖、序列化寫入)? 要 → Durable Objects。
- 是不是「讀多寫少、能接受幾十秒延遲」的鍵值? 是 → KV;若是大檔案 → R2。
運算面同理:預設 Worker 跑在最靠近使用者的節點;若 Worker 頻繁存取特定後端資料庫,啟用 Smart Placement 讓它自動移到最靠近資料庫的節點;連外部 Postgres/MySQL 用 Hyperdrive 消除連線建立成本;重度或長時間運算交給 Queues / Workflows 非同步處理,別阻塞請求。
三、幾個關鍵術語
- Service Bindings(服務綁定): Worker 之間直接呼叫的機制,無需公開 HTTP URL,同節點延遲約 0.1–0.5ms。推薦用 RPC 模式(被呼叫方
extends WorkerEntrypoint)。 - isolate: Workers 的執行單元,輕量、快速啟動、依流量動態建立與回收。跨請求不保證共享記憶體——這是所有反模式的根源。
ctx.waitUntil(): 讓執行期「等待」回應送出後的背景工作完成(最多 30 秒),避免背景 Promise 被靜默丟棄。
實作範例
理論看完,我們把幾個核心模式的骨架實際寫出來,程式碼可直接作為專案起點。
1. API Gateway + 微服務組合(Service Bindings RPC)
先看被呼叫方——認證 Worker 用 WorkerEntrypoint 暴露 RPC 方法:
// auth-worker/src/index.ts — 被呼叫的認證微服務
import { WorkerEntrypoint } from 'cloudflare:workers';
interface Env {
SESSIONS_KV: KVNamespace;
}
export default class AuthWorker extends WorkerEntrypoint<Env> {
// RPC 方法:其他 Worker 可直接呼叫,類型安全、零網路延遲
async verifyToken(token: string): Promise<{ userId: string; role: string }> {
const userId = await this.env.SESSIONS_KV.get(`session:${token}`);
if (!userId) throw new Error('Invalid token');
return { userId, role: 'user' };
}
}
再看閘道 Worker——透過 Service Binding 呼叫認證服務,並依路徑分派到其他微服務:
// api-gateway/src/index.ts — 統一入口:認證 + 路由
interface Env {
AUTH_SERVICE: Service<import('../../auth-worker/src/index').default>;
USER_SERVICE: Fetcher; // HTTP 模式的使用者 Worker
PRODUCT_SERVICE: Fetcher; // HTTP 模式的商品 Worker
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// 1. 邊緣守門:先做認證(RPC 呼叫,近乎零延遲)
try {
const token = request.headers.get('Authorization')?.replace('Bearer ', '') ?? '';
await env.AUTH_SERVICE.verifyToken(token);
} catch {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. 依路徑分派到對應微服務
if (url.pathname.startsWith('/api/users')) {
return env.USER_SERVICE.fetch(request);
}
if (url.pathname.startsWith('/api/products')) {
return env.PRODUCT_SERVICE.fetch(request);
}
return new Response('Not Found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
對應的 wrangler.jsonc 用 services 宣告綁定:
// api-gateway/wrangler.jsonc
{
"name": "api-gateway",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"services": [
{ "binding": "AUTH_SERVICE", "service": "auth-worker" },
{ "binding": "USER_SERVICE", "service": "user-worker" },
{ "binding": "PRODUCT_SERVICE", "service": "product-worker" }
]
}
2. BFF + fan-out/fan-in(邊緣聚合)
BFF 的骨架:一次請求,並行(扇出)取得多個後端資料,再(扇入)合併成前端要的形狀。用 Promise.allSettled 讓單一後端失敗時優雅降級:
// bff-worker/src/index.ts — 為前端聚合多後端,單點失敗不整頁崩潰
interface Env {
USER_SERVICE: Service;
ORDER_SERVICE: Service;
RECO_SERVICE: Service;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const userId = new URL(request.url).searchParams.get('userId');
if (!userId) return new Response('Missing userId', { status: 400 });
// 扇出:並行打三個後端,總延遲 = max(三者) 而非總和
const [user, orders, recos] = await Promise.allSettled([
env.USER_SERVICE.getUser(userId),
env.ORDER_SERVICE.getRecentOrders(userId, 5),
env.RECO_SERVICE.getRecommendations(userId),
]);
// 扇入:合併結果,失敗的部分優雅降級為預設值
return Response.json({
user: user.status === 'fulfilled' ? user.value : null,
orders: orders.status === 'fulfilled' ? orders.value : [],
recommendations: recos.status === 'fulfilled' ? recos.value : [],
});
},
} satisfies ExportedHandler<Env>;
重點: BFF 場景一定用 Promise.allSettled 而非 Promise.all——Promise.all 只要一個 reject,整批就 reject,推薦服務掛掉會害整頁 500;allSettled 則讓你逐一檢查成敗,把可用的資料先回給前端。
3. 快取層(Cache-aside)+ 選對儲存
一個把「快取層」與「儲存選型」結合的實例:讀取商品時先查 KV 快取(讀多寫少、可接受最終一致),未命中才查 D1(結構化、需 SQL),再非同步寫回快取:
// 讀取優先快取:KV 當快取層,D1 當事實來源(source of truth)
interface Product { id: string; name: string; price: number; }
interface Env {
PRODUCT_CACHE: KVNamespace; // 快取層:讀多寫少
DB: D1Database; // 事實來源:需要 SQL 查詢
}
async function getProduct(id: string, env: Env, ctx: ExecutionContext): Promise<Product | null> {
const cacheKey = `product:${id}`;
// 1. 先查 KV 快取(邊緣就近,約三成請求命中)
const cached = await env.PRODUCT_CACHE.get<Product>(cacheKey, 'json');
if (cached) return cached;
// 2. 未命中,回源查 D1
const product = await env.DB
.prepare('SELECT id, name, price FROM products WHERE id = ? AND active = 1')
.bind(id)
.first<Product>();
// 3. 非同步寫回快取,不阻塞回應(用 waitUntil 確保背景工作不被丟棄)
if (product) {
ctx.waitUntil(
env.PRODUCT_CACHE.put(cacheKey, JSON.stringify(product), { expirationTtl: 300 })
);
}
return product ?? null;
}
這段同時示範了三個觀念:快取層模式(cache-aside)、儲存選型(KV 快取 + D1 事實來源各司其職)、以及 ctx.waitUntil() 正確處理回應後的背景寫入。
常見錯誤與最佳實踐
好架構的另一半,是認得並避開反模式。以下四個是 Workers 上最常見、也最容易在生產環境爆炸的設計陷阱。
反模式一:單體巨 Worker(把所有邏輯塞進一個 Worker)。
把驗證、商品、訂單、金流、Email、Webhook……全部塞進同一個 fetch handler,用一長串 if (path === ...) 分派。問題不在效能,而在維護與部署:任何一行改動都要重新部署整包、任何一個模組出錯都可能拖垮全部、bundle 越滾越大逼近 10MB 上限、團隊之間互相踩程式碼。正確做法: 用 Service Bindings 把職責拆成獨立 Worker(閘道 / 認證 / 使用者 / 商品),各自獨立部署。因為 Service Bindings 的呼叫延遲僅 0.1–0.5ms,拆分幾乎沒有效能成本——這正是上面 API Gateway 範例的設計。
反模式二:把 Worker 當常駐伺服器寫(依賴全域可變狀態)。
這是最隱蔽、最難在本機重現的坑。傳統 Node.js 伺服器是長期存活的進程,你可以在模組層級放連線池、記憶體快取、累加計數器。但 Workers 跑在 isolate 上,同一 isolate 可能同時服務多個請求,也可能隨時被回收:
// ❌ 反模式:模組層級可變狀態——並行請求下互相污染
let currentUserId: string | null = null; // 危險!
let requestCount = 0; // 危險!
export default {
async fetch(request: Request, env: Env): Promise<Response> {
currentUserId = request.headers.get('X-User-Id'); // 請求 A 設定
requestCount++;
// 若 isolate 同時處理請求 B,currentUserId 已被 B 覆蓋,
// 請求 A 這裡讀到的是「別人的」使用者 ID!
const data = await env.KV.get(`user:${currentUserId}`);
return Response.json({ userId: currentUserId, data });
},
};
// ✅ 正確:請求作用域資料一律透過參數傳遞,不碰模組層級變數
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const userId = request.headers.get('X-User-Id'); // 只活在這次請求
const data = await env.KV.get(`user:${userId}`);
return Response.json({ userId, data });
},
};
判斷準則:模組層級變數只能放常數或遺失可接受的快取(正則、設定值、region 對照表);絕不能放請求特定 ID、使用者資料、累加計數器、認證 token。心智模型:假設每個請求都是全新 isolate 的第一個請求。
反模式三:跨請求共享狀態放在記憶體(斷路器、限流器、鎖)。
反模式二的延伸:傳統開發常見的「記憶體中斷路器」「記憶體計數器」在 Workers 完全無效——不同請求由不同 isolate 處理,isolate 間不共享記憶體,狀態會「消失」或「不一致」。正確做法: 任何跨請求要共享的狀態,必須外置到持久層,依需求選:
- 強一致計數/序列化(限流、斷路器、分散式鎖、聊天室) → Durable Objects。
- 最終一致、讀多寫少(功能旗標、設定、session) → KV。
- 需要 SQL 查詢/交易的共享資料 → D1。
這正是前面「儲存選型」的實戰價值——選對持久層,才是跨請求狀態的唯一正解。
反模式四:過度微服務(一開始就拆成一堆 Worker)。
反模式一的反向錯誤。看到「Service Bindings 讓拆分幾乎免費」就把每個小功能都拆成獨立 Worker,結果是:綁定關係盤根錯節、本機開發要同時啟動十幾個 Worker、一個簡單改動要跨多個 repo/部署。正確做法: 從單一 Worker 開始,只在真正需要「獨立部署」「不同擴展策略」「不同團隊邊界」時才拆。讓部署獨立性而非效能來驅動拆分——效能從來不是 Workers 拆或不拆的理由。
最佳實踐小結:
- 先合後分——從單一 Worker 起步,依「部署獨立性」逐步拆成微服務,別過早也別過晚。
- 假設每個請求都是全新 isolate——請求作用域資料走參數,模組層級只放常數與可拋棄快取。
- 跨請求狀態一律外置——限流/斷路器/鎖用 Durable Objects,設定/session 用 KV,結構化共享資料用 D1。
- 選對儲存是最重要的架構決策——用資料形狀與一致性需求對齊 KV / D1 / DO / R2,別憑感覺。
- 背景工作包
ctx.waitUntil()——回應送出後的寫入、記錄,不包就會被靜默丟棄。
小結
上一篇《框架整合》,我們把主流框架部署上 Workers,解決了「怎麼放上去」;這一篇《架構模式與反模式》,我們拉高視角,把前面所學的零件組裝成一套架構觀:
- 六個常見模式——API Gateway、BFF、fan-out/fan-in、快取層、Service Bindings 微服務組合、edge-first 資料就近,各自解決不同的組織與延遲問題。
- 儲存與運算選型——用資料形狀與一致性需求對齊 KV(鍵值/最終一致)、D1(SQL/ACID)、Durable Objects(強一致/協調)、R2(大檔案),運算面搭配 Smart Placement、Hyperdrive、Queues。
- 四大反模式——單體巨 Worker、把 Worker 當常駐伺服器、跨請求共享狀態放記憶體、過度微服務,以及各自的正解。
一句話收束整篇:Workers 的架構好壞,常常和傳統伺服器的直覺相反——因為它是 edge-first、isolate-based。記住「先合後分」「假設每個請求都是全新 isolate」「跨請求狀態一律外置」「選對儲存」這四條,你的應用就能跑得穩、擴得動。
本篇也正式開啟整個系列的最佳實踐段落。掌握了「怎麼組織架構」,下一步就是「怎麼讓它跑得更快」。下一篇《效能最佳化》,我們會深入 Workers 的效能調校:並行請求、快取策略、Smart Placement、Bundle 大小、減少子請求——把這一篇設計好的架構,再榨出最後一分延遲。我們下一篇見。
想查閱 Workers 架構模式與最佳實踐的官方最新指南,可以參考 Cloudflare Workers 最佳實踐官方文件(各項功能名稱、限制與支援矩陣以當下官方文件為準)。