CodeGym /課程 /ChatGPT Apps /擴充與部署:負載平衡、後端服務叢集、blue/green 與 canary

擴充與部署:負載平衡、後端服務叢集、blue/green 與 canary

ChatGPT Apps
等級 16 , 課堂 3
開放

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_giftssuggest_gifts → 叢集 A(Gift REST API);
  • tool 呼叫 generate_greeting_card 或複雜的 agent 工作流程 → 叢集 B(Agents REST 服務或 worker);
  • 工具 create_orderconfirm_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(新版本)。

流程大致如下:

  1. 在 Green 環境部署新版本(v2):與現行相同的一組 gateway + 後端叢集,只是尚未接收真實流量。
  2. 在 Green 上跑完必要測試:自動化測試、smoke 測試、透過 ChatGPT Dev Mode 的手動驗證。
  3. 發佈瞬間,切換負載平衡/路由設定,讓 100% 的生產流量進到 Green。
  4. Blue 繼續並存,作為「備援跑道」。要是出事,可在幾秒內切回去。

對 GiftGenius 而言,可以有 mcp-gateway-blue.example.commcp-gateway-green.example.com。ChatGPT App 生產環境「指向」官方 MCP 端點(gateway),而在發佈時,你更改 DNS/LB 設定,讓網域 mcp-gateway.example.com 指到 green。

優點:

  • 瞬間切換、可隨時切回;
  • 遇到問題可先回滾,再從容修復;
  • 沒有「半數新、半數舊」的狀態。

缺點:

發佈期間得維持兩套完整環境,也就是資源成本 ×2。因此通常把這策略用在關鍵後端——例如 commerce 叢集 C 與 MCP Gateway 本身,因為結帳與入口不可出任何狀況。

Canary 發佈:煤礦裡的小金絲雀

Canary 是更省資源的作法:不需要兩套生產,改為逐步把小部分流量導到新版本,並密切觀察。

範例流程:

  1. 把 Gift REST API 叢集 A 的 v2 部署到同一個池,或另外一個小型 canary 池。
  2. 設定負載平衡器或 MCP Gateway,使例如 1% 與禮物相關的 tool 呼叫走 v2,而 99% 走 v1。
  3. 觀察指標:錯誤率、延遲、業務指標(轉換率、成功結帳)。
  4. 若一切正常——逐步放大流量比例: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 次(平均 12 RPS,高峰更多)時,建議有 35 個 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_IDCLUSTER_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 操作比例與主機成本,連結到你的應用業務指標,會非常有幫助。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION