Масштабирование¶
Когда один сервер перестаёт справляться, у тебя два пути: дать ему больше железа, или поставить рядом второй (пятый, сотый). Оба пути имеют свои ограничения и боль. Здесь разбираемся, когда и какой выбирать.
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¶
Это другое: ты НЕ режешь одну таблицу — ты разводишь разные БД по бизнес-доменам.
✅ 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
Простой алгоритм:
- Сначала вертикально. Это самый дешёвый и простой шаг. Дай Postgres больше RAM — query planner и shared_buffers скажут спасибо.
- Потом read replicas + cache. Большинство сервисов 90% read.
- Потом federation. Делишь по доменам (auth / orders / catalog).
- 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+ доменов с собственными командами.
📝 Подумай¶
- У тебя Postgres растёт по 100 ГБ в месяц, сейчас 5 ТБ. Будешь шардировать? По какому ключу?
- Сервис: чат для команд (как Slack). Какой ключ шардирования выберешь и почему?
- Что плохого в sharding по
created_atдля активной записывающей системы?
Ответ
- Шардирование — крайнее средство. Сначала: партиционирование по дате
(старые сообщения в архивные партиции), агрессивное архивирование/удаление,
перевод аналитики в read replicas. Если всё ещё мало — шардирование по
tenant_id(организация), потому что чтения почти всегда привязаны к одной команде. tenant_id(workspace_id). Чтения вокруг одной команды → данные одной команды на одном шарде → fast queries. Hash-based чтобы избежать hot shard (большие команды распределить).- Hot shard: все новые записи идут на последнюю партицию/шард, пока остальные спят. Insert RPS концентрируется в одном месте → write contention, replication lag, неравномерный disk I/O.
Дальше: consistency.md — как договариваются между собой распределённые узлы.