Горизонтальное масштабирование как операция на открытом сердце
Для кого: инженеры, отвечающие за аптайм и трафик. Если в вашей архитектуре до сих пор есть "sticky sessions", а деплой = молитва — эта глава должна выбить почву из-под ног и заставить переделать основу.
Провокация №1: ваши пользователи привязаны к конкретному серверу?
-
Sticky sessions = долговая яма. Как только state хранится в памяти процесса, вы не сможете балансировать нагрузку честно. Один сервер перегорит, остальные будут скучать.
-
Проверьте себя: зайдите в конфиг load balancer'а. Если там
ip_hash,cookieили «session affinity» — вы до сих пор в 2008 году. -
Переходный план (если вы зарыты в legacy):
Неделя Действие Метрика успеха 1 Dual-write сессий: и в память, и в Redis 100% записей дублируются 2 10% трафика без sticky Error rate < 0.1% 3 50% трафика без sticky P95 latency не вырос 4 100% без sticky, удаляем affinity Сессии переживают убийство сервера -
Цель: любое приложение должно быть stateless, а state — вынесен в управляемые компоненты: Redis, БД, message bus.
1. Statless против Stateful: разбираем по косточкам
1.1 Тест на stateless
- Убейте произвольный сервер приложения.
- Запросите
/api/profileс того же клиента. - Если пользователь вылетел из сессии — вы 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 Connections | Long-polling, WebSockets | Работает только если серверы корректно закрывают соединения |
| IP Hash / Sticky | Только если state вынесен не полностью | Нагрузка может неравномерно распределяться |
| Weighted RR | Разные типы железа (t3.small vs c6i.2xlarge) | Требует пересчёта весов при каждом апгрейде |
| Least Response Time | Nginx Plus/Envoy в продвинутых сценариях | Нужна лицензия / дополнительная телеметрия |
| Power of Two Choices | Envoy/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 Стратегии
- Redis + TTL + репликация: используйте Sentinel/Cluster. Ключ
session:{id}→ JSON. TTL продлевайте при каждом обращении. - JWT: минимальный state. Включите ротацию ключей, короткий TTL, refresh-токены.
- Opaque tokens + storage: хэш токена + user data в БД/Redis.
- 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 сценарий
- Drain traffic:
kubectl cordon + drain/nginx -s quit. - Graceful shutdown: приложение закрывает входящие соединения, ждёт 30 секунд, завершает воркеры.
- Health-check gate: пока
/readyzне возвращает 200 + «warmed=true», сервер не получает трафик. - Логи: в 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 серверов (горизонтальное масштабирование)
Компоненты:
-
Load Balancer (ALB):
- $16.20/месяц (фиксированная часть)
- $0.008/LCU-час (переменная часть)
- ~$25/месяц при умеренной нагрузке
-
3× API серверы:
- Instance:
t3.large(2 vCPU, 8GB) каждый - Стоимость: $0.0832/час × 3 = $180/месяц
- Instance:
Итого: $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 |
| Availability | 99% (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 серверов) | Выгоднее |
|---|---|---|---|
| 200 | t3.medium ($36/мес) | LB + 2×t3.small ($50/мес) | ⬅️ Вертикальное |
| 500 | t3.xlarge ($120/мес) | LB + 2×t3.medium ($95/мес) | ➡️ Горизонтальное |
| 1000 | c6i.2xlarge ($245/мес) | LB + 3×t3.large ($205/мес) | ➡️ Горизонтальное |
| 2000 | c6i.4xlarge ($490/мес) | LB + 5×t3.large ($330/мес) | ➡️ Горизонтальное |
Вывод: После 500 RPS горизонтальное масштабирование почти всегда дешевле.
5.3 Reserved Instances vs On-Demand: экономия до 72%
Сценарий: Вы знаете, что кластер будет работать минимум год.
Стратегия экономии:
| Тип | Стоимость t3.large | Экономия vs On-Demand | Commitment |
|---|---|---|---|
| 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 года)
| Метрика | AWS | Self-hosted |
|---|---|---|
| Инфраструктура (36 мес) | $10,620 | $7,200 |
| DevOps (36 мес) | $0 (managed) | $28,800 |
| Setup time | 1 час | 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ч) | 200 | 5 серверов | 2 сервера | 60% |
| 06:00-18:00 (12ч) | 800 | 5 серверов | 4 сервера | 20% |
| 18:00-22:00 (4ч) | 1500 | 5 серверов | 7 серверов | -40% (переплата) |
| 22:00-00:00 (2ч) | 500 | 5 серверов | 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 (пример месяца):
| Team | Servers | ALB | Total | % от общего |
|---|---|---|---|---|
| payments | 3×t3.large | 1 ALB | $105 | 35% |
| orders | 2×t3.large | 1 ALB | $85 | 28% |
| users | 2×t3.medium | shared ALB | $60 | 20% |
| shared (monitoring) | 1×t3.small | — | $50 | 17% |
| Total | $300 | 100% |
Польза:
- ✅ Прозрачность расходов
- ✅ Стимул оптимизировать (команды видят свои 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)
Что сделали:
- Right-sizing: c5.xlarge → t3.large (достаточно CPU)
- Reserved Instances: 5 серверов baseline (1yr)
- Auto-scaling: 5-10 серверов в зависимости от нагрузки
- CDN для статики: CloudFront + S3
- 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. Практика: без этого нет смысла идти дальше
- Stateless check:
- Шаг 1: раз в день в off-peak убивайте один сервер и проверяйте UX.
- Шаг 2: когда метрики стабильны, повышайте частоту до 1 раза в 4 часа.
- Шаг 3: продвинутая стадия — каждые 30 минут, включая пиковые часы. Критерий: пользователи не теряют state.
- Load balancer drill:
- Прогоните
wrk/k6со сценарием long-polling и short requests. - Найдите, какой алгоритм балансировки даёт минимальный latency для вашего трафика.
- Прогоните
- Session store migration:
- Перенесите хотя бы один тип state (например, корзину) в Redis/JWT.
- Измерьте разницу в error rate при падении сервера.
- Zero-downtime rehearsal:
- На staging выполните rolling деплой по шагам из раздела 4.1.
- Снимите метрики до/после, докажите, что 5xx нет.
- Chaos LB/AZ:
- Отключите одну Availability Zone / LB-инстанс.
- Метрики успеха: failover < 2 минут, error rate < 5%, ручное вмешательство = 0.
- FinOps анализ (НОВОЕ):
- Посчитайте стоимость вашего текущего кластера (или придуманного сценария)
- Сравните: on-demand vs reserved instances vs spot
- Найдите break-even point для вашей нагрузки
- Предложите план экономии на 30%
Безопасность: Все эксперименты выполняйте в изолированном окружении (staging/kлон прода). Ни один тест не должен бить по продуктивным БД, брокерам и внешним API. Ограничьте бюджет в облаке, чтобы runaway-скрипт не сжёг деньги.
Что дальше
Урок 03 — вертикальное масштабирование и оптимизация кода. После того как трафик научился бегать по нескольким серверам, нужно заставить каждый сервер работать на максимум: профилирование, cluster-mode, pool'ы БД. Не прыгайте туда, пока не выполнили практику отсюда.