Skip to main content
Back to course
Архитектура высоконагруженных веб-приложений
3 / 1127%

Горизонтальное масштабирование как операция на открытом сердце

80 минут

Для кого: инженеры, отвечающие за аптайм и трафик. Если в вашей архитектуре до сих пор есть "sticky sessions", а деплой = молитва — эта глава должна выбить почву из-под ног и заставить переделать основу.

Провокация №1: ваши пользователи привязаны к конкретному серверу?

  • Sticky sessions = долговая яма. Как только state хранится в памяти процесса, вы не сможете балансировать нагрузку честно. Один сервер перегорит, остальные будут скучать.

  • Проверьте себя: зайдите в конфиг load balancer'а. Если там ip_hash, cookie или «session affinity» — вы до сих пор в 2008 году.

  • Переходный план (если вы зарыты в legacy):

    НеделяДействиеМетрика успеха
    1Dual-write сессий: и в память, и в Redis100% записей дублируются
    210% трафика без stickyError rate < 0.1%
    350% трафика без stickyP95 latency не вырос
    4100% без sticky, удаляем affinityСессии переживают убийство сервера
  • Цель: любое приложение должно быть stateless, а state — вынесен в управляемые компоненты: Redis, БД, message bus.

1. Statless против Stateful: разбираем по косточкам

1.1 Тест на stateless

  1. Убейте произвольный сервер приложения.
  2. Запросите /api/profile с того же клиента.
  3. Если пользователь вылетел из сессии — вы stateful и не готовы к горизонтальному масштабированию.

1.2 Как выносить state

  • Сессии → Redis/Sentinel/Cluster. TTL, репликация, мониторинг. Обязательно включите автоматическое продление TTL при обращении.
  • Аутентификация → JWT/opaque токены. Минимизируйте state: токен содержит всё необходимое, серверы проверяют подпись.
  • Очереди: фоновые задачи не должны висеть в памяти API. Kafka/RabbitMQ/SQS.
  • Кеш: сессионные данные и rate limiting хранятся в Redis/KeyDB, а не в Map внутри Node.js.

1.3 Контроль

  • Настройте тест, который каждые 5 минут убивает случайный pod/EC2 instance. Если error rate растёт, значит, state ещё внутри приложения.

Провокация №2: Load balancer — ваш единственный point of failure?

Если LB лежит — весь кластер мёртв. Симуляция: остановите Nginx/ALB на staging. Система пережила? Если нет, читайте дальше.

2. Load Balancer: стратегии и тёмные углы

2.1 Архитектура

  • Горизонтальный LB слой: минимум 2 инстанса (Nginx/HAProxy/envoy) + health-checker.
  • DNS: используйте Route 53/Cloud DNS с health checks, чтобы при падении LB трафик мигрировал.
  • Наблюдаемость: метрики upstream_response_time, active_connections, 502/504.

2.2 Алгоритмы

АлгоритмКогда использоватьРиски
Round RobinОднородные сервера, короткие запросыНе видит загрузку CPU/памяти
Least ConnectionsLong-polling, WebSocketsРаботает только если серверы корректно закрывают соединения
IP Hash / StickyТолько если state вынесен не полностьюНагрузка может неравномерно распределяться
Weighted RRРазные типы железа (t3.small vs c6i.2xlarge)Требует пересчёта весов при каждом апгрейде
Least Response TimeNginx Plus/Envoy в продвинутых сценарияхНужна лицензия / дополнительная телеметрия
Power of Two ChoicesEnvoy/NGINX Plus, высококонкурентные средыПочти как least_conn, но дешевле по телеметрии

Кейс: e-commerce с акциями. Во время пика 20% запросов long-polling. Round Robin приводит к переполнению очередей на одном сервере. Переключение на least_conn + лимит активных соединений спасает.

2.3 Health-check — не формальность

  • Passive HC: max_fails, fail_timeout. Настройте так, чтобы сервер исключался после 2–3 подряд 5xx.
  • Active HC: отдельный эндпоинт /readyz, который проверяет БД, кэш, внешние API. Liveness (/healthz) — просто проверка процесса.
  • Правило: здоровый сервер = тот, что успел прогреть кэш и подключиться к БД. Не пускайте трафик раньше.
  • Пример:
    curl -f "http://localhost:3000/readyz" \
      -H "X-Health-Check: deep" \
      --max-time 3 \
      --retry 2 \
      --retry-delay 1

2.4 Конфигурация Nginx (проверенный шаблон)

upstream backend {
  least_conn;
  server app1.internal:3000 max_fails=2 fail_timeout=15s weight=2;
  server app2.internal:3000 max_fails=2 fail_timeout=15s weight=2;
  server app3.internal:3000 max_fails=2 fail_timeout=15s weight=1;
  keepalive 48;
}
 
server {
  listen 443 ssl http2;
  server_name api.example.com;
 
  # Rate limiting
  limit_req_zone $binary_remote_addr zone=api_limit:20m rate=200r/s;
 
  log_format json_analytics '{"time":"$time_iso8601","status":$status,"request_time":$request_time,"upstream_addr":"$upstream_addr","upstream_response_time":"$upstream_response_time","upstream_status":"$upstream_status","request":"$request"}';
  access_log /var/log/nginx/access.log json_analytics;
 
  location /api/ {
    limit_req zone=api_limit burst=100 nodelay;
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_buffering on;
    proxy_connect_timeout 5s;
    proxy_read_timeout 60s;
    proxy_next_upstream error timeout http_502 http_503;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 1s;
  }
 
  location /readyz { proxy_pass http://backend; access_log off; }
  location /healthz { return 200 'ok'; }
}

Тест: nginx -t, nginx -s reload, затем wrk -t8 -c64 -d60s https://api.example.com/readyz. Проверяйте upstream_status в логах.

Провокация №3: Сессии всё ещё в памяти?

Если вы храните Map<sessionId, data> в приложении, деплой = убийство пользователей. Это неприемлемо.

3.1 Стратегии

  1. Redis + TTL + репликация: используйте Sentinel/Cluster. Ключ session:{id} → JSON. TTL продлевайте при каждом обращении.
  2. JWT: минимальный state. Включите ротацию ключей, короткий TTL, refresh-токены.
  3. Opaque tokens + storage: хэш токена + user data в БД/Redis.
  4. WebSocket state: выносите в отдельный state store (например, Redis pub/sub или Kafka). Серверы должны быть взаимозаменяемы.

JWT без иллюзий: короткий TTL (минуты) + refresh-токены обязательны. Payload только base64, не кладите туда чувствительные данные. Подпись проверяйте всегда и продумайте revocation (через blacklist или короткий TTL).

3.2 Контроль качества

  • Инструмент: chaos-проверка SESSION_KILL — трафик идёт через LB, случайный сервер убивается, пользователь не теряет state.
  • Метрика: время восстановления сессии после падения сервера < 1 секунды.

Провокация №4: деплой = 5xx?

Если каждое обновление = штурм Slack, вы ещё не умеете deploy без downtime.

4.1 Rolling deployment сценарий

  1. Drain traffic: kubectl cordon + drain / nginx -s quit.
  2. Graceful shutdown: приложение закрывает входящие соединения, ждёт 30 секунд, завершает воркеры.
  3. Health-check gate: пока /readyz не возвращает 200 + «warmed=true», сервер не получает трафик.
  4. Логи: в Kibana/CloudWatch ищите 5xx во время деплоя. Допустимый всплеск — < 0.5% от общего трафика.

4.2 Blue/Green vs Rolling

  • Blue/Green: мгновенное переключение DNS/Ingress между версиями. Цена — двойное потребление ресурсов.
  • Rolling: дешевле, но требует идеального graceful shutdown. Не подходит для радикальных миграций (schema change).
  • Canary: 5–10% трафика идёт на новую версию, остальное — на старую. Сравните error rate, P99 latency и бизнес-KPI в течение 15–30 минут. Если новая версия не хуже по всем метрикам — расширяем. Требует маршрутизации и feature flag'ов, но ловит баги до массового инцидента.
    • Метрики: error_rate_new <= error_rate_old, P99_new <= P99_old * 1.1, бизнес-KPI без деградации, CPU/RAM в пределах бюджета.

4.3 Graceful shutdown для Node.js/Go/Java

  • Закройте входящие соединения (server.close() / http.Shutdown).
  • Дождитесь завершения активных запросов (таймаут 30–60 с).
  • Освободите ресурсы (БД, очереди).
  • Отдавайте /readyz = 503, чтобы LB перестал слать трафик.

Провокация №5: тестируете ли вы отказ load balancer’а?

  • Сценарий: выключите один LB-инстанс. Трафик должен автоматически перейти на резервный. В DNS — health check + failover. В Kubernetes — минимум два ingress controller’а.
  • Метрики:
    • failover < 30 секунд,
    • error rate < 2% во время переключения,
    • через 5 минут состояние «как было».

5. Экономика горизонтального масштабирования: считаем деньги

Важно: Архитектор, не считающий деньги, — угроза бизнесу. Каждое решение о масштабировании должно иметь экономическое обоснование.

5.1 Стоимость scale-out: 1 сервер vs кластер

Сценарий: API сервис на Node.js, 500 RPS, требует 4 vCPU и 8GB RAM.

Вариант A: Один мощный сервер (вертикальное масштабирование)

AWS EC2 (us-east-1, on-demand):

  • Instance: c6i.2xlarge (8 vCPU, 16GB)
  • Стоимость: $0.34/час = $245/месяц
  • Headroom: 100% (можем вырасти до 1000 RPS)

Плюсы:

  • ✅ Простота (один сервер)
  • ✅ Нет затрат на load balancer
  • ✅ Минимальная операционная сложность

Минусы:

  • 🔴 Single point of failure
  • 🔴 Zero-downtime деплой невозможен
  • 🔴 Нет отказоустойчивости

Вариант B: Кластер из 3 серверов (горизонтальное масштабирование)

Компоненты:

  1. Load Balancer (ALB):

    • $16.20/месяц (фиксированная часть)
    • $0.008/LCU-час (переменная часть)
    • ~$25/месяц при умеренной нагрузке
  2. 3× API серверы:

    • Instance: t3.large (2 vCPU, 8GB) каждый
    • Стоимость: $0.0832/час × 3 = $180/месяц

Итого: $205/месяц (LB + серверы)

Плюсы:

  • ✅ High availability (падение 1 сервера = 66% capacity)
  • ✅ Zero-downtime деплой (rolling update)
  • ✅ Готовность к росту (легко добавить 4й, 5й сервер)

Минусы:

  • 🟡 Операционная сложность (3 сервера вместо 1)
  • 🟡 Stateless architecture обязательна

Сравнение стоимости

МетрикаВариант A (1 сервер)Вариант B (3 сервера)Дельта
Стоимость/месяц$245$205-$40 (дешевле!)
Стоимость/год$2,940$2,460-$480
Availability99% (single instance)99.95% (multi-AZ)+0.95%
RTO при падении5-10 минут (restart)0 секунд (авто failover)
Операционная сложностьНизкаяСредняя

Вывод: Горизонтальное масштабирование не только надежнее, но и дешевле при правильном выборе инстансов!


5.2 Break-even point: когда scale-out становится выгоднее

Вопрос: При каком RPS горизонтальное масштабирование начинает окупаться?

Расчет:

Cost_vertical = instance_cost (растет ступенчато при апгрейде)
Cost_horizontal = LB_cost + (num_servers × server_cost)
 
Break-even: когда Cost_horizontal < Cost_vertical

Пример (Node.js API):

RPSВертикальное (1 сервер)Горизонтальное (N серверов)Выгоднее
200t3.medium ($36/мес)LB + 2×t3.small ($50/мес)⬅️ Вертикальное
500t3.xlarge ($120/мес)LB + 2×t3.medium ($95/мес)➡️ Горизонтальное
1000c6i.2xlarge ($245/мес)LB + 3×t3.large ($205/мес)➡️ Горизонтальное
2000c6i.4xlarge ($490/мес)LB + 5×t3.large ($330/мес)➡️ Горизонтальное

Вывод: После 500 RPS горизонтальное масштабирование почти всегда дешевле.


5.3 Reserved Instances vs On-Demand: экономия до 72%

Сценарий: Вы знаете, что кластер будет работать минимум год.

Стратегия экономии:

ТипСтоимость t3.largeЭкономия vs On-DemandCommitment
On-Demand$0.0832/час ($60/мес)0%Нет
Reserved 1yr (no upfront)$0.053/час ($38/мес)-37%1 год
Reserved 1yr (all upfront)$0.048/час ($35/мес)-42%1 год + $420 сразу
Savings Plans 1yr$0.050/час ($36/мес)-40%1 год, flexible
Spot Instances$0.025/час ($18/мес)-70%Могут отозвать

Рекомендация для production кластера из 3×t3.large:

Baseline capacity (всегда нужны): 2 сервера
→ Reserved Instances: 2×$35/мес = $70/мес
 
Burst capacity (иногда нужны): 1-3 сервера
→ On-Demand или Spot: 1×$60/мес = $60/мес
 
Итого: $130/мес вместо $180/мес
Экономия: $50/мес = $600/год

5.4 Cloud vs Self-hosted: TCO сравнение

Сценарий: 5 серверов API (5×t3.large = 10 vCPU, 40GB RAM total)

Вариант A: AWS (managed cloud)

Компоненты:

  • 5× t3.large (Reserved 1yr): $175/мес
  • ALB: $25/мес
  • Data transfer out (500GB): $45/мес
  • EBS volumes (5×100GB gp3): $40/мес
  • CloudWatch logs: $10/мес
  • Итого: $295/месяц

Плюсы:

  • ✅ Zero DevOps для инфраструктуры
  • ✅ Автоматические бэкапы, патчи
  • ✅ Global availability (multi-region легко)

Минусы:

  • 🔴 Высокая стоимость data transfer
  • 🔴 Vendor lock-in

Вариант B: Self-hosted (bare metal или Hetzner)

Компоненты:

  • 2× Dedicated servers (16 vCPU, 64GB RAM каждый): €89/мес × 2 = $190/мес
  • Nginx LB (на тех же серверах): $0
  • Bandwidth (20TB included): $0
  • Backup storage (1TB): $10/мес
  • Итого серверы: $200/месяц

DevOps costs (скрытые):

  • Зарплата DevOps инженера (1 FTE): $8,000/мес
  • Амортизация времени на поддержку: ~10% времени = $800/мес
  • Итого с DevOps: $1,000/месяц

Плюсы:

  • ✅ Низкая стоимость железа
  • ✅ Нет ограничений по bandwidth
  • ✅ Full control

Минусы:

  • 🔴 Операционная сложность
  • 🔴 Нужна команда DevOps
  • 🔴 Медленное масштабирование (заказ сервера = дни)

TCO сравнение (3 года)

МетрикаAWSSelf-hosted
Инфраструктура (36 мес)$10,620$7,200
DevOps (36 мес)$0 (managed)$28,800
Setup time1 час40 часов
Downtime costs (estimated)$500 (99.95%)$2,000 (99.5%)
Total (3 года)$11,120$38,000

Вывод: AWS выгоднее для команд < 50 инженеров. Self-hosted — только при большом масштабе (100+ серверов) или специфичных требованиях.


5.5 Auto-scaling: экономия 40-60% в off-peak

Проблема: В ночное время (00:00-06:00) RPS падает на 70%, но вы платите за все серверы.

Решение: Auto-scaling по CPU или RPS.

Пример конфигурации (AWS Auto Scaling):

min_capacity: 2 # Всегда минимум 2 для HA
max_capacity: 10 # Максимум при пиковой нагрузке
target_cpu: 60% # Поддерживаем CPU около 60%
 
scale_out:
  metric: CPUUtilization > 70%
  cooldown: 300s # Подождать 5 минут перед добавлением еще одного
 
scale_in:
  metric: CPUUtilization < 40%
  cooldown: 600s # Подождать 10 минут перед удалением

Экономия (реальный кейс):

Время сутокRPSБез auto-scalingС auto-scalingЭкономия
00:00-06:00 (6ч)2005 серверов2 сервера60%
06:00-18:00 (12ч)8005 серверов4 сервера20%
18:00-22:00 (4ч)15005 серверов7 серверов-40% (переплата)
22:00-00:00 (2ч)5005 серверов3 сервера40%

Средняя экономия за сутки: ~30%

Стоимость:

  • Без auto-scaling: 5 серверов × 24 часа = 120 server-hours/день
  • С auto-scaling: ~84 server-hours/день (оценка)
  • Экономия: $60/мес → $42/мес = $18/мес (30%)

5.6 Cost allocation: кто платит за инфраструктуру?

Проблема: Несколько команд используют один кластер. Как распределить costs?

Решение: Tagging + cost allocation.

Пример (AWS Cost Allocation Tags):

# Каждый сервер/LB должен иметь теги:
Environment: production
Team: payments
Product: checkout-api
CostCenter: engineering-backend

Отчет по costs (пример месяца):

TeamServersALBTotal% от общего
payments3×t3.large1 ALB$10535%
orders2×t3.large1 ALB$8528%
users2×t3.mediumshared ALB$6020%
shared (monitoring)1×t3.small$5017%
Total$300100%

Польза:

  • ✅ Прозрачность расходов
  • ✅ Стимул оптимизировать (команды видят свои costs)
  • ✅ Chargeback модель (можно списывать с бюджета команд)

5.7 Cost optimization checklist

Перед масштабированием задайте эти вопросы:

  • Нужно ли вообще масштабировать? Может быть, проблема в медленном запросе?
  • Вертикальное vs горизонтальное? Посчитайте break-even point.
  • Reserved Instances? Если capacity стабильна, экономьте 40%.
  • Auto-scaling? Если нагрузка предсказуема, экономьте 30%.
  • Spot Instances? Для stateless workloads — экономия 70%.
  • Right-sizing? Проверьте CPU/Memory utilization (может быть, переплачиваете).
  • Data transfer costs? Используйте CloudFront/CDN для статики.
  • Cross-AZ traffic? $0.01/GB между AZ — может быть дорого.

5.8 Реальный кейс: оптимизация $10k/мес → $3k/мес

Компания: E-commerce стартап, 2,000 RPS пик.

Было (до оптимизации):

  • 10× c5.xlarge (on-demand): $1,700/мес
  • 3× ALB: $75/мес
  • Data transfer: $500/мес
  • Logs/metrics: $200/мес
  • Total: $2,475/мес (только API, без БД)

Проблемы:

  • ❌ Over-provisioned (CPU utilization 20-30%)
  • ❌ Все on-demand
  • ❌ Нет auto-scaling
  • ❌ Статика через API (data transfer)

Что сделали:

  1. Right-sizing: c5.xlarge → t3.large (достаточно CPU)
  2. Reserved Instances: 5 серверов baseline (1yr)
  3. Auto-scaling: 5-10 серверов в зависимости от нагрузки
  4. CDN для статики: CloudFront + S3
  5. Consolidated ALBs: 3 → 1 (path-based routing)

Стало:

  • 5× t3.large (Reserved): $175/мес
  • 0-5× t3.large (On-Demand, auto-scale): ~$150/мес avg
  • 1× ALB: $25/мес
  • Data transfer (90% через CDN): $50/мес
  • CloudFront: $30/мес
  • Logs/metrics (sampling): $50/мес
  • Total: $480/мес

Экономия: $2,475 → $480 = -$1,995/мес (-81%) ROI: Работа заняла 1 неделю (стоимость ~$2,000). Окупилось за 1 месяц.


6. Практика: без этого нет смысла идти дальше

  1. Stateless check:
    • Шаг 1: раз в день в off-peak убивайте один сервер и проверяйте UX.
    • Шаг 2: когда метрики стабильны, повышайте частоту до 1 раза в 4 часа.
    • Шаг 3: продвинутая стадия — каждые 30 минут, включая пиковые часы. Критерий: пользователи не теряют state.
  2. Load balancer drill:
    • Прогоните wrk/k6 со сценарием long-polling и short requests.
    • Найдите, какой алгоритм балансировки даёт минимальный latency для вашего трафика.
  3. Session store migration:
    • Перенесите хотя бы один тип state (например, корзину) в Redis/JWT.
    • Измерьте разницу в error rate при падении сервера.
  4. Zero-downtime rehearsal:
    • На staging выполните rolling деплой по шагам из раздела 4.1.
    • Снимите метрики до/после, докажите, что 5xx нет.
  5. Chaos LB/AZ:
    • Отключите одну Availability Zone / LB-инстанс.
    • Метрики успеха: failover < 2 минут, error rate < 5%, ручное вмешательство = 0.
  6. FinOps анализ (НОВОЕ):
    • Посчитайте стоимость вашего текущего кластера (или придуманного сценария)
    • Сравните: on-demand vs reserved instances vs spot
    • Найдите break-even point для вашей нагрузки
    • Предложите план экономии на 30%

Безопасность: Все эксперименты выполняйте в изолированном окружении (staging/kлон прода). Ни один тест не должен бить по продуктивным БД, брокерам и внешним API. Ограничьте бюджет в облаке, чтобы runaway-скрипт не сжёг деньги.

Что дальше

Урок 03 — вертикальное масштабирование и оптимизация кода. После того как трафик научился бегать по нескольким серверам, нужно заставить каждый сервер работать на максимум: профилирование, cluster-mode, pool'ы БД. Не прыгайте туда, пока не выполнили практику отсюда.