Как строить API, которые масштабируются

Reading time: 6 minutes

Last modified:

Диаграмма архитектуры масштабирования API

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

Это даёт время. Но не решает проблему.

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

Типичная ошибка

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

Исправление этих проблем позже означает рефакторинг под давлением продакшена, с живыми данными, пока пользователи жалуются. Экстренные миграции в 2 часа ночи стоят больше инженерного времени и бизнес-рисков, чем правильное выстраивание паттернов с самого начала.

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

Что ломается при 10 000 активных пользователей в день

На этом уровне доминируют проблемы базы данных. Данных ещё управляемый объём, но паттерны доступа начинают обнажать слабое проектирование запросов.

N+1 запросы. Классика: загружается список заказов, и для каждого заказа делается отдельный запрос к базе за клиентом. Один эндпоинт = сотни запросов. При 10 заказах незаметно. При 10 000 — катастрофа. Используйте eager loading, JOIN-запросы или паттерн DataLoader.

Отсутствие индексов. Полное сканирование таблицы из 10 строк невидимо. На таблице из 500 000 строк это займёт секунды. Каждый столбец, используемый в WHERE, JOIN или ORDER BY, нуждается в индексе. Используйте EXPLAIN для ваших запросов. Это не опционально.

Отсутствие пагинации. Эндпоинты, возвращающие неограниченные результаты (GET /orders, возвращающий все 80 000 заказов), рано или поздно будут таймаутить, заканчивать память или насыщать сеть. Пагинация на основе курсора или кейсета быстрее и надёжнее офсетной пагинации при масштабе.

Синхронные операции, которые должны быть асинхронными. Отправка письма подтверждения в рамках ответа API, синхронное изменение размера изображения при загрузке, вызов стороннего вебхука с ожиданием ответа. Всё это добавляет задержку и точки отказа к вашему API, не имеющие отношения к его основной функции. Поставьте их в очередь.

Что ломается при 100 000 активных пользователей в день

При 100k DAU оптимизация запросов к базе — это уже базовый уровень, эти проблемы должны быть решены. Здесь ломается инфраструктура.

Отсутствие слоя кэширования. Если API пересчитывает один и тот же ответ на одни и те же входные данные при каждом запросе, вычислительные ресурсы и нагрузка на базу тратятся впустую. Слой Redis или Memcached перед часто читаемыми, редко изменяемыми данными (каталог товаров, настройки пользователя, конфигурация) может снизить нагрузку на базу на 60–80%.

Исчерпание соединений с базой данных. Каждый экземпляр API открывает соединения с базой. При масштабировании экземпляров API у каждого из них есть свой пул соединений. Без пулера соединений (PgBouncer для PostgreSQL — стандартный ответ) можно исчерпать лимит соединений базы раньше, чем её вычислительные ресурсы. Это легко решаемая проблема, вызывающая драматические отказы.

Медленная сериализация. Если API сериализует большие объекты в JSON наивно — загружая целые ORM-объекты и рекурсивно сериализуя их связи — делается намного больше работы, чем нужно. Явно указывайте, какие поля сериализовать. Не загружайте строки, которые не нужны.

Отсутствие CDN для статических ресурсов. Изображения товаров, статические файлы, ответы API, которые действительно статичны — они никогда не должны касаться серверов приложений. CDN обрабатывает их на граничных узлах за долю стоимости и с долей задержки.

Что ломается при 1 000 000 активных пользователей в день

При 1M DAU проблемы становятся архитектурными, а не тактическими. Дизайн отдельных сервисов к этому моменту в основном нормальный. Ломается система в целом.

Развёртывание в одном регионе. Один регион — один радиус поражения. Инцидент облачного провайдера в us-east-1 выводит ваш продукт из строя. Практически, задержка для пользователей на другом конце планеты заметно хуже. Мультирегиональное развёртывание — active/active или active/passive — это ответ, но оно вводит проблемы консистентности, требующие осознанного проектирования.

Отсутствие реплик для чтения. Операции записи идут на основную базу данных. Операции чтения — обычно 80%+ трафика — могут идти на реплики. Без реплик каждое чтение конкурирует с каждой записью в одной базе. Это решаемо и должно быть решено задолго до 1M DAU.

Отсутствие rate limiting. При таком масштабе API — это мишень. Злоупотребления, скрейпинг и баги клиентов, генерирующих тысячи запросов в секунду, потребляют ресурсы, предназначенные для легитимных пользователей. Ограничение частоты запросов на аутентифицированного пользователя, на IP и на эндпоинт — не опция. Реализуйте на уровне API-шлюза или балансировщика нагрузки, а не в коде приложения.

Монолитный сервис авторизации. Если каждый запрос к API делает синхронный вызов к центральному сервису авторизации для валидации токена — этот сервис становится единой точкой отказа всей системы. Stateless-валидация JWT, которая происходит локально на каждом сервисе, — стандартный ответ.

Принципы проектирования, работающие при любом масштабе

Stateless-сервисы. Не храните состояние сессии в памяти сервера приложений. Если можно направить одного и того же пользователя к любому экземпляру и получить тот же результат — можно горизонтально масштабироваться без координации.

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

Явные контракты API с версионированием. Версионируйте API (/v1/, /v2/) и относитесь к ломающим изменениям как к намеренным релизам. Клиенты, не обновившиеся, не должны ломаться.

Асинхронность там, где не нужна немедленная задержка. Всё, что не нужно делать до ответа клиенту, не должно делаться. Уведомления, аналитические события, побочные эффекты — всё это относится к очереди.

Стратегия кэширования

Кэширование — наиболее эффективная оптимизация в инженерии API и наиболее часто неправильно используемая.

Что кэшировать: данные, которые часто читаются, редко изменяются и дорого вычислять или запрашивать. Каталоги товаров, данные профилей пользователей, конфигурация, агрегированные метрики.

Где кэшировать: внутрипроцессный кэш (самый быстрый, но только на экземпляр и не общий) для очень горячих, стабильных данных; распределённый кэш, например Redis (немного медленнее, общий для всех экземпляров) для данных, требующих консистентности; CDN для полного кэширования ответов публичных, неаутентифицированных эндпоинтов.

Инвалидация кэша — сложная часть. Безопасные паттерны: устанавливать короткие TTL на изменяемые данные (30–300 секунд), использовать событийную инвалидацию при записи (публиковать событие, инвалидировать ключ кэша), никогда не кэшировать данные, где устаревшие значения вызывают проблемы корректности.

Практический чеклист масштабирования

Область 10k DAU 100k DAU 1M DAU
Оптимизация запросов (индексы, N+1) Обязательно Обязательно Обязательно
Пагинация на всех списковых эндпоинтах Обязательно Обязательно Обязательно
Асинхронность для некритических операций Обязательно Обязательно Обязательно
Слой кэширования (Redis/Memcached) Желательно Обязательно Обязательно
Пулер соединений (PgBouncer) Желательно Обязательно Обязательно
CDN для статических ресурсов Желательно Обязательно Обязательно
Реплики для чтения Опционально Желательно Обязательно
Rate limiting Опционально Обязательно Обязательно
Мультирегиональное развёртывание Опционально Опционально Обязательно
Stateless JWT-авторизация Рекомендуется Обязательно Обязательно

Строите бэкенд и хотите правильно выстроить архитектуру до масштабирования? Напишите нам на hello@cimpleo.com. Разберём ваш текущий дизайн и скажем, где риск.

Table of Contents