Разработка 16 июня 2026 · 12 мин чтения 519 0

Database scaling: sharding, replication, partitioning — практическое руководство

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

Эта дисциплина включает три тесно связанных, но различных техники: репликацию (копии данных для отказоустойчивости и масштабирования чтения), партиционирование (разделение таблицы на сегменты внутри одного сервера), шардирование (распределение данных между серверами). Разбираем устройство каждого подхода, типичные архитектурные паттерны, готовые решения и trade-offs, с которыми приходится считаться в production.

Vertical scaling vs Horizontal scaling

Vertical scaling — апгрейд железа существующей машины. Простой путь без архитектурных изменений: переход на 64 ядра, 1 ТБ памяти, NVMe SSD. Минусы — предел физической мощности одного сервера, экспоненциальный рост цены на топовое железо, отсутствие отказоустойчивости (одна машина — single point of failure).

Horizontal scaling — распределение работы между несколькими серверами. Сложнее в архитектуре, но даёт практически безграничный потолок производительности и встроенную отказоустойчивость через избыточность. Большинство современных high-load систем используют гибрид: вертикальное масштабирование до разумного предела (типично 32–64 ядра, 256–512 ГБ RAM), затем горизонтальное.

Аспект Vertical scaling Horizontal scaling
Сложность реализации Минимальная — апгрейд железа Высокая — изменение архитектуры
Потолок производительности Физический лимит сервера Практически безграничный
Стоимость роста Экспоненциальная Линейная (при правильной архитектуре)
Отказоустойчивость SPOF без репликации Встроенная избыточность
Операционная сложность Простая Высокая — мониторинг, балансировка, миграции

Replication: типы и архитектуры

Репликация — создание копий базы данных на нескольких серверах. Цели: отказоустойчивость (если падает primary, secondary берёт на себя нагрузку), масштабирование чтения (читать можно с любой реплики), географическое распределение (реплика в каждом регионе для низкой латентности локальных запросов).

Master-slave (primary-replica)

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

Master-master

Несколько узлов принимают запись параллельно, изменения синхронизируются между ними. Решает проблему bottleneck’а записи, но создаёт сложность с конфликтами: что делать, если две ноды одновременно изменили одну запись? Стратегии разрешения варьируются от last-write-wins до бизнес-логики на уровне приложения.

Streaming replication в PostgreSQL

Один из самых распространённых вариантов в реальных проектах. Primary пишет в WAL (Write-Ahead Log), реплики постоянно вычитывают этот лог и применяют изменения у себя. Может работать синхронно (запись считается успешной только после подтверждения от реплик) или асинхронно (быстрее, но возможна потеря последних транзакций при сбое primary).

Logical replication

Передаются не физические страницы данных, а логические события — INSERT/UPDATE/DELETE с конкретными значениями. Позволяет реплицировать выборочные таблицы, выполнять обновление мажорной версии БД без даунтайма, синхронизировать данные между разными движками. Дороже streaming replication по нагрузке.

Тип репликации Синхронность Применение
Synchronous Запись ждёт подтверждения реплики Финансовые системы, критичные данные
Asynchronous Запись завершается до синхронизации Высоконагруженные системы, аналитика
Semi-synchronous Подтверждение от хотя бы одной реплики Компромисс надёжности и скорости
Streaming (физическая) Передача WAL Высокая производительность, полная копия
Logical Передача событий Выборочная репликация, миграции

Read replicas: разгрузка чтения

Read replicas — реплики, обслуживающие только чтение. В большинстве приложений соотношение чтений к записям сильно скошено в пользу чтений — 80/20, 90/10, иногда 99/1. Распределение чтений на несколько реплик линейно масштабирует throughput до количества реплик.

На уровне приложения нужно явно разделять запросы: запись и чтение, требующее consistency, идёт в primary; всё остальное — в реплики. Это даётся неравномерно: некоторые ORM поддерживают автоматический роутинг (например, через read-only флаг в транзакции), другие требуют явного указания соединения для каждого запроса.

Главная ошибка с read replicas — забыть про replication lag. Сразу после записи прочитать с реплики может вернуть старое значение, потому что репликация не успела примениться. Для критичных read-after-write сценариев нужен либо чтение из primary, либо ожидание синхронизации.

Consistency: strong vs eventual в репликации

Consistency-модель определяет, какие гарантии получает клиент при чтении. Strong consistency — после успешной записи любое последующее чтение увидит новое значение, независимо от того, с какой реплики читают. Eventual consistency — реплики «догонят» новое значение через какое-то время, и в этот промежуток разные клиенты могут видеть разное.

Strong consistency удобна разработчику, но дорога: требует синхронной репликации или чтения из primary. Eventual consistency дешевле и быстрее, но накладывает требования на дизайн приложения — нужно учитывать, что данные могут «отставать» на сотни миллисекунд или больше. Большинство современных систем выбирают eventual для большей части операций и strong только там, где это критично.

Replication lag и его последствия

Replication lag — задержка между записью в primary и появлением изменения на реплике. Нормально для асинхронной репликации составляет миллисекунды, но при высокой нагрузке может вырасти до секунд или минут. Длительный лаг — симптом проблемы: либо primary не справляется с потоком записей, либо реплика не успевает применять WAL.

Мониторинг лага — обязательная часть эксплуатации. Алерты на лаг свыше определённого порога (10 секунд для большинства систем, 1 секунда для высоконагруженных), графики динамики, корреляция с нагрузкой. При обнаружении растущего лага типичные действия: проверка нагрузки на primary, ресурсы реплики, длинные транзакции, блокирующие применение WAL.

Partitioning: вертикальное и горизонтальное

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

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

Горизонтальное партиционирование разделяет таблицу по строкам — например, по дате создания (партиция на месяц), по диапазону ID, по геолокации. Особенно эффективно для time-series данных и логов: операции с конкретным периодом затрагивают только одну партицию, удаление старых данных — это DROP PARTITION вместо медленного DELETE.

  • By range: разбиение по диапазонам значений (даты, ID)
  • By list: разбиение по списку явных значений (категории, страны)
  • By hash: разбиение по hash-функции (равномерное распределение)
  • By composite key: комбинация нескольких полей

Sharding: стратегии распределения данных

Шардирование — разделение данных между несколькими серверами с разной физической базой данных на каждом. Каждый шард обслуживает свою часть данных независимо. Запросы маршрутизируются в нужный шард по ключу шардирования.

Hash-based sharding

Ключ шардирования прогоняется через хэш-функцию, результат определяет, на каком шарде живут данные. Даёт равномерное распределение нагрузки, но плохо работает с range-запросами (нужно опросить все шарды) и сложно изменить количество шардов (требуется ресхардинг). Применяется через consistent hashing для смягчения проблемы переразделения.

Range-based sharding

Данные разбиваются по диапазонам значений ключа: пользователи с ID 1–1 000 000 на шарде A, 1 000 001–2 000 000 на шарде B и так далее. Удобно для range-запросов, но создаёт hot spots — если активность концентрируется в одном диапазоне (например, новые регистрации), один шард перегружен.

Geo-based sharding

Распределение по географии: данные пользователей из Беларуси на серверах в Минске, данные из РФ — на московских, и так далее. Снижает латентность, упрощает соответствие требованиям локализации данных. Минус — некоторые операции требуют cross-region запросов, что медленно и сложно.

Стратегия Сильные стороны Слабые стороны
Hash-based Равномерное распределение Сложность range-запросов и ресхардинга
Range-based Эффективны диапазонные запросы Hot spots на популярных диапазонах
Geo-based Низкая латентность для локальных запросов Сложности с cross-region операциями
Directory-based Гибкая ручная настройка Дополнительный сервис маршрутизации

Distributed transactions: 2PC и saga

Транзакции, затрагивающие несколько шардов, — отдельная боль. Локальный ACID работает в пределах одной БД, но не в распределённой системе. Two-Phase Commit (2PC) — классический протокол: координатор сначала спрашивает все участники о готовности, потом, если все подтвердили, отправляет команду commit. Проблема — блокировка ресурсов на время протокола, что снижает throughput.

Saga — альтернативный подход. Распределённая транзакция разбивается на цепочку локальных транзакций, каждая со своей компенсирующей операцией. Если одна из шагов проваливается, выполняются compensation для уже завершённых шагов. Saga масштабируется лучше 2PC, но требует продуманной бизнес-логики: некоторые операции не имеют простой компенсации.

Connection pooling и его роль на масштабе

Каждое соединение с PostgreSQL потребляет несколько мегабайт памяти. При тысячах одновременных соединений сервер тратит больше ресурсов на их обслуживание, чем на полезную работу. Connection pooling — промежуточный слой, держащий пул переиспользуемых соединений и мультиплексирующий клиентов поверх него.

PgBouncer — самый распространённый пулер для PostgreSQL. Может работать в трёх режимах: session (одно соединение на сессию клиента), transaction (соединение возвращается в пул после транзакции), statement (соединение возвращается после каждого statement). Transaction mode — оптимальный для большинства веб-приложений: высокая утилизация пула при сохранении transactional semantics.

Caching layers: Redis, Memcached

Перед базой данных почти всегда есть кэш-слой. Redis и Memcached — два мейнстрим-варианта. Кэширование снижает нагрузку на БД в разы и улучшает латентность ответов с десятков миллисекунд до единиц.

Стратегии кэширования варьируются: cache-aside (приложение само управляет кэшем), read-through (кэш сам загружает данные из БД при промахе), write-through (запись идёт одновременно в кэш и БД), write-behind (запись в кэш, асинхронный сброс в БД). Каждая имеет свои trade-offs по consistency и latency.

Vitess, Citus, CockroachDB: готовые решения

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

Vitess (созданный YouTube, теперь под CNCF) — шардирующий прокси перед MySQL. Берёт на себя маршрутизацию запросов, управление шардами, ресхардинг, поддержку DDL на масштабе. Используется крупными компаниями вроде Slack и Square.

Citus — расширение PostgreSQL, превращающее его в распределённую БД. Транспарентно для приложения: запросы пишутся как обычные SQL, расширение само распределяет их между шардами и собирает результаты. Подходит для команд, уже работающих с PostgreSQL и не желающих переходить на новый стек.

CockroachDB — distributed SQL с нуля. Совместим с PostgreSQL-протоколом, но архитектурно построен как распределённая БД с consensus-протоколом (Raft) для гарантий consistency. Используется там, где нужна сильная consistency через географически распределённый кластер.

Решение База Уровень распределённости
Vitess MySQL Шардирование с прокси
Citus PostgreSQL Расширение для распределённого PG
CockroachDB Distributed SQL Полностью распределённая БД с Raft
YugabyteDB Distributed SQL PostgreSQL-совместимая распределённая БД
Patroni PostgreSQL HA Управление HA-кластером PostgreSQL

Когда нужен NoSQL: CAP-теорема в практике

CAP-теорема утверждает: распределённая система не может одновременно гарантировать Consistency (все узлы видят одни данные), Availability (любой запрос получает ответ) и Partition tolerance (система продолжает работать при сбое связи между узлами). При network partition приходится выбирать между C и A.

Реляционные БД исторически ориентированы на CP — в случае сетевого раздела предпочтительнее ошибиться, чем вернуть неактуальные данные. NoSQL-системы вроде Cassandra, DynamoDB, Riak часто выбирают AP — отвечать даже устаревшими данными лучше, чем не отвечать вовсе. Этот выбор определяет, какая БД подходит для какой задачи: финансовые транзакции — CP, лента социальной сети — может быть AP.

Practical scaling roadmap

Типичный путь масштабирования системы. Начальная стадия — одна БД, вертикальное масштабирование при росте нагрузки. Затем — добавление read replicas для масштабирования чтения, оптимизация запросов, добавление индексов и кэша. На следующем этапе — партиционирование больших таблиц, вынос горячих данных в отдельные сегменты, более агрессивное кэширование.

Шардирование — последний шаг, когда исчерпаны более простые варианты. Архитектурно дорогая операция, требующая изменения приложения, выбор ключа шардирования, миграции данных. Поэтому к шардированию обычно приходят системы с миллионами активных пользователей и тысячами запросов в секунду.

  • Шаг 1: вертикальное масштабирование + оптимизация запросов и индексов
  • Шаг 2: read replicas для разгрузки чтения
  • Шаг 3: кэширующий слой (Redis/Memcached)
  • Шаг 4: партиционирование больших таблиц
  • Шаг 5: вынос отдельных доменов в отдельные БД (микросервисная архитектура)
  • Шаг 6: шардирование критических таблиц или переход на distributed SQL

Часто задаваемые вопросы

Когда переходить от read replicas к шардированию?

Read replicas масштабируют только чтение. Если bottleneck’ом становится запись (high write throughput, large write workload), реплики не помогут. Симптомы — высокий write IOPS на primary, рост latency записи, replication lag, не успевающий рассасываться. На этом этапе пора думать о шардировании или distributed SQL.

Чем партиционирование отличается от шардирования?

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

Как выбрать ключ шардирования?

Ключ должен обеспечивать равномерное распределение нагрузки и присутствовать в большинстве запросов (иначе придётся опрашивать все шарды). Типичные кандидаты: user_id для систем с user-центричной нагрузкой, tenant_id для multi-tenant SaaS, timestamp для time-series данных. Антипаттерн — ключ, концентрирующий нагрузку (например, country_id, где 90% пользователей из одной страны).

Нужно ли использовать Vitess или Citus сразу?

Если объёмы данных и нагрузка пока вертикально решаемы — нет, добавит сложности без пользы. Если уже близко к потолку одного сервера и виден дальнейший рост — да, лучше внедрить заранее, до момента, когда станет критично. Внедрение под нагрузкой намного сложнее, чем подготовка инфраструктуры в спокойный период.

Что такое hot spot в шардированной системе?

Hot spot — шард, на который приходится непропорционально большая нагрузка. Возникает при range-based шардировании, когда активность концентрируется в одном диапазоне, или при неравномерных user-паттернах в hash-based (один очень активный клиент в multi-tenant системе). Решения: ресхардинг hot-области в более мелкие шарды, перенос горячих данных в отдельный шард, оптимизация query-паттернов.

Можно ли обойтись без шардирования с помощью distributed SQL?

Можно, но distributed SQL имеет свои trade-offs: выше латентность простых запросов (из-за consensus-протоколов), сложнее эксплуатация, специфические ограничения по типам нагрузки. Для multi-region систем с требованием strong consistency distributed SQL — хороший выбор. Для классических single-region high-load систем традиционное шардирование часто эффективнее.

Заключение

Масштабирование базы данных — дисциплина с десятками инструментов и десятком принципов выбора между ними. Универсального ответа на вопрос «как масштабировать БД» не существует: правильное решение зависит от паттернов нагрузки (доля чтений и записей, размер данных, требования к consistency), бизнес-контекста (готовность к downtime, географическое распределение пользователей), зрелости команды (опыт работы с распределёнными системами).

Общая инженерная мудрость — не торопиться с архитектурными перестройками. Вертикальное масштабирование плюс оптимизация запросов решают проблемы большинства систем до уровня сотен тысяч пользователей. Read replicas и кэширование добавляют ещё на порядок. Партиционирование решает рост объёма данных. Шардирование и distributed SQL появляются в архитектуре, когда другие пути исчерпаны или явно неэффективны.

Зрелая команда выбирает технологии не по моде, а по конкретным требованиям своей системы. Postgres с правильно настроенной репликацией и партиционированием обслуживает огромные нагрузки. Cassandra сияет на специфических задачах. Distributed SQL — для multi-region с сильной consistency. MongoDB — для документ-ориентированных задач с гибкой схемой. Каждый инструмент имеет свою нишу, и понимание этой ниши важнее знания популярных названий.