Часть V: Высоконагруженные системы
Глава 19

Масштабирование — вертикальное, горизонтальное и за гранью

Как расти, не ломая систему каждую неделю.

Представьте, что вы открыли интернет-магазин. Сегодня у вас 100 посетителей в день. Всё летает. Через месяц — 10 000. Сервер начинает задыхаться. Через год — миллион.

И вот вопрос: что вы делаете в этот понедельник?

Вариантов, на самом деле, не так много. Можно купить сервер побольше. Можно поставить рядом ещё один и как-то делить между ними нагрузку. А можно сначала подумать: «А не жрём ли мы ресурсы впустую?»

Эта глава — про то, как расти. И про то, как не разориться на этом росте.


#19.1 Вертикальное масштабирование (scale up): big box, big money

Вертикальное масштабирование (vertical scaling, scale up) — это самый интуитивный подход. У вас тормозит сервер? Купите сервер мощнее. Добавьте RAM, поставьте процессор с большим количеством ядер, перейдите на NVMe-диски.

Когда это работает:

  • Нагрузка упирается в конкретный ресурс — памяти не хватает, CPU уходит в 100%
  • У вас монолит, и переписывать его под распределённую архитектуру — полгода работы
  • Трафик растёт предсказуемо, без резких скачков

Цена вопроса:

Современные «железные монстры» выглядят внушительно. Сервер класса AWS m7i.metal-48xl — 192 vCPU, 768 GB RAM. Стоимость — около $7,000 в месяц в он-деманд (on-demand). Dedicated-инстансы вроде Oracle Exadata могут стоить сотни тысяч долларов в год.

И вот в чём подвох: производительность не масштабируется линейно с ценой.

Ресурс Удвоение стоимости Прирост производительности
CPU 2x ~1.8x (закон Амдала)
RAM 2x 2x (если упирались в память)
Диск (IOPS) 2x ~1.5x (упирается в шину)

Закон Амдала (Amdahl's Law) — ваш главный враг при вертикальном масштабировании. Если 10% вашего кода строго последовательны, то даже бесконечное количество ядер даст вам максимум 10-кратный прирост. А на практике — последовательная часть почти всегда больше 10%.


#19.1.1 Когда scale up — правильный выбор

Не спишитесь на «горизонтальное масштабирование — это единственно правильно». Есть сценарии, где вертикальное не просто ок, а необходимо:

  • Базы данных (особенно SQL) — шардирование (sharding) сложно, больно и дорого. Для многих проектов проще купить сервер с 64 GB RAM и держать всю рабочую БД в памяти. Подробнее — в главе 20.
  • In-memory вычисления — Redis, Memcached, ClickHouse. Пока данные влезают в один сервер — не усложняйте.
  • Legacy-системы — переписывать 15-летнего монстра на микросервисы никто не даст вам бюджет. А докупить RAM — дадут.
  • Low-latency трейдинг / gaming — каждая микросекунда на сетевой round-trip между серверами съедает прибыль.

Но есть потолок. И упираются в него быстро.


#19.1.2 Потолок: почему нельзя расти вечно

Самый большой виртуальный сервер у AWS (на момент написания) — u-24tb1.metal с 24 TB RAM. Да, не опечатка — терабайт. Но что дальше? В природе не существует машины с 100 TB RAM в одном адресном пространстве.

А если бы и существовала — одно переключение DNS, одна ошибка в конфиге, и ваша супер-машина лежит. Вместе со всем сервисом. Single Point of Failure (SPOF) в чистом виде.

Так что рано или поздно scale up упирается либо в физический предел «железа» (hardware ceiling), либо в неприемлемую стоимость. И тогда вы смотрите в сторону горизонтального масштабирования.


#19.2 Горизонтальное масштабирование (scale out): statelessness как необходимость

Горизонтальное масштабирование (horizontal scaling, scale out) — это когда вместо одного мощного сервера вы ставите N одинаковых «дешёвых». И распределяете нагрузку между ними через балансировщик.

Звучит просто. Спойлер: дьявол в деталях.

#19.2.1 Главное условие — statelessness

Если ваше приложение хранит состояние (state) локально — сессии пользователей, загруженные файлы, кеш в памяти процесса — вы не сможете его горизонтально масштабировать. Потому что запрос от одного и того же пользователя может прийти на разные инстансы, и второй инстанс ничего не будет знать о состоянии, созданном в первом.

Классическая проблема (и я тоже наступал на эти грабли):

# Плохо: состояние в памяти процесса
session_store = {}

@app.route("/login")
def login():
    user = authenticate(request)
    session_store[request.remote_addr] = user.id  # упс!
    return "ok"

@app.route("/profile")
def profile():
    user_id = session_store.get(request.remote_addr)  # может не быть!
    ...

Как только вы поставите второй экземпляр приложения, половина пользователей будет получать «перенаправьте на логин» при каждом втором запросе.

Решение: вынести состояние наружу.

  • Сессии: Redis / Memcached (shared session store)
  • Загруженные файлы: S3 / MinIO / NFS
  • Очереди: Kafka / RabbitMQ / SQS
  • Кеш: Redis / CDN (глава 21)

Да, это добавляет latency (глава 22) на каждый запрос — вместо чтения из локальной памяти вы читаете по сети. Но это единственный способ иметь больше одного экземпляра.

Практическое правило

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


#19.2.2 Балансировка: round-robin, sticky sessions и их проклятие

Когда у вас N инстансов, нужен балансировщик (load balancer), который решает, какой запрос на какой сервер отправить.

Варианты:

Round-robin — запросы распределяются по кругу. Просто, работает, не требует состояния. Но не учитывает нагрузку на каждый инстанс.

Least connections — отправляем на сервер с наименьшим количеством активных соединений. Лучше, чем round-robin, но для коротких запросов разница минимальна.

Sticky sessions (session affinity) — закрепляем пользователя за конкретным сервером. Это костыль, а не решение. Если сервер упадёт — пользователь потеряет сессию. Если вы добавите новый сервер — нужно перераспределение. Sticky sessions — признак того, что вы что-то не до конца вынесли наружу.

Единственная адекватная причина использовать sticky sessions — WebSocket-соединения, где переподключение к другому серверу будет дороже, чем хранение сессии в Redis.

Кстати

Кстати: AWS ALB, NGINX, HAProxy, Envoy — все умеют sticky sessions. Вопрос не в технологии, а в архитектурном решении.


#19.2.3 Statelessness = эластичность

Когда ваше приложение stateless, вы получаете суперспособность: эластичность (elasticity). Вы можете в 9:00 утра иметь 3 инстанса, в 12:00 — 30, а в 23:00 — снова 3. Без перезагрузки, без потери данных, без ночных смен.

AWS Auto Scaling, Google Cloud Managed Instance Groups, Kubernetes Horizontal Pod Autoscaler — все они требуют одного и того же: приложение, которому всё равно, сколько его копий бежит.


#19.3 Auto-scaling: HPA, KEDA, predictive scaling

Ручное масштабирование — это когда админ видит, что CPU 95%, и вручную добавляет сервер. В 2025-м году так уже не делают. Во-первых, потому что вы не успеете — нагрузка на typical веб-сервисе удваивается за минуты. Во-вторых, «сидеть и смотреть на графики» — это не та работа, за которую платят сеньорскую зарплату.

#19.3.1 Horizontal Pod Autoscaler (HPA)

В Kubernetes HPA — основной механизм автоскейлинга. Идея простая: вы говорите «хочу, чтобы CPU был в районе 50%», и HPA сам добавляет/убирает поды.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 70

Выглядит просто, но есть нюансы:

  • HPA не мгновенен. Он опрашивает metrics server раз в 15 секунд (по умолчанию), потом принимает решение, потом запускает под — тот должен подняться. От начала всплеска до готового пода может пройти 1-3 минуты. Это вечность для резких пиков.
  • Thrashing (маятник). Если нагрузка колеблется вокруг порога, HPA будет то добавлять, то убирать поды. Решение — --horizontal-pod-autoscaler-downscale-stabilization (дефолт 5 минут). Поды убираются не сразу, ждут стабилизации.
  • CPU не всегда хорошая метрика. Ваше приложение может упираться в I/O, соединения с БД или очередь запросов, а CPU будет 10%. HPA будет считать, что всё ок, пока база не ляжет.

Практический совет: используйте кастомные метрики (custom metrics) — RPS (requests per second), latency p99, длина очереди. HPA умеет с ними работать:

- type: Pods
  pods:
    metric:
      name: requests_per_second
    target:
      type: AverageValue
      averageValue: 1000

#19.3.2 KEDA — событийно-управляемый скейлинг

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

KEDA (Kubernetes Event-Driven Autoscaling) — это надстройка над HPA, которая умеет скейлить на основе событий:

  • Длина очереди Kafka/SQS/RabbitMQ
  • Количество запросов в БД
  • Cron-расписание
  • Prometheus-метрики

Пример: у вас worker, который обрабатывает сообщения из Kafka. Пока сообщений нет — держать под бессмысленно. Как только в очереди появляются сообщения — KEDA поднимает столько воркеров, сколько нужно.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-worker
spec:
  scaleTargetRef:
    name: worker-deployment
  triggers:
  - type: kafka
    metadata:
      topic: orders
      bootstrapServers: kafka-cluster:9092
      lagThreshold: "10"
      consumerGroup: workers

KEDA транслирует это в HPA-метрику, и HPA делает своё дело. Разница: HPA реагирует на состояние, KEDA — на события.

#19.3.3 Predictive scaling

Следующий уровень — когда автоскейлинг смотрит в будущее, а не в прошлое.

Cloud providers:

  • AWS Predictive Scaling — анализирует историю трафика за последние 14 дней, строит модель и заранее (!) добавляет ресурсы под ожидаемый пик.
  • Google Cloud Scheduler + MIG — можно настроить скейлинг по расписанию (каждый будний день в 9:00 +5 подов).

Для собственных кластеров: Prophet (Facebook), или даже линейная регрессия на исторических данных вашего мониторинга. Cron-скейлинг — самая простая форма предиктивного масштабирования: «каждый день в 20:00 у нас в 2 раза больше пользователей, добавляем 10 подов за 30 минут до».

     Трафик
        ↑
        |     ┌──────────┐
        |    ┌┤          ├──────────
        |   ┌┤┘          │
        |  ┌┤┘           │
        | ┌┤┘            │
        ├─┤│←──predictive→│
        | ││              │
        | │└──────────────┘
        └────────────────────────→ Время
               ↑
         HPA сработает здесь

Золотая середина: комбинируйте подходы.

  1. Predictive scaling (или cron) — пред-подготовка (provisioning) под известные паттерны.
  2. HPA — реакция на неожиданные всплески.
  3. KEDA — скейлинг под event-driven нагрузку.

#19.4 Connection management — pooling, multiplexing

Когда у вас 3 инстанса, проблема соединений не заметна. Когда 300 — она становится главной головной болью.

#19.4.1 Connection pooling

Каждое соединение к базе данных — это ресурс. Открытие TCP-соединения — миллисекунды. SSL handshake — ещё десятки миллисекунд. Установка нового соединения на каждый HTTP-запрос — путь к тому, что база упадёт под 5000 соединений, хотя реально обрабатывается 50 запросов в секунду.

Решение: пул соединений (connection pool). Приложение держит N открытых соединений к БД и переиспользует их.

# Хорошо: connection pool
from sqlalchemy import create_engine

engine = create_engine(
    "postgresql://...",
    pool_size=20,          # максимум 20 соединений
    max_overflow=10,       # +10 при пике, потом закроются
    pool_timeout=30,       # ждать соединения 30 сек
    pool_pre_ping=True,    # проверять, что соединение живо
)

Типичные проблемы с пулами:

  • Pool starvation: 50 инстансов × 20 соединений = 1000 соединений к БД. А база держит максимум 500. Настройте пул адекватно числу инстансов.
  • Leaked connections: забыли закрыть соединение — оно утекло. Через час пул пуст, база жива, но приложение не работает.
  • Stale connections: если соединение долго простаивает, прокси (или firewall) может его закрыть. pool_pre_ping=True решает эту проблему.
Число соединений ≠ производительность.

Число соединений ≠ производительность. Постгрес (PostgreSQL) на одном соединении может легко делать 1000 простых запросов в секунду. 100 соединений с 1000 запросов каждое — это не 100 000 RPS, а блокировки на строках и IO. Подробнее — глава 20.

#19.4.2 Multiplexing — одно соединение для всего

HTTP/1.1 — одно соединение = один запрос. Чтобы сделать 10 запросов — нужно 10 соединений (или ждать ответа на каждый).

HTTP/2 и gRPC (основанный на HTTP/2) решают это через мультиплексирование (multiplexing): одно TCP-соединение, и по нему параллельно идут несколько потоков (streams).

HTTP/1.1:               HTTP/2:
┌─────────┐            ┌─────────┐
│ req #1  │            │ req #1  │
│         │            │ req #2  │
│         │            │ req #3  │
└─────────┘            │ req #4  │
┌─────────┐            └─────────┘
│ req #2  │            (один TCP туннель)
│         │
│         │
└─────────┘
(три TCP соединения)

gRPC идёт дальше — одно долгоживущее соединение (persistent connection) может обслуживать тысячи запросов в секунду. Никакого overhead на handshake.

Когда это критично:

  • Microservice-to-microservice коммуникация (service mesh — Envoy, Linkerd — все строятся на этих принципах)
  • Streaming данных (реалтайм обновления, логи)
  • High-frequency trading / real-time bidding

#19.4.3 Connection management на уровне базы: PgBouncer, RDS Proxy

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

Решение: connection proxy.

  • PgBouncer — лёгкий прокси для PostgreSQL. Держит пул соединений к базе и раздаёт их приложениям. Может работать в режимах: session (настоящее соединение), transaction (соединение только в рамках транзакции), statement (на каждый запрос). Транзакционный режим — золотая середина для веб-приложений.
  • RDS Proxy (AWS) — управляемый прокси для RDS. Умеет скейлиться, аутентификацию через IAM, защиту от резких пиков соединений.
  • ProxySQL — то же самое для MySQL.

Разница в цифрах: PostgreSQL без прокси держит ~300-500 соединений на типичной машине. С PgBouncer — 10 000+ приложений могут коннектиться к прокси, а он держит к базе 50 соединений.


#19.5 Альтернатива масштабированию: оптимизация

Прежде чем платить за 50 серверов, спросите себя: «А всё ли я сделал, чтобы не платить?»

Банальная истина, которую я видел нарушенной в каждом третьем проекте: оптимизация всегда дешевле масштабирования.

Вот иерархия «что сделать до того, как покупать железо»:

#19.5.1 Кеширование (глава 21)

Самый дешёвый способ увеличить пропускную способность (throughput). Если 80% запросов — это чтение одного и того же, кеш (cache) даст вам десятикратное улучшение без единого нового сервера.

Быстрый пример:

  • Без кеша: запрос к API → запрос к БД (50ms) → ответ
  • С кешем: запрос к API → проверка Redis (1ms) → если есть, сразу ответ

Средний RPS сервера без кеша — 100. С кешем — 1000. «Но» — только для read-heavy сценариев. Подробное погружение — в главе 21.

#19.5.2 CDN (глава 23)

Отдавать статику с сервера приложения — это как возить песок на Камазе, когда можно поставить конвейер. Content Delivery Network (CDN) — это ваш конвейер.

Почему это важно:

  • Статика (CSS, JS, изображения) составляет 60-80% веса страницы
  • CDN кладёт её на граничные сервера (edge), географически ближе к пользователю
  • Запросы к статике даже не доходят до вашего сервера

Средний сайт, подключивший CDN, снижает нагрузку на origin-сервер на 70-85%. Глава 23 — про CDN и edge-вычисления.

#19.5.3 Сжатие (compression)

Это не про gzip (хотя и про него тоже).

  • Текст: gzip / brotli — уменьшают размер ответа в 5-10 раз. Brotli на 20% эффективнее gzip.
  • Изображения: WebP / AVIF — дают 30-50% экономии по сравнению с JPEG/PNG.
  • API: Protocol Buffers, MessagePack, CBOR вместо JSON — уменьшают размер payload в 3-10 раз.

Brotli удивительно недооценён. Включите его на уровне NGINX/Envoy — и 80% пользователей (все современные браузеры) получат ответы, сжатые на 20% эффективнее gzip. Бесплатно.

#19.5.4 Оптимизация запросов (N+1, индекс, пагинация)

Самое частое, что я вижу, когда меня зовут разбираться с производительностью — N+1 запрос в ORM.

# N+1 — 1001 запрос к БД
for user in User.query.all():  # 1 запрос
    print(user.profile.name)  # N запросов (1000)
# Решение — eager loading
for user in User.query.join(User.profile).all():  # 1 запрос
    print(user.profile.name)

Иногда рефакторинг одного запроса даёт больше, чем добавление 10 серверов.


#19.5.5 Decision Matrix: когда что выбирать

Сценарий Scale up Scale out Оптимизация
База данных упирается в CPU +- ++ (шардирование) + (индексы, кеш)
API серверы — высокая latency ++ ++ + (кеширование)
Легаси, нельзя менять архитектуру ++ — +-
Сезонные пики (Чёрная пятница) — ++ +-
In-memory кеш (Redis) ++ +- (кластеризация) +
Фронтенд (отдача статики) — ++ (CDN) ++ (сжатие)
Worker (фоновые задачи) — ++ (очередь + scale out) +
Реалтайм (WebSocket) + ++ +-

Правильная стратегия в 2025-м году выглядит так:

  1. Оптимизация — сначала выжать максимум из того, что есть
  2. Vertical scaling — до тех пор, пока стоимость не становится абсурдной
  3. Horizontal scaling — когда упёрлись в потолок
  4. Auto-scaling — сделать шаги 2-3 автоматическими

И никогда — никогда! — не переходите к шагу 3, не сделав шаг 1.


#19.6 Кейс: Discord — от 1M до 100M пользователей

Discord — один из самых впечатляющих примеров правильного (и местами неправильного) масштабирования. Они прошли путь от игрового чата для гиков до платформы с сотнями миллионов пользователей, и документально описали каждый шаг.

#19.6.1 Начало: один монолит на Python

Когда Discord запускался (2015), это был один Python-сервис (на библиотеке gevent). Один сервер. Одна база. Всё работало быстро — меньше 1000 пользователей.

#19.6.2 Первая проблема: MongoDB и кастомный шардинг

Discord начал с MongoDB. К 2016 году данных стало так много, что одна инстанция MongoDB не справлялась — документы в коллекции messages весили гигабайты.

Они сделали кастомный шардинг: серверное решение определяло, на каком шарде (shard) лежат сообщения конкретного канала, и направляло запрос туда.

Но: кастомный шардинг — это всегда ад. Они описывают это так: "We wrote our own sharding. Don't do that." Каждое добавление шарда требовало ручного вмешательства, миграции данных, простоев (downtime).

#19.6.3 Переход на Cassandra

2017 год. Discord переезжает с MongoDB на Apache Cassandra для сообщений. Почему Cassandra?

  • Мастер-мастер репликация (no single point of failure)
  • Линейная масштабируемость (добавил узел — получил + производительность)
  • Оптимизирована под write-heavy нагрузку (в Discord сообщения пишутся гораздо чаще, чем читаются)

Их метрики (из официального блога):

  • До: 1 MongoDB кластер, latency ~35ms, регулярные простои под нагрузкой
  • После: 12 Cassandra узлов, latency ~5ms, zero downtime при добавлении узлов

Урок: масштабирование БД — это не просто «поставить ещё одну». Это выбор правильной модели данных и правильной СУБД под ваш паттерн нагрузки. Подробнее — глава 20.

#19.6.4 Проблема с Rust и Go: Go Voice

Отдельная история — голосовой движок Discord. Он был написан на Python + C++ (libsodium для энкодинга аудио), но к 2017 году на один голосовой сервер (voice server) приходилось 100 000+ пользователей, и задержки (latency) начали расти.

Инженеры Discord переписали голосовой модуль на Rust и Go. Rust — для критичного к аудиопроцессинга (где каждая микросекунда на счету). Go — для управляющих сервисов (REST API голосовых серверов).

Результат: снижение latency в 3 раза на один воркер, при том же железе.

Урок: иногда масштабирование — это не «добавить серверов», а «сделать один сервер эффективнее». Вертикальная оптимизация как горизонтальное масштабирование.

#19.6.5 Архитектура к 2021 году (100M+ DAU)

К моменту, когда Discord перешагнул 100 миллионов активных пользователей в день (DAU), архитектура выглядела так:

  • API Gateway — Go (переписали с Python из-за производительности)
  • Сервис сообщений — Cassandra (12+ узлов, кластер в разных зонах доступности)
  • Сервис пользователей — Postgres (шардированная по user_id)
  • Голосовые сервера — Rust, запущенные рядом с пользователями (edge nodes)
  • Кеш — Redis (до 1.5M операций/сек на пике)
  • Очереди — RabbitMQ для асинхронных задач
  • Мониторинг — Prometheus + Grafana + собственные дашборды

Ключевое: они не стали переписывать всё сразу. Каждый проблемный сервис переписывался/переезжал по мере того, как он становился узким местом (bottleneck). Эволюция, а не революция.


#19.7 Кейс: Twitter — Fail Whale и как вышли из кризиса

История Twitter — это классический пример того, как «быстрый запуск» (move fast) оборачивается архитектурным долгом, который аукается годами.

#19.7.1 Рождение Fail Whale

2007 год. Twitter растёт как на дрожжах — 400% в год. У них Ruby on Rails монолит, одна база MySQL. Команда — 10 человек.

В 2007 году на SXSW трафик вырос в 3 раза за неделю. И Twitter начал падать. Регулярно. Каждую неделю.

И тогда появился Fail Whale — изображение кита, которого поднимают птички. Оно стало символом «Твиттер упал». В 2008-2009 Fail Whale показывали так часто, что это стало мемом:

  • 2008: 84 часа downtime за год
  • 2009: ~6-8 часов в месяц (улучшили, но не решили)
  • Downtime мог случиться в любой момент — часто кита видели в прайм-тайм

Корневая причина: монолит на Rails не умел масштабироваться. Одна БД MySQL — SPOF. Любая проблема с кешем (Memcached) роняла весь сервис.

#19.7.2 Первая попытка: «Руби — не виноват»

В 2008-2009 была популярна теория «Rails не тянет». Twitter даже начал эксперименты с Scala (которые потом вылились в новую стековую архитектуру). Но правда: Rails был не причиной, а катализатором. Проблема была в том, что вся логика была в монолите.

Они пытались: ставить более мощные сервера (scale up). Менять настройки MySQL. Тюнить кеш. Это помогало на месяц — потом нагрузка снова превышала capacity.

#19.7.3 Выход: сервис-ориентированная архитектура

2010-2011 год. Twitter начинает дробить монолит на сервисы.

Первое, что вынесли — Timeline service. Генерация ленты («Что нового») — самая тяжёлая операция. Раньше она выполнялась синхронно: Rails делал запрос к MySQL для отображения каждого твита в ленте.

Вместо этого они:

  1. Сделали сервис "Timeline" на Java (потом Scala) — он пред-рендерит ленту при загрузке твита
  2. Используют Redis для кеша Timeline (операция — O(1) append в список)
  3. API отдаёт готовую ленту без запросов к основной БД

Результат: latency чтения ленты упала с ~800ms до ~5ms. При этом количество серверов не изменилось.

Дальше вынесли:

  • Search (Earlybird — на Lucene, собственный поисковый движок)
  • Auth (отдельный сервис аутентификации)
  • Media (отдельный сервис для изображений и видео)

#19.7.4 Переход на Scala и Finagle

Twitter (вернее, команда сервисов) написали собственный RPC-фреймворк Finagle на Scala. Ключевая фишка — устойчивость к ошибкам:

  • Circuit Breaker — если сервис B не отвечает, Finagle не шлёт ему запросы, а сразу возвращает fallback
  • Load shedding — если сервис перегружен, он отбрасывает часть запросов, а не падает
  • Request hedging — Finagle может послать один запрос на несколько инстансов и взять первый ответ

Результат к 2013 году:

  • Fail Whale почти исчез (показывался раз в несколько месяцев, вместо нескольких раз в день)
  • Система выдерживала рост до 500M+ твитов в день
  • Downtime за 2013 год — менее 1 часа

#19.7.5 Fail Whale — что пошло не так (для нас)

Ошибки Twitter, которые мы можем не повторять:

  1. Масштабирование как единственная стратегия. Они 2 года пытались «добавить серверов» и тюнить конфиги, не меняя архитектуру.
  2. Отсутствие резервирования. Одна БД, одно приложение — одна точка отказа.
  3. Игнорирование queue-based architecture. Запись твита должна быть асинхронной (fault isolation). Тысячи людей пишут твиты, даже если генерация ленты временно отвалилась. [Anecdotal: я видел ту же ошибку в стартапе — синхронная генерация email-рассылки на каждый заказ «роняла» всю систему на Чёрную пятницу.]
  4. Отсутствие capacity planning. Они не знали, сколько трафика придет завтра, и реагировали постфактум.

Удивительно, что та же история повторяется снова и снова. Человечество изобрело Fail Whale в 2007. Но в 2025-м всё ещё есть сервисы, падающие под нагрузкой распродаж. Просто теперь кита заменили «502 Bad Gateway» и «Something went wrong».

Сравните с Discord — их подход «эволюция, не революция» и «решаем проблему, когда она становится bottleneck, а не когда роняет всю систему» — это то, что отличает зрелый инженерный подход.


#19.8 «Не делай так»: анти-паттерны масштабирования

#19.8.1 Scale up как единственная стратегия

Симптомы: каждый квартал выделяем бюджет на более мощные сервера. Графики CPU и RAM ползут вверх. Стоимость — ещё быстрее.

Почему это плохо: вы не решаете проблему архитектуры, вы откладываете её. Чем дольше вы тянете, тем больше технического долга накапливается. Переход на горизонтальную архитектуру через 3 года такого подхода будет стоить как переписать всё с нуля.

#19.8.2 Игнорирование оптимизации: «проще добавить сервер, чем чинить код»

Симптомы: серверов в 5 раз больше, чем нужно. 90% запросов можно кешировать, но кеша нет. CDN не подключён. Изображения отдаются в полном разрешении.

Почему это плохо: N+1 запрос в ORM лечится изменением одной строки кода. CDN настраивается за час. Brotli включается в NGINX конфиге. А вы платите за 50 серверов, когда хватило бы 10.

Моя любимая цитата из реальной code review: «Мы не можем добавить кеш, это компромисс с консистентностью». На что правильный ответ: «Вы согласны платить $50,000/мес за то, чтобы не думать о консистентности?»

#19.8.3 «Мы напишем свой балансировщик»

Симптомы: команда пишет собственный reverse proxy на Python/Node.js/Go. Потому что «NGINX слишком сложный» или «Kong не даёт нужной гибкости».

Почему это плохо: NGINX, HAProxy, Envoy, Traefik — на них держатся миллионы продакшн-систем. Они протестированы, профилированы, оптимизированы. Ваш балансировщик на Python будет жрать 20% CPU на проксирование трафика, пока вы будете писать фичу «правильный health check».

#19.8.4 Sticky sessions как архитектурное решение

Симптомы: «У нас sticky sessions, и мы добавляем новый сервер — надо подождать, пока пользователи перераспределятся».

Почему это плохо: sticky sessions означает, что ваше приложение хранит состояние там, где не должно. Вы потеряли главное преимущество scale out — эластичность. Любой сбой сервера означает потерю сессий его пользователей.

Решение: Redis / Memcached для сессий. Пара часов работы — и вы избавились от проблемы навсегда.

#19.8.5 Auto-scaling мании: поды плодятся как кролики

Симптомы: настроили HPA по CPU, а приложение упирается в IO. HPA видит 10% CPU и не скейлит. База падает. Или наоборот: HPA добавил 100 подов из-за кратковременного пика, а потом они висят 10 минут (пока downscale stabilization не сработает), жрут ресурсы кластера, блокируют配额 других сервисов.

Решение:

  • Используйте адекватные метрики (latency p99, RPS, длина очереди)
  • Настройте min/max replicas с запасом
  • Ограничьте максимальное количество подов (resource quotas)
  • Используйте pod priority class — критичные сервисы не должны голодать из-за того, что менее важный сервис расскейлился

#19.9 Чек-лист для самопроверки

  • Приложение stateless? Можно запустить второй инстанс без потери данных?
  • Сессии вынесены в централизованное хранилище (Redis/Memcached)?
  • Connection pool настроен с учётом числа инстансов?
  • Кеш включён для повторяющихся запросов?
  • Статика отдаётся через CDN?
  • Включено сжатие (brotli/gzip) на уровне балансировщика?
  • HPA настроен по релевантным метрикам, а не только CPU?
  • Есть capacity planning — знаете, сколько трафика ожидать завтра?
  • Асинхронная обработка (очереди) для долгих операций?
  • Используете sticky sessions? Если да — осознанно и с пониманием последствий?

#19.10 Ключевые выводы

  1. Вертикальное масштабирование — не зло, а инструмент. Для БД, кеша и легаси это часто единственный адекватный путь. Но у него есть потолок — hardware ceiling и закон Амдала.

  2. Горизонтальное масштабирование начинается с выноса состояния. Stateless — единственный способ иметь эластичную систему. Всё, что может быть вынесено наружу (сессии, файлы, кеш), должно быть вынесено.

  3. Оптимизация дешевле масштабирования. Кеш, CDN, сжатие, индексы в БД — сделайте это до того, как заказывать новые сервера. Восьмидесяти процентам проектов не нужно больше одного-двух серверов — нужно перестать тратить ресурсы впустую.

  4. Автоскейлинг — это三层 (три уровня): predictive (план), HPA (реакция), KEDA (события). Не ограничивайтесь одной метрикой.

  5. Управление соединениями — это инфраструктура. Прокси вроде PgBouncer, пулы соединений, мультиплексирование — разница между «база жива» и «база упала под 5000 соединений».

  6. История повторяется. Twitter показал, как не надо (Fail Whale). Discord показал, как надо (эволюция, data-driven решения). Разница — в умении вовремя увидеть bottleneck и решить его до того, как он станет катастрофой.


Дальше: В главе 20 разберём масштабирование баз данных — шардирование, репликацию, CQRS. В главе 21 — кеширование (все уровни, от браузерного кеша до distributed cache). В главе 23 — CDN и edge computing как альтернатива масштабированию бэкенда.


#Источники

  • Discord Engineering Blog — "How Discord Stores Billions of Messages" (2017), "Why Discord is Switching from Go to Rust" (2020)
  • Twitter Engineering Blog — "Handling 20x Traffic Increase" (2010), "Fail Whale Autopsy" (2011), "Finagle: A Protocol-Agnostic RPC System" (2012)
  • Amazon Web Services — AWS re:Invent 2022: "Predictive scaling deep dive"
  • Kubernetes documentation — "Horizontal Pod Autoscaling"
  • KEDA documentation — "Event-driven autoscaling"