JavaRush /Курсы /ChatGPT Apps /Шлюз и защита периметра: proxy, rate limiting, очереди и ...

Шлюз и защита периметра: proxy, rate limiting, очереди и backpressure

ChatGPT Apps
16 уровень , 1 лекция
Открыта

1. Зачем вообще защищать периметр ChatGPT App

В классическом веб‑приложении пользователь — это браузер, который довольно предсказуемо стучится по вашим эндпоинтам. В мире ChatGPT Apps у вас появляется новый тип клиента: LLM, которая сама решает, когда и какие инструменты вызвать.

Модель может:

  • в одном диалоге несколько раз подряд вызвать один и тот же tool;
  • экспериментировать: «а если еще раз позвать suggest_gifts c чуть другими параметрами?»;
  • работать параллельно на сотнях пользователей.

Добавьте сюда возможных ботов, тестовые скрипты, ошибки в собственном коде (например, бесконечный цикл, который постоянно триггерит tool‑call), и вы получите почти идеальный рецепт DoS‑а по доброй воле.

Вишенка на торте — стоимость. Каждый tool‑call может:

  • тянуть внешние платные API (курьеры, платежи, каталоги),
  • вызывать другие LLM (например, RAG‑поиск),
  • запускать тяжёлые фоновые задачи.

Без ограничений и защиты периметра один «неудачный» клиент может:

  • положить вам все backend‑сервисы за gateway (Gift API, Commerce API и т.п.),
  • выбить лимиты внешних API,
  • и ощутимо поджечь бюджет на модели.

Задача этой лекции — показать, как gateway/proxy + rate limiting + очереди + backpressure превращают эту потенциальную катастрофу в управляемую систему.

Insight

Платформа ChatGPT не предоставляет вообще никаких механизмов защиты вашего MCP-сервера от внешнего трафика. Любой интернет-клиент может отправлять на него запросы, включая утилиты вроде MCP Jam.

Все что вам может предложить ChatGPT — это ограничить входящий трафик по IP-адресам, настроив обратный прокси (например, NGINX) на работу с allowlist. Если IP-фильтрация не настроена, ваш MCP-сервер остаётся полностью открытым, что небезопасно. Ни для вас, ни для ваших пользователей.

2. Proxy/Gateway как «щит» перед backend‑сервисами и агентами

Сначала напомним картинку, но теперь уже через призму защиты.

Представьте типовую схему:

flowchart LR
  ChatGPT["ChatGPT / Виджет"]
    --> GW["MCP Gateway (Auth, Rate Limit, Logs)"]

  GW --> GiftAPI["Gift REST API (подбор подарков)"]
  GW --> CommerceAPI["Commerce REST API (checkout, ACP)"]
  GW --> Analytics["Analytics Service / REST API"]

  GW --> Queue["Очередь задач"]
  Queue --> Worker["Background workers"]

Gateway стоит между внешним миром (ChatGPT, webhooks, тестовые клиенты) и всем остальным. Он:

  • видит абсолютно все входящие запросы;
  • первым проверяет токен и формат запроса;
  • умеет отбрасывать заведомо невозможные вещи (левый host, странный path, слишком большой body);
  • принимает решение, к какому внутреннему REST‑/HTTP‑сервису запрос вообще имеет смысл отправлять.

На этом же уровне появляются:

  • rate limiting — ограничиваем, сколько запросов можно сделать за интервал времени;
  • простейший backpressure — отказываем, если сервисы под капотом уже захлебываются;
  • переход на асинхронность — тяжелые штуки сразу в очередь, ответ клиенту: «принято, ждем».

То есть gateway — это не только «router», но и «бронежилет». Главное — не превращать его в «монолит всего бизнеса», об этом мы уже говорили в прошлой лекции.

3. Какие потоки трафика нужно контролировать

В экосистеме ChatGPT App обычно есть три главных типа трафика, которые нас интересуют с точки зрения ограничений и защиты.

Во‑первых, это MCP tool‑calls от ChatGPT. Это все, что приходит через MCP‑протокол: вызовы suggest_gifts, get_product_details, create_checkout_session и прочих инструментов. Модель может генерировать их довольно бодро, особенно когда под капотом еще и Agents.

Во‑вторых, это исходящие запросы от наших backend‑ов к внешним API. Внутри сервиса у нас могут быть свои rate limits на сторонние системы: каталоги, логистика, платежи. Нарушить их — получить блокировку, штрафы или снижение качества.

В‑третьих, это входящие вебхуки — уведомления от ACP, платёжных провайдеров (Stripe и т.п.), курьеров. Они приходят независимо от активности пользователей. Если наш endpoint тормозит или отвечает ошибкой, внешняя система начнет делать повторные попытки (retries) и может устроить вам «шторм» из повторных уведомлений.

Для GiftGenius это выглядит так:

  • пользователь и модель активно дергают suggest_gifts и find_similar_gifts;
  • checkout‑tool дергает ACP/коммерческий backend;
  • после оплаты платёжка присылает webhooks payment.succeeded / payment.failed.

Все эти потоки сходятся в одну точку — Gateway, а значит, именно там и разумно ставить «счетчики, фильтры и пробки».

4. Rate limiting: базовая защита и экономия денег

Что такое rate limiting в нашем контексте

Rate limiting — это механизм, который ограничивает количество запросов от конкретного клиента за единицу времени. Идея стара как интернет, но в контексте ChatGPT Apps она сразу решает три задачи:

  • не дает одному клиенту (или багу) положить ваши сервисы;
  • помогает соблюдать лимиты внешних API;
  • защищает ваш кошелек от неконтролируемых вызовов моделей.

Классические алгоритмы:

  • фиксированное окно (Fixed Window),
  • скользящее окно (Sliding Window),
  • ведро с токенами (Token Bucket),

нам важно знать скорее концептуально: «в течение минуты не более N запросов», «каждый запрос съедает токен, токены пополняются со скоростью X в секунду» и т.п. Реализацию обычно берёт на себя библиотека или API Gateway.

Где ставить лимиты

Лимиты можно ставить на разных уровнях.

На уровне обратного прокси (Nginx, Cloudflare, AWS API Gateway) удобно:

  • отсекать самый дикий трафик по IP;
  • ограничивать размер тела запроса;
  • защищаться от простых DDoS‑паттернов.

На уровне MCP Gateway (приложение) полезно делать более «осмысленный» rate limiting:

  • по пользователю (userId из токена),
  • по организации (tenantId),
  • по типу операции (например, create_checkout_session жестко лимитируем, search — мягче),
  • по источнику (webhook vs tool‑call).

И отдельно можно добавлять лимиты уже в самих микросервисах для особенно дорогих операций, но это следующий уровень детализации.

Как выбирать ключ для лимитов

Самая распространенная ошибка — лимитировать по IP‑адресу. В случае ChatGPT это довольно бесполезно:

  • все запросы могут идти из одного диапазона OpenAI,
  • разные пользователи будут «сидеть» за одним и тем же IP.

Нам намного интереснее:

  • userId — конкретный пользователь в вашем приложении;
  • tenantId — организация (если вы делаете B2B и один чат используется многими сотрудниками);
  • API‑токен или clientId, если у вас несколько интеграций.

В GiftGenius обычно достаточно userId + tenantId, извлеченных из токена, который ChatGPT передает в вызовах MCP.

Простая реализация rate limiting в TypeScript

Представим, что у нас есть маленький MCP Gateway на Express. Добавим туда простейший rate limiting: не более 30 tool‑calls в минуту на одного пользователя.

// Примитивный rate limiting: N запросов в минуту на userId
const WINDOW_MS = 60_000;
const MAX = 30;
const hits = new Map<string, { ts: number; count: number }>();

function rateLimit(req: Request, res: Response, next: NextFunction) {
  const userId = (req.headers["x-user-id"] as string) ?? "anonymous";
  const now = Date.now();
  const rec = hits.get(userId) ?? { ts: now, count: 0 };

  if (now - rec.ts > WINDOW_MS) {      // Окно "протухло" — начинаем заново
    rec.ts = now;
    rec.count = 0;
  }
  rec.count += 1;
  hits.set(userId, rec);

  if (rec.count > MAX) {
    return res.status(429).json({
      error: "rate_limit_exceeded",
      retryAfterSec: 60,
      message: "Too many tool calls, please retry later."
    });
  }
  next();
}

А теперь используем его в маршруте MCP:

// Применяем middleware ко всем MCP tool-calls
app.post("/mcp/tools/call", rateLimit, async (req, res) => {
  const result = await callBackendForTool(req.body); // REST-вызов в Gift/Commerce/Analytics API
  res.json(result);
});

Ключевые моменты:

  • мы возвращаем осмысленную ошибку (error: "rate_limit_exceeded"), а не просто 500;
  • модель сможет прочитать эту ошибку, понять, что произошло, и корректно объяснить пользователю, а не начинать галлюцинировать.

В реальном продакшене счетчики, конечно, живут не в памяти одного процесса, а в Redis или другом общем хранилище, чтобы всё работало в кластере. Но для понимания принципа этого достаточно.

Rate limiting и лимиты на уровне gateway защищают нас от лавины запросов, но они не решают другую проблему — отдельные операции всё равно могут быть очень тяжёлыми и долго выполняться. Здесь уже одного синхронного HTTP недостаточно, и на сцену выходят очереди и асинхронные задачи.

5. Очереди и асинхронные задачи: когда синхронно уже нельзя

Проблема таймаутов ChatGPT

Даже если вы аккуратно настроили rate limiting, ChatGPT (и вообще HTTP‑клиенты) не любят, когда ответ приходит очень долго. Платформа ограничивает время выполнения tool‑call’а, и если вы будете ждать, пока не закончит работать какой‑нибудь «супер‑рекомендательный» алгоритм, то:

  • пользователь увидит вечный спиннер;
  • платформа оборвёт запрос по таймауту;
  • модель решит, что «что‑то пошло не так», и начнет выдумывать объяснения.

Решение: переводить тяжелые операции в асинхронный режим. Классический паттерн:

  1. Gateway принимает запрос.
  2. Ставит задачу в очередь.
  3. Сразу возвращает ответ 202 Accepted с jobId.
  4. Отдельный worker забирает задачи из очереди и обрабатывает.
  5. Клиент (наш виджет или даже ChatGPT через дополнительный tool) периодически спрашивает статус по jobId или получает нотификацию через MCP‑событие.

В терминах ChatGPT App это обычно выглядит как два инструмента: первый tool принимает запрос, ставит задачу в очередь и возвращает jobId, второй — позволяет модели или виджету по этому jobId узнавать статус и забирать результат. Дополнительно те же события о прогрессе можно дублировать через MCP‑нотификации.

Мини‑очередь для GiftGenius (пример кода)

Допустим, у нас есть тяжёлый инструмент generate_large_gift_report, который может работать десятки секунд. В реальном App он мог бы как раз возвращать только jobId, а отдельный tool get_report_status позволял бы модели или виджету по этому jobId узнавать состояние и забирать результат. На уровне Gateway сделаем для него отдельный endpoint с очередью.

type Job = { id: string; payload: any };
const queue: Job[] = [];
const MAX_QUEUE = 100;

app.post("/mcp/tools/generate_report", (req, res) => {
  if (queue.length >= MAX_QUEUE) {
    return res.status(503).json({
      error: "system_busy",
      message: "System is busy, please retry later."
    });
  }

  const job: Job = { id: crypto.randomUUID(), payload: req.body };
  queue.push(job);
  res.status(202).json({ jobId: job.id, status: "accepted" });
});

И примитивный воркер, который раз в 200 мс берет одну задачу:

async function processJob(job: Job) {
  // Здесь вызываем реальный backend-сервис или агентный workflow через REST
  await handleHeavyGiftReport(job.payload);
}

setInterval(async () => {
  const job = queue.shift();
  if (!job) return;
  await processJob(job);
}, 200);

Понятно, что это сильно упрощённый пример:

  • в реальной жизни очередь живет в Redis, SQS, Kafka и т.п.;
  • статус задачи хранится где‑то еще, чтобы его можно было запросить;
  • worker’ов обычно несколько.

Но концепция уже ясна: Gateway не держит запрос открытым, пока все не сделается. Он принимает, ставит в работу и отвечает быстро.

6. Backpressure: как не утонуть в собственной очереди

Чем backpressure отличается от rate limiting

Rate limiting в первую очередь отвечает на вопрос: «сколько запросов может делать один клиент за интервал времени?». Это защита от «одного слишком активного пользователя» или бага на стороне конкретного клиента.

Backpressure же говорит: «а сколько всего задач/запросов наша система способна переварить одновременно, не развалившись?». Это уже про общий объем нагрузки, независимо от того, от кого она прилетела.

Пример:

  • rate limiting: «пользователь не может дергать suggest_gifts чаще, чем 30 раз в минуту»;
  • backpressure: «в очереди не может быть больше 100 невыполненных задач, иначе мы начинаем отказывать всем новым запросам».

В идеале эти механизмы дополняют друг друга: rate limit держит клиентов в узде, backpressure спасает систему, если все равно налетело очень много народу.

Простая реализация ограничения активных задач

Один из самых простых вариантов backpressure — ограничить число активных вызовов под капотом. Например: не держать одновременно больше 50 активных tool‑calls к конкретному backend‑/REST‑сервису (Gift API, Commerce API и т.п.).

let activeCalls = 0;
const MAX_ACTIVE = 50;

app.post("/mcp/tools/call", async (req, res) => {
    if (activeCalls >= MAX_ACTIVE) {
        return res.status(429).json({
            error: "gateway_overloaded",
            message: "Gateway is temporarily overloaded, please retry later."
        });
    }

    activeCalls += 1;
    try {
        const result = await callBackendForTool(req.body); // REST-вызов в Gift/Commerce/Analytics API
        res.json(result);
    } catch (err) {
        console.error("Tool call error", err);
        res.status(500).json({ error: "internal_error" });
    } finally {
        activeCalls -= 1;
    }
});

Что здесь происходит:

  • пока количество одновременно выполняющихся запросов меньше MAX_ACTIVE, мы пропускаем новый call;
  • если лимит исчерпан, сразу отвечаем осмысленной ошибкой;
  • важно обязательно уменьшать счетчик в finally, чтобы при ошибках не потерять «слоты».

Это и есть простейшее backpressure: мы честно говорим клиенту: «не могу сейчас, попробуй позже», вместо того чтобы бездумно принимать всё и умирать.

В дальнейшем можно:

  • делать разные MAX_ACTIVE для разных типов операций (например, checkout почти всегда пропускать, а генерацию отчетов ограничивать жёстче),
  • переключать лимиты динамически в зависимости от метрик нагрузки.

7. Вебхуки и «штормы»: защита входящих событий

До этого мы смотрели в основном на запросы, которые инициируем мы или ChatGPT (tool‑calls, исходящие запросы, async‑job’ы). Но в жизни есть ещё один важный источник нагрузки на Gateway — входящие вебхуки от внешних систем.

Вебхуки — это обратная сторона медали: если tool‑calls инициируем мы (через модель), то вебхуки инициирует внешний сервис. Это как раз тот третий тип трафика из раздела 4, который мы не контролируем по времени и частоте, но обязаны уметь переваривать без падений. Платежка, ACP, логистика — все они шлют уведомления (вебхуки) на наш endpoint при каждом значимом изменении: «платеж прошел», «заказ создан», «доставка обновила статус».

Проблемы начинаются, когда:

  • наш endpoint отвечает медленно;
  • отвечает ошибкой;
  • периодически недоступен.

Тогда внешний сервис, следуя лучшим практикам, начинает делать повторные попытки (retries). И если не повезет, вы получите «шторм» вебхуков — десятки или сотни повторных событий, которые стараются «достучаться» до вас любой ценой.

Чтобы не погибнуть от такой заботы, на уровне Gateway стоит:

  1. Лимитировать входящие вебхуки по источнику: например, «не более 10 событий в минуту на один event_type от конкретного провайдера».
  2. Проверять подпись до парсинга JSON: HMAC‑подпись или аналогичный механизм позволяет отбрасывать фальшивые запросы.
  3. Делать обработку событий идемпотентной: по event_id или аналогичному полю, чтобы повторные события не приводили к дубликатам заказов или платежей.
  4. При сильном шторме включать дополнительный backpressure: временно отвечать «503: попробуйте позже», если downstream‑сервисы не успевают.

Простейший пример (идея, а не продакшен‑код):

app.post("/webhooks/stripe", rateLimitWebhook, (req, res) => {
    const sig = req.headers["stripe-signature"] as string;
    if (!isValidSignature(req.rawBody, sig)) {
        return res.status(400).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    if (isAlreadyProcessed(event.id)) {
        return res.json({ received: true }); // идемпотентность
    }

    handleStripeEvent(event);
    res.json({ received: true });
});

Здесь на уровне Gateway мы:

  • применяем отдельную политику rate limiting для вебхуков;
  • валидируем подпись до того, как доверять содержимому;
  • защищаемся от дублей через isAlreadyProcessed.

8. Применяем к GiftGenius: пример политики лимитов и очередей

Теперь давайте отвлечемся от абстракций и посмотрим, как это может выглядеть для нашего учебного GiftGenius.

Представим три ключевых сценария:

  1. Поиск подарков (suggest_gifts, find_similar_gifts).
  2. Создание заказа / checkout (create_checkout_session, confirm_order).
  3. Прием вебхуков от платежного провайдера и ACP.

Для каждого сценария логично определить:

  • по какому ключу считаем лимит;
  • сколько запросов в минуту позволяем;
  • что делаем при превышении.

Например:

Сценарий Ключ лимита Лимит в минуту Поведение при превышении
Поиск подарков userId 30 429 + совет «сузить параметры поиска»
Создание заказа userId + tenantId 5 429 + текст «слишком много попыток, проверьте заказы»
Входящие вебхуки provider + eventType 10 429/503, лог, возможная деградация

Для вебхуков обычно логичнее лимитировать по комбинации «провайдер + тип события», а дубли отсекать отдельным механизмом идемпотентности по event_id.

В коде это превращается в разные middleware’ы: rateLimitSearch, rateLimitCheckout, rateLimitWebhook.

Для тяжелых операций, вроде «сгенерировать большой PDF‑отчет по подаркам за год», мы используем очередь и асинхронный паттерн, который уже показали выше. Gateway в таком случае:

  • принимает запрос от ChatGPT;
  • ставит задачу в очередь;
  • возвращает jobId и подсказку для модели, как получить статус;
  • ограничивает размер очереди (backpressure), чтобы не переполнить систему.

Важно помнить: и rate limiting, и backpressure — это не только про безопасность и надежность, но и про UX. Гораздо приятнее услышать от ассистента: «Сервис сейчас перегружен, давай попробуем через минуту», чем сидеть с крутилкой до таймаута или увидеть «Internal Server Error».

9. Мини‑практикум: добавляем защиту в наш MCP Gateway

Чтобы материал не остался теорией, давайте соберем мини‑практикум, который вы можете реализовать в своем учебном проекте.

Rate limiting для всех MCP tool‑calls

Добавьте middleware rateLimit (как выше) и подключите его к /mcp/tools/call. Для начала можно взять очень простой лимит: 30 запросов в минуту на userId. Потом поиграйтесь:

  • уменьшите лимит и посмотрите, как ваш App и модель на это реагируют;
  • сделайте разные лимиты для разных типов tools, передавая, например, toolName в middleware.

Простейший backpressure по активным вызовам

Добавьте счетчик activeCalls и ограничение MAX_ACTIVE. Попробуйте имитировать нагрузку (например, скриптом, который шлет пачку запросов), и посмотрите, в какой момент Gateway начнет отвечать gateway_overloaded.

Здесь важно именно поведение: вы не ждете, пока всё упадет, а отказываетесь брать новые задачи, честно говоря клиенту, что сейчас слишком жарко.

Очередь для тяжелого инструмента

Выберите одну тяжелую операцию (или искусственно сделайте «тяжелой» — вставив setTimeout/долгий fetch) и переведите ее на паттерн «очередь + jobId». Минимально:

  • endpoint POST /mcp/tools/generate_report — ставит задачу в очередь и возвращает jobId;
  • endpoint GET /jobs/:id — возвращает статус (pending, done, error, плюс, возможно, результат);
  • воркер, который раз в X миллисекунд дергает processJob.

Этого достаточно, чтобы понять, как будет выглядеть реальная интеграция с BullMQ или другим queue‑движком.

10. Типичные ошибки при защите периметра

Ошибка №1: Лимитировать только по IP.
В мире ChatGPT Apps это почти бесполезно: большая часть запросов прилетает с адресов OpenAI, и все ваши пользователи окажутся за одним и тем же IP. В итоге кто‑то один выжжет лимит для всех, а настоящий виновник останется неизвестен. Правильнее лимитировать по userId, tenantId или токену, а IP использовать только как очень грубый фильтр на уровне обратного прокси.

Ошибка №2: Возвращать голый 500 вместо осмысленной ошибки.
Если при превышении лимита или перегрузке вы просто шлёте 500 Internal Server Error, модель ничего не понимает и начинает выдумывать. А вот структурированная ошибка с кодом (rate_limit_exceeded, gateway_overloaded) и человекочитаемым описанием позволяет LLM корректно объяснить ситуацию пользователю и, при необходимости, попробовать еще раз позже.

Ошибка №3: Делать бесконечную очередь без backpressure.
Иногда кажется: «давайте просто всё ставить в очередь, а там разберемся». На практике очередь разрастается до тысяч задач, задержки растут, память заканчивается, а пользователи так и не видят результата. Всегда ограничивайте размер очереди и количество активных операций. Лучше честно отказать новым запросам с 503 или 429, чем превратить очередь в черную дыру.

Ошибка №4: Полагаться только на rate limiting и игнорировать вебхуки.
Многие защищают только входящий трафик от ChatGPT, а вебхуки оставляют «как‑нибудь само». Когда платежный провайдер начинает делать повторные попытки, именно вебхуки могут устроить вам самый настоящий шторм. Для эндпоинтов вебхуков нужны свои лимиты, проверка подписи и идемпотентная обработка. Иначе легко получить десяток дублей одного и того же заказа.

Ошибка №5: Хранить все счетчики и очередь только в памяти одного инстанса.
Для учебного проекта это нормально, но в продакшене при масштабировании Gateway до нескольких инстансов счетчики на каждом узле начнут «жить своей жизнью», лимиты перестанут быть глобальными, а перезапуск узла обнулит очередь. В реальной системе для хранения состояния лимитов и очередей используют общее хранилище (Redis, облачные очереди и т.д.). Мы еще поговорим об этом в лекциях про масштабирование и продакшен.

Ошибка №6: Пихать бизнес‑логику внутрь Gateway «раз уж он и так везде посредник».
Иногда возникает соблазн: «ну давайте прямо в Gateway решим, какие подарки показывать, нам же все равно туда приходят запросы». В итоге gateway превращается в монолит с кучей логики, который одновременно и маршрутизатор, и бизнес‑мозг, и логгер. Это сильно усложняет масштабирование и сопровождение. Gateway должен оставаться сетевым/инфраструктурным слоем: аутентификация, авторизация, лимиты, кэш, маршрутизация — да; подбор подарков — нет.

Ошибка №7: Считать, что «мы маленькие, нас это не касается».
Часто думают: «Ну у нас же не миллион пользователей, можно обойтись без gateway/лимитов». На деле даже один баг в клиентском коде (или в промпте, который заставляет модель дёргать tool по кругу) может устроить вам маленький, но очень локальный апокалипсис. Базовый rate limiting и хотя бы примитивный backpressure — это не роскошь, а зубная щетка продакшена: использовать нужно с самого начала, пока болеть не начало.

1
Задача
ChatGPT Apps, 16 уровень, 1 лекция
Недоступна
Fixed-window rate limiting для одного API endpoint
Fixed-window rate limiting для одного API endpoint
1
Задача
ChatGPT Apps, 16 уровень, 1 лекция
Недоступна
Mini Gateway Proxy + backpressure по числу активных запросов
Mini Gateway Proxy + backpressure по числу активных запросов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ