Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Шардинг баз данных: стратегии и практика

Шардинг баз данных: стратегии разбиения и практические грабли

Как работает шардинг: ключ разбиения, hash/range/гео-стратегии, шардирование в Postgres и через прокси, проблемы cross-shard операций и когда шардинг ещё не нужен.

Прямой ответ

Шардинг (sharding) — горизонтальное разбиение данных по нескольким узлам (шардам) по ключу разбиения (shard key) — каждая строка живёт на одном шарде (запрос к конкретному ключу — один шард — ёмкость — «одна БД не справляется — данные/нагрузка на N»); зачем (и почему это крайняя мера): вертикальный предел (одна машина — потолок CPU/RAM/диск — ёмкость — «сервер максимум» — дальше только шардинг или scale-up дороже), нагрузка записи (одна БД — предел write-IOPS — ёмкость — шардинг распределяет запись), объём данных (TB/PB — одна БД не хранит — ёмкость); шардинг — последняя ступень: сначала — индексы/vacuum (запросы), партиции (одна БД, но логическое разбиение — проще шардинга), реплики (чтение), кэш (Redis) — «шардинг до оптимизации» — антипаттерн (техдолг — ёмкость); шардинг != партиционирование: партиционирование — одна БД, таблицы разбиты логически (партиции — проще — один узел), шардинг — данные на разных физических узлах (сложнее — распределённость — event-driven — «шардинг — партиционирование между машинами»); стратегии разбиения: (1) hash (hash(shard_key) % N — равномерное распределение — «user_id % 4» — ёмкость — минус: rebalance при добавлении шарда (N меняется — данные перераспределяются — ёмкость — consistent hashing смягчает); (2) range (по диапазону — «заказы 2024 на шард 1, 2025 на шард 2» — партиции-аналогия — минус: горячие диапазоны (новые данные — один шард — ёмкость); (3) гео (по региону — «EU на шард 1, US на шард 2» — data residency — compliance + близость — ёмкость); (4) по tenant (multi-tenant — «данные клиента X на шарде Y» — SaaS — ёмкость — изоляция); выбор shard key (критично): правильный ключ — запросы к одному шарду (user_id — «все заказы юзера на одном шарде» — ёмкость), неправильный — cross-shard (запрос без shard_key — fan-out на все шарды — ёмкость — «выбрал ключ, теперь все запросы cross-shard» — ошибка — техдолг); проблемы шардинга: cross-shard операции (JOIN/транзакция/агрегат через шарды — сложно/медленно — транзакции — «JOIN между шардами» — 2PC/координация — saga), rebalance (добавить/убрать шард — перемещение данных — ёмкость — «расширил кластер — данные переезжают»), уникальность (UNIQUE через шарды — сложно — констрейнты — «email уникален глобально» — cross-shard проверка), глобальные счётчики (AUTOINCREMENT — каждый шард свой — координация — ёмкость); решения: Citus (Postgres-шардинг — распределённый Postgres — Postgres — Citus — «шардинг без переписывания»), Vitess (MySQL-шардинг — YouTube — Vitess — MySQL), шардинг на уровне приложения (приложение маршрутизирует запросы — гибко, но код знает о шардах — техдолг); когда шардинг НЕ нужен: данные помещаются (одна БД + партиции + индексы — ёмкость — «шардинг для 10 ГБ» — безумие), чтение масштабируется репликами (реплики), запись не упирается (ёмкость — «шардинг до предела» — преждевременно — техдолг).

Практика: шардирование в Postgres через Citus

Citus (распределённый Postgres — практичный старт): distributed-таблицы (данные по shard key на worker-нодах — Postgres — Citus), reference-таблицы (малые справочники реплицированы на все ноды — JOIN без cross-shard — Citus); создание: SELECT create_distributed_table('orders', 'user_id') (шардирование по user_id — Citus); запросы: с shard_key в WHERE — один шард (быстро — ёмкость), без — fan-out на все (медленно — ёмкость — «запрос без user_id» — cross-shard — колокция (related-таблицы шардированы по одному ключу — JOIN внутри шарда — Citus); ** Vitess** (MySQL): tablet (шард MySQL — MySQL), VSchema (определяет шардирование — Vitess), vtgate (прокси — маршрутизирует запросы — приложение не знает о шардах — техдолг); шардинг на уровне приложения: маршрутизация (приложение вычисляет шард по ключу — «user_id % 4 → шард 2» — ёмкость — гибко, но код связан с шардированием — техдолг); антипаттерны: шардинг до оптимизации (индексы/партиции/реплики — сначала — ёмкость), неправильный shard key (cross-shard запросы — техдолг), шардинг без rebalance-плана («добавил шард — данные переехали — downtime» — ёмкость — consistent hashing — Citus).

Практический сценарий: SaaS-платформа — шардинг по tenant

Контекст: multi-tenant SaaS (10k клиентов), одна Postgres-БД (заказы/документы/аналитика), боль: объём 2 ТБ + рост, write-нагрузка упирается в одну БД (ёмкость — p99 записи 2 с — SLO — vacuum не успевает — vacuum), но сначала оптимизировали: индексы (убрали seq scan), партиции (orders по месяцам), реплики (чтение на 3 реплики), кэш (Redis) — помогло, но объём/запись всё равно уперлись (ёмкость — шардинг — следующий шаг); решение — шардинг по tenant_id (SaaS-естественный ключ — данные клиента изолированы — запросы клиента — один шард — ёмкость): (1) Citus (распределённый Postgres — Citus — Postgres — create_distributed_table('orders', 'tenant_id') — 8 шардов на 4 worker-нодах); (2) колокция (orders, order_items, documents — все по tenant_id — JOIN внутри шарда — Citus), reference-таблицы (страны/валюты — на всех нодах — Citus); (3) запросы: клиентский запрос (tenant_id в WHERE — один шард — быстро — ёмкость), админский (без tenant_id — fan-out — медленно — ёмкость — «отчёт по всем клиентам» — cross-shard — отдельная аналитическая БД — ClickHouse — CQRS-read); (4) rebalance (новый worker — Citus перемещает шарды — онлайн — Citus), уникальный email (глобально — cross-shard — решено через отдельный сервис регистрации — микросервисы — констрейнты); (5) data residency (EU-клиенты на EU-шардах — гео-шардинг — compliance — GDPR); результат: p99 записи 2 с → 200 мс (×10 — нагрузка распределена — ёмкость), объём на шард — 250 ГБ (управляемо — vacuum успевает — vacuum), масштабирование (добавить worker — данные переехали — ёмкость); цена: cross-shard админские запросы (отчётность — отдельная БД — ClickHouse — CQRS), сложность (Citus-кластер эксплуатировать — ёмкость — зависимости), rebalance (перемещение данных — плановая операция — ёмкость); вывод: шардинг решили после оптимизации (индексы/партиции/реплики/кэш — сначала — ёмкость), ключ — tenant_id (естественный для SaaS — ёмкость — ADR — «шардинг — не первый шаг» — техдолг).

FAQ

Чем шардинг отличается от партиционирования и репликации? Шардинг — данные на разных физических узлах по ключу (горизонтальное разбиение — ёмкость), партиционирование — одна БД, таблицы логически разбиты (партиции — проще — один узел), репликация — одни и те же данные на нескольких узлах (чтение масштабируется — реплики — не разбивает данные); разница: шардинг решает объём/запись (данные распределены — ёмкость), партиционирование — управление внутри БД (vacuum/индексы по партициям — партиции), репликация — чтение/доступность (реплики); порядок: индексы → партиции → реплики → кэш → шардинг (последняя ступень — ёмкость — «шардинг первым» — антипаттерн — техдолг); «шардинг = партиционирование» — нет (шардинг — между машинами, партиционирование — внутри БД — партиции — ёмкость — разные уровни — ADR).

Как выбрать shard key и что будет при неправильном выборе? Правильный ключ: запросы к одному шарду (user_id/tenant_id — «данные юзера вместе» — ёмкость — high-cardinality (много значений — равномерное распределение — ёмкость), соответствует запросам (большинство запросов с этим ключом — ёмкость); неправильный: cross-shard запросы (ключ не в WHERE большинства запросов — fan-out на все шарды — ёмкость — «выбрал дату, а запросы по user_id» — все cross-shard — медленно — техдолг), горячие шарды (низкая cardinality — «по стране» — одна страна = один шард перегружен — ёмкость); при неправильном: rebalance (смена ключа — перемещение всех данных — ёмкость — «сменил shard key» — миграция всего — downtime — миграция), переписывание (запросы без ключа — добавить ключ — техдолг); вывод: ключ выбирается по запросам (не «удобно» — «большинство запросов с этим ключом» — ёмкость — ADR — смена ключа — дорого — техдолг).

Что такое cross-shard операции и почему они сложны? Cross-shard — операция затрагивает несколько шардов (JOIN между данными на разных шардах, транзакция через шарды, агрегат «сумма по всем» — ёмкость); сложность: координация (2PC для транзакции через шарды — медленно/хрупко — транзакции — saga), fan-out (запрос на все шарды + агрегация — ёмкость — «отчёт по всем» — медленно), JOIN (данные на разных шардах — перемещение/координация — ёмкость); решения: колокция (related-таблицы по одному ключу — JOIN внутри шарда — Citus), reference-таблицы (справочники на всех шардах — JOIN локален — Citus), отдельная аналитика (cross-shard отчёты — ClickHouse — CQRS-read — не в шардированной БД), saga (распределённая транзакция — saga); «cross-shard — это норм» — нет (медленно/сложно — ёмкость — проектировать так, чтобы операции были внутри шарда — ADR — техдолг).

Когда шардинг действительно нужен, а когда — преждевременен? Нужен: данные не помещаются на одну БД (TB/PB — ёмкость), write-нагрузка упирается (после индексов/партиций/реплик/кэша — ёмкость), вертикальный предел (максимум сервера — ёмкость); преждевременен: данные помещаются (10–100 ГБ — одна БД + партиции — ёмкость), чтение масштабируется репликами (реплики), запись не упирается (ёмкость), до оптимизации (индексы/vacuum/партиции — не сделаны — техдолг); цена преждевременности: сложность (cross-shard/rebalance/уникальность — ёмкость) без нужды (техдолг — «шардинг для стартапа» — преждевременно); вывод: шардинг — после того, как одна БД (+ партиции/реплики) объективно не справляется (ёмкость — ADR — «шардинг — не масштаб по умолчанию» — техдолг — event-driven).

Источники: PostgreSQL docs (partitioning), Citus docs, Vitess docs — ссылки в шапке.