MAATRIX / Блог / Шардирование базы данных: основы

Шардирование базы данных: основы

Шардирование базы данных: основы

MAATRIX

Однажды один сервер базы данных перестаёт справляться: диски забиты, CPU в потолке, запросы тормозят даже с индексами и кэшем. Первая мысль, которая приходит в голову после пары статей на Хабре — «надо шардировать». Это разумная идея, но только если вы уже перепробовали более простые и дешёвые варианты. В этой статье — что такое шардирование на самом деле, как выбрать ключ, какую сложность вы покупаете вместе с масштабируемостью и когда оно вам действительно нужно, а когда нет.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое шардирование

Шардирование (sharding, horizontal partitioning) — это разбиение одной большой таблицы или базы данных на несколько независимых частей, шардов, каждая из которых хранится на отдельном сервере. Строки распределяются между шардами по какому-то ключу — например, по ID пользователя. Пользователи с ID от 1 до 1 000 000 лежат на шарде 1, от 1 000 001 до 2 000 000 — на шарде 2, и так далее.

Ключевое отличие от обычного партиционирования в том, что шарды физически разнесены по разным серверам с отдельными CPU, RAM и диском. Партиционирование внутри одной базы (например, PARTITION BY RANGE в PostgreSQL) делит таблицу на части ради удобства обслуживания и скорости запросов, но всё это по-прежнему один сервер с одним ограничением по железу. Шардирование снимает именно это ограничение — суммарная ёмкость системы растёт вместе с числом серверов.

Важно понимать: шардирование — не бесплатный масштаб. Взамен вы получаете распределённую систему со всеми её сложностями: сетевые задержки между шардами, отсутствие единой транзакции на всю базу, ручное перераспределение данных при росте.

Шардирование, репликация и партиционирование — в чём разница

Эти три термина постоянно путают, хотя решают разные задачи.

ПодходЧто решаетЧто НЕ решает
Вертикальное масштабированиеНе хватает CPU/RAM/диска — берём сервер мощнееЕсть физический потолок (максимальная конфигурация железа)
Репликация (master-replica)Отказоустойчивость, разгрузка чтения на репликиОбъём записи всё ещё упирается в один master
ПартиционированиеУдобство обслуживания больших таблиц на одном сервереНе увеличивает суммарные ресурсы CPU/RAM/диска
ШардированиеОбъём данных и нагрузка на запись превышают возможности одного сервераРезко усложняет запросы, транзакции и эксплуатацию

На практике эти подходы комбинируют: каждый шард сам по себе — это master с одной или несколькими репликами для отказоустойчивости. Шардирование отвечает на вопрос «как распределить запись между многими серверами», репликация — «что делать, если один сервер упадёт».

Если у вас пока просто медленная база на VPS, для начала стоит проверить более дешёвые пути — например PostgreSQL или MySQL — что выбрать для сервера и настройку репликации PostgreSQL на VPS, прежде чем переходить к шардированию.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Как выбрать ключ шардирования — самое критичное решение

Ключ шардирования (shard key) определяет, в какой шард попадёт конкретная строка. Это решение принимается один раз и потом стоит очень дорого поменять, поэтому к нему стоит отнестись серьёзнее всего остального в этой статье.

Требования к хорошему ключу:

  • Высокая кардинальность — много уникальных значений, иначе часть шардов останется почти пустой.
  • Равномерное распределение нагрузки — не только данных, но именно запросов на чтение и запись.
  • Соответствие паттерну запросов — большинство запросов должны затрагивать один шард, а не собирать данные со всех.

Частые ошибки:

  • Дата регистрации / автоинкремент ID как ключ. Новые пользователи всегда попадают в последний шард — он получает всю нагрузку записи, пока остальные простаивают. Это классический hotspot.
  • Ключ с низкой кардинальностью, например country или plan_type — если 80% пользователей из одной страны, один шард раздувается, остальные почти не используются.
  • Ключ, не совпадающий с паттерном запросов. Если шардируете по user_id, а частый запрос — «все заказы за сегодня по всем пользователям», он превращается в scatter-gather по всем шардам вместо одного точного попадания.

Три основных стратегии распределения по ключу:

1. Range-based — диапазоны значений ключа
   shard_1: user_id 1..1000000
   shard_2: user_id 1000001..2000000
   Плюс: просто добавлять новые шарды сверху.
   Минус: новые данные почти всегда льются в последний шард (hotspot).

2. Hash-based — хэш от ключа определяет шард
   shard_id = hash(user_id) % количество_шардов
   Плюс: равномерное распределение.
   Минус: диапазонные запросы (range scan) становятся дороже,
          а добавление нового шарда требует пересчёта хэшей
          почти для всех строк (решается consistent hashing).

3. Directory-based (lookup table) — отдельная таблица-справочник
   "какой user_id на каком шарде"
   Плюс: гибкость, можно переносить отдельных пользователей.
   Минус: сама таблица-справочник — узкое место и точка отказа,
          её саму придётся реплицировать и держать быстрой.

На практике для большинства нагруженных SaaS-приложений выбирают hash-based шардирование по user_id, tenant_id или account_id — по тому полю, которое присутствует почти в каждом запросе к базе.

Архитектурные схемы: кто занимается маршрутизацией

Есть три принципиально разных способа реализовать шардирование.

Шардирование на уровне приложения. Логика «какой шард обслуживает этот запрос» зашита в код приложения или в отдельный слой (ORM с поддержкой шардов, самописный роутер). Максимальный контроль, но и максимум ответственности — ресеардинг, миграции схемы, консистентность внешних ключей между шардами — всё вручную.

Прокси-слой перед базой. Приложение общается с прокси как с обычной базой, а прокси сам решает, на какой шард отправить запрос. Пример для PostgreSQL — расширение Citus (превращает PostgreSQL в распределённую базу с прозрачным шардированием), для MySQL — Vitess (изначально разработан в YouTube) или ProxySQL с ручной маршрутизацией. Это заметно снижает сложность на стороне приложения, но добавляет ещё один компонент в инфраструктуру, который тоже может стать узким местом.

Встроенное шардирование в самой СУБД. MongoDB с sh.shardCollection() и компонентами mongos/config servers — шардирование как штатная функция базы. Плюс в том, что это «из коробки», минус — вы привязаны к конкретной СУБД и её конкретной модели шардирования.

Для запуска тестового кластера на Citus поверх PostgreSQL база данных распределяется примерно так:

-- на координаторе
CREATE EXTENSION citus;
SELECT citus_add_node('shard-host-1', 5432);
SELECT citus_add_node('shard-host-2', 5432);

-- превращаем обычную таблицу в распределённую
SELECT create_distributed_table('orders', 'user_id');

После этого Citus сам решает, на какой физический сервер отправить запрос, исходя из значения user_id в условии WHERE.

Честная сложность: что ломается после шардирования

Это раздел, который часто пропускают в статьях про шардирование, а зря — именно здесь цена решения становится ощутимой.

  • JOIN между шардами исчезает. Запрос, который раньше был простым JOIN orders ON users.id = orders.user_id, при шардировании по разным ключам для users и orders может потребовать отдельных запросов к нескольким серверам и склейки результатов в коде приложения. Решение — шардировать связанные таблицы по одному и тому же ключу (co-location), тогда связанные строки физически оказываются на одном шарде.
  • Транзакции ACID перестают быть тривиальными. Транзакция, затрагивающая несколько шардов, требует двухфазного коммита (2PC) или паттерна saga — оба варианта сложнее и медленнее локальной транзакции на одном сервере.
  • Агрегаты по всей базе (COUNT, SUM, аналитика) требуют scatter-gather — запрос уходит на все шарды параллельно, а результаты объединяются на координаторе. Это работает, но добавляет задержку и нагрузку сразу на все серверы одновременно.
  • Ресеардинг — добавление новых шардов при росте — трудоёмкая операция. При hash-based подходе с простым % N добавление шарда меняет распределение почти всех строк. Практическое решение — consistent hashing или запуск сразу с «виртуальными шардами» (например, 4096 логических шардов на меньшем числе физических серверов, которые потом можно перераспределять без пересчёта хэшей).
  • Автоинкременты становятся глобальной проблемой. Если ID генерируется независимо на каждом шарде, значения начнут повторяться между шардами. Нужен либо глобальный генератор ID (например, по схеме Snowflake — timestamp + shard_id + counter), либо UUID.
  • Бэкапы и миграции схемы (ALTER TABLE) нужно проводить на каждом шарде отдельно — либо синхронно везде, либо с продуманной стратегией постепенного раскатывания.

Если у вас уже болит с производительностью MySQL или PostgreSQL, часто причина не в объёме данных, а в конкретных медленных запросах — почитайте про медленные запросы MySQL: нередко проблема решается индексом, а не распределённой архитектурой.

Когда шардирование реально нужно, а когда это преждевременная оптимизация

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

  1. Оптимизация запросов и индексов. Плохой план запроса на большой таблице легко даёт 10-100-кратное замедление — и это чинится без единой новой строчки инфраструктуры.
  2. Вертикальное масштабирование. Современные выделенные серверы и VPS с NVMe и десятками гигабайт RAM закрывают потребности абсолютного большинства проектов — от небольшого SaaS до среднего интернет-магазина. Прежде чем шардировать, посчитайте, во сколько раз мощнее сервер вам реально нужен — см. как рассчитать конфигурацию сервера под нагрузку.
  3. Репликация с разделением чтения и записи. Если у вас 90% читающих запросов и 10% пишущих (типичная картина для большинства веб-приложений), пара read-реплик снимает нагрузку с master без единого изменения в модели данных.
  4. Кэширование горячих данных (Redis/Memcached перед базой) часто убирает саму необходимость гонять тяжёлые запросы к СУБД при каждом обращении.

Шардирование стоит рассматривать всерьёз, когда:

  • объём записи (не чтения — читение решается репликами и кэшем) превышает то, что тянет один сервер, даже после оптимизации запросов;
  • объём данных физически не помещается на диски одного сервера в разумной конфигурации;
  • вы уже упёрлись в вертикальный потолок (максимальная доступная конфигурация выделенного сервера) и репликация не снимает нагрузку записи.

Если ни один из этих трёх пунктов не про вас — скорее всего, вам нужен более мощный сервер и грамотные индексы, а не распределённая архитектура с её operational-нагрузкой.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Шардирование — это то же самое, что партиционирование?

Нет. Партиционирование делит таблицу на части внутри одного сервера ради удобства обслуживания, шардирование разносит данные по разным физическим серверам ради масштабирования.

Можно ли шардировать без изменения кода приложения?

Частично да, если использовать прокси-слой вроде Citus для PostgreSQL или Vitess для MySQL — они берут маршрутизацию на себя. Но паттерны запросов (JOIN, транзакции) всё равно придётся пересматривать под ограничения распределённой базы.

Сколько шардов нужно для старта?

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

Что делать, если один ключ шардирования не подходит под все паттерны запросов?

Это нормальная ситуация — универсального ключа часто не существует. Решают либо через денормализацию и дублирование данных под разные паттерны доступа, либо через отдельное аналитическое хранилище (data warehouse), куда данные стекаются со всех шардов для сложных агрегатов.

MongoDB проще шардировать, чем PostgreSQL или MySQL?

У MongoDB шардирование встроено в СУБД и настраивается декларативно, у реляционных баз обычно требуется расширение (Citus) или отдельный слой (Vitess). Но сама сложность работы с распределёнными данными — JOIN, транзакции, ресеардинг — никуда не девается независимо от СУБД.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →