CodeGym /コース /ChatGPT Apps /スケーリングとデプロイ: 負荷分散、バックエンドサービスのクラスター、blue/green と canary

スケーリングとデプロイ: 負荷分散、バックエンドサービスのクラスター、blue/green と canary

ChatGPT Apps
レベル 16 , レッスン 3
使用可能

1. この講義のテーマと重要性

想像してください。GiftGenius を Vercel 上に寂しく 1 台だけ置き、MCP を外部に公開しつつ自分たちの REST サービスへもアクセスする 1 つの MCP‑gateway、エージェント用のバックエンドも 1 つ、そして「なんとか動いている」。これは pet プロジェクトや最初の 100 名のユーザーまではまだ許容できます。

しかし、OpenAI があなたの App を Store に掲載し、たとえばクリスマス前の特集に突然載ったら、「3000 番ポートで動く 1 台の gateway」は悲劇に変わります。tool 呼び出しのキュー、タイムアウト、500 エラー、Store のレーティング低下、そして「なぜ売上ピークに落ちていたのか」というマーケからのメール。

この講義の目標は、GiftGenius(および任意の ChatGPT App)を、ロードバランサの背後で同一のインスタンスが多数動くシステムとして考えられるようになることです。加えて、丁寧なリリース戦略と、「うまくいかなかったらどうやってすぐ戻すか」という明確な手順を理解します。

2. 水平スケーリングと stateless デザイン

まず基本の考え方から。もし MCP Gateway や内部バックエンドサービスが、特定プロセスのメモリに重要な状態を保持しているなら、それを水平スケーリングするのはほぼ不可能です。

垂直スケーリング vs 水平スケーリング

先に用語を整理します。

垂直スケーリングは、1 台のサーバーに「筋肉を盛る」やり方です。CPU を増やす、RAM を増やす。手早く、初期は安く済むこともありますが、限界が明確で、単一インスタンスがsingle point of failureになります。その強力な 1 台が落ちれば、すべてが落ちます。

水平スケーリングは、ロードバランサの背後に複数のサービスインスタンスを走らせるやり方です。各インスタンスは比較的小さく、重要な状態はメモリに持たず、外部ストア(Postgres、Redis、オブジェクトストレージ)に置きます。負荷に応じてインスタンスを自由に増減できます。

MCP Gateway やバックエンドサービス(Gift REST API、Commerce REST API、Analytics Service / REST API など)において、水平スケーリングは実質必須です。ChatGPT からのトラフィックは突発的に増えることがあります(季節、Store のプロモ、バズった TikTok など)。その時に「1 台のサーバーが持ちこたえるよう祈る」のではなく、インスタンスを増やせば済むようにしておくべきです。

MCP Gateway とバックエンドにおける stateless サービスとは

水平スケーリングを成立させるには、サービスは可能な限り stateless である必要があります。

Stateless の本講義での意味は次のとおりです。

  • サービスは、ビジネスロジックに影響するユーザー固有の長寿命の状態を、特定プロセスのメモリに保持しない。
  • 重要な状態は外部のデータベース、キュー、キャッシュ、S3 風ストレージに置く。
  • 特定インスタンスが落ちても、別のインスタンスが外部ストレージからコンテキストを取得して処理を継続できる。

GiftGenius に当てはめると:

  • ユーザーのギフト候補履歴、Like/Dislike、カートは Postgres などに保存する。
  • 長時間タスクのキュー(大量の候補生成、メール配信など)は Redis/Cloud Queue のようなブローカーに置く。
  • 複雑なエージェント系ワークフロー用の別サービスがあるなら、チェックポイントや長期メモリは単一プロセスの RAM ではなく、そのサービス専用ストアに保存する。

MCP Gateway や任意のバックエンドサービスのインスタンスは「ペットではなく家畜」に変わります。ビジネスデータを失うことなく、容赦なく落として作り直せます。

ミニ例: メモリ内状態を外部ストアに移す

かつて非常に単純な MCP‑tool add_to_cart を作り、gateway 経由で内部ロジックを呼び出し、そのロジックがカートをプロセスのメモリに保持していたとします(デモではたまにやります。ダメだと理解している限りは問題ありません)。

// 悪い例: バックエンドサービスのプロセスメモリにカートを保持
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 に行き、ユーザーのカートが分裂してしまいます。

正しいやり方は、カートを外部の DB やキャッシュに出すことです。概念的には(大幅に簡略化):

// 良い例: カートは外部ストアに保存
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. 負荷分散: トラフィックはどうやってバックエンドクラスターに届くか

インスタンスが 2 台以上になった瞬間、リクエストを振り分ける存在が必要です。人気ピザ店の配達管理と同じです。配達員も顧客も多く、ロジックがないとカオスになります。

L4 と 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 接続をサポートする。
  • インスタンス選択時にこの種の接続を考慮できる(RPS だけでなくアクティブ接続数を考慮できる LB もある)。

これは重要です。たとえば 2 分間テキストをストリームする「おしゃべりな」tool 呼び出しは、接続が張り付きます。このような接続が 1 台に偏り過ぎたら、そのインスタンスを一時的に「軽く」し、新規接続を他へ振る必要があります。

4. バックエンドクラスター: 役割ごとに分ける

次の合理的なステップは、「大きなバックエンドサービス 1 個」という発想をやめ、負荷特性と重要度に応じて複数のクラスターに分けることです。

GiftGenius のクラスター別アーキテクチャ例

モジュール 16 の内容を踏まえると、GiftGenius には次のような構成を推奨できます。

クラスター 役割 負荷特性 スケーリングの特徴
A: Gift REST API / 軽量ツール 商品の検索、リスト整形、簡単な計算 高 RPS、短い応答(< 500 ms)、CPU 負荷は小 CPU/RPS に応じてスケール、多数の小さなインスタンス
B: Agents / Heavy Jobs REST サービス LLM 呼び出し、複雑なワークフロー、メッセージカード生成 低 RPS、長い応答(10s–2min)、IO ヘビー タスクキューの長さに応じてスケール、ワーカー利用可
C: Commerce REST API / ACP チェックアウト、決済プロバイダ統合、ACP 高い信頼性が必須、厳格な SLO 独立デプロイ、変更は慎重かつゆっくり

これは本質的に bulkheads(隔壁)パターンの実装です。もしクラスター B が複雑なテキスト生成で「トークンを燃やし」始めても、支払いのクラスター C は独自のリソースプールとスケーリングを持つため動作を継続できます。

Gateway 経由でどう見えるか

モジュール最初の講義で説明した MCP Gateway は、すべての MCP トラフィックを受け、バックエンドクラスターにルーティングします。例えば次のように:

  • tool 呼び出し list_giftssuggest_gifts → クラスター A(Gift REST API)
  • tool 呼び出し generate_greeting_card や複雑なエージェントワークフロー → クラスター B(Agents REST サービスまたはワーカー)
  • create_orderconfirm_payment → クラスター C(Commerce REST API)

この背後には共通のロードバランサ 1 台でも、複数のロードバランサでも構いません(たとえば 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

図は理想化されていますが、要点は反映しています。異なる負荷タイプは、1 つの MCP Gateway の背後で、それぞれ別のバックエンドクラスターに分けるということです。

5. デプロイ戦略: なぜ blue/green と canary が必要か

次は、ユーザーに気づかれず、あなたが夜ぐっすり眠れるように、どのように更新していくかです。

アンチパターン: 本番に「上書き」デプロイ

最も簡単で最も危険な戦略です。稼働中のクラスター(例: クラスター A の Gift REST API)に対して、新しいイメージを既存の上に展開し、コンテナを入れ替えたり、プロセスを再起動したりします。

問題点:

  • 一部インスタンスが新、他が旧という状態になると、とくに DB スキーマを変更した場合、システム挙動が予測不能になります。
  • 問題発生時のロールバックは「以前の状態で再デプロイ」で、数分かかる場合があります。
  • デプロイの瞬間、どのインスタンスもまだ立ち上がっておらず短時間のダウンタイムが起きうる。

Kubernetes や PaaS のローリングアップデートで多少は緩和されるものの、根本は同じです。明確な戦略がなければ、「同時に異なるバージョンが処理している」グレーゾーンが大きくなります。

Blue/Green デプロイ: 2 つの環境と瞬時の切り替え

Blue/Green は、Blue(現行本番)と Green(新バージョン)の、ほぼ同一な 2 つの環境を同時に持つアプローチです。

流れの概略:

  1. Green 環境に新バージョン(v2)を展開します。gateway とバックエンドクラスターのセットは同じですが、まだ実トラフィックは流しません。
  2. Green に対して必要なテストをすべて実施します。自動テスト、スモーク、ChatGPT Dev Mode を使った手動確認など。
  3. リリース時に、100% の本番トラフィックが Green に行くように、ロードバランサ/ルーティングを切り替えます。
  4. Blue は「待機滑走路」として残します。問題があれば数秒でトラフィックを戻せます。

GiftGenius の場合、mcp-gateway-blue.example.commcp-gateway-green.example.com を用意できます。本番の ChatGPT App は公式の MCP エンドポイント(gateway)を向いており、リリース時に mcp-gateway.example.com というドメイン名が green を指すように DNS/LB の設定を切り替えます。

メリット:

  • 双方向に即時切り替え可能。
  • 問題はロールバック後に落ち着いて修正できる。
  • 「クラスターの半分が新、半分が旧」という状態がない。

デメリット:

リリースの間は 2 つの完全な環境を維持するため、コストが 2 倍かかります。そのため、この戦略は特に重要なバックエンド(例: コマースのクラスター C や MCP Gateway)に適用されることが多いです。チェックアウトや入口は決して壊したくない部分です。

Canary リリース: 炭鉱のカナリア

Canary リリースは、より経済的な選択です。2 つの本番環境を同時に維持せず、新バージョンを少量のトラフィックに徐々に展開し、細心の注意を払って監視します。

シナリオ例:

  1. クラスター A(Gift REST API)の v2 を、同一プール内または小規模なカナリア用プールにデプロイします。
  2. ロードバランサまたは MCP Gateway を設定し、たとえばギフト関連の tool 呼び出しの 1% を v2、99% を v1 に流します。
  3. メトリクスを監視します。エラー率、レイテンシ、ビジネス指標(コンバージョン、成功したチェックアウトなど)。
  4. 問題なければ徐々に割合を上げます。1% → 5% → 10% → 50% → 100%。問題があれば即座にロールバックします。

ChatGPT Apps の文脈では、canary はコードだけでなくプロンプトの実験にも有用です。エージェントサービスの system プロンプト新バージョンは挙動を大きく変える可能性があるため、まずは小さなユーザー群で検証したほうがよいでしょう。

Gateway または LB は、「どのリクエストをカナリア扱いにするか」を次のような基準で判断できます。

  • 完全なランダム(例: 全リクエストの 1%)。
  • userId に基づく(一部ユーザーを恒久的に実験群へ)。
  • 専用ヘッダや Cookie(社内テスト用)。

以下は gateway 内のルーティングロジックを示す疑似 TypeScript(イメージ)。

// Gateway 内の疑似コード: simple random canary 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
}

実運用では、当然ながら runtime の Math.random() ではなく、ルールをコンフィグ/フィーチャーフラグに切り出すはずですが、考え方は同じです。トラフィックの一部を canary バージョンに、残りを安定版に送ります。

6. ロールバックは戦略の必須要素

「ロールバックは修正より速く」を心に刻みましょう。

つまり、リリース後にエラーが増え、ユーザーから「壊れている」と言われたら、本番でバグ修正を英雄的に試みるのではなく、「大きな赤いロールバック」ボタンを押すべきです。

Vercel(すでに Next.js 部分の GiftGenius をデプロイ済みのプラットフォーム)のような環境では特に自然です。各デプロイはイミュータブルなアーティファクトであり、Vercel はすばやく前の状態へ戻せます。

Kubernetes などのオーケストレータで展開している MCP Gateway やバックエンドクラスターの場合は、kubectl rollout undo がその役割を担います。直前の Pod セットやイメージに巻き戻せます。

重要なのは、「今トラフィックを処理しているバージョン」をログおよび UI で把握できるようにすることです。たとえば次のようにします。

  • version/health や他の診断用エンドポイントに含める(すでに上で実装)。
  • リリース ID をヘッダ経由でログに流す(例: X-Release-Id)。

ミニ例: ChatGPT App のウィジェット内から参照するための、Next.js の API ルート。ビルドバージョンを返します。

// 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)と迅速なロールバックを見てきました。次の実務的な問いは、「実トラフィックに耐えつつ、費用面で破綻しないために、本番でどのクラスターを何台持つべきか」です。

数式にこだわる必要はありませんが、ある程度は必要です。スケーリングは負荷と経済性に紐づけます。1 日/1 秒あたりのリクエスト数、重い LLM 呼び出しの数、それにかかるコスト。

大まかな目安として:

  • 10k 件/日のリクエスト(平均 0.1 RPS 程度)なら、MCP Gateway は 1〜2 台、Gift REST API/Agents ワーカーも数台で十分に回せます。
  • 100k 件/日(平均 12 RPS、ピークはそれ以上)なら、gateway とクラスター A(Gift REST API)を 35 台、重いエージェント用のクラスター B、そして専用の commerce クラスターを持つべきです。
  • 1M 件/日(2 桁 RPS、祝期のピーク)なら、クラスター化は必須で、LLM エージェント向けの専用リソース、強力なキャッシュ、エッジ層(これは別講義)も必要になります。

これは厳密な数値ではなく、負荷の桁感を評価し、ボトルネック、スケーリング方法、コストを事前に考えるための指針です。

GiftGenius にとっては祝期への備えが特に重要です。年末年始、クリスマス、バレンタイン、ブラックフライデー。負荷は数倍に膨れ上がる可能性があり、それでもシステムが耐えられるようにしておくべきです。

8. 実践ミニ例: GiftGenius のデプロイ進化

ここまでの内容をまとめ、GiftGenius のデプロイ進化を簡単に描いてみます。
stateless な gateway とバックエンド、負荷分散、クラスター分割、そしてリリース戦略(blue/green、canary)を段階的に適用します。

ベースレベル: Vercel/Kubernetes 上に gateway + backend を 1 つ

講座のある段階でこれを実施しています。Apps SDK を用いた Next.js アプリを Vercel に 1 つデプロイし、その中に MCP エンドポイントと単純なバックエンドロジック(Gift/Commerce)が同居。かなりモノリシックです。

メリットは明白です。シンプル、安価、ミスの余地が少ない。

しかし致命的な欠点が 1 つ。大規模トラフィックにはスケールしづらく、更新にも弱いことです。

レベル 2: 独立した MCP Gateway + 複数のバックエンドクラスター

次のステップ:

  • MCP Gateway を独立したサービスへ切り出す(Node/Go/NGINX+Lua など手段は問わない)。
  • Gift REST API(クラスター A)のインスタンスを複数、エージェント用のワーカー/サービス(クラスター B)も複数起動する。
  • commerce は専用サービス(クラスター C)に分け、場合によっては専用 DB/インフラに置く。

ここで L7 の負荷分散、ヘルスチェック、可能なら水平スケーリングを導入します。

レベル 3: リリース戦略

このレベルでは次を追加します。

  • commerce クラスター C(必要なら MCP Gateway も)に対して Blue/Green を適用し、チェックアウトや認証を最大限安定化する。
  • Gift REST API とエージェントサービスのクラスターに対して Canary リリースを適用し、新しい 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 からは依然として 1 つの MCP 入口(gateway)に見えることです。

9. バージョニングと診断のための小さなコード断片

すでにヘルスエンドポイントと /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_IDCLUSTER_ID を環境変数から読み取り、
  • それらを構造化ログとして出力し、
  • 「どのバージョン/クラスターでエラーが多いか?」といった分析に活用できるようにしています。

ChatGPT App の側から見ると完全に透過的ですが、開発者にとっては大きなメリットです。とくに canary/blue‑green と組み合わせると効果的です。

10. ChatGPT App のスケーリングとデプロイでよくある誤り

エラー №1: セッション/ユーザー状態を gateway やバックエンドプロセスのメモリに保持する。
このやり方は水平スケーリングを阻害します。2 台目のインスタンスが登場した瞬間に、状態が分断されてしまうからです。とくに、カート、検索結果、ワークフロー進捗をメモリに保持するのは危険です。これらはすべて外部ストア(DB、キャッシュ、エージェント状態用の専用ストア)に置くべきです。

エラー №2: 「強力な 1 台」で十分だと思い込む。
垂直スケーリングは初期には便利ですが、実際の成長には弱いです。マシンには物理的限界があり、単一プロセスは single point of failure になります。ChatGPT は予測不能なスパイクをもたらし得ます。MCP Gateway とバックエンドクラスターには、ほぼ常に stateless デザインと複数インスタンス+ロードバランサが必要です。

エラー №3: 明確な戦略なしに本番へ「上書き」デプロイする。
本番クラスターでコンテナ/プロセスをそのまま更新すると、一部が旧版、一部が新版という中間状態になります。エラー時のロールバックも「再デプロイ」で手間と時間がかかります。はるかに信頼できるのは、2 つの環境を持つ(blue/green)か、少なくとも少量トラフィックを流す canary バージョンを用意することです。

エラー №4: 迅速なロールバック計画がない。
悪いシナリオ: リリース後にメトリクスが悪化し、ユーザーが苦情を出しているのに、その時点でロールバック手順を考え始める。良いシナリオ: 事前に即時ロールバック手段(blue/green の切替、rollout undo、Vercel のロールバック)を用意し、ログやヘルスエンドポイントでバージョンを明確化し、「まず戻す、原因は後で調べる」という厳格なルールを持つこと。

エラー №5: 負荷タイプの分離なしに「何でも入り」の単一クラスターにする。
祝辞テキスト生成(LLM エージェント)とチェックアウトが同一クラスターにあると、モデル側の遅延(timeout、トークン増)などの問題が決済にも波及します。タスクタイプごとのクラスター分離(Gift REST API/軽量ツール、Agents ヘビー、Commerce REST API)と、それぞれに個別の制限/リソースを設けることが、堅牢性の重要な一歩です。

エラー №6: アーキテクチャと経済性が結びついていない。
「ノードをもう 2 台足そう」と気軽にやりがちですが、各 LLM 呼び出しや各インスタンスにはコストがかかります。簡単なキャパシティプランニング(負荷と費用の見積もり)を怠ると、スケール不足で本番を落とすか、過剰スケールで利益を失います。リクエスト数、重い LLM 操作の割合、ホスティング費用を、アプリのビジネス指標と結びつけて考えることが重要です。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION