API Gateway в системном дизайне: роль, BFF, rate limiting и отличие от балансировщика
Что делает API Gateway: маршрутизация, аутентификация, rate limiting, агрегация запросов, BFF. Чем шлюз отличается от балансировщика и service mesh и как не сделать его единой точкой отказа.
Коротко
- 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:
- Клиент получает токен у сервиса авторизации.
- Каждый запрос приходит с
Authorization: Bearer <token>. - Шлюз проверяет подпись, срок действия и издателя. Нет токена или он невалиден — сразу
401, до сервисов запрос не доходит. - Шлюз передаёт дальше проверенный идентификатор, например в заголовке
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 Gateway | Service mesh | |
|---|---|---|---|
| Главная задача | Распределить нагрузку между экземплярами | Управлять внешним API | Управлять связью между сервисами |
| Трафик | Любой, часто на входе | Снаружи внутрь (north-south) | Между сервисами (east-west) |
| Уровень | L4 или L7 | L7, понимает HTTP/gRPC и API | L7 (в 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» и молча пойти дальше. Хороший ответ занимает минуту и показывает, что ты понимаешь, зачем он здесь. Общая структура ответа на такие задачи — в статье о подходах к системному дизайну.
Что сказать сразу
- Зачем он в этой системе. «Клиентов три типа, сервисов много, нужна единая точка для аутентификации и лимитов».
- Что в нём есть. Маршрутизация, проверка JWT, rate limiting по пользователю, TLS, request id для трассировки.
- Чего в нём нет. «Бизнес-правила остаются в сервисах, шлюз тонкий».
- Как он не падает. Stateless, несколько экземпляров за балансировщиком, таймауты к сервисам.
Частые уточняющие вопросы
- Чем шлюз отличается от балансировщика? Балансировщик распределяет нагрузку между одинаковыми экземплярами. Шлюз понимает API: маршрутизирует по смыслу запроса, проверяет права, считает квоты.
- Где хранить счётчики лимитов, если шлюзов десять? Общий Redis с атомарными операциями или приблизительные локальные лимиты. Что делать при отказе Redis.
- Не станет ли шлюз узким местом? Горизонтальное масштабирование, отсутствие состояния, вынос тяжёлой работы, мониторинг p99 задержки самого шлюза.
- Зачем BFF, если есть шлюз? Разные клиенты с разными потребностями и командами. Один общий слой агрегации начинает мешать всем.
- Как проверять токен, не обращаясь каждый раз к сервису авторизации? Подпись JWT проверяется локально по публичному ключу, ключи кэшируются. Минус — отозвать токен до истечения срока сложнее: короткий срок жизни плюс список отзыва.
- Как сделать канареечный релиз? Правило маршрутизации с весами или по заголовку, метрики ошибок по версии, автоматический откат.
Типичные ошибки
- Бизнес-логика в шлюзе. Через полгода он знает о скидках, складах и статусах заказов, и любое изменение требует его релиза.
- Шлюз для внутреннего трафика. Сервисы ходят друг к другу через внешний шлюз: лишний хоп, лишняя нагрузка, смешанные политики.
- Забыть про отказоустойчивость. Один экземпляр шлюза на схеме — первое, за что зацепится интервьюер.
- Путать роли. «Шлюз, балансировщик и mesh — это одно и то же» — неверно, даже если их реализует один продукт.
- Перегружать ответ продуктами. Сравнение Kong и Envoy по фичам интервьюеру обычно не нужно. Нужны компромиссы: что вынесено на границу, почему, чего это стоит.
Итог
API Gateway — это граница системы. Он прячет внутреннее устройство от клиентов и делает в одном месте то, что иначе повторил бы каждый сервис: маршрутизацию, проверку токенов, лимиты, TLS, трассировку. Полезен, пока остаётся тонким. Опасен, если превращается в монолит с бизнес-логикой или работает в единственном экземпляре. На интервью назови его роль, перечисли функции, проведи границу с балансировщиком и mesh и сам скажи, как он не станет единой точкой отказа.