1. 这节课讲什么,以及为何重要
想象你把 GiftGenius 停留在这样一个阶段:它孤零零地跑在 Vercel 上,一个 MCP‑gateway 实例(同时对外实现 MCP 并访问你的 REST 服务)、一个面向 agents 的后端,一切“貌似还能跑”。对一个 pet 项目和最初的 100 位用户,这还勉强能忍。
但一旦 OpenAI 把你的 App 加到 Store,并在圣诞节前突然上了首页,“3000 端口上的一个 gateway”就会变成很悲伤的故事:tool 调用排队、超时、500 错误、在 Store 的评分下跌,以及市场团队的邮件:“为什么在销售高峰期整个系统都趴了?”。
本课的目标是学会把 GiftGenius(以及任何 ChatGPT App)当作负载均衡器后面的一组同构实例来思考。同时——搞清楚更稳妥的发布策略,以及“如果出问题如何回滚”的清晰方案。
2. 横向扩展与 stateless 设计
先从一个基本理念开始:如果你的 MCP Gateway 或内部后端服务把重要状态保存在某个进程的内存里,就几乎不可能进行良好的横向扩展。
垂直扩展 vs 横向扩展
先厘清术语。
垂直扩展——给单台服务器“加肌肉”:更多 CPU、更多内存。上手快、初期有时更便宜,但有硬上限,而且让单个实例成为 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;
- 长任务队列(批量生成推荐、邮件推送)放在 Redis/Cloud Queue 之类的代理中;
- 如果有负责复杂 agent 工作流的独立服务,它把检查点和长久记忆保存在自己的存储中,而不是某个进程的 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. 负载均衡:流量如何进入后端服务集群
只要你的服务实例数超过 1,就需要有人来在它们之间分配请求。这就像热门披萨店的订单调度:骑手很多、客户很多,没有调度逻辑——就是混乱。
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 服务中,一个 health check 可能长这样:
// 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 / 重活 REST 服务 | 调用 LLM、复杂工作流、生成贺卡 | RPS 低,响应长(10s–2min),IO 密集 | 按任务队列长度扩展,可采用 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 服务或 workers);
- 工具 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 中,滚动更新会稍微缓和这些问题,但本质相同:没有明确策略时,就会存在“大量灰色地带”,不同版本同时处理流量。
Blue/Green 部署:双环境,瞬时切换
Blue/Green 是指同时维护两套几乎一致的环境:Blue(当前生产)与 Green(新版本)。
流程示意:
- 在 Green 环境部署新版本(v2):这是一套与生产等价的 gateway + 后端集群,只是暂不接真实流量。
- 在 Green 上跑完所有需要的测试:自动化测试、smoke 场景、通过 ChatGPT Dev Mode 的手测。
- 发布时切换负载均衡/路由,让 100% 生产流量进入 Green。
- Blue 继续“并排”存在,作为“备用跑道”。若有问题,几秒内切回 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 自身,确保 checkout 和入口不被破坏。
Canary 发布:矿井里的“小金丝雀”
Canary 发布更节省:无需两套完整生产,只需把新版本逐步放量到一小部分流量,并密切观察。
大致过程:
- 将 Gift REST API 的 A 集群 v2 部署到同一资源池,或部署到单独的小型 canary 资源池。
- 配置负载均衡或 MCP Gateway,比如让 1% 与礼物相关的 tool 调用进入 v2,99% 仍然进入 v1。
- 观察指标:错误率、延迟、业务指标(转化、成功 checkout 等)。
- 若一切正常——逐步增加比例:1% → 5% → 10% → 50% → 100%。若异常——立即回滚。
在 ChatGPT Apps 语境下,canary 不仅适用于代码,也适用于 prompt 实验:agent 服务的 system‑prompt 新版本可能会显著改变行为,最好先在一小部分用户上试验。
Gateway 或 LB 可以基于不同信号来决定“哪些请求进入 canary”:
- 随机(例如 1% 的所有请求);
- 按 userId(部分用户被持续分配到实验组);
- 按特殊的 header 或 cookie(用于内部测试)。
下面是 gateway 中的伪 TypeScript 路由逻辑(仅说明思路):
// Gateway 伪代码:5% 随机金丝雀
function routeToGiftBackendCluster(ctx: { userId?: string | null }) {
const rnd = Math.random();
if (rnd < 0.05) {
return "gift-api-v2"; // canary
}
return "gift-api-v1"; // stable
}
在真实场景中,你当然不会在运行时代码里写 Math.random(),而是把规则放到配置/特性开关中,但思路一致:部分流量进入 canary 版本,其余流量进入稳定版本。
6. 把回滚当作策略的必要组成
我很早就学到一个好规则:回滚应当比修复更快。
这意味着,一旦发布后错误频出、用户在反馈“都坏了”,不要在生产上硬修 bug。先按下那个大大的红色“回滚”按钮。
在像 Vercel 这样的托管平台(我们已经在其上部署了 GiftGenius 的 Next.js 部分)中,这很自然:每次部署都是不可变产物,Vercel 支持快速回滚到上一个版本。
对部署在 Kubernetes 或其它编排器中的 MCP Gateway 与后端集群来说,kubectl rollout undo 就是你的朋友:回到上一批 pod 与镜像。
关键是记录并展示当前对外服务的版本。例如可以:
- 在 /health 与其它诊断端点中返回 version(我们上面已这么做);
- 把发布标识通过请求头打到日志里(例如 X-Release-Id)。
小示例:一个 Next.js API 路由,用于在 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",
});
}
这样的端点对调试也很有用:你可以直接询问生产实例“当前跑的是哪个版本”,而不用猜“最新构建到底发布了没?”。
7. 容量规划:GiftGenius 需要多少实例
我们已经讨论了如何安全发布新版本(blue/green、canary)以及在出现问题时快速回滚。剩下一个务实问题:到底需要多少实例、哪些集群,才能撑住真实流量、又不把你成本拖垮?
不必过分迷信公式,但还是要有些量化。扩展要与负载和经济性挂钩:每天/每秒多少请求、多少重型 LLM 调用、一天花多少钱。
为简单起见,可以按数量级去思考:
- 每天 10k 次对 GiftGenius 的请求(平均约 0.1 RPS),1–2 个 MCP Gateway 实例外加两三个 Gift REST API/Agents worker 实例就能轻松应对;
- 每天 100k 次请求(平均 1–2 RPS,峰值更高)时,建议有 3–5 个 gateway 实例 + Gift REST API 集群,另配一个 B 集群处理重型 agent,并为 commerce 提供独立的集群;
- 每天 1M 次请求(两位数 RPS,节假日更高峰),就肯定需要多集群、为 LLM agents 预留专属资源、强力缓存以及 edge 层(另有一讲专门讨论)。
这些不是精确数字,而是提醒你评估负载量级并提前思考:瓶颈在哪、如何扩展、成本几何。
对 GiftGenius 来说,尤其要为节假日做准备:新年、圣诞、情人节、黑色星期五。负载可能成倍增长,而你的系统应该能扛住。
8. 实用小示例:GiftGenius 的部署演进
把所有内容串起来,我们画一个 GiftGenius 的简单演进路径。
这里会依次用到上面所讲的内容:gateway 与后端服务的 stateless 设计、负载均衡、独立集群与发布策略(blue/green、canary)。
基础级别:Vercel/Kubernetes 上的一个 gateway + 一个后端
课程中的某一刻你已经实现过:一个在 Vercel 上的 Next.js 应用(集成 Apps SDK),里面既有 MCP 端点,也有简单的后端逻辑(Gift/Commerce)并合在一个服务中。整体上比较单体。
优点显而易见:简单、便宜、出错面少。
唯一但致命的缺点:无法在高流量下扩展,也不利于平滑更新。
第二级:独立 MCP Gateway + 多个后端集群
下一步:
- 把 MCP Gateway 抽成独立服务(Node/Go/NGINX+Lua 都行);
- 起多个 Gift REST API 实例(A 集群)以及多个用于 agents 的 worker/服务(B 集群);
- 为 commerce 抽出独立服务(C 集群),必要时使用独立数据库/基础设施。
此时就用上了经典的 L7 负载均衡、健康检查,并尽可能做横向扩展。
第三级:发布策略
在这一层你引入:
- 对 commerce 集群 C(以及可选的 MCP Gateway)采用 Blue/Green,确保 checkout 与认证的最大稳定性;
- 对 Gift REST API 与 agent 服务采用 Canary 发布,放心地试验新的 tool 与 agent 版本,而不至于“一刀切”把生产搞挂。
示意:
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 来说,入口仍然是一处(gateway)。
9. 版本与诊断的代码片段
我们已经看到 health 端点与 /api/version。再给出一个示例:如何在 gateway 侧的 MCP tool 处理器里记录版本与集群,便于后续做度量分析。
以 tool suggest_gifts 为例:它在 Gift REST API 中实现为一个 REST 端点,通过 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 或后端进程的内存中。
这种做法扼杀横向扩展:一旦有第二个实例,状态就会在它们之间“分裂”。尤其危险的是在内存中保存购物车、搜索结果或工作流进度。这些都应放在外部存储——数据库、缓存或专门的 agent 状态存储里。
错误 2:以为“一台高配服务器”就够了。
垂直扩展在初期确实省事,但在真实增长面前效果很差:单机有物理上限、单进程是单点故障,而 ChatGPT 可能带来不可预测的流量峰值。对 MCP Gateway 与后端集群而言,几乎总需要 stateless 设计与多个实例在负载均衡器后面。
错误 3:在生产上“就地更新”,没有清晰发布策略。
如果只是更新生产集群中的容器/进程,就会出现中间态:部分流量走旧版本,部分走新版本;一旦出错,回滚就变成“再部署一次”。更可靠的做法是要么双环境(blue/green),要么至少维护一个 canary 版本的后端服务,只让小比例流量进入。
错误 4:没有快速回滚预案。
糟糕的剧本:发布了、指标全红、用户抱怨、你才开始想怎么回滚。正确的剧本:预先准备好一键回滚(blue/green 切换、rollout undo、Vercel rollback),在日志和 health 端点中暴露清晰的版本标识,并坚持“先回滚,后排查”的铁规。
错误 5:一个“大一统”集群承载所有负载。
如果贺卡生成(LLM agents)与 checkout 在同一集群里,任何模型侧的问题(延迟、超时、token 增长)都可能把支付一起拖垮。按任务类型拆分(Gift REST API/轻工具、重型 Agents 服务、Commerce REST API)并为每个集群设置独立限额/资源,是迈向稳健性的关键一步。
错误 6:架构与经济性脱节。
很容易一拍脑门就“再加两台节点”,却忘了每次 LLM 调用与每个实例都要花钱。没有基本的容量规划(对负载与成本的估算),要么扩展不足把生产撑垮,要么扩展过度把利润吃光。把请求量、重型 LLM 操作比例与托管成本同业务指标绑定起来,会很有帮助。
GO TO FULL VERSION