1. О чём вообще эта лекция и почему это важно
Представьте, что вы оставили GiftGenius на той стадии, где он одиноко живёт на Vercel, один инстанс MCP‑gateway (который одновременно реализует MCP наружу и ходит в ваши REST‑сервисы), один backend для агентов, всё «как-то работает». Это ещё терпимо для pet‑проекта и первых 100 пользователей.
Но как только OpenAI добавит ваш App в Store и он внезапно попадёт на главную подборку перед Рождеством, «один gateway на 3000 порту» превратится в очень грустную историю: очередь tool‑вызовов, таймауты, 500‑е ошибки, падение рейтинга в Store и письма от маркетинга в стиле «а почему всё лежало в пик продаж?».
Наша задача в этой лекции — научиться думать о GiftGenius (и любом ChatGPT App) как о системе из множества одинаковых инстансов за балансировщиком. Плюс — разобраться с аккуратными стратегиями релизов и понятной схемой «как откатиться, если что-то пошло не так».
2. Горизонтальное масштабирование и stateless‑дизайн
Начнём с базовой идеи: если ваш MCP Gateway или внутренний backend‑сервис хранит важное состояние в памяти конкретного процесса, его практически невозможно нормально масштабировать горизонтально.
Вертикальное vs горизонтальное масштабирование
Сначала разберём терминологию.
Вертикальное масштабирование — это когда вы просто «накручиваете мышцу» одному серверу: больше CPU, больше RAM. Это быстро, иногда дешёво на старте, но имеет жёсткий предел и делает один инстанс single point of failure: если этот мощный монстр падает, падает всё.
Горизонтальное масштабирование — это когда вы запускаете несколько экземпляров сервиса за балансировщиком. Каждый инстанс относительно маленький, ничего критичного в памяти не хранит, а состояние уезжает во внешние хранилища (Postgres, Redis, object storage). Можно свободно добавлять и убирать инстансы под нагрузку.
Для MCP Gateway и backend‑сервисов (Gift REST API, Commerce REST API, Analytics Service / REST API и т.п.) горизонтальное масштабирование фактически обязательно: ChatGPT может внезапно направить вам в разы больше трафика (сезон, промо в Store, какой-то вирусный TikTok), и вы должны просто добавить инстансов, а не «молиться, чтобы один сервер выдержал».
Что такое stateless‑сервис в контексте MCP Gateway и backend‑ов
Чтобы горизонтальное масштабирование работало, сервис должен быть максимально stateless.
Stateless в нашем контексте значит:
- сервис не хранит в памяти уникальное, долгоживущее состояние пользователя, от которого зависит бизнес‑логика;
- любое важное состояние хранится во внешней БД, очереди, кэше, S3‑подобном хранилище;
- если конкретный инстанс упал, другой инстанс может продолжить обслуживать пользователя, просто «подхватив» контекст из внешнего хранилища.
Для GiftGenius это означает, что:
- история подборов подарков пользователя, его лайки/дизлайки и корзина лежат, например, в Postgres;
- очереди длительных задач (массовая генерация подборок, рассылка email‑подборок) лежат в брокере типа Redis/Cloud Queue;
- если есть отдельный сервис для сложных агентных workflow, он хранит чекпоинты и долгоживущую память в своём сторе, а не в RAM одного процесса.
Инстанс MCP Gateway или любого backend‑сервиса превращается в «корову, а не домашнего питомца»: его можно безжалостно убить и пересоздать, не потеряв бизнес‑данные.
Мини‑пример: перенос состояния из памяти в внешнее хранилище
Представим, что вы когда-то сделали очень простой MCP‑tool add_to_cart, который через gateway обращается к внутренней логике и та хранит корзину в памяти процесса (да, так иногда делают в демках — и это нормально, пока вы понимаете, что так нельзя в проде):
// ПЛОХО: корзина в памяти процесса backend-сервиса
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;
}
Горизонтальное масштабирование здесь невозможно: один запрос попадёт на инстанс А, другой — на инстанс 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;
}
Теперь не важно, какой именно инстанс backend‑сервиса обрабатывает запрос, пришедший через gateway: корзина едина для всех.
3. Балансировка нагрузки: как трафик попадает в кластеры backend‑сервисов
Как только у вас появляется больше одного инстанса сервиса, нужен кто‑то, кто будет распределять запросы между ними. Это как распределитель заказов в популярной пиццерии: курьеров много, клиентов много, и без логики – хаос.
L4 vs L7, и почему нас в основном интересует L7
Балансировщик может работать на разных уровнях:
- L4 (TCP/UDP) просто перекидывает байты от клиента на один из бекендов, не особо понимая, что там за протокол;
- L7 (HTTP) понимает, что перед ним HTTP‑запрос, умеет смотреть на путь, заголовки, куки, иногда даже на тело.
Для ChatGPT App‑архитектуры с MCP Gateway и REST‑сервисами нам почти всегда нужен L7‑балансировщик: всё общается по HTTP/SSE, и хочется уметь маршрутизировать по пути, домену, заголовкам (например, для canary‑релизов) и делать health‑checks.
Health‑checks и снятие «больных» инстансов из ротации
Балансировщик должен периодически проверять, что инстансы живы. Самый простой способ — иметь GET /health или /readyz endpoint, который возвращает 200 OK, если всё хорошо.
В Node/TypeScript‑сервисе, который работает как MCP Gateway или backend, health‑чек может выглядеть так:
// 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. Кластеры backend‑сервисов: разделяем по задачам, а не всё в одну кучу
Логичный следующий шаг — перестать думать об одном «большом backend‑сервисе» и разбить систему на несколько кластеров в зависимости от характера нагрузки и критичности.
Пример архитектуры GiftGenius по кластерам
Все собранные данные по модулю 16 рекомендуют нам такую схему для GiftGenius:
| Кластер | Что делает | Характер нагрузки | Особенности масштабирования |
|---|---|---|---|
| A: Gift REST API / лёгкие инструменты | Поиск товаров, форматирование списков, простые вычисления | Высокий RPS, короткие ответы (< 500 ms), мало CPU | Масштабируем по CPU/RPS, много мелких инстансов |
| B: Agents / Heavy Jobs REST‑сервис | Вызовы LLM, сложные workflow, генерация поздравлений | Низкий RPS, долгие ответы (10s–2min), IO‑heavy | Масштабируем по длине очереди задач, можно использовать воркеры |
| C: Commerce REST API / ACP | Checkout, интеграция с платежным провайдером, ACP | Критичная надёжность, жёсткие SLO | Отдельный деплой, медленные и осторожные изменения |
По сути, это реализация паттерна bulkheads (отсеки): если кластер B внезапно начинает «жечь CPU токенами» при генерации сложных текстов, кластер C с оплатой продолжит работать, потому что у него свой пул ресурсов и своё масштабирование.
Как это выглядит через Gateway
MCP Gateway, описанный в первой лекции модуля, видит весь входящий MCP‑трафик и маршрутизирует его по backend‑кластерам. Примерно так:
- tool‑вызовы list_gifts, suggest_gifts → кластер A (Gift REST API);
- tool‑вызовы generate_greeting_card или сложные agent‑workflow → кластер B (Agents REST‑сервис или воркеры);
- инструменты create_order, confirm_payment → кластер C (Commerce REST API).
За этим уже может стоять один общий балансировщик или несколько балансировщиков (например, отдельный L7‑LB перед commerce, чтобы еще сильнее изолировать).
Можно изобразить общую картинку:
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
Схема слегка идеализирована, но отражает главный принцип: разные типы нагрузки — разные backend‑кластеры за одним MCP Gateway.
5. Стратегии деплоя: зачем нужны blue/green и canary
Теперь перейдём к тому, как обновлять всё это хозяйство так, чтобы пользователи не замечали, а вы могли спокойно спать по ночам.
Анти‑пример: деплой «поверх продакшена»
Самая простая и самая опасная стратегия: вы берёте действующий кластер (например, кластер Gift REST API A), запускаете новый образ поверх старого, подменяете контейнеры или перезапускаете процессы.
Какие тут проблемы:
- пока часть инстансов уже новая, а часть старая, система может вести себя непредсказуемо (особенно, если схема БД менялась);
- если что-то пошло не так, откат — это новый деплой «как было», который может занимать минуты;
- в момент деплоя вполне можно получить краткий даунтайм, когда ни один инстанс ещё не поднялся.
В Kubernetes и PaaS это немного смягчается rolling‑обновлениями, но общая идея та же: без чёткой стратегии у вас много «серой зоны», где разные версии кода одновременно обрабатывают трафик.
Blue/Green‑деплой: две среды и мгновенное переключение
Blue/Green — это подход, при котором у вас одновременно существуют два почти идентичных окружения: Blue (текущий продакшен) и Green (новая версия).
Схематично процесс выглядит так:
- Разворачиваете новую версию (v2) в Green‑окружении: это такой же набор gateway + backend‑кластеров, только пока без реального трафика.
- Прогоняете на Green все необходимые тесты: автотесты, smoke‑сценарии, ручные проверки через ChatGPT Dev Mode.
- В момент релиза переключаете балансировщик/маршрутизацию так, чтобы 100% боевого трафика шло в Green.
- Blue продолжает жить рядом в качестве «запасного аэродрома». Если что‑то пойдёт не так, переключаете трафик обратно за считанные секунды.
Для GiftGenius это может выглядеть так: у вас есть mcp-gateway-blue.example.com и mcp-gateway-green.example.com. ChatGPT App в проде «смотрит» на официальный MCP‑endpoint (gateway), а при релизе вы меняете конфиг DNS/LB так, чтобы доменное имя mcp-gateway.example.com указывало уже на green.
Плюсы:
- мгновенный переключатель «туда‑сюда»;
- любую проблему можно лечить уже после отката;
- нет состояния «пол‑кластера новая, пол‑кластера старая».
Минусы:
На время релиза нужно держать два полных окружения, то есть оплачивать ресурсы ×2. Поэтому такую стратегию чаще всего применяют к критичным backend‑сервисам — например, commerce‑кластеру C и самому MCP Gateway, где ломать checkout и входную точку нельзя ни при каких обстоятельствах.
Canary‑релизы: маленькая «канарейка» в угольной шахте
Canary‑релиз — более экономный вариант: вы не поднимаете два полных продакшена, а выкатываете новую версию постепенно на небольшую долю трафика и внимательно за ней наблюдаете.
Примерный сценарий:
- Деплоите версию v2 кластера Gift REST API A в тот же пул или в отдельный маленький канареечный пул.
- Настраиваете балансировщик или MCP Gateway так, чтобы, скажем, 1% tool‑вызовов, связанных с подарками, шёл на v2, а 99% — на v1.
- Смотрите метрики: error rate, latency, специфичные бизнес‑метрики (conversion, успешные checkout’ы).
- Если всё хорошо — постепенно увеличиваете долю: 1% → 5% → 10% → 50% → 100%. Если плохо — срочно откатываете.
В контексте ChatGPT Apps canary особенно полезен не только для кода, но и для экспериментов с prompt’ами: новая версия system‑prompt’а для agent‑сервиса может радикально изменить поведение, и лучше сначала проверить его на небольшой выборке пользователей.
Gateway или LB могут решать, какой запрос считается «канареечным», по разным признакам:
- случайным образом (например, 1% всех запросов);
- по userId (часть пользователей попадает в эксперимент навсегда);
- по специальному заголовку или cookie (для внутреннего тестирования).
Небольшой пример логики маршрутизации в псевдо‑TypeScript (для иллюстрации идеи в gateway):
// Псевдокод в 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
}
В реальной жизни вы, конечно, не будете делать это через Math.random() в runtime‑коде, а вынесете правила в конфиг/фича‑флаги, но логика очень похожа: часть трафика идёт на canary‑версии backend‑сервиса, остальное — на стабильную.
6. Rollback как обязательная часть стратегии
Когда-то давно я усвоил хорошее правило: откат должен быть быстрее фикса.
Это значит, что если после релиза посыпались ошибки и пользователи пишут «всё ломается», не нужно героически чинить баг на проде. Нужно нажать большую красную кнопку «откатиться».
В контексте платформ вроде Vercel (на которых мы уже разворачивали Next.js‑часть GiftGenius) это очень естественно: каждый деплой — immutable артефакт, и Vercel позволяет быстро откатиться к предыдущему.
Для MCP Gateway и backend‑кластеров, развернутых в Kubernetes или другом оркестраторе, эту роль выполняет kubectl rollout undo: вы откатываетесь к предыдущему набору pod’ов и образов.
Главное — логировать и отображать версию, которая сейчас обслуживает трафик. Например, можно:
- добавлять version в /health и другие диагностические endpoint’ы (мы уже это делали выше);
- прокидывать идентификатор релиза через заголовки в логи (например, 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",
});
}
Такой endpoint полезен и для отладки: вы можете спрашивать у прод‑инстанса, какая именно версия сейчас работает, и не гадать «а точно ли выкатился последний билд?».
7. Capacity planning: сколько инстансов нужно под GiftGenius
Мы уже обсудили, как безопасно выкатывать новые версии (blue/green, canary) и быстро откатываться при проблемах. Остался практичный вопрос: а сколько вообще инстансов и каких кластеров держать в проде, чтобы всё это выдержало реальный трафик и не разорило вас по деньгам?
Без фанатизма в формулы, но чуть‑чуть надо. Масштабирование нужно связывать с нагрузкой и экономикой: сколько запросов в день/секунду, сколько тяжёлых LLM‑вызовов, сколько это стоит в деньгах.
Для простоты можно мыслить порядками:
- при 10k запросов в день к GiftGenius (примерно 0.1 RPS в среднем) вы легко проживёте на одном‑двух инстансах MCP Gateway и паре инстансов Gift REST API/Agents‑воркеров;
- при 100k запросов в день (1–2 RPS средних, в пике — больше) уже стоит иметь 3–5 инстансов gateway + кластера Gift REST API, отдельный кластер B для тяжёлых агентов и выделенный commerce‑кластер;
- при 1M запросов в день (десятки RPS, пиковые нагрузки в праздники) вам точно понадобятся кластера, выделенные ресурсы под LLM‑агентов, агрессивный кэш и edge‑слой (о нём отдельная лекция).
Это не строгие числа, а способ заставить себя оценивать порядок нагрузки и думать заранее: где узкие места, как вы будете масштабировать, и сколько это будет стоить.
Для GiftGenius особенно важно готовиться к праздникам: Новый год, Рождество, День святого Валентина, Чёрная пятница. Нагрузка может вырасти в разы, а вы бы хотели, чтобы система это пережила.
8. Практический мини‑пример: эволюция деплоя GiftGenius
Чтобы собрать всё воедино, давайте нарисуем простую эволюцию деплоя GiftGenius.
Здесь мы последовательно применим всё, о чём говорили выше: stateless‑дизайн gateway и backend‑сервисов, балансировку нагрузки, раздельные кластеры и стратегии релизов (blue/green, canary).
Базовый уровень: один gateway + backend на Vercel/Kubernetes
В какой‑то момент курса вы уже сделали это: одно Next.js‑приложение с Apps SDK на Vercel, внутри которого живёт и MCP‑endpoint, и простая backend‑логика (Gift/Commerce) в одном сервисе. Всё довольно монолитно.
Плюсы понятны: просто, дешево, мало мест, где можно ошибиться.
Минус ровно один, но критичный: это никак не масштабируется под серьёзный трафик и плохо переносит обновления.
Уровень 2: отдельный MCP Gateway + несколько backend‑кластеров
Следующий шаг:
- выносите MCP Gateway в отдельный сервис (Node/Go/NGINX+Lua, неважно);
- запускаете несколько инстансов Gift REST API (кластер A) и несколько воркеров/сервисов для агентов (кластер B);
- на commerce выделяете отдельный сервис (кластер C), возможно — на отдельной базе/инфраструктуре.
Уже здесь включается классическая L7‑балансировка, health‑checks и, по возможности, горизонтальное масштабирование.
Уровень 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
В реальности может быть чуть сложнее (отдельный Blue/Green только для commerce, canary только для gift‑кластеров), но идею это иллюстрирует: вы всегда знаете, какая версия куда идёт, при этом для ChatGPT всё по‑прежнему выглядит как одна MCP‑точка входа (gateway).
9. Небольшие фрагменты кода для версионирования и диагностики
Мы уже видели health‑endpoint и /api/version. Добавим ещё пример, как можно логировать версию и кластер в обработчике MCP‑tool’а на стороне gateway, чтобы потом легко «свести» метрики.
Представим tool suggest_gifts, который реализован как REST‑endpoint в 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 из env;
- пишем их в структурированные логи;
- дальше их легко использовать для анализа: «на какой версии/кластере у нас сейчас сыпется больше ошибок?».
С точки зрения ChatGPT App это вообще прозрачно, но для вас как разработчика — огромный плюс, особенно в сочетании с canary/blue‑green.
10. Типичные ошибки при масштабировании и деплое ChatGPT App
Ошибка №1: хранить состояние сессии/пользователя в памяти gateway или backend‑процесса.
Такой подход убивает горизонтальное масштабирование: как только у вас возникает второй инстанс, состояние «расслаивается» между ними. Особенно опасно хранить в памяти корзину, результаты поиска или прогресс workflow. Всё это должно жить во внешнем хранилище — БД, кэше или специализированном сторе для состояния агента.
Ошибка №2: думать, что «одного мощного сервера» достаточно.
Вертикальное масштабирование удобно на старте, но плохо работает при реальном росте: есть физический предел машины, один процесс становится single point of failure, а ChatGPT может принести непредсказуемый всплеск трафика. Для MCP Gateway и backend‑кластеров почти всегда нужен stateless‑дизайн и несколько инстансов за балансировщиком.
Ошибка №3: выкатывать новые версии «поверх продакшена» без чёткой стратегии.
Если вы просто обновляете контейнеры/процессы в боевом кластере, получаете промежуточное состояние, где часть трафика идёт на старую версию, часть — на новую, а при ошибке откат превращается в «передеплой ещё раз». Гораздо надёжнее держать либо два окружения (blue/green), либо хотя бы отдельную canary‑версию backend‑сервиса, куда идёт малая доля трафика.
Ошибка №4: отсутствие быстрого rollback‑плана.
Плохой сценарий: релиз прошёл, метрики красные, пользователи жалуются, а вы только начинаете думать, как откатываться. Правильный сценарий: заранее подготовленная возможность моментального отката (blue/green‑переключатель, rollout undo, Vercel rollback), понятные идентификаторы версий в логах и health‑endpoint’ах, и жёсткое правило «откатиться сначала, разбираться потом».
Ошибка №5: один общий кластер «на всё» без разделения по видам нагрузки.
Если генерация поздравительных текстов (LLM‑агенты) и checkout живут в одном кластере, любая проблема на стороне моделей (задержки, timeouts, рост токенов) может положить и оплату. Разделение на кластеры по типам задач (Gift REST API / лёгкие инструменты, Agents‑heavy сервис, Commerce REST API) и отдельные лимиты/ресурсы для каждого кластера — важный шаг к устойчивости.
Ошибка №6: отсутствие связи между архитектурой и экономикой.
Легко увлечься идеей «а давайте ещё поднимем пару нод», забыв, что каждый LLM‑вызов и каждый инстанс стоят денег. Без простейшего capacity planning’а (оценки нагрузок и стоимости) можно либо недомасштабироваться и уронить прод, либо перемасштабироваться и лишиться маржинальности. Здесь полезно связывать число запросов, процент тяжёлых LLM‑операций и стоимость хостинга с бизнес‑метриками приложения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ