1. 這堂課在講什麼,以及為什麼重要
想像你把 GiftGenius 停在一個人住在 Vercel 的階段:一個 MCP‑gateway(同時對外實作 MCP 並呼叫你的 REST 服務)、一個代理後端,一切「勉強能跑」。對於 pet project 和前 100 位用戶這還能接受。
但一旦 OpenAI 把你的 App 加進 Store,然後在聖誕節前忽然上了首頁精選,「連接埠 3000 上的一個 gateway」就會變成很悲傷的故事:tool 呼叫排隊、逾時、500 錯誤、Store 評分下滑,還有行銷部門來信:「為什麼旺季全都掛了?」。
本講的目標——學會把 GiftGenius(或任何 ChatGPT App)當成位於負載平衡器之後、由多個同質實例組成的系統來思考。再加上掌握穩健的發版策略,以及一套清楚「如果出事如何回滾」的方案。
2. 水平擴充與 stateless 設計
從一個基本觀念開始:如果你的 MCP Gateway 或內部後端服務把重要狀態存放在特定程序的記憶體裡,幾乎不可能好好做水平擴充。
垂直擴充 vs 水平擴充
先釐清術語。
垂直擴充——就是替單一伺服器「加肌肉」:更多 CPU、更多 RAM。起步快速,有時候也便宜,但上限很硬,且會讓單一實例變成 single point of failure:這台猛獸一倒,全部跟著倒。
水平擴充——是在負載平衡器後面啟動多個服務執行個體。每個實例都相對小,不在記憶體保存關鍵狀態;狀態放在外部儲存(Postgres、Redis、物件儲存)。可以依負載自由增減實例。
對 MCP Gateway 與後端服務(Gift REST API、Commerce REST API、Analytics Service / REST API 等)來說,水平擴充幾乎是必需的:ChatGPT 可能突然導來數倍流量(季節、Store 推廣、某個爆紅的 TikTok),你要做的是加實例,而不是「祈禱單機撐得住」。
MCP Gateway 與後端中的 stateless 服務是什麼
要讓水平擴充有效,服務應盡可能 stateless。
Stateless 在這裡的意思是:
- 服務不在記憶體保存會影響商業邏輯的使用者獨特長期狀態;
- 任何重要狀態都放在外部資料庫、佇列、快取、S3 類型儲存;
- 若某個實例掛了,另一個實例能從外部儲存「接上」上下文,繼續服務使用者。
對 GiftGenius 而言,這表示:
- 使用者的禮物清單歷史、喜歡/不喜歡與購物車,例如都放在 Postgres;
- 長任務佇列(批量生成清單、email 推送)放在像 Redis/Cloud Queue 的佇列系統;
- 若有專門處理複雜代理工作流程的服務,它要把 checkpoint 與長期記憶放在自己的儲存裡,而不是某個程序的 RAM。
MCP Gateway 或任何後端服務的實例要變成「牛群而非寵物」:可以毫不猶豫地銷毀再重建,而不會丟失商業資料。
迷你範例:把狀態從記憶體移到外部儲存
想像你曾經做過一個非常簡單的 MCP tool add_to_cart,它透過 gateway 呼叫內部邏輯,而那段邏輯把購物車存放在程序記憶體裡(是的,demo 偶爾會這樣——只要你清楚生產環境不能這麼做就好):
// 不佳:購物車存放在後端服務程序的記憶體中
const inMemoryCarts = new Map<string, string[]>();
export async function addToCart(userId: string, sku: string) {
const cart = inMemoryCarts.get(userId) ?? [];
cart.push(sku);
inMemoryCarts.set(userId, cart);
return cart;
}
這裡無法做水平擴充:一個請求進到實例 A,另一個進到實例 B,使用者的購物車就會不同步。
正確做法——把購物車移到外部資料庫或快取。概念上(大幅簡化):
// 良好:購物車存放在外部儲存
import { db } from "./db";
export async function addToCart(userId: string, sku: string) {
await db.cartItems.insert({ userId, sku }); // 簡化示意
const cart = await db.cartItems.findMany({ where: { userId } });
return cart;
}
現在不論是哪個後端服務實例透過 gateway 處理請求,購物車對所有實例都是一致的。
3. 負載平衡:流量如何進到後端叢集
一旦服務超過一個實例,就需要有人在它們之間分配請求。這就像熱門披薩店的出單員:外送員很多、客人很多,沒有規則就會一團亂。
L4 vs L7,以及為什麼我們主要關注 L7
負載平衡器可以在不同層工作:
- L4(TCP/UDP)只是把位元組從客戶端轉給某個後端,對協議本身不了解;
- L7(HTTP)知道自己面對的是 HTTP 請求,能看路徑、標頭、cookie,甚至有時能看內文。
對 MCP Gateway 與 REST 服務的 ChatGPT App 架構,我們幾乎總是需要 L7 負載平衡:一切都走 HTTP/SSE,而且我們希望能依路徑、網域、標頭(例如 canary 發佈)做路由,並做健康檢查。
健康檢查與移除「生病」的實例
負載平衡器要定期檢查實例是否存活。最簡單的方法——提供 GET /health 或 /readyz 端點,當一切正常時回傳 200 OK。
在作為 MCP Gateway 或後端運作的 Node/TypeScript 服務中,健康檢查可以長這樣:
// apps/gateway/src/http/health.ts
import { type Request, type Response } from "express";
export function healthHandler(req: Request, res: Response) {
res.json({
status: "ok",
version: process.env.RELEASE_ID ?? "dev",
});
}
負載平衡器每隔 N 秒探測一次 /health。如果開始回 5xx 或逾時,該實例就會被移出輪轉,新的流量不再打到那裡。
Streaming / SSE 的注意事項
MCP Gateway 經常透過 SSE(Server‑Sent Events)工作,特別是你用到部分結果的串流時。負載平衡器必須:
- 支援長時間存活的 HTTP 連線;
- 在選擇實例時能考量這些連線(有些 LB 會考慮連線數,而不只 RPS)。
這很重要,因為一個「話很多」的 tool 呼叫,若串流文字 2 分鐘,就會作為一條活躍連線掛著。如果某個實例上的此類連線太多,就需要暫時「卸載」它——把新的連線送到其他實例。
4. 後端叢集:按任務切分,而不是混成一團
合乎邏輯的下一步——別再把系統當成一個「大後端服務」,而是依負載特性與關鍵性拆成多個叢集。
GiftGenius 的叢集架構範例
綜合第 16 模組的內容,對 GiftGenius 我們推薦這樣的方案:
| 叢集 | 職責 | 負載特性 | 擴充特性 |
|---|---|---|---|
| A: Gift REST API / 輕量工具 | 商品搜尋、清單格式化、簡單計算 | 高 RPS、短回應(< 500 ms)、CPU 需求低 | 依 CPU/RPS 擴充,許多小型實例 |
| B: Agents / Heavy Jobs REST 服務 | LLM 呼叫、複雜工作流程、賀詞生成 | RPS 低、回應時間長(10s–2min)、I/O 密集 | 依任務佇列長度擴充,可使用 worker |
| C: Commerce REST API / ACP | Checkout、金流服務整合、ACP | 高可靠性、嚴格 SLO | 獨立部署,變更緩慢且謹慎 |
這本質上是 bulkheads(隔艙)模式:如果叢集 B 在生成長文本時突然「燃燒 CPU token」,處理支付的叢集 C 仍能繼續工作,因為它有自己的資源池與擴充機制。
透過 Gateway 觀察的樣子
在本模組第一堂課介紹的 MCP Gateway 會看到所有進來的 MCP 流量,並把它們路由到各後端叢集。大致如下:
- tool 呼叫 list_gifts、suggest_gifts → 叢集 A(Gift REST API);
- tool 呼叫 generate_greeting_card 或複雜的 agent 工作流程 → 叢集 B(Agents REST 服務或 worker);
- 工具 create_order、confirm_payment → 叢集 C(Commerce REST API)。
在這背後可以是同一個總負載平衡器,或多個負載平衡器(例如在 commerce 前方再放一個 L7‑LB 以強化隔離)。
可以畫出整體示意:
flowchart LR
ChatGPT((ChatGPT))
GW[MCP Gateway]
LBA[LB Gift API Cluster A]
LBB[LB Agents/Workers Cluster B]
LBC[LB Commerce API Cluster C]
A1[Gift REST API A-1]
A2[Gift REST API A-2]
B1[Agents Service B-1]
B2[Agents Service B-2]
C1[Commerce REST API C-1]
C2[Commerce REST API C-2]
ChatGPT --> GW
GW -->|tools: gifts| LBA
GW -->|agents workflows| LBB
GW -->|commerce| LBC
LBA --> A1
LBA --> A2
LBB --> B1
LBB --> B2
LBC --> C1
LBC --> C2
這張圖有點理想化,但反映了核心原則:不同負載類型——在同一個 MCP Gateway 後面的不同後端叢集。
5. 發佈策略:為什麼需要 blue/green 與 canary
接著談如何更新整套系統,讓使用者毫無感覺,而你能安心入眠。
反例:直接覆蓋生產環境的部署
最簡單也最危險的策略:你拿現有叢集(例如 Gift REST API 的叢集 A),把新映像跑在舊的上方,替換容器或重啟進程。
問題在於:
- 在一部分實例已是新版本、另一部分仍是舊版本時,系統可能表現不可預期(尤其當資料庫結構有更動);
- 若出問題,回滾就變成再來一次「把原本版本部署回去」,可能要花上數分鐘;
- 在部署過程可能出現短暫停機,因為還沒有任何實例完全起來。
在 Kubernetes 與 PaaS 會透過 rolling update 略為改善,但本質相同:沒有明確策略時,你會有很長一段「灰色地帶」,不同版本同時在處理流量。
Blue/Green 部署:兩套環境,瞬間切換
Blue/Green 是同時維持兩個幾乎一樣的環境:Blue(當前生產)與 Green(新版本)。
流程大致如下:
- 在 Green 環境部署新版本(v2):與現行相同的一組 gateway + 後端叢集,只是尚未接收真實流量。
- 在 Green 上跑完必要測試:自動化測試、smoke 測試、透過 ChatGPT Dev Mode 的手動驗證。
- 發佈瞬間,切換負載平衡/路由設定,讓 100% 的生產流量進到 Green。
- Blue 繼續並存,作為「備援跑道」。要是出事,可在幾秒內切回去。
對 GiftGenius 而言,可以有 mcp-gateway-blue.example.com 與 mcp-gateway-green.example.com。ChatGPT App 生產環境「指向」官方 MCP 端點(gateway),而在發佈時,你更改 DNS/LB 設定,讓網域 mcp-gateway.example.com 指到 green。
優點:
- 瞬間切換、可隨時切回;
- 遇到問題可先回滾,再從容修復;
- 沒有「半數新、半數舊」的狀態。
缺點:
發佈期間得維持兩套完整環境,也就是資源成本 ×2。因此通常把這策略用在關鍵後端——例如 commerce 叢集 C 與 MCP Gateway 本身,因為結帳與入口不可出任何狀況。
Canary 發佈:煤礦裡的小金絲雀
Canary 是更省資源的作法:不需要兩套生產,改為逐步把小部分流量導到新版本,並密切觀察。
範例流程:
- 把 Gift REST API 叢集 A 的 v2 部署到同一個池,或另外一個小型 canary 池。
- 設定負載平衡器或 MCP Gateway,使例如 1% 與禮物相關的 tool 呼叫走 v2,而 99% 走 v1。
- 觀察指標:錯誤率、延遲、業務指標(轉換率、成功結帳)。
- 若一切正常——逐步放大流量比例:1% → 5% → 10% → 50% → 100%。若不正常——立刻回滾。
在 ChatGPT Apps 的情境中,canary 不僅適用於程式碼,也適用於 prompt 實驗:agent 服務的 system prompt 新版本可能徹底改變行為,先在小部分使用者上試水溫更安全。
Gateway 或 LB 可以用不同準則決定哪些請求屬於「canary」:
- 隨機(例如 1% 的所有請求);
- 依 userId(部分使用者永久落在實驗組);
- 依特殊標頭或 cookie(用於內部測試)。
下面用偽 TypeScript 示意 gateway 中的路由邏輯:
// Gateway 中的偽代碼:簡單隨機 5% canary
function routeToGiftBackendCluster(ctx: { userId?: string | null }) {
const rnd = Math.random();
if (rnd < 0.05) {
return "gift-api-v2"; // canary
}
return "gift-api-v1"; // stable
}
在實務中你當然不會在 runtime 用 Math.random(),而是把規則放進設定/feature flag,但邏輯很相似:一部分流量進 canary 版本,其餘走穩定版。
6. 把回滾當成策略的必要組成
我很久以前學到一條好規則:回滾必須比修復還快。
意思是,若發佈後錯誤暴增、使用者抱怨「全都壞了」,不要在生產上英勇救火。該做的是按下大紅按鈕「回滾」。
在像 Vercel 這樣的平台(我們已在其上部署 GiftGenius 的 Next.js 部分)這非常自然:每次部署都是不可變成品,Vercel 允許快速回滾到前一版。
對部署在 Kubernetes 或其他編排器上的 MCP Gateway 與後端叢集,則可用 kubectl rollout undo:回到前一組 pod 與映像。
關鍵——記錄並顯示目前正在服務流量的版本。例如可以:
- 在 /health 與其他診斷端點中加入 version(前面已示範);
- 透過標頭把發佈識別碼寫進日誌(例如 X-Release-Id)。
迷你範例:一個 Next.js API route,讓 ChatGPT App 小工具查詢組建版本:
// apps/web/app/api/version/route.ts
export async function GET() {
return Response.json({
version: process.env.RELEASE_ID ?? "dev",
builtAt: process.env.BUILT_AT ?? "unknown",
});
}
這類端點對除錯也很實用:你可以直接問生產實例目前跑的是哪個版本,而不用猜「最後一次 build 真的上線了嗎?」。
7. 容量規劃:GiftGenius 需要多少實例
我們已談到如何安全發佈(blue/green、canary)與快速回滾。接下來的實務問題是:要在生產準備多少實例、哪些叢集,才能撐住真實流量,又不把成本燒爆?
不必沉迷於公式,但多少要量化。擴充要與負載與成本綁在一起:每天/每秒多少請求、多少重 LLM 呼叫、一天要花多少錢。
可以先用量級思考:
- 若每天 10k 次對 GiftGenius 的請求(平均約 0.1 RPS),一到兩個 MCP Gateway 實例、再加上二個 Gift REST API/Agents worker 實例就能應付;
- 每天 100k 次(平均 1–2 RPS,高峰更多)時,建議有 3–5 個 gateway 與 Gift REST API 叢集實例、獨立的 B 叢集處理重型代理、以及專屬的 commerce 叢集;
- 每天 1M 次(數十 RPS、節慶高峰)時,一定需要多叢集、專門的 LLM 代理資源、積極快取與 edge 層(另有一講)。
這些不是精確數字,而是幫助你評估負載量級並提早思考:瓶頸在哪裡、如何擴充、以及成本會是多少。
對 GiftGenius 而言,尤其要為節慶做好準備:新年、聖誕節、情人節、黑色星期五。負載可能倍數成長,而你希望系統能安然度過。
8. 實作小例:GiftGenius 的部署演進
把所有內容串起來,我們畫一條簡單的 GiftGenius 部署演進路徑。
會依序套用前述所有要點:gateway 與後端服務的 stateless 設計、負載平衡、分離叢集與發佈策略(blue/green、canary)。
基礎層級:Vercel/Kubernetes 上的一個 gateway + 後端
在課程的某個階段你已經這麼做過:一個在 Vercel 的 Next.js 應用(含 Apps SDK),裡面同時有 MCP 端點與簡單的後端邏輯(Gift/Commerce)整在一個服務裡,頗為單體。
優點很明顯:簡單、便宜、容易不出錯。
缺點只有一個,但致命:無法隨著嚴苛流量擴充,更新也不平順。
層級 2:獨立 MCP Gateway + 多個後端叢集
下一步:
- 把 MCP Gateway 拆成獨立服務(Node/Go/NGINX+Lua 均可);
- 啟動多個 Gift REST API 實例(叢集 A)以及多個代理 worker/服務(叢集 B);
- 把 commerce 獨立成服務(叢集 C),甚至用獨立資料庫/基礎設施。
此時就用上典型的 L7 負載平衡、健康檢查,並視情況進行水平擴充。
層級 3:發佈策略
在這個層級你加入:
- Blue/Green 用於 commerce 叢集 C(也可以加在 MCP Gateway),確保 checkout 與授權極致穩定;
- Canary 發佈給 Gift REST API 與 agent 服務叢集,能安心嘗試新版本的 tool 與代理而不會一口氣打壞整個生產。
示意:
flowchart LR
ChatGPT((ChatGPT))
GWBlue[Gateway Blue]
GWGreen[Gateway Green]
LB[Traffic Switch]
subgraph Prod
LB --> GWBlue
LB -.canary,% .-> GWGreen
end
ChatGPT --> LB
實際情況可能更複雜(僅對 commerce 做 Blue/Green、僅對 gift 叢集做 canary),但核心觀念如圖:你永遠知道哪個版本服務了哪裡,同時對 ChatGPT 來說,看起來仍然是一個 MCP 入口(gateway)。
9. 版本與診斷的小段程式碼
我們已看過健康檢查端點與 /api/version。再增加一個例子:在 gateway 端的 MCP tool 處理器中記錄版本與叢集,方便後續彙整指標。
假設 tool suggest_gifts 以 REST 端點形式實作在 Gift REST API 中,並透過 gateway 呼叫:
import { type McpToolHandler } from "@modelcontextprotocol/sdk";
export const suggestGifts: McpToolHandler<{
occasion: string;
budget: number;
}> = async ({ input, meta }) => {
const releaseId = process.env.RELEASE_ID ?? "dev";
const clusterId = process.env.CLUSTER_ID ?? "gift-api-A";
console.log("[suggest_gifts]", {
releaseId,
clusterId,
userId: meta.userId,
occasion: input.occasion,
});
// 這裡 MCP Gateway 依照路由表呼叫 Gift REST API,
// 工具本身只是 REST 呼叫的薄包裝
return {
content: [{ type: "text", text: "Gift ideas..." }],
};
};
在這裡我們:
- 從環境變數讀取 RELEASE_ID 與 CLUSTER_ID;
- 把它們寫入結構化日誌;
- 後續即可用於分析:「哪個版本/叢集目前錯誤更多?」。
對 ChatGPT App 來說這是透明的,但對開發者是巨大助益,尤其搭配 canary/blue‑green。
10. 擴充與部署 ChatGPT App 的常見錯誤
錯誤 1:把工作階段/使用者狀態存放在 gateway 或後端程序的記憶體中。
這會扼殺水平擴充:一旦出現第二個實例,狀態就會「分裂」。特別危險的是把購物車、搜尋結果或工作流程進度放在記憶體。這些都應該放在外部儲存——資料庫、快取或代理狀態專用儲存。
錯誤 2:以為「一台高配機器」足夠。
垂直擴充在起步方便,但真實成長時效果很差:單機有物理極限,單一程序是單點故障,而 ChatGPT 可能帶來不可預期的流量尖峰。對 MCP Gateway 與後端叢集,幾乎總需要 stateless 設計與多實例置於負載平衡器後。
錯誤 3:在生產上直接覆蓋新版本,沒有清楚策略。
若你只是更新生產叢集的容器/進程,就會得到一個過渡狀態:部分流量走舊版、部分走新版;而一旦出錯,回滾就變成「再部署一次舊版」。更可靠的是要嘛維持兩套環境(blue/green),要嘛至少有一個 canary 版本的後端服務,承接少量流量。
錯誤 4:沒有快速回滾計畫。
糟糕的劇本:發布完成、指標爆紅、使用者抱怨,你才開始想怎麼回滾。正確的劇本:預先準備好瞬間回滾能力(blue/green 開關、rollout undo、Vercel rollback),在日誌與健康檢查端點清楚標示版本,並遵守「先回滾、後調查」的鐵律。
錯誤 5:所有東西共用一個叢集,沒有依負載類型分離。
如果賀卡生成(LLM 代理)與結帳在同一個叢集,任何模型端問題(延遲、逾時、token 暴增)都可能拖垮支付。按任務類型拆分叢集(Gift REST API/輕量工具、Agents‑heavy 服務、Commerce REST API),並為每個叢集設定獨立的限制/資源,是提升韌性的關鍵一步。
錯誤 6:架構與經濟脫節。
很容易沉迷於「再加幾台節點吧」,卻忘了每次 LLM 呼叫與每個實例都要花錢。缺乏基本的容量規劃(負載與成本估算)要嘛擴充不足把生產打爆,要嘛過度擴充把毛利吃光。把請求量、重型 LLM 操作比例與主機成本,連結到你的應用業務指標,會非常有幫助。
GO TO FULL VERSION