Перейти к содержанию

Масштабирование

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

Vertical vs horizontal scaling

🧩 Простыми словами.

  • Вертикальное (scale up). Тот же сервер, но мощнее: 8 → 64 ядер, 16 → 512 ГБ RAM, NVMe вместо HDD. Простое, но дорогое и упирается в потолок железа. Падёт инстанс — упадёт всё.
  • Горизонтальное (scale out). Много одинаковых небольших серверов, балансировщик распределяет нагрузку. Дешевле в пересчёте на запрос, отказоустойчивее, но сложнее: нужна stateless архитектура и LB.

⚙️ Под капотом. Вертикальное — это t3.medium → t3.2xlarge в облаке. Один запрос к БД, одна транзакция, никаких распределённых проблем. Горизонтальное — это «10 podов API за nginx, у всех одна Postgres-база, общий Redis для кэша».

💥 Зачем это нужно. Чтобы выдерживать рост и не падать целиком.

graph LR
    subgraph Vertical
        S1[Server 8 CPU<br/>32 GB] --> S2[Server 32 CPU<br/>256 GB]
    end
    subgraph Horizontal
        LB[Load Balancer]
        LB --> H1[Node]
        LB --> H2[Node]
        LB --> H3[Node]
        LB --> H4[Node]
    end
Критерий Vertical Horizontal
Сложность Низкая Высокая
Цена Растёт нелинейно Линейно
Отказоустойчивость Single point of failure Redundancy
Потолок Размер самой большой VM Практически бесконечный
Состояние Можно держать локально Должно быть external

🛠 Применение в Go-проекте. Stateless API на Go отлично шкалируется горизонтально: пакуешь в Docker, запускаешь N реплик, перед ними nginx или envoy. Главное — НЕ хранить сессии в памяти инстанса (только в Redis), не писать на локальный диск (только в S3/Postgres), и использовать context.Context для отмены висящих горутин при graceful shutdown.

srv := &http.Server{Addr: ":8080", Handler: r}
go func() { _ = srv.ListenAndServe() }()

<-ctx.Done() // SIGTERM
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx) // ждём пока завершатся inflight-запросы

Типичные ошибки.

  • Stateful API: хранят сессии в sync.Map инстанса → пользователя кидает каждый запрос на разные ноды, авторизация ломается.
  • «Просто увеличим RAM на Postgres» — масштабируешь всё, кроме CPU и I/O. Через месяц упрёшься снова.
  • Горизонтальное без LB → клиенты должны знать про все инстансы. Так делать нельзя.

Sharding

Sharding — это когда данных столько, что один сервер БД не справляется (write RPS или объём диска). Делишь данные на куски (shards) и кладёшь на разные сервера.

🧩 Простыми словами. Библиотека на 10 миллионов книг. Один склад не вмещает — делишь книги по первой букве: A–E на склад 1, F–J на склад 2, и т.д. Когда читатель приходит за книгой Smith, идёшь на склад 4.

Range-based sharding

Делишь по диапазону ключа.

shard_1: user_id 1..1_000_000
shard_2: user_id 1_000_001..2_000_000
shard_3: user_id 2_000_001..3_000_000

✅ Плюсы: range-запросы работают на одном шарде («все юзеры от 100 до 500»). ❌ Минусы: hot shards. Новые юзеры все на последнем — он горит. Twitter, кстати, этим переболел.

Hash-based sharding

shard = hash(user_id) % N. Распределяет ровно, но range-запросы превращаются в scatter-gather по всем шардам.

graph LR
    Q[Query user_id=42] --> H{hash 42 mod 4}
    H -->|=2| S2[Shard 2]
    Q2[Range query] --> All
    All --> S0[Shard 0]
    All --> S1[Shard 1]
    All --> S2
    All --> S3[Shard 3]

❌ Беда: при увеличении N с 4 до 5 пересчитываются почти все ключи — ребаланс больно. Решается consistent hashing (см. load-balancing.md).

Directory-based sharding

Отдельный сервис-каталог: «user 42 → shard 17». Дорого (extra hop), но гибко: можешь руками перекидывать пользователей. Так делает Foursquare.

Federation

Это другое: ты НЕ режешь одну таблицу — ты разводишь разные БД по бизнес-доменам.

db_users        — auth, профили
db_payments     — транзакции, биллинг
db_content      — посты, комменты

✅ Cross-team scaling, разные тех-команды — разные БД. ❌ Дорого делать JOIN'ы между ними (их нет — нужно делать application-side join).

🛠 Применение в Go-проекте. Postgres + Citus / Vitess делают шардирование прозрачным. На уровне приложения шарды разводят так:

type ShardRouter struct {
    pools [N]*pgxpool.Pool
}

func (r *ShardRouter) For(userID int64) *pgxpool.Pool {
    return r.pools[hashFNV(userID)%N]
}

// в сервисе
db := router.For(userID)
db.QueryRow(ctx, "SELECT * FROM users WHERE id=$1", userID)

Типичные ошибки.

  • Шардируют преждевременно. На 10 ГБ Postgres справится один. Сначала read replicas, потом sharding. См. databases.md.
  • Неправильный ключ шардирования: например, по created_at — все новые записи на один шард.
  • Cross-shard транзакции. Распределённый коммит дорогой и нестабильный — обычно избегают (саги, см. microservices.md).

Partitioning

Похоже на sharding, но внутри одной СУБД. Postgres PARTITION BY RANGE (created_at) разводит огромную таблицу на куски по дате — старые партиции можно дропнуть/ заархивировать одним движением, индексы меньше, vacuum быстрее.

CREATE TABLE events (id bigint, created_at timestamptz, ...)
PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_01 PARTITION OF events
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

⚙️ Когда применять. Time-series данные, логи, события. Не нужно на «users».

Когда что выбирать: дерево решений

graph TD
    Start[Растёт нагрузка] --> Q1{CPU/RAM?}
    Q1 -->|Да| V[Vertical: + железа]
    Q1 -->|Нет| Q2{Read RPS?}
    Q2 -->|Да| RR[Read replicas + cache]
    Q2 -->|Нет| Q3{Write RPS / Storage?}
    Q3 -->|Да| Q4{Можно по доменам?}
    Q4 -->|Да| Fed[Federation]
    Q4 -->|Нет| Sh[Sharding]
    V --> Done[Done]
    RR --> Done
    Fed --> Done
    Sh --> Done

Простой алгоритм:

  1. Сначала вертикально. Это самый дешёвый и простой шаг. Дай Postgres больше RAM — query planner и shared_buffers скажут спасибо.
  2. Потом read replicas + cache. Большинство сервисов 90% read.
  3. Потом federation. Делишь по доменам (auth / orders / catalog).
  4. Sharding — в последнюю очередь. Это сложно и дорого.

🔥 Пример: news feed на 100M DAU

DAU: 100M
Avg posts read per user: 200/day
Read RPS: 100e6 × 200 / 86400 ≈ 230k RPS
Avg posts written: 5/day → write RPS ≈ 5800

Read/Write ratio = 40:1

Стратегия:

  • Read. Cache (Redis cluster) + read replicas Postgres. Hot users — pre-computed feed в Redis, обновляется через Kafka.
  • Write. Sharding posts по hash(user_id). Один shard ≈ 600 RPS, держит.
  • Storage. Партиционирование по created_at, старые посты в холодное хранилище.

Полный разбор feed → case-studies.md.

🤖 Что спрашивает AI-ментор

  • В чём разница между vertical и horizontal scaling? Какие минусы у каждого?
  • Что такое sharding? Чем отличается от partitioning?
  • Почему range-based sharding может привести к hot shard, и как этого избежать?
  • Что такое federation? Когда выбирать её, а не sharding одной таблицы?
  • Что произойдёт с consistent hashing при добавлении нового шарда?

📊 Уровни глубины

L1. Понимаешь vertical vs horizontal. Знаешь термин sharding.

L2. Можешь выбрать стратегию (range / hash / directory) и обосновать. Понимаешь hot shard, replica lag. Использовал Postgres partitioning.

L3. Видел реальные проблемы шардирования: ребаланс, cross-shard JOIN, миграция схемы между шардами. Знаешь Vitess/Citus, использовал consistent hashing. Спроектировал федерацию из 5+ доменов с собственными командами.

📝 Подумай

  1. У тебя Postgres растёт по 100 ГБ в месяц, сейчас 5 ТБ. Будешь шардировать? По какому ключу?
  2. Сервис: чат для команд (как Slack). Какой ключ шардирования выберешь и почему?
  3. Что плохого в sharding по created_at для активной записывающей системы?
Ответ
  1. Шардирование — крайнее средство. Сначала: партиционирование по дате (старые сообщения в архивные партиции), агрессивное архивирование/удаление, перевод аналитики в read replicas. Если всё ещё мало — шардирование по tenant_id (организация), потому что чтения почти всегда привязаны к одной команде.
  2. tenant_id (workspace_id). Чтения вокруг одной команды → данные одной команды на одном шарде → fast queries. Hash-based чтобы избежать hot shard (большие команды распределить).
  3. Hot shard: все новые записи идут на последнюю партицию/шард, пока остальные спят. Insert RPS концентрируется в одном месте → write contention, replication lag, неравномерный disk I/O.

Дальше: consistency.md — как договариваются между собой распределённые узлы.