.NET Tutorial
Senior7 мин чтенияВопрос 2 из 12

Масштабирование: вертикальное и горизонтальное, stateless, балансировка нагрузки L4/L7?

Вертикальное и горизонтальное

Вертикальное

4 CPU, 16 ГБ

32 CPU, 256 ГБ

Горизонтальное

Балансировщик

4 CPU

4 CPU

4 CPU

4 CPU

Вертикальное

4 CPU, 16 ГБ

32 CPU, 256 ГБ

Горизонтальное

Балансировщик

4 CPU

4 CPU

4 CPU

4 CPU

Два способа справиться с ростом нагрузки
Вертикальное Горизонтальное
Сложность Минимальная Нужны stateless, балансировка, распределённые данные
Предел Самая мощная машина Практически нет
Стоимость Растёт нелинейно Линейно, можно использовать дешёвые машины
Отказоустойчивость Одна машина — одна точка отказа Отказ узла — снижение мощности, а не простой
Обновление С простоем (или нужна вторая машина) Rolling update без простоя
Где уместно БД, ранняя стадия продукта Веб-сервисы, обработчики очередей

Практический совет: не стесняйтесь вертикально масштабировать БД — современный сервер с 64+ ядрами и терабайтом RAM справляется с огромной нагрузкой, а горизонтальное масштабирование записи в БД (шардирование) — одно из самых дорогих решений в архитектуре.

Stateless-сервисы

Балансировщик

API экземпляр 1

API экземпляр 2

API экземпляр 3

Redis:
сессии, кэш,
rate limit

БД

Объектное хранилище:
файлы

Балансировщик

API экземпляр 1

API экземпляр 2

API экземпляр 3

Redis:
сессии, кэш,
rate limit

БД

Объектное хранилище:
файлы

Где хранить состояние

Что мешает горизонтальному масштабированию в .NET-приложении:

Проблема Решение
IMemoryCache с данными, которые должны быть общими IDistributedCache/HybridCache на Redis, или локальный кэш с коротким TTL и инвалидацией
Сессии в памяти (AddSession с in-memory) Распределённая сессия или stateless JWT/cookie
Файлы на локальном диске S3 / Azure Blob
Data Protection keys на диске (cookie не расшифровываются на другом экземпляре) PersistKeysToStackExchangeRedis / Blob / БД
Фоновые задачи, выполняемые каждым экземпляром Распределённая блокировка, очередь, отдельный worker, Quartz с кластерным режимом
SignalR-подключения на разных серверах Redis backplane / Azure SignalR Service
In-memory rate limiting Распределённый лимит (Redis) или на шлюзе

Балансировка L4 и L7

/api/orders

/api/catalog

Клиенты

L4 балансировщик
(NLB): TCP, IP/порт,
миллионы соединений

L7 (Nginx / Envoy / YARP /
Ingress): HTTP, TLS, пути

L7 экземпляр 2

orders-api ×3

catalog-api ×5

/api/orders

/api/catalog

Клиенты

L4 балансировщик
(NLB): TCP, IP/порт,
миллионы соединений

L7 (Nginx / Envoy / YARP /
Ingress): HTTP, TLS, пути

L7 экземпляр 2

orders-api ×3

catalog-api ×5

Уровни балансировки
L4 (транспортный) L7 (прикладной)
Видит IP, порт, TCP/UDP URL, метод, заголовки, cookie, тело
Маршрутизация По соединению По запросу: путь, хост, заголовок, вес (canary)
TLS Проходит насквозь (или терминирует) Терминирует, видит содержимое
Производительность Выше Ниже (парсинг HTTP)
Дополнительно — Ретраи, таймауты, rate limiting, кэш, сжатие, аутентификация
Примеры AWS NLB, Azure Load Balancer, IPVS Nginx, Envoy, HAProxy, AWS ALB, YARP, Kubernetes Ingress

Алгоритмы распределения

Алгоритм Как работает Когда
Round robin По кругу Одинаковые экземпляры и запросы
Weighted round robin С весами Разная мощность серверов, canary (5% на новую версию)
Least connections Туда, где меньше активных соединений Запросы разной длительности, WebSocket
Least response time / EWMA Туда, где быстрее отвечают Неоднородная нагрузка, «медленный» экземпляр
IP hash / consistent hashing Один и тот же ключ → один и тот же сервер Локальные кэши, шардированные данные
Power of two choices Выбрать 2 случайных, взять менее загруженный Большие кластеры, хорошо и дёшево

Sticky sessions

Привязка клиента к экземпляру (по cookie). Нужна, если состояние хранится в памяти (старые приложения, SignalR без backplane). Минусы: неравномерная нагрузка, потеря сессии при падении экземпляра, проблемы при масштабировании вниз. Правильный путь — сделать сервис stateless.

Health checks и вывод из ротации

Экземпляр BЭкземпляр AБалансировщикloop[каждые 5 секунд]B исключён изротации после NнеудачGET /health/ready200GET /health/ready503 (БД недоступна)весь трафик на A
Экземпляр BЭкземпляр AБалансировщикloop[каждые 5 секунд]B исключён изротации после NнеудачGET /health/ready200GET /health/ready503 (БД недоступна)весь трафик на A
Балансировщик исключает больной экземпляр

Плюс outlier detection (Envoy, service mesh): исключать экземпляр, который отвечает ошибками 5xx, даже если health check проходит.

Автомасштабирование

  • Горизонтальное (Kubernetes HPA, облачные auto scaling groups): по CPU, памяти, RPS, длине очереди (KEDA для потребителей сообщений).
  • Учитывайте время прогрева: новый под .NET стартует, JIT-компилирует, прогревает пулы — масштабируйтесь заранее (минимум экземпляров, предиктивное масштабирование), используйте ReadyToRun/Native AOT.
  • Масштабирование приложения переносит нагрузку на зависимости: 50 экземпляров × пул 100 соединений = 5000 соединений к PostgreSQL. Нужны PgBouncer, лимиты пулов.

Масштабирование базы данных

Оптимизация запросов
и индексов

Кэш
(Redis, HybridCache)

Реплики для чтения

Вертикальное
масштабирование БД

Разделение по сервисам /
функциональное партиционирование

Шардирование

Оптимизация запросов
и индексов

Кэш
(Redis, HybridCache)

Реплики для чтения

Вертикальное
масштабирование БД

Разделение по сервисам /
функциональное партиционирование

Шардирование

Путь масштабирования данных

Идите слева направо: каждый следующий шаг дороже по сложности. Шардирование — когда всё остальное исчерпано (подробнее — в вопросе о репликации и шардировании).

Вопросы на засыпку

Почему stateless-сервисы проще масштабировать?

Любой экземпляр обрабатывает любой запрос: балансировщик может отправлять трафик куда угодно, экземпляры можно добавлять и удалять в любой момент (автомасштабирование, rolling update), падение одного не теряет данные клиентов.

Что такое закон Амдала применительно к масштабированию?

Ускорение от параллелизма ограничено последовательной частью работы. Если 10% запроса — это обращение к единственной БД, которая не масштабируется, то добавление серверов приложения не ускорит систему больше чем в 10 раз. Узкое место определяет предел масштабирования.

Как балансировщик понимает, что экземпляр «здоров»?

По активным проверкам (периодические запросы к readiness-эндпоинту) и пассивным (анализ ошибок и таймаутов реальных запросов — outlier detection). Readiness должен проверять готовность обслуживать трафик, но не каскадно все зависимости, иначе сбой одной БД выведет все экземпляры одновременно.

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

При клиентской балансировке (gRPC, service mesh sidecar, HttpClient с service discovery) клиент сам знает список экземпляров и выбирает. Нет лишнего сетевого перехода и единой точки отказа, но логика распределена по клиентам. Серверная — отдельный балансировщик, проще для клиентов.

Итог

Вертикальное масштабирование просто, но ограничено; горизонтальное требует stateless-сервисов с состоянием во внешних хранилищах и балансировщика (L4 для скорости, L7 для HTTP-маршрутизации, TLS и ретраев). Выбирайте алгоритм распределения под характер нагрузки, исключайте больные экземпляры health checks и учитывайте, что узким местом обычно становится база данных — её масштабируют поэтапно: индексы, кэш, реплики, вертикально и лишь затем шардирование.