架構模式與反模式:Workers 應用怎麼設計才穩 | Cloudflare 完整教學

2026/09/15
架構模式與反模式:Workers 應用怎麼設計才穩 | Cloudflare 完整教學

這一篇要拉高視角,談 Cloudflare Workers 應用的架構模式反模式。前面 45 篇我們學會了各種零件——KV、D1、Durable Objects、R2、Queues、Service Bindings、框架整合;這一篇要把它們組裝起來,看看業界常見的架構模式(BFFAPI Gatewayfan-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 的架構也一樣:BFFAPI 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.jsoncservices 宣告綁定:

// 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 最佳實踐官方文件(各項功能名稱、限制與支援矩陣以當下官方文件為準)。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →