API Gateway в системном дизайне: роль, BFF, rate limiting и отличие от балансировщика

Что делает API Gateway: маршрутизация, аутентификация, rate limiting, агрегация запросов, BFF. Чем шлюз отличается от балансировщика и service mesh и как не сделать его единой точкой отказа.

13 мин чтениясистемный дизайн · микросервисы · api gateway · собеседования

Коротко

  • API Gateway — единая входная дверь для внешних клиентов. Он принимает запрос, решает, в какой сервис его отправить, и по дороге делает общую работу: проверяет токен, считает лимиты, пишет логи.
  • Шлюз решает задачи на границе системы. Всё, что одинаково для всех сервисов и касается внешнего мира, выносится в него. Бизнес-логика там не живёт.
  • Балансировщик распределяет нагрузку, шлюз управляет API. На практике один продукт часто умеет и то и другое, но на интервью важно разделять роли.
  • Service mesh решает похожие задачи для трафика между сервисами (east-west), шлюз — для трафика снаружи (north-south).
  • Шлюз — потенциальная единая точка отказа и узкое место. Его делают stateless, держат несколько экземпляров за балансировщиком и не нагружают лишним.

Зачем нужен шлюз

Представь мобильное приложение магазина, за которым два десятка микросервисов: каталог, корзина, заказы, отзывы, рекомендации, профиль. Без шлюза клиент должен знать адрес каждого из них.

Что из этого вырастает:

  • Клиент привязан к внутренней структуре. Разделили сервис заказов на два — выпускай новую версию приложения и жди, пока пользователи обновятся.
  • Каждый сервис заново решает общие задачи. Проверка JWT, CORS, лимиты, логирование — двадцать реализаций с двадцатью разными багами.
  • Много сетевых вызовов с телефона. Экран товара требует пять запросов по мобильной сети, каждый со своей задержкой.
  • Большая поверхность атаки. Наружу торчат все сервисы, включая те, что писались «только для своих».

Шлюз прячет всё это за одним адресом. Клиент обращается к api.shop.example, а дальше уже не его забота.

Идея описана в статье о паттернах микросервисов. Здесь разберём её подробнее: что именно шлюз делает, где проходят его границы и что о нём спрашивают на собеседовании.

Что делает API Gateway

Маршрутизация

Базовая функция. Шлюз смотрит на путь, метод, заголовки или хост и выбирает upstream — группу экземпляров нужного сервиса.

# Условный конфиг, не привязанный к конкретному продукту routes: - match: { path_prefix: /api/v1/orders } upstream: orders-service timeout: 2s - match: { path_prefix: /api/v1/catalog, method: GET } upstream: catalog-service cache_ttl: 30s - match: { path_prefix: /api/v2/orders, header: { X-Canary: 'true' } } upstream: orders-service-v2

На маршрутизации держатся полезные вещи:

  • Версионирование API. /v1 и /v2 могут вести в разные сервисы, пока старые клиенты не уйдут.
  • Канареечные релизы. 5% трафика уходит на новую версию, остальное — на старую.
  • Переписывание путей. Снаружи /api/v1/orders/42, внутри /orders/42 — сервис не знает о публичной схеме URL.
  • Миграция с монолита (паттерн Strangler Fig). Новые маршруты отправляются в микросервисы, остальное по-прежнему в монолит. Клиенты разницы не замечают.

Откуда шлюз знает адреса сервисов? Либо из статического конфига, либо из service discovery: Consul, DNS, Kubernetes Service. Во втором случае экземпляры можно добавлять и убирать без перезапуска шлюза.

Аутентификация и авторизация

Шлюз — естественное место для проверки, кто пришёл. Типичная схема с JWT:

  1. Клиент получает токен у сервиса авторизации.
  2. Каждый запрос приходит с Authorization: Bearer <token>.
  3. Шлюз проверяет подпись, срок действия и издателя. Нет токена или он невалиден — сразу 401, до сервисов запрос не доходит.
  4. Шлюз передаёт дальше проверенный идентификатор, например в заголовке X-User-Id, а исходный токен пробрасывает или убирает — зависит от политики.

Важная граница: шлюз хорошо отвечает на вопрос «кто это и может ли он вообще пользоваться этим API». Вопрос «может ли пользователь 17 отменить заказ 42» — бизнес-правило, и решать его должен сервис заказов. Если перенести такую логику в шлюз, он начнёт знать о доменной модели и превратится в распределённый монолит.

Ещё одна ловушка — слепое доверие к заголовкам. Если сервис верит X-User-Id, то внутренняя сеть должна быть закрыта: иначе кто угодно пришлёт этот заголовок в обход шлюза. Отсюда принцип zero trust: сервисы всё равно проверяют, что запрос пришёл от доверенного источника, например по mTLS.

Rate limiting и квоты

Шлюз видит весь входящий трафик, поэтому лимиты удобно считать именно здесь: по API-ключу, пользователю, IP или тарифу. Лишние запросы получают 429 Too Many Requests с заголовком Retry-After.

Тонкость в том, что экземпляров шлюза несколько. Если каждый считает лимит в своей памяти, клиент при пяти экземплярах фактически получает пятикратный лимит. Варианты:

  • Общий счётчик в Redis. Точно, но добавляет сетевой вызов на каждый запрос и делает Redis критичной зависимостью.
  • Локальный лимит, поделённый на число экземпляров. Дёшево, но неточно при неравномерной балансировке.
  • Гибрид: локальные счётчики, которые периодически синхронизируются с общим хранилищем.

Отдельный вопрос — что делать, если Redis недоступен. Обычно выбирают fail open: лимит временно не применяется, но сервис работает. Fail closed, когда всё отклоняется, оправдан только для дорогих или опасных операций.

Алгоритмы (token bucket, скользящее окно) и атомарность счётчиков подробно разобраны в статье про rate limiter.

Агрегация запросов

Экрану товара нужны карточка, отзывы и рекомендации из трёх сервисов. Вместо трёх запросов с телефона клиент делает один, а шлюз (или отдельный слой за ним) параллельно опрашивает сервисы и собирает ответ.

Ключевая мысль — не все части ответа одинаково важны. Без карточки товара страница не имеет смысла, а без рекомендаций — вполне. Поэтому у необязательных частей свой таймаут и запасное значение:

import asyncio async def call(name, delay, fail=False): await asyncio.sleep(delay) if fail: raise RuntimeError(f"{name} недоступен") return {"source": name} async def with_fallback(coro, timeout, fallback): try: return await asyncio.wait_for(coro, timeout) except Exception: return fallback async def product_page(product_id): product, reviews, recs = await asyncio.gather( call("catalog", 0.05), # обязательная часть with_fallback(call("reviews", 0.5), 0.2, []), # медленный: не дождёмся with_fallback(call("recs", 0.01, fail=True), 0.2, []), # упал ) return {"product": product, "reviews": reviews, "recommendations": recs} print(asyncio.run(product_page(42))) # {'product': {'source': 'catalog'}, 'reviews': [], 'recommendations': []}

Время ответа здесь определяется самым медленным вызовом, но не дольше таймаута необязательных частей. Если упадёт каталог, gather пробросит исключение, и шлюз вернёт ошибку — так и задумано.

Агрегация — то место, где шлюз чаще всего обрастает бизнес-логикой. Как только появляются условия вида «если пользователь премиум, добавить блок скидок», пора выносить это в отдельный сервис или BFF.

Остальные сквозные задачи

  • Терминация TLS. Шифрование снимается на шлюзе, сертификаты обновляются в одном месте.
  • Кэширование ответов на публичные GET-запросы: каталог, справочники. Подробности — в статье о стратегиях кэширования.
  • Преобразование протоколов. Снаружи REST/JSON, внутри gRPC. Или наоборот: WebSocket снаружи, очередь внутри.
  • Наблюдаемость. Шлюз присваивает запросу X-Request-Id (или trace id), пишет access-логи и метрики: RPS, задержки по маршрутам, доля ошибок.
  • Устойчивость. Таймауты, ограниченные повторы, circuit breaker к деградировавшему сервису.
  • Валидация размера тела, схемы запроса, заголовков — мусор отсекается до сервисов.

Backend for Frontend

Один шлюз для всех клиентов быстро становится компромиссом. Мобильному приложению нужны компактные ответы и минимум запросов. Веб-сайту — больше данных за раз. Партнёрскому API — стабильный контракт, который годами не меняется. Когда всё это в одном месте, любое изменение задевает всех.

BFF (Backend for Frontend) — отдельный слой API на каждый тип клиента:

мобильное приложение веб партнёры | | | [ BFF mobile ] [ BFF web ] [ Public API ] \ | / +--------------+-------------+ | каталог · заказы · отзывы · профиль

Принципы:

  • BFF принадлежит команде клиента. Мобильная команда сама решает, какие поля отдавать и как агрегировать, не дожидаясь команды бэкенда.
  • BFF тонкий. Агрегация, преобразование форматов, адаптация под экран. Доменная логика остаётся в сервисах.
  • Общие задачи всё равно уровнем выше. TLS, аутентификацию, базовые лимиты часто оставляют на общем edge-шлюзе перед BFF, чтобы не дублировать.

Цена — больше сервисов и риск дублирования кода между BFF. Если клиент один, BFF не нужен. Частая альтернатива — GraphQL-шлюз: клиент сам описывает, какие поля ему нужны, и потребность в отдельных BFF снижается.

Шлюз, балансировщик и service mesh

Это самый частый уточняющий вопрос. Границы размыты, потому что инструменты пересекаются: тот же NGINX или Envoy может работать и балансировщиком, и шлюзом, и прокси внутри mesh. Поэтому сравнивать стоит роли, а не продукты.

БалансировщикAPI GatewayService mesh
Главная задачаРаспределить нагрузку между экземплярамиУправлять внешним APIУправлять связью между сервисами
ТрафикЛюбой, часто на входеСнаружи внутрь (north-south)Между сервисами (east-west)
УровеньL4 или L7L7, понимает HTTP/gRPC и APIL7 (в sidecar или на узле)
Типичные функцииАлгоритмы распределения, health checksАутентификация, лимиты, агрегация, версионированиеmTLS, повторы, таймауты, трассировка между сервисами
Кто знает о нёмКлиент видит один адресВнешние клиенты и их разработчикиСервисы о нём не знают, прокси перехватывает трафик

Как это выглядит вместе:

клиент → L4/L7 балансировщик → экземпляры шлюза → (BFF) → сервисы ↕ service mesh между сервисами

Балансировщик стоит перед шлюзом и распределяет трафик между его экземплярами. Шлюз, в свою очередь, балансирует запросы между экземплярами сервисов — ту же задачу, но уровнем глубже. Алгоритмы и проверки здоровья разобраны в статье о балансировке нагрузки.

Service mesh (Istio, Linkerd и другие) ставит прокси рядом с каждым сервисом и берёт на себя mTLS, повторы и трассировку во внутреннем трафике. Это не замена шлюзу: mesh не занимается API-ключами внешних партнёров и не собирает ответ для мобильного экрана. Многие mesh-решения при этом имеют собственный ingress gateway — входную точку, которая берёт на себя часть функций шлюза.

Готовые решения

На интервью конкретный продукт обычно не важен, но назвать пару вариантов полезно. Общая картина, без претензии на полноту:

  • NGINX — веб-сервер и обратный прокси, на котором многие собирают шлюз своими руками: маршрутизация, TLS, базовые лимиты.
  • Kong — шлюз поверх NGINX/OpenResty с плагинами для аутентификации, лимитов, логирования. Есть открытая и коммерческая версии.
  • Envoy — прокси, изначально созданный в Lyft. Используется как data plane во многих service mesh (например, в Istio) и как основа ряда шлюзов.
  • Управляемые облачные шлюзы — AWS API Gateway, а также аналоги у других облаков. Не нужно администрировать серверы, есть интеграция с облачной авторизацией и бессерверными функциями. Платишь за запросы и миришься с ограничениями платформы — их стоит смотреть в актуальной документации.
  • Spring Cloud Gateway и подобные библиотеки — если команда хочет писать логику шлюза на своём языке.

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

Единая точка отказа и узкое место

Весь внешний трафик идёт через шлюз. Упал шлюз — упала вся система, даже если каждый сервис здоров. Это главный аргумент против, и на интервью его надо назвать самому, не дожидаясь вопроса.

Как с этим живут:

  • Stateless-экземпляры. Шлюз не хранит сессии в памяти. Состояние — в токене или во внешнем хранилище. Тогда экземпляры взаимозаменяемы и масштабируются горизонтально.
  • Несколько экземпляров за балансировщиком в разных зонах доступности. Сам балансировщик тоже резервируется: пара с общим виртуальным IP, DNS с несколькими адресами или облачный балансировщик.
  • Таймауты на каждый upstream. Один медленный сервис не должен занять все соединения шлюза и уронить остальные маршруты. Помогает изоляция ресурсов по маршрутам (bulkhead) и circuit breaker.
  • Минимум работы на запрос. Каждый плагин добавляет задержку. Тяжёлая агрегация и преобразования — в BFF или отдельные сервисы.
  • Осторожные деплои конфига. Ошибочное правило маршрутизации ломает всё сразу. Конфиг шлюза проходит ревью, тесты и раскатывается постепенно, как код.
  • Зависимости шлюза тоже критичны. Если каждый запрос ходит в Redis за лимитом или в сервис авторизации за проверкой токена, их отказ становится отказом шлюза. Отсюда локальная проверка JWT по публичному ключу и fail open для лимитов.

Ещё один риск — организационный. Если шлюз ведёт одна команда, а маршруты нужны всем, она становится бутылочным горлышком. Помогает самообслуживание: команды описывают свои маршруты декларативно, а платформенная команда отвечает за сам шлюз.

Когда шлюз не нужен

  • Монолит или пара сервисов. Балансировщика и middleware в приложении достаточно.
  • Только внутренний трафик. Сервисы общаются между собой — это задача service discovery и, возможно, mesh.
  • Шлюз ради шлюза. Каждый промежуточный узел добавляет задержку, точку отказа и место для ошибок в конфиге.

Как говорить об API Gateway на интервью

В задачах на системный дизайн шлюз обычно появляется на первой схеме между клиентом и сервисами. Ошибка — нарисовать прямоугольник с подписью «API Gateway» и молча пойти дальше. Хороший ответ занимает минуту и показывает, что ты понимаешь, зачем он здесь. Общая структура ответа на такие задачи — в статье о подходах к системному дизайну.

Что сказать сразу

  1. Зачем он в этой системе. «Клиентов три типа, сервисов много, нужна единая точка для аутентификации и лимитов».
  2. Что в нём есть. Маршрутизация, проверка JWT, rate limiting по пользователю, TLS, request id для трассировки.
  3. Чего в нём нет. «Бизнес-правила остаются в сервисах, шлюз тонкий».
  4. Как он не падает. Stateless, несколько экземпляров за балансировщиком, таймауты к сервисам.

Частые уточняющие вопросы

  • Чем шлюз отличается от балансировщика? Балансировщик распределяет нагрузку между одинаковыми экземплярами. Шлюз понимает API: маршрутизирует по смыслу запроса, проверяет права, считает квоты.
  • Где хранить счётчики лимитов, если шлюзов десять? Общий Redis с атомарными операциями или приблизительные локальные лимиты. Что делать при отказе Redis.
  • Не станет ли шлюз узким местом? Горизонтальное масштабирование, отсутствие состояния, вынос тяжёлой работы, мониторинг p99 задержки самого шлюза.
  • Зачем BFF, если есть шлюз? Разные клиенты с разными потребностями и командами. Один общий слой агрегации начинает мешать всем.
  • Как проверять токен, не обращаясь каждый раз к сервису авторизации? Подпись JWT проверяется локально по публичному ключу, ключи кэшируются. Минус — отозвать токен до истечения срока сложнее: короткий срок жизни плюс список отзыва.
  • Как сделать канареечный релиз? Правило маршрутизации с весами или по заголовку, метрики ошибок по версии, автоматический откат.

Типичные ошибки

  • Бизнес-логика в шлюзе. Через полгода он знает о скидках, складах и статусах заказов, и любое изменение требует его релиза.
  • Шлюз для внутреннего трафика. Сервисы ходят друг к другу через внешний шлюз: лишний хоп, лишняя нагрузка, смешанные политики.
  • Забыть про отказоустойчивость. Один экземпляр шлюза на схеме — первое, за что зацепится интервьюер.
  • Путать роли. «Шлюз, балансировщик и mesh — это одно и то же» — неверно, даже если их реализует один продукт.
  • Перегружать ответ продуктами. Сравнение Kong и Envoy по фичам интервьюеру обычно не нужно. Нужны компромиссы: что вынесено на границу, почему, чего это стоит.

Итог

API Gateway — это граница системы. Он прячет внутреннее устройство от клиентов и делает в одном месте то, что иначе повторил бы каждый сервис: маршрутизацию, проверку токенов, лимиты, TLS, трассировку. Полезен, пока остаётся тонким. Опасен, если превращается в монолит с бизнес-логикой или работает в единственном экземпляре. На интервью назови его роль, перечисли функции, проведи границу с балансировщиком и mesh и сам скажи, как он не станет единой точкой отказа.

Потренироваться в спокойной обстановке

Все задачи