Высоконагруженные веб-системы: как проектировать архитектуру под рост нагрузки
Как проектировать высоконагруженные веб-системы: поиск узких мест, масштабирование backend и базы данных, кэширование, очереди, отказоустойчивость, нагрузочное тестирование и наблюдаемость.
Высокая нагрузка редко возникает в тот момент, когда команда её ждёт.
Система может месяцами работать нормально, а затем внезапно получить в несколько раз больше запросов из-за рекламной кампании, нового клиента, сезонного пика или резко выросшего количества пользователей.
В этот момент становится видно главное: архитектура должна не просто обслуживать нормальный трафик, а предсказуемо вести себя при росте нагрузки и отказах отдельных компонентов.
При этом highload — это не магическая цифра запросов в секунду. Для одной системы 500 запросов в секунду могут быть серьёзной нагрузкой, а другая может обрабатывать значительно больший поток.
Поэтому проектирование высоконагруженной системы начинается не с выбора Kubernetes, базы данных или очереди.
Оно начинается с понимания того, где именно заканчивается запас производительности и что происходит после этого.
Highload начинается с требований, а не с технологий
Первый вопрос при проектировании системы под нагрузку:
Какую нагрузку система должна выдерживать?
Нужно определить хотя бы приблизительные параметры:
Количество пользователей
Запросов в секунду
Пиковый RPS
Размер запросов и ответов
Допустимая задержка
Доля чтения и записи
Объём данных
Требования к доступности
Например:
Средняя нагрузка: 500 RPS
Пик: 2 000 RPS
P95 latency: < 300 ms
Доступность: 99,9%
Эти числа уже позволяют обсуждать архитектуру предметно.
Google в своих практиках проектирования больших систем отдельно подчёркивает важность capacity planning: инженеры должны переводить абстрактную схему системы в конкретные оценки ресурсов и учитывать возможные failure domains ещё до того, как система попадёт в production.
Точные значения могут меняться, а вот сам принцип универсален:
архитектуру под нагрузку нужно проектировать относительно измеримых требований.
Не масштабируйте то, что ещё не является узким местом
Одна из самых распространённых ошибок — заранее масштабировать всё подряд.
Например:
Купили больше CPU
Добавили серверы
Поставили Redis
Добавили Kafka
Перешли на Kubernetes
А потом выясняется, что проблема была в одном SQL-запросе.
Или внешний API отвечает 800 миллисекунд.
Или несколько backend-инстансов одновременно создают огромную нагрузку на одну таблицу.
AWS прямо рекомендует определять bottleneck через мониторинг, tracing и нагрузочное тестирование, а не считать CPU и RAM достаточными показателями производительности.
Поэтому нормальный цикл выглядит так:
Нагрузка
↓
Метрики
↓
Поиск bottleneck
↓
Изменение
↓
Повторное измерение
Это кажется очевидным, но именно нарушение этого цикла часто приводит к дорогому overengineering.
Производительность — это не только RPS
Система может выдерживать большой RPS и при этом быть неудобной для пользователя.
Поэтому кроме throughput нужно смотреть на latency.
Например:
P50 = 80 ms
P95 = 180 ms
P99 = 2 500 ms
Среднее значение при этом может выглядеть вполне нормально.
Но один из ста запросов будет ждать 2,5 секунды.
Для пользователя это уже заметная деградация.
Cloudflare при проектировании производительности также рекомендует смотреть на высокие перцентили серверной задержки, а не ограничиваться средним или медианным значением; для server-side latency особое значение имеет P99.
Поэтому в production полезно знать хотя бы:
P50
P95
P99
для критических API и операций.
Самый простой способ масштабировать backend — добавить экземпляры
Если backend не хранит состояние локально, его относительно просто масштабировать горизонтально:
Load Balancer
/ | \
/ | \
API #1 API #2 API #3
Новые запросы распределяются между экземплярами.
Но это работает только при выполнении нескольких условий.
Приложение не должно зависеть от:
- локальной сессии;
- файлов конкретного сервера;
- памяти конкретного процесса;
- временного состояния, которое не синхронизируется между экземплярами.
Например, загруженный файл лучше хранить не в:
/var/www/uploads
на конкретном сервере, а в общем объектном хранилище или другом внешнем storage.
То же касается session state и других данных, которые должны быть доступны любому экземпляру приложения.
Горизонтальное масштабирование backend — это прежде всего отделение состояния от экземпляра приложения.
Масштабирование базы данных сложнее
С backend всё относительно просто:
1 server
→ 3 servers
С базой данных всё сложнее.
Главная причина — состояние.
Когда десятки экземпляров приложения используют одну БД:
API #1 ─┐
API #2 ─┤
API #3 ─┼──→ PostgreSQL
API #4 ─┘
база становится потенциальным bottleneck.
Именно поэтому база данных должна проектироваться отдельно от application tier.
Первый уровень оптимизации — запросы и структура данных.
AWS отдельно рекомендует использовать индексы, оптимизацию запросов, а при необходимости — partitioning и другие стратегии организации данных, поскольку неэффективный data access может ограничить масштабируемость всей системы.
Очень часто быстрее исправить запрос:
SELECT *
FROM orders
WHERE customer_id = ?;
и добавить корректный индекс, чем добавлять ещё пять backend-серверов.
Репликация помогает разгрузить чтение
Если система преимущественно читает данные, один из вариантов — read replicas:
┌──→ Primary
│
Application ────┤
├──→ Replica #1
├──→ Replica #2
└──→ Replica #3
Запись идёт в primary, а часть чтений можно направить на replicas.
Но это не бесплатное масштабирование.
После репликации появляется вопрос:
Насколько свежими должны быть данные?
Если replication lag составляет, например, несколько секунд, пользователь может:
создать заказ
↓
сразу запросить заказ
↓
попасть на replica
↓
ещё не увидеть созданные данные
Для некоторых сценариев это допустимо.
Для других — нет.
То есть репликация базы является не только инфраструктурным решением, но и изменением семантики чтения.
Кэш снимает нагрузку, но создаёт новую задачу
Кэширование — один из самых эффективных способов уменьшить количество обращений к origin и базе.
Упрощённо:
Request
↓
Cache
┌─┴─┐
hit miss
│ ↓
│ DB
│ ↓
└── Cache
↓
Response
Если один и тот же объект запрашивают тысячи раз, нет смысла каждый раз заново вычислять его или читать из базы.
Cloudflare прямо использует caching как средство уменьшения запросов к origin и снижения latency; tiered cache дополнительно ограничивает количество запросов к origin при cache miss.
Но у кэша появляется цена:
Когда данные становятся устаревшими?
AWS отдельно отмечает, что caching может резко улучшить производительность, но требует чёткой стратегии обновления и invalidation, иначе система рискует возвращать неправильные данные.
Поэтому кэш — это не просто:
Redis = faster
Нужно определить:
Что кэшируем?
Как долго?
Когда инвалидируем?
Кто является source of truth?
Что происходит при cache miss?
Что происходит при недоступности cache?
Cache stampede может снова ударить по базе
Представим, что популярный объект имеет TTL 60 секунд.
Пока значение находится в кэше:
1 000 RPS → Redis
TTL заканчивается.
И сотни или тысячи запросов одновременно обнаруживают:
MISS
После чего все идут в базу:
1 000 RPS
↓
1 000 запросов в DB
Вместо разгрузки кэш создаёт кратковременный всплеск нагрузки.
Поэтому для некоторых сценариев применяют:
- request coalescing;
- locking;
- stale-while-revalidate;
- background refresh;
- разные стратегии TTL.
Cloudflare, например, использует cache lock, чтобы не отправлять множество одинаковых запросов к origin одновременно, а stale-while-revalidate позволяет отдавать устаревшее содержимое, пока свежая версия обновляется в фоне.
Это хорошо показывает важный принцип:
масштабирование — это не только добавление ресурсов, но и управление формой нагрузки.
Очередь нужна не для «ускорения всего»
Очередь полезна тогда, когда операцию необязательно выполнять непосредственно в пользовательском запросе.
Например:
POST /order
↓
Создание заказа
↓
200 OK
↓
RabbitMQ / Kafka
↓
Worker
├── email
├── CRM
├── аналитика
└── документ
Пользователь получает ответ после критической части операции, а второстепенная работа выполняется асинхронно.
Это позволяет сгладить пики:
Пик нагрузки
↓
Queue
↓
Worker'ы
↓
Плавная обработка
Но очередь не отменяет проблему нагрузки.
Если поступает:
10 000 задач/сек
а worker'ы обрабатывают:
5 000 задач/сек
очередь будет расти.
Поэтому для очередей необходимо контролировать:
Queue depth
Consumer throughput
Processing time
Retry count
Dead-letter messages
Очередь — это механизм развязки и буферизации, а не бесконечный источник производительности.
Идемпотентность становится обязательной
Чем больше в системе асинхронных операций и сетевых взаимодействий, тем вероятнее повторная доставка.
Например:
PaymentSucceeded
PaymentSucceeded
или:
Webhook #812
Webhook #812
Если обработчик не идемпотентен, повторное сообщение может:
создать дубликат заказа;
начислить деньги дважды;
отправить два письма;
создать две задачи.
Поэтому критичные операции должны уметь распознавать повторную обработку.
Один из распространённых вариантов:
event_id = 8af93...
и запись обработанных событий:
event_id → processed
Повторное событие уже не выполняет бизнес-операцию второй раз.
Это особенно важно при retry и временных сетевых сбоях.
Ошибки нужно масштабировать так же, как и нагрузку
Под нагрузкой проблема редко выглядит как одна ошибка.
Например:
DB стала отвечать медленнее
↓
API начинает ждать
↓
растёт количество открытых соединений
↓
растёт latency
↓
клиенты повторяют запросы
↓
нагрузка увеличивается ещё сильнее
Получается feedback loop.
Поэтому в критичных системах нужны защитные механизмы:
Timeout
Retry с ограничением
Circuit breaker
Rate limiting
Bulkhead
Load shedding
Backpressure
Google SRE прямо рассматривает load shedding и graceful degradation как инструменты защиты системы при перегрузке. При этом autoscaling должен успевать увеличивать capacity до того, как система начнёт отбрасывать трафик.
Главная идея здесь — не дать перегрузке одного компонента уничтожить всю систему.
Graceful degradation лучше полной остановки
Не каждая функция приложения одинаково важна.
Допустим, основная операция:
Создание заказа
А дополнительные:
Рекомендации
Аналитика
Персонализация
Дополнительные уведомления
Если сервис рекомендаций перестал работать, нет смысла делать недоступным оформление заказа.
Можно построить:
Order
↓
работает
Recommendations
↓
fallback
Analytics
↓
async
Notification
↓
retry
Google описывает graceful degradation как возможность сохранять минимально необходимую функциональность даже при проблемах в системе вместо перехода из состояния «полностью работает» в «полностью не работает».
Для критичных платформ такой подход может быть важнее, чем максимальная производительность в штатном режиме.
Rate limiting защищает систему от самой нагрузки
Иногда проблема возникает не потому, что система плохо написана.
Просто один клиент делает слишком много запросов.
Например:
Normal client
→ 10 RPS
Problem client
→ 5 000 RPS
Если не ограничивать такой трафик, один источник может занять значительную часть capacity.
Поэтому полезны ограничения:
Per IP
Per user
Per token
Per API key
Per tenant
Причём лимиты могут быть разными для разных операций.
Например:
GET /products
→ 100 RPS
POST /orders
→ 20 RPS
POST /reports/generate
→ 2 RPS
Последний endpoint может создавать тяжёлую фоновую работу, поэтому его бессмысленно ограничивать так же, как дешёвый GET.
Высокая нагрузка требует правильного разделения типов работы
Хороший пример:
Синхронный путь:
HTTP
↓
Validation
↓
DB
↓
Response
и:
Асинхронный путь:
Queue
↓
Worker
↓
External API
↓
Document generation
↓
Notification
Чем тяжелее операция, тем меньше оснований выполнять её внутри пользовательского HTTP-запроса.
Но и здесь есть нюанс.
Если абсолютно всё отправлять в очередь, пользователь может не получить нужного результата вовремя.
Поэтому критический путь нужно проектировать отдельно.
Полезный вопрос:
Что действительно необходимо завершить до ответа пользователю?
Всё остальное потенциально может быть вынесено за этот boundary.
Наблюдаемость — часть производительности
Нельзя оптимизировать систему, которую невозможно измерить.
Для критичных компонентов нужны хотя бы:
RPS
Latency
Error rate
CPU
Memory
DB latency
Slow queries
Cache hit ratio
Queue depth
External API latency
AWS рекомендует end-to-end monitoring и tracing для понимания latency, traffic patterns и узких мест, а также отдельно предупреждает, что одних стандартных метрик инфраструктуры недостаточно.
Например, CPU может быть:
40%
а API уже отвечать:
3 секунды
Причина может находиться в:
PostgreSQL
Redis
external API
lock
network
Поэтому CPU ≠ performance.
Нагрузочные тесты должны повторять реальный сценарий
Нагрузка:
GET /
GET /
GET /
может показать отличный результат.
А настоящая нагрузка:
POST /login
GET /profile
GET /products
POST /cart
POST /order
GET /order
может перегрузить систему совершенно иначе.
Поэтому нагрузочный тест должен моделировать реальные профили запросов.
Причём полезно проверять не только штатный режим:
100%
но и:
150%
200%
500%
от ожидаемой нагрузки.
Так можно увидеть, как система деградирует после достижения capacity.
Google SRE при проектировании больших систем отдельно рекомендует учитывать capacity planning и graceful degradation, то есть заранее понимать не только нормальную работу, но и поведение за пределами расчётной мощности.
Самая опасная часть системы часто находится за пределами вашего backend
Система может быть идеально масштабирована, но при этом зависеть от внешнего API:
API
↓
External Provider
Если внешний сервис отвечает:
2 секунды
то ваш endpoint уже не сможет быть быстрее этого ограничения без кеширования, асинхронности или изменения сценария.
Если внешний сервис временно не отвечает:
timeout
↓
retry
↓
retry
↓
retry
можно случайно создать ещё большую нагрузку.
Поэтому retry должен иметь ограничения и backoff.
Для внешних систем полезно заранее определить:
timeout
maximum retries
backoff
circuit breaker
fallback
И главное — понять, является ли внешний вызов критическим.
Масштабирование должно быть многоуровневым
Вместо идеи:
«Нам нужно больше серверов»
лучше смотреть на всю цепочку:
Client
↓
CDN / Edge
↓
Load Balancer
↓
API
↓
Cache
↓
Database
↓
Queue
↓
Workers
↓
External Services
У каждого слоя свой bottleneck.
Например:
CDN
→ снимает статический трафик
Cache
→ снижает чтение БД
DB indexes
→ уменьшают стоимость запросов
Queue
→ сглаживает пики
Workers
→ масштабируют тяжёлые операции
Load balancer
→ распределяет HTTP-нагрузку
Cloudflare в своей reference architecture именно так разделяет путь запроса на network, optimization, caching и origin layers, подчёркивая, что производительность достигается оптимизацией всей цепочки, а не одного компонента.
Не всё нужно масштабировать горизонтально
Иногда проще и дешевле сначала сделать вертикальное масштабирование.
Например:
8 CPU / 16 GB RAM
↓
16 CPU / 32 GB RAM
Если стоимость такого решения приемлема, а bottleneck находится в CPU или памяти, это может быть лучшим вариантом.
Горизонтальное масштабирование сложнее:
1 instance
→
5 instances
→
20 instances
Появляются:
- балансировка;
- синхронизация;
- распределённые логи;
- внешние state stores;
- дополнительные сетевые взаимодействия.
Поэтому горизонтальное масштабирование — не идеология, а инструмент.
Когда действительно нужно разделять сервисы
Если один компонент имеет принципиально другой профиль нагрузки, его может быть разумно выделить.
Например:
API
↓
Order Service
и отдельно:
Document Workers
Потому что генерация документов:
CPU-heavy
а обычный API:
latency-sensitive
Если оставить их в одном пуле ресурсов, тяжёлая операция может вытеснить пользовательские запросы.
После разделения:
API
→ 10 instances
Workers
→ 4 instances
каждую часть можно масштабировать независимо.
Именно такие измеримые причины обычно сильнее аргумента «микросервисы — это современно».
Что я считаю правильным подходом к highload
Я бы проектировал систему примерно по следующей логике.
Сначала определить нагрузку
RPS
P95/P99
Data volume
Peak traffic
SLA
Затем найти самые дорогие операции
DB
CPU
Network
External API
Storage
После этого убрать лишнюю работу
Cache
Batching
Async processing
Query optimization
CDN
Потом масштабировать bottleneck
Vertical scaling
Horizontal scaling
Read replicas
Workers
Partitioning
И только затем усложнять архитектуру
Separate services
Distributed systems
Multiple data stores
Complex orchestration
Это помогает не строить распределённую систему там, где проблему можно решить индексом или одним кэшем.
Что стоит закладывать заранее
Есть несколько вещей, которые редко бывают лишними:
Метрики
Логи
Tracing
Timeout
Idempotency
Health checks
Graceful degradation
Load testing
Database migrations
Также полезно заранее продумать:
Как система переживает отказ Redis?
Что произойдёт при недоступности PostgreSQL?
Что будет, если очередь вырастет в 100 раз?
Что произойдёт при падении внешнего API?
Что увидит пользователь при деградации?
Такие вопросы гораздо полезнее вопроса:
«А нужен ли нам Kubernetes?»
Итог
Highload — это не количество серверов и не конкретная технология.
Это способность системы предсказуемо работать при росте нагрузки, локальных отказах и временном превышении расчётной мощности.
Для этого обычно нужны не один, а несколько взаимосвязанных механизмов:
Измерение
↓
Поиск bottleneck
↓
Кэширование
↓
Оптимизация БД
↓
Асинхронная обработка
↓
Горизонтальное масштабирование
↓
Отказоустойчивость
↓
Graceful degradation
Главный принцип прост:
Не масштабируйте систему целиком. Масштабируйте конкретное ограничение, которое вы подтвердили измерениями.
Хорошая highload-архитектура не пытается заранее решить все возможные проблемы. Она позволяет понять, где находится предел, контролируемо пройти этот предел и сохранить работоспособность системы даже тогда, когда отдельные компоненты начинают работать хуже штатного режима.
Именно поэтому проектирование под высокую нагрузку — это прежде всего инженерия ограничений, измерений и отказов, а уже потом выбор конкретных технологий.
Подробнее о проектировании высоконагруженных веб-платформ и backend-систем — в разделе услуг.
Релевантные разделы
Читайте также
Архитектура веб-платформ: как проектировать систему, которая растёт
Как проектировать архитектуру веб-платформ, которая выдерживает рост функциональности, команды и нагрузки: модульность, границы ответственности, данные, интеграции, наблюдаемость и выбор между монолитом и микросервисами.
Автоматизация бизнес-процессов: с чего начать и где искать эффект
Как находить процессы для автоматизации, оценивать экономический эффект, выбирать между готовыми платформами и собственной разработкой и строить автоматизацию, устойчивую к ошибкам и изменениям.