Оглавление
- Введение
- Обзор архитектуры шардирования в PostgreSQL
- Алгоритмы распределения данных между шардами
- Подходы к реализации шардирования в PostgreSQL
- Плюсы и минусы шардирования
- Сравнение с репликацией и партиционированием
- Архитектура решения с Apache ShardingSphere
- Виды шардирования в Apache ShardingSphere
- YAML-конфигурация шардирования и репликации
- Интеграция с Spring Boot: конфигурация и аннотации
- Транзакции и откаты при шардировании
- Масштабирование системы: добавление шардов и миграция данных
- Мониторинг и отладка распределённой БД
- Заключение
Введение
Современные веб-приложения и сервисы сталкиваются с непрерывно растущими объёмами данных и нагрузками, с которыми стандартные подходы к организации баз данных перестают справляться. Вертикальное масштабирование — увеличение мощности серверов — имеет пределы, поэтому всё чаще на первый план выходит горизонтальное масштабирование, или шардирование.
В данной статье мы подробно рассмотрим шардирование применительно к базам данных PostgreSQL. Сначала будут представлены общие подходы и алгоритмы распределения данных между шардами: шардирование по диапазонам (Range-Based Sharding), по хешу (Hash-Based Sharding), использование каталогов соответствий и географическое шардирование. Мы обсудим сильные и слабые стороны каждого подхода, чтобы помочь вам выбрать наиболее подходящий вариант.
Во второй части статьи мы перейдём к конкретному практическому примеру: рассмотрим, как реализовать шардирование PostgreSQL в приложении на платформе Spring Boot с использованием Apache ShardingSphere. Мы покажем, как правильно конфигурировать шардирование, какие аннотации и настройки использовать в Spring, как управлять транзакциями, масштабировать систему, а также как организовать мониторинг и диагностику распределённой базы.
Таким образом, от общих принципов мы перейдём к реальному рабочему решению на примере Spring Boot и Apache ShardingSphere, чтобы вы могли сразу применить полученные знания на практике.
Обзор архитектуры шардирования в PostgreSQL
Шардирование (от англ. sharding) – это метод горизонтального масштабирования, при котором данные базы распределяются по нескольким серверам. Каждый сервер хранит лишь часть общей базы – свой шард. В совокупности шарды содержат полный набор данных, но каждая отдельная нода оперирует только своей долей. За счет этого достигается разгрузка: объем данных и количество операций на каждый сервер уменьшаются, повышая скорость работы и общую пропускную способность системы. Важно не путать шардирование с репликацией: при репликации несколько серверов хранят копии одной и той же базы (ведущий и ведомые узлы), тогда как при шардировании каждый узел хранит уникальную часть данных и не дублирует другие. Шардирование также можно рассматривать как разновидность партиционирования, но если классическое партиционирование делит таблицы внутри одной СУБД, то шардирование распределяет данные по нескольким экземплярам СУБД.
Общая архитектура шардированной системы обычно включает дополнительный уровень для маршрутизации запросов. Типичная схема: клиент ? маршрутизатор (роутер) ? шарды. Клиентское приложение выполняет обычные SQL-запросы (например, SELECT * FROM users WHERE user_id = 123
), не зная о наличии нескольких шардов. Маршрутизатор перехватывает запрос, по значению ключа шардинга определяет, к какому серверу (шарду) обратиться, и перенаправляет запрос туда. Шард (узел базы PostgreSQL) выполняет полученный запрос и возвращает результат обратно маршрутизатору, который уже отдает данные клиенту. Благодаря такому посреднику логика распределения данных скрыта от клиента – приложение не нужно модифицировать под несколько баз, вся сложность сосредоточена в маршрутизаторе или серверной части.
В роли маршрутизатора может выступать сам компонент приложения (код, определяющий нужный шард) или специализированный прокси/балансировщик, либо координатор (мастер-узел) в случае некоторых расширений PostgreSQL. Например, в расширении Citus один узел кластера выполняет роль координатора: он хранит метаданные о шардах и распланировывает распределенные запросы, перенаправляя части запросов на рабочие узлы. В других подходах роль маршрутизатора может выполнять отдельный сервис или библиотека. Главное, что для выбора шарда необходим определенный ключ шардинга – поле (или набор полей), по которому принимается решение о размещении строки данных. Выбор удачного ключа критически важен: он должен обеспечивать равномерное распределение данных и однозначно указывать на шард, содержащий конкретную запись.
Каждый шард в общем случае представляет собой полноценную базу PostgreSQL, которая может иметь свою внутреннюю архитектуру высокодоступности – например, репликацию (ведущий/ведомый) для отказоустойчивости. В контексте шардирования набор реплик, относящихся к одному шару, иногда называют реплика-сетом (аналогично тому, как это принято, например, в MongoDB). То есть для каждого шарда можно настроить синхронную или асинхронную репликацию на один или несколько резервных серверов, чтобы выход из строя основного узла не приводил к потере данных или недоступности всей доли данных кластера. Таким образом, шардированный кластер объединяет свойства распределенной системы (данные разбиты по узлам) и репликации (каждый узел может быть резервирован). В идеале такая система для внешнего пользователя выглядит как единая база данных, полностью поддерживающая транзакционность и консистентность ACID, несмотря на то что под капотом работает множество PostgreSQL-инстансов.
Следует отметить, что PostgreSQL не имеет встроенного механизма шардинга «из коробки» (по крайней мере, на момент PostgreSQL 15-16). В отличие от некоторых NoSQL-баз или специализированных распределенных СУБД, PostgreSQL в стандартной поставке не умеет автоматически разделять таблицы по узлам. Однако существуют внешние решения и расширения, добавляющие такую функциональность. Исторически сообщество PostgreSQL долго обсуждает реализацию собственного шардинга, но пока единого консенсуса не достигнуто. Тем не менее, благодаря развитию возможностей декларативного партиционирования (sectioning) и Foreign Data Wrapper (FDW), а также появлению расширений, сегодня доступно несколько практических способов реализовать шардирование на PostgreSQL. Далее мы подробно рассмотрим эти подходы, но сначала разберемся, какими методами можно делить данные на шарды.
Алгоритмы распределения данных между шардами
Основу шардирования составляет алгоритм, по которому строки (записи) распределяются между разными шардами. Цель – определить, в какой шард должна попасть каждая новая запись, и уметь по ключу шардинга найти нужный шард при чтении. Существует несколько распространенных стратегий, каждая из которых имеет свои плюсы, минусы и области применения. Рассмотрим основные алгоритмы распределения данных:
Шардирование по диапазонам (Range-Based Sharding)
Пример горизонтального шардирования по диапазонам значений ключа (user_id). Пользователи с id в диапазоне 1–1,000,000 хранятся на шарде 1, id 1,000,001–2,000,000 – на шарде 2, остальные – на шарде 3.
Диапазонное шардирование делит данные по непрерывным диапазонам значений ключа. Администратор определяет границы диапазонов для ключа шардинга, и каждая такая граница образует отдельный шард. Например, можно разбить пользователей по user_id
: 1–1_000_000 – первый шард, 1_000_001–2_000_000 – второй, 2_000_001 и далее – третий, и так далее. Все записи с user_id
, попадающим в определенный диапазон, хранятся в одном шарде.
Плюсы этого метода – простота и логичность. Легко понять, где лежат данные: достаточно сравнить значение ключа с границами. Запросы, фильтрующие данные по этому ключу, могут маршрутизироваться непосредственно на нужный шард без лишних операций. Кроме того, связанные данные с близкими значениями ключа оказываются в одном узле, что может ускорять range-запросы (например, выборку за определенный период времени, если ключ – дата). Диапазонное шардирование часто используют для временных данных (таймлайны, логи): например, каждый шард хранит данные за определенный год или месяц.
Однако есть и минусы. Основная проблема – неравномерная нагрузка при неравномерном распределении самих значений. Если данные поступают последовательно (монотонно возрастающий ключ), то сначала заполняется первый шард, затем нагрузка переключается на следующий и т.д., вместо параллельной работы всех узлов. В примере с форумом и шардингом по user_id
сначала весь трафик идет в шард 1, потом во 2 и лишь затем в 3. Это приводит к перекосу: одни шарды перегружены, а другие простаивают. Решать проблему можно предварительным заданием разумных диапазонов или периодическим перераспределением (расщеплением) горячих диапазонов, но это усложняет эксплуатацию. Таким образом, range-sharding хорошо подходит для данных с естественным распределением диапазонов (например, по алфавиту фамилий пользователей или географическим зонам), а также удобен при необходимости сегментировать данные по времени. Но при бурном росте данных может потребоваться пересмотр границ и решардирование – перенос части данных в новые диапазоны, что является трудоемкой задачей (подробнее о проблеме решардинга см. ниже).
Шардирование по хешу (Hash-Based Sharding)
Принцип хеш-шардирования: равномерное распределение ключей по шардам с помощью хеш-функции. В примере user_id=2001
преобразуется хеш-функцией в значение 548291, которое по модулю числа шардов (3) дает 2 – значит запись попадет в Шард 2.
При хеш-шардировании для вычисления целевого шарда применяется хеш-функция к значению ключа. Алгоритм выглядит так: берется значение ключа шардинга (например, user_id
), вычисляется его хеш (через функцию вроде MD5, SHA-1, CRC32, MurmurHash и т.п.), полученное число отображается на индекс шарда – чаще всего берется остаток от деления хеша на количество шардов. Например, если хеш от 2001 равен 548291, то 548291 % 3 = 2
, значит запись идет во второй шард. В результате такого подхода значения ключа распределяются практически равномерно случайным образом между всеми узлами.
Главное достоинство хеш-стратегии – равномерное распределение нагрузки. Если хеш-функция хорошая, то никаких “горячих” шардов не будет: независимо от порядка или закономерностей поступления данных, записи расползаются по разным узлам приблизительно поровну. Это отличный вариант, когда невозможно предсказать характер обращений к данным или когда важно сбалансировать нагрузку на запись/чтение между узлами. Например, социальные сети часто используют хеширование по идентификатору пользователя, чтобы равномерно распределить миллионы аккаунтов по множеству серверов.
Недостаток хеш-шардирования – потеря “смысла” в расположении данных. Так как ключи дробятся по псевдослучайному признаку, то связанные по диапазону записи разлетаются в разные шарды. Это затрудняет range-запросы (выборки по диапазону значений ключа): их придется выполнять на всех шардах и агрегировать результат. Кроме того, добавление нового шарда в кластер – нетривиальная задача: при изменении числа узлов обычно приходится перехешировать значительную часть данных, перераспределяя их по новому количеству модулей. Существует усовершенствование – консистентное хеширование (circular sharding), при котором шарды организуются в кольцо и при добавлении/удалении узла перераспределяется только часть ключей, но это более сложная схема. В любом случае, динамическое масштабирование хеш-кластера требует продуманного решения (например, зарезервировать заранее больше “виртуальных” шардов, чем физических узлов, как это сделано в некоторых системах, и при расширении просто переносить виртуальные сегменты на новые серверы). В целом, хеш-шардирование – отличный выбор для равномерной нагрузки, но менее гибкий для сложных запросов по диапазонам и для изменения топологии кластера.
Каталог (директория) соответствий (Directory-Based Sharding)
Схема маршрутизации запросов при шардировании через центральный каталог соответствий. Маршрутизатор обращается к каталогу, чтобы узнать, на каком шарде лежат данные с order_id = 98765
(по правилу: диапазон 90000–99999 хранится на Shard 3).
Directory-based подход вводит отдельный справочник (каталог) – централизованную таблицу маршрутизации, где хранится отображение диапазонов или конкретных значений ключа на идентификаторы шардов. Фактически, это внешняя маппинг-таблица: по ключу шардинга можно найти номер шарда. Маршрутизатор сначала делает запрос в каталог соответствий, получает ответ – например, что значение X
находится на шарде №3 – и затем направляет основной запрос на нужный узел.
Такой метод более гибкий по сравнению с чисто алгоритмическими (диапазон или хеш), потому что логику распределения можно менять, не затрагивая код маршрутизатора. Достаточно обновить записи в каталоге. Это упрощает перенос отдельных клиентов или диапазонов на другие узлы, позволяет иметь неравномерные сегменты (например, если одному клиенту выделен свой шард, а остальные поделены по группам). Directory-based sharding часто используется, когда требуется тонко контролировать размещение данных или при необходимости динамически масштабировать систему. Например, крупному арендатору (тенанту) в SaaS-приложении можно вручную назначить отдельный шард, просто добавив соответствующее правило в каталог, не меняя алгоритмы хеширования.
Минусы каталога – это дополнительный уровень индирекций и, потенциально, единая точка отказа. Если каталог – централизованный сервис/таблица, его отказ сделает невозможным маршрутизацию. Его нужно резервировать или дублировать, что усложняет систему. Также любой запрос требует сначала обращения к каталогу, что добавляет задержку. Однако обычно каталог сравнительно небольшого размера и может храниться в памяти, а поиск в нем осуществляется по хеш-таблице или дереву, поэтому накладные расходы минимальны. Многие современные системы фактически комбинируют подходы: используют хеш или диапазон как основу, но хранят метаданные о распределении в центральной координаторной базе (например, Citus хранит распределение шардов в координаторе, YDB – в выделенном механизме и т.п.).
Географическое шардирование (Geographical Sharding)
Географическое (региональное) шардирование – частный случай directory-based или range-подхода, когда ключом шардинга служит регион/локация данных. Например, глобальный сервис может разделить пользователей по географическому признаку: европейские пользователи на одном сервере (европейский датацентр), американские – на другом, азиатские – на третьем и т.д. Таким образом данные каждого региона хранятся ближе к самим пользователям, уменьшая задержки доступа и позволяя учитывать локальные требования (например, хранение персональных данных в пределах страны).
Обычно для гео-шардирования в структуре данных есть поле, указывающее регион (country_code, region_id и т.п.), и шард выбирается на основе его значения. Это похоже на шардирование по списку значений (list partitioning) – например: “US -> Shard 1, EU -> Shard 2, APAC -> Shard 3”. Преимущество стратегии в том, что она улучшает пользователям опыт (быстрый доступ к близлежащему серверу) и может снизить нагрузку на межконтинентальные каналы. Кроме того, разные шарды могут настраиваться по-разному (например, часовые пояса, локальные резервные копии).
Недостатки географического разделения – потенциал неравномерности. Разные регионы генерируют разный объем данных: может оказаться, что один регион (например, США) требует в разы больше ресурсов, чем другой. Тогда кластер может испытывать перекос нагрузки по шардам. Также сложнее выполнять глобальные агрегаты – например, построить мировой рейтинг пользователей придется объединением данных с всех региональных баз. Тем не менее гео-шардирование широко применяется в распределенных системах, требующих низкой задержки по всему миру (социальные сети, онлайн-игры, CDN-статистика и т.д.).
Отметим, что к алгоритмам распределения можно отнести и вертикальное шардирование, но оно несколько выходит за классическую дефиницию (по сути это разделение таблиц по столбцам между разными базами). Вертикальное разбиение больше относится к разделению по функциональности или микросервисной архитектуре, когда разные подсистемы (с разными наборами таблиц) выносятся на отдельные базы. Иногда вертикальное разделение сочетают с горизонтальным шардингом внутри каждой группы таблиц. В контексте PostgreSQL под вертикальным шардингом можно понимать разбивку одной “толстой” таблицы на две по столбцам (как показано в примерах), но чаще под этим имеют в виду именно выделение разных доменов данных в разные базы (например, профиль пользователя – в одной БД, лог активности – в другой). Такой подход улучшает производительность запросов (меньше ширина таблиц) и масштабирует конкретные узкие места, но требует изменения схемы и логики приложения.
Подводя итог, выбор стратегии шардинга зависит от характера данных и запросов. Если нужна простота и понятность – выбирают диапазоны. Если важна балансировка нагрузки – хеширование. Если требуется гибкость и контролируемое разбиение – каталог соответствий или географический признак. В реальных системах нередко комбинируют подходы (например, в распределенной временн?й базе можно применить гибрид: сначала партиционирование по дате, а затем шардирование по хешу внутри каждого временного раздела). Грамотное планирование ключа и алгоритма шардинга закладывается на этапе проектирования системы, так как изменять эти правила “на лету” непросто.
Подходы к реализации шардирования в PostgreSQL
Как отмечалось, PostgreSQL сам по себе не распределяет данные по узлам автоматически – для этого нужны либо дополнительные инструменты, либо архитектурные решения на уровне приложения. Рассмотрим различные подходы к внедрению шардинга в PostgreSQL, которые сегодня используются на практике:
- Ручное шардирование и логика на уровне приложения – когда всю ответственность за разбиение базы берет на себя разработчик (архитектор) и код приложения.
- Использование расширений распределенной БД – например, Citus (Hyperscale), историческое pg_shard и др., которые добавляют в PostgreSQL прозрачный уровень шардирования.
- Шардирование через Foreign Data Wrapper (FDW) – объединение нескольких серверов PostgreSQL “вручную” средствами внешних таблиц и встроенного партиционирования.
- Отдельные форки и решения – специализированные кластеры PostgreSQL и надстройки (Postgres-XL, Postgres-XC, Greenplum, Shardman и т.д.), а также распределенные NewSQL-СУБД, совместимые с протоколом Postgres.
Рассмотрим плюсы, минусы и особенности каждого подхода подробнее.
Ручное шардирование и приложение-роутер
Самый простой (с точки зрения отсутствия внешних зависимостей) способ – реализовать шардирование вручную. Это означает, что вы самостоятельно создаете несколько экземпляров PostgreSQL (несколько баз данных на разных серверах), делите между ними данные по какому-то признаку и прописываете логику маршрутизации запросов в приложении или промежуточном сервисе. Проще говоря, приложение само должно решать, в какую базу идти для каждого запроса. Такой подход еще называют client-side sharding или in-app шардирование.

Рис. 1: Архитектура ручного шардирования с маршрутизацией на уровне приложения
Как это выглядит? Обычно на старте проекта выбирают ключ шардинга (например, customer_id
). Затем создается N одинаковых по схеме баз данных (шардов). В коде приложения прописывается функция маршрутизации: например, вычислять shard_id = hash(customer_id) % N
– и на основе этого значения выбирать соответствующий пул соединений или DSN. Все операции с БД в приложении проходят через эту функцию. Таким образом, при вставке данных приложение решает, в какой шард их записать, а при чтении – из какого шарда прочитать.
Пример: команда AliExpress Russia (Order Management System) описывает свой опыт in-app шардинга PostgreSQL для хранения порядка 8 ТБ данных заказов. Они разбили данные на 64 шарда, а маршрутизация реализована на уровне .NET-приложения с использованием Npgsql (драйвер PostgreSQL). Ключ шардинга – идентификатор заказа, преобразованный хеш-функцией, обеспечивающей равномерное распределение между 64 узлами. При запуске приложения инициализируется пул подключений ко всем шардам, а затем каждая операция направляется на нужный узел.
Преимущества ручного подхода:
- Полный контроль – разработчик точно знает, где что хранится, и может настроить кастомную логику. Нет “магии” – шардинг реализован библиотечными средствами (пул подключений, простая математика для выбора шарда).
- Простота архитектуры – отсутствуют дополнительные сложные слои. По сути, это несколько независимых PostgreSQL плюс немного кода. Нет зависимости от сторонних расширений: обновление PostgreSQL не привязано к совместимости с расширением.
- Гибкость в выборе стратегии – можно реализовать любой алгоритм (хеш, диапазон, список, каталог) прямо в коде. Например, Ozon Tech в своем туториале по шардингу на Go показывает, как приложение может использовать простое правило: остаток от деления ID на число шардов. Если со временем понадобится поменять правило – разработчик меняет код маршрутизатора и мигрирует данные соответственно.
Однако минусы весьма существенны:
- Сложность разработки и поддержки. Ручной шардинг – «велосипед», который требует тщательной проработки. Нужно учесть массу деталей: от схемы распределения ключей до ошибок сетевого взаимодействия. Неправильная реализация может приводить к потерям или дублированию данных. Кроме того, каждый разработчик в команде должен понимать эту логику при работе с БД.
- Отсутствие кросс-шардовых возможностей по умолчанию. Например, JOIN между шардами или агрегат по всем данным – такие запросы придется реализовывать вручную (делать несколько запросов и сводить результаты в коде). Вручную же придется решать проблему глобальных уникальных идентификаторов – автоинкремент в каждом шарде будет генерировать пересекающиеся ключи, поэтому нужно предусмотреть стратегию (например, префиксы/диапазоны для каждого шарда или генерация UUID/Snowflake). Компания Instagram, например, внедряя шардирование на Postgres, реализовала свой генератор 64-битных ID (по принципу Twitter Snowflake) на основе комбинации таймстемпа, номера логического шарда и локального счетчика – такой подход обеспечил глобальную уникальность ID при вставке в тысячи шардов параллельно.
- Нет поддержки распределенных транзакций. Обычные
BEGIN ... COMMIT
работают только внутри одного шарда. Если бизнес-операция затрагивает две разные базы (например, перевод денег со счета A на счетеShard1 на счет B на Shard2), обеспечить атомарность крайне сложно. Либо нужно сводить все такие операции к eventually consistent механизму на уровне приложения, либо внедрять двухфазный коммит (2PC) самостоятельно. Многие вручную реализованные решения попросту не поддерживают межшардовые транзакции, накладывая это ограничение на уровень бизнес-логики (например, не допускать изменения данных двух клиентов из разных шардов в одной транзакции). - Рутинные задачи усложняются. Бекапы, восстановление, миграции схемы – все нужно повторять для каждого шарда. Добавление нового шарда требует продуманного плана (решардирование существующих данных, обновление конфигов). Мониторинг тоже усложняется: нужно отслеживать сразу несколько СУБД.
Стоит подчеркнуть: ручной подход разумен на ранних стадиях, когда горизонтальное масштабирование только планируется. Многие стартапы начинают с монолитной базы, затем переходят к клиентскому шардингу, когда появляются проблемы с масштабом. Однако по мере роста системы затраты на поддержку такого решения тоже растут. Поэтому далее появилось много полуручных, полуавтоматических решений, облегчающих жизнь разработчику.
Расширение Citus (распределенный кластер PostgreSQL)
Одним из наиболее популярных решений для горизонтального масштабирования PostgreSQL является расширение Citus. Оно начиналось как сторонний проект (Citus Data), а в 2019 году было приобретено Microsoft и теперь доступно как open-source расширение и как облачный сервис (Azure Cosmos DB for PostgreSQL, ранее известный как Hyperscale (Citus)). Citus превращает кластер узлов PostgreSQL в единую распределенную базу, добавляя поддержку шардирования таблиц и распределенного выполнения запросов.
Архитектура Citus: один из узлов кластера обозначается координатором (master), остальные – рабочие узлы (workers). На координаторе устанавливается расширение citus
(CREATE EXTENSION citus;
), после чего он становится точкой входа для запросов. Рабочие узлы регистрируются командой SELECT master_add_node(...)
с указанием их адресов. Данные размещаются на шардах (партициях) рабочих узлов, а координатор хранит метаданные (внутренние таблицы с распределением шардов). Когда приходит запрос от приложения на координатор, Citus определяет, к каким шардам его направить, запускает параллельные запросы на воркерах и агрегирует результат.

Рис. 2: Архитектура распределённого кластера PostgreSQL с расширением Citus
Использование Citus: разработчик в своем приложении по-прежнему работает с PostgreSQL, но должен выполнить некоторую настройку. Во-первых, нужно определиться, какую таблицу распределять и по какому ключу. Например, пусть имеется таблица логов app_logs(service_name, user_id, log_time, message)
– решаем шардировать по полю service_name
. На координаторе сначала создается обычная таблица, затем одной командой превращается в распределенную:
CREATE EXTENSION citus;
CREATE TABLE app_logs (
log_id bigserial,
service_name text NOT NULL,
user_id int,
log_time timestamptz default now(),
message text,
PRIMARY KEY (log_id, service_name)
);
SELECT create_distributed_table('app_logs', 'service_name');
Эта команда разрезает логическую таблицу на шарды по значению service_name
(по умолчанию используя хеш-шардирование) и распихивает их по рабочим узлам. Теперь при выполнении INSERT
или SELECT
Citus будет действовать как маршрутизатор: вычислять хеш от service_name
и отправлять запись в соответствующий шард на нужном узле. Если запрос фильтрует по service_name
(как SELECT * FROM app_logs WHERE service_name='auth-service'
), координатор знает, на каком узле лежат данные этого сервиса, и пошлет запрос только туда. А вот запросы без фильтра (например, агрегирование по всем сервисам) координатор разошлет параллельно на все узлы и затем соберет результаты. Для оптимизации Citus поддерживает концепцию reference tables – таблиц, дублированных на всех шардах (например, справочник статусов, к которому нужно джойнить), и co-located tables – нескольких распределенных таблиц, шардированных по одному ключу (это позволяет делать локальный JOIN на узле). В совокупности, Citus стремится предоставить максимально прозрачный опыт: вы пишете стандартный SQL, а он исполняется на кластере.

Рис. 3: Последовательность обработки запроса при ручном шардировании
Плюсы Citus:
- Прозрачность для разработчика. Приложение может подключаться к координатору и выполнять SQL, как обычно. Citus сам ведает, куда пойдут данные. Нет необходимости вручную писать логику маршрутизации в коде приложения – достаточно один раз настроить распределенные таблицы.
- Полнота SQL-функциональности (в основном). Citus поддерживает сложные запросы: JOIN, агрегаты, подзапросы. Разумеется, есть ограничения (например, JOIN двух больших распределенных таблиц по неколокированному ключу – неэффективен и может не поддерживаться оптимизатором), но в целом для распространенных сценариев (мультитенантные приложения, аналитические запросы в реальном времени) покрытия достаточно. Есть поддержка распределенных транзакций – координатор может координировать 2PC между узлами, что позволяет делать изменения в нескольких шардах атомарно (хотя это и с накладными расходами).
- Автоматизация управления. Citus предоставляет функции для ребалансировки –
rebalance_table_shards
для перераспределения данных при добавлении новых узлов, расщепления слишком больших шардов, слияния мелких и т.д. Также имеется интеграция с резервным копированием, мониторингом (в облачной версии). - Комбинация с партиционированием. Можно совмещать встроенное партиционирование PostgreSQL и шардинг Citus. Например, таблица может быть сначала партиционирована по дате, а затем каждая партиция распределена по узлам (Citus поддерживает вложенное партиционирование). Это особенно удобно для массивных временных данных, позволяя эффективно удалять старые разделы и параллельно масштабировать по узлам.
Минусы Citus и особенності:
- Ограничения на схему и ключи. Нужно заранее выбирать распределяемые таблицы и их ключи. Поменять решение потом – непросто (потребуется перекачивать данные). Не каждую таблицу имеет смысл шардировать – например, маленькие справочники проще держать на координаторе (как reference). Таким образом, архитектура базы под Citus требует планирования.
- Ограниченная совместимость расширений. Не все расширения PostgreSQL будут работать корректно в распределенном режиме. Например, что-то сильно привязанное к локальному узлу или не учитывающее sharding (хранимки, триггеры, специфичные типы) – нужно проверять. Однако многие вещи, типа PostGIS, полнотекстового поиска, сейчас уже поддерживаются.
- Зависимость от версии PostgreSQL. Citus – активно развиваемое расширение. Оно догоняет новые версии Postgres, но с некоторым лагом. Хотя в последние годы (Citus 11, 12) интеграция стала лучше, все же перед обновлением кластера PostgreSQL нужно убедиться, что ваша версия Citus совместима.
- Развертывание и отладка. Самостоятельный разворот Citus-кластера требует настроек (шарды, узлы, конфигурация координатора). В Azure это делается автоматически. При отладке производительности распределенных запросов может потребоваться понимание, как Citus планирует запрос (есть функция
EXPLAIN ANALYZE VERBOSE
показывающая план с обращениями к шардам).
В целом, Citus считается промышленно готовым решением для шардинга PostgreSQL. Он особенно хорошо подходит для многопользовательских SaaS-систем (каждому клиенту – своя доля данных на кластер), для высоконагруженных time-series и аналитических систем, где требуется масштабировать объем данных и частоту запросов за пределы одного узла. Среди примеров использования – крупные установки PostgreSQL с десятками узлов: торговые площадки, телеметрия, игровые бэкенды. С введением Schema-based sharding в Citus 12 стало проще мигрировать существующие многосхемные базы (с множеством схематически одинаковых шариков) на распределенный кластер. С каждым релизом Citus расширяет поддерживаемые случаи, делая шардирование все более “нативным” для PostgreSQL.
Историческая справка: расширение pg_shard – предшественник Citus. Это был проект компании Citus Data, который позволял шардинг таблиц в Postgres с репликацией для отказоустойчивости. Pg_shard был выпущен около 2015 года, но вскоре функциональность была интегрирована в Citus, и отдельно pg_shard перестал развиваться. Сейчас о pg_shard упоминают лишь в историческом контексте – фактически, если вы используете Citus, вы получаете все возможности pg_shard и больше.
Шардирование с помощью Foreign Data Wrappers (FDW)
Другой подход, который можно реализовать, не прибегая к нестандартным форкам – это использовать механизм Foreign Data Wrapper (postgres_fdw) и встроенное партиционирование PostgreSQL. Идея в том, чтобы завести на главном сервере PostgreSQL таблицу-парент, разбитую на разделы (PARTITION BY HASH, например), а каждый раздел сделать FOREIGN TABLE на удаленный сервер (шард). Таким образом, когда на основной сервер прилетает запрос к большой таблице, он прозрачно проксирует его на нужный узел (если запрос фильтруется по ключу партиции, то сработает constraint exclusion и затронет только один foreign-таблицу). По сути, основная БД играет роль маршрутизатора, используя встроенные средства партиционирования и FDW для доступа к шардам.

Рис. 4: Архитектура шардирования PostgreSQL с использованием Foreign Data Wrapper
Преимущество такого решения – все средства “из коробки” PostgreSQL. Не нужно внешних расширений. И планировщик знает про разделы, может выполнять объединения результатов. Такой способ обсуждался и в сообществе как потенциальный путь к “официальному” шардингу. Однако на практике у подхода через postgres_fdw много ограничений:
- Нет полного pushdown’a запросов. Запросы ко внешним таблицам не всегда полностью выполняются на удаленной стороне. Например, сложный
JOIN
между двумя шардами придётся делать на главном узле, вытаскивая кучу данных по сети – это узкое место производительности. Citus решает это своим планировщиком, а vanilla FDW – нет. - Проблемы с изменением схемы. Добавить колонку в распределенную таблицу – нужно повторить операцию на каждом узле вручную, нет единой команды. Любые DDL не транслируются автоматически на шарды.
- Нет автоматического ребалансинга. Если один шард разросся, нет встроенного механизма перенести часть данных на другой сервер – все вручную: создавай новый сервер, переназначай партицию, возможно, дели таблицы.
- Отсутствие глобальных функций. Нет поддержки глобальных уникальных ограничений, последовательностей, типов данных – все такие объекты локальны конкретному серверу и никак не согласованы.
- Ограниченная параллелизация. Планировщик PostgreSQL научился некоторому параллелизму с FDW, но далеко не так эффективно, как специализированные решения. Например, агрегат по всем разделам может выполняться последовательно опрашивая узлы, вместо параллельного выполнения на них и объединения.
- Отсутствие распределенной 2PC. Если транзакция модифицирует данные на нескольких внешних серверах, велика вероятность частичного коммита при сбоях – PostgreSQL не координирует транзакции между серверами (если не использовать вручную PREPARE TRANSACTION и внешний координатор).
- Нагрузка на основной узел. Главный сервер становится точкой консолидации. Он хранит все партиции (пусть и пустые), держит открытые подключения ко всем нодам, обрабатывает координацию. Его отказ влечет недоступность всей системы (нужен фейловер).
- Отсутствие встроенной репликации между шардами. Если нужно иметь резерв шарда, то на каждом удаленном узле надо настроить стандартную репликацию. Это повышает надежность, но не прозрачно для основного сервера – он видит только один endpoint шарда.
В блоге CitusData прямо говорится, что sharding через postgres_fdw – это, по сути, manual sharding с небольшой возможностью выполнять простые запросы через одну точку, но без многих ключевых функций, необходимых для масштабируемости. Тем не менее, такой метод может быть оправдан для относительно простых сценариев – например, разделить пользовательские данные по десятку серверов и иметь возможность с одного узла делать селекты по конкретному пользователю.

Рис. 5: Последовательность обработки запроса при использовании FDW
Комьюнити PostgreSQL продолжает улучшать и FDW, и партиционирование – возможно, в будущем появится более полная встроенная поддержка шардирования. Уже в PostgreSQL 15-16 появились, например, публичные подписки логической репликации, позволяющие частично реплицировать таблицы на узлы, глобальные SNP-идентификаторы транзакций (подготовка для распределенных транзакций). Проекты вроде Postgres-XL/XC шли по пути создания своего планировщика и распределенной СУБД на основе Postgres, но они требуют форка ядра и отстают от актуальных версий.
Альтернативные решения и форки PostgreSQL
Помимо вышеупомянутых, существуют и другие системы, предоставляющие функциональность распределенной базы на основе PostgreSQL:
- Postgres-XL / Postgres-XC. Это ветви PostgreSQL, развивавшиеся для создания распределенного кластера с единым образцом базы. Postgres-XL поддерживает как распределение таблиц по узлам, так и репликацию, рассчитан как на OLTP, так и на OLAP нагрузки. Однако его развитие не синхронизировано с основной веткой Postgres (на момент 2023 г. последняя версия XL базируется на Postgres 10). Проект предлагается для спецслучаев, но не очень массово используется в сообществе. Отметим, что XL – довольно комплексная система с глобальным транзакционным менеджером, распределенным планировщиком и т.д., требующая серьезной экспертизы при эксплуатации.
- Greenplum Database. Форк PostgreSQL, ориентированный на аналитические нагрузки (OLAP). Greenplum изначально проектировался как MPP (massively parallel processing) СУБД для запросов анализа по большим данным. Он использует партиционирование и распределение таблиц, но рассчитан на большие батчевые запросы, менее оптимален для транзакционных нагрузок. В контексте шардинга его можно упомянуть как пример специализированного решения – по сути Greenplum “шардинг” интегрирован на уровне ядра, но за счет отказа от некоторых возможностей классического Postgres (например, ограниченная поддержка уникальных ключей, отсутствие полноценных FK и пр. в распределенных таблицах, поскольку это не критично для аналитики).
- Shardman (Postgres Pro). Компания Postgres Professional (разработчики оригинального Postgres в России) ведет проект Shardman – расширение для реализации шардинга непосредственно на PostgreSQL. Shardman работает поверх PostgreSQL 14+ с набором патчей, используя секционирование и FDW как базис, но добавляя распределенный менеджер транзакций, глобальные индексы, утилиты администрирования кластера (демоны на узлах, etcd как хранение метаданных). Цель – добиться прозрачного для приложения масштабирования без отказа от ACID. Проект все еще в разработке (планируется перенос на PostgreSQL 17, интеграция новых возможностей). Shardman – интересен тем, что показывает направление, куда движется PostgreSQL: возможно, через некоторое время подобные наработки войдут в основную ветку или будут доступны как расширение “первого класса”.
- NewSQL базs с интерфейсом Postgres. Хотя это уже не PostgreSQL, упомянем, что существуют распределенные СУБД, совместимые с протоколом Postgres, например CockroachDB, YugabyteDB. Они позволяют использовать драйвер PostgreSQL и выполнять SQL-запросы, а под капотом реализуют шардинг и репликацию (часто с компромиссами по отношению к строгой консистентности или производительности). Эти решения зачастую ориентированы на облегчение миграции: вы получаете масштабируемость и отказоустойчивость “как в NoSQL”, но не переписывая код, если он использует PostgreSQL. Однако внутри – это совершенно иные движки (не на основе ядра Postgres), поэтому их поведение и возможности могут отличаться от классического PostgreSQL (например, CockroachDB имеет ограниченную поддержка функций SQL, а Yugabyte не поддерживает некоторые экстеншены Postgres, и т.д.).
Подводя итог по подходам: наиболее зрелым решением для шардинга PostgreSQL сегодня является Citus, если нужна относительно бесшовная интеграция. Для легковесных случаев и небольшого числа узлов можно использовать FDW+партиционирование, осознавая ограничения. Если же требования специфичны или вы не можете (не хотите) вводить в стек СУБД что-то помимо “чистого” Postgres, то ручное шардирование – хоть и трудоемкий, но жизнеспособный путь, проверенный многими компаниями (Instagram, Asana, AliExpress и т.д.). Вероятно, будущее за развитием встроенных возможностей Postgres, но на 2025 год для продакшена приходится выбирать из внешних решений или своих реализаций.
Плюсы и минусы шардирования
Применение шардирования как архитектурного решения дает значительные преимущества в масштабируемости, но не лишено и недостатков. Рассмотрим основные плюсы и минусы.
Преимущества шардинга
- Горизонтальное масштабирование нагрузки. Это главное достоинство: шардирование позволяет распределить как объем хранимых данных, так и нагрузку чтения/записи между несколькими узлами. Вместо упорания в лимиты одного сервера (CPU, RAM, диск, сеть) мы можем добавлять новые машины в кластер, тем самым повышая общую производительность. В идеале, при правильном ключе, 2 сервера обрабатывают в ~2 раза больше транзакций, 10 серверов – в ~10 раз и т.д.
- Масштабирование объема данных. Даже если нагрузки нет, но данных очень много, шардирование позволяет выйти за ограничения одного инстанса. Например, если база достигла нескольких терабайт и уже не помещается на одном быстром диске или бэкап/восстановление занимает неприемлемо много времени, то разбивка на шарды решает эту проблему. Вместо одной таблицы на 2 млрд строк мы можем иметь 10 таблиц по 200 млн на разных узлах. Это уменьшает объем индексов на каждой ноде и может улучшать время ответа (поскольку индекс-деревья меньше, кэшируется больше эффективно и т.п.).
- Локализация данных для пользователей. Географическое шардирование, как упоминалось, позволяет хранить данные ближе к их потребителям. Это снижает latency запросов для распределенных по миру приложений. Пользователи из Европы общаются с европейским датацентром, из Азии – с азиатским, и все довольны быстротой отклика.
- Разграничение по функциональности / бизнесу. Иногда шардирование используют для изоляции разных частей системы. Например, мультитенантное SaaS-приложение – каждому клиенту можно дать выделенный шард. Это повышает безопасность (данные клиентов полностью изолированы друг от друга на уровне базы), упрощает перенос конкретного клиента (можно переместить его шард на отдельный кластер, если он вырос), позволяет кастомизировать конфигурации под клиента. В веб-сервисах часто применяется подход “один крупный клиент – одна база”. Шардирование упрощает это, предоставляя шаблон.
- Комбинация с репликацией для высокой доступности. Шардирование не исключает репликацию – наоборот, обычно каждый шард реплицируется на один или более стендбай-узлов для отказоустойчивости. Таким образом, при выходе из строя целого сервера теряется только один фрагмент данных, и даже он может быть поднят за счет реплики. Сама идея шардинга – убрать единую точку отказа, связанную с одной большой базой: если такой сервер упадет, вся система простаивает; а при шардах риск распределяется. (Конечно, появляется новая точка отказа – маршрутизатор или координатор, но и он может быть резервирован.)
Недостатки шардинга
- Сложность архитектуры и приложения. Внедрение шардинга резко повышает сложность системы. Нужно управлять множеством баз вместо одной. Необходимо либо писать дополнительную логику (если делаете кастомный роутер), либо внедрять сторонний продукт (изучать его, адаптировать). Для разработчиков и DevOps это усложнение: больше компонентов – больше потенциальных точек отказа, больше места для ошибок конфигурации. Управление конфигурацией, секьюрностью, обновлениями – все умножается на количество шардов. Как отмечает Ozon Tech, при клиентском шардировании “экземпляры БД даже не подозревают о существовании друг друга, шардированием управляет стороннее приложение – со всеми вытекающими рисками”.
- Ограничения на операции с данными. Самая серьезная жертва – потеря некоторых возможностей привычной реляционной модели. Например, глобальные уникальные ограничения (UNIQUE, FOREIGN KEY на весь набор данных) становится либо невозможным, либо крайне неэффективным. Нельзя простым способом гарантировать уникальность значения на всех шардах (если только не придумывать централизованный генератор идентификаторов или не делать запросы во все шарды перед вставкой). Транзакции – как уже обсуждалось, ACID-гарантии в пределах одного шарда есть, но распределенные транзакции тяжелы (2PC) или не используются, поэтому приходится проектировать систему так, чтобы минимизировать сценарии, требующие изменений на нескольких шардах сразу. JOIN-ы между данными разных шардов – тоже под вопросом: либо медленно через приложение, либо нужны механизмы вроде того же Citus, который способен часть джойнов выполнять распределенно. В простом случае, если вам надо объединить таблицы, лежащие на разных серверах, придется получать куски и джойнить на стороне приложения.
- Проблемы с агрегатами и аналитикой. Любой запрос, охватывающий более одного шарда (например, “выбрать 100 последних событий из всей системы” или “подсчитать общее количество пользователей”) требует обращения ко всем шардам и слияния результатов. Это называется запросы “scatter-gather” (разбросать и собрать). Они могут сильно нагружать систему, особенно если делаются часто. Приходится либо поддерживать промежуточные агрегаты (например, хранить отдельную денормализованную таблицу с глобальными итогами), либо использовать сторонние решения (например, выгружать данные из всех шардов в хранилище для анализа).
- Распределение нагрузки может быть неравномерным. Неправильный выбор shard key грозит феноменом hot shard – когда один узел оказывается узким местом, а остальные простаивают. Например, если шардировать интернет-магазин по
region
, то может выясниться, что 50% трафика – из одного региона, и один шард перегружен, а остальные на четверть мощности. Переразбиение данных (решардирование) – сложная процедура: надо либо переводить часть данных на другие узлы вручную, либо останавливать систему и перестраивать ключ. Поэтому на этапе дизайна крайне важно спрогнозировать объемы и паттерны доступа. Но в реальности нагрузки меняются, данные “перетекают”, потому полностью избежать hot-spot’ов сложно. - Усложнение процессов администрирования. Резервное копирование шардированного кластера – нетривиальная задача. Нужно либо на каждом шарде запускать бэкап последовательно, либо иметь распределенный инструмент. Восстановление – еще хуже, особенно если данные частично устарели на разных узлах. Миграции схемы – на десятке узлов выполнить ALTER TABLE нужно синхронно, иначе получим разнобой версий. Мониторинг – нужно следить за здоровьем множества серверов, собирать метрики и логи со всех. Шард-менеджмент – если упал один шард, система частично недоступна; нужно уметь оперативно поднимать реплику, перенаправлять туда трафик. В распределенной системе много операционных хлопот, требующих автоматизации и умелого администрирования.
- Повышенные требования к инфраструктуре. Больше серверов – больше стоимость обслуживания (хотя железо может быть дешевле поштучно, но все равно). Сеть между шардами должна быть быстрой и надежной (особенно если есть кросс-шардовые запросы). Необходимо учитывать консистентность времени (при распределенных транзакциях – проблема часов, хотя Postgres в целом может на это не полагаться, но некоторые решения требуют синхронизации). Существуют и лимиты, например, если у вас слишком много шардов, на стороне приложения может возникнуть нехватка файловых дескрипторов или соединений (нужны пулы, шардинг самих пулов и пр.).
Таким образом, шардирование – это всегда компромисс. Мы обмениваем удобство и простоту классической монолитной базы на масштабируемость. Поэтому применять шардинг имеет смысл только когда действительно достигнуты или близки пределы одиночной базы, и другие оптимизации исчерпаны (индексы, более мощное железо, разделение чтения/записи через реплики и т.п.). Как говорится, “Sharding is a solution to have when you need it, not before” – преждевременное “расшардивание” приносит больше проблем, чем решает.
Сравнение с репликацией и партиционированием
Шардирование – не единственный подход к масштабированию баз данных. Часто используются и/или комбинируются такие методы, как репликация (master-slave, master-master) и партиционирование таблиц. Важно понимать, чем они отличаются от шардирования, и в каких случаях что применять.
Шардирование vs Репликация. При репликации мы создаем копии одной и той же базы на нескольких узлах. Как правило, есть ведущий сервер (принимает записи) и несколько ведомых (только читают копию данных с некоторым лагом). Репликация решает задачу масштабирования чтения и повышение отказоустойчивости: можно распределить SELECT-запросы по репликам, разгрузив мастер, а также при падении мастера переключиться на одну из реплик. Однако по сути это вертикальное масштабирование по нагрузке: каждый узел все равно содержит полный объем данных. Репликация не дает выигрыша в занимаемом объеме или возможности хранить больше данных, чем на одном сервере – каждый репликат хранит то же самое. Она также не увеличивает совокупную пропускную способность на запись: все изменения все равно последовательно проходят через мастер (или конфликтно через мульти-мастер, что еще сложнее). Поэтому если проблема – объем данных или нагрузка на запись, репликация не поможет. Шардирование же напротив, масштабирует объем и write throughput, так как разные узлы принимают разные данные.
Зато репликация проще: логически данные те же, все запросы (кроме особо читающих) можно отправлять на мастер как раньше, консистентность строгая (в рамках одного узла), разработчику не нужно менять модель данных. В идеале, системы дополняют друг друга: шардирование + репликация – каждый шард имеет реплики. Репликация без шардирования применяется на ранних этапах, когда один сервер еще тянет нагрузку по объему, но хочется распределить чтение и иметь резерв. Репликация не требует изменений в приложении (если не считать настройку routing read-only запросов). Так что, сначала масштабируют чтение репликами, а уже потом масштабируют запись – шардингом. Естественно, при шардинге тоже можно использовать реплики. Отличие хорошо подмечено: “при репликации дополнительные узлы – это резерв и балансировка, а при шардировании – составные части единого хранилища”.
Шардирование vs Партиционирование. В PostgreSQL давно есть секционирование таблиц (партиционирование) – логическое разбиение одной большой таблицы на набор поменьше по заданному ключу (range, list, hash). Партиционирование может улучшить производительность запросов (если те фильтруют данные по ключу партиционирования, читается только нужный раздел), облегчает управление данными (например, быстро удалить старый раздел – вместо множества DELETE
), снижает конкуренцию за блокировки в очень больших таблицах и т.д.. Однако стандартное партиционирование выполняется внутри одной базы, на одном сервере. Его задача – оптимизировать работу на одном инстансе, а не масштабировать за пределы узла. Можно сказать, партиционирование – это способ “разделяй и властвуй” над одной таблицей: разбив ее на части, СУБД эффективнее управляет данными. Но все части хранятся в той же базе, просто как отдельные таблицы.
В отличие от шардинга, партиционирование не увеличивает пределы по объему (диск тот же) или CPU (процессор один), но позволяет лучше использовать имеющиеся ресурсы. Часто партиционирование является прелюдией к шардированию: вы делите таблицу на части, а затем эти части можно распределить по разным узлам (именно это делает метод через FDW, да и Citus под капотом создает партиции-шарды). Поэтому партиционирование и шардирование не взаимоисключающие техники, а взаимодополняющие. Как отмечают инженеры Citus, это ложная дихотомия – можно и нужно использовать оба подхода там, где это выгодно.
Основные отличия: при партиционировании в контексте PostgreSQL у нас один узел, один кластер БД, просто внутри него много таблиц-партиций. При шардировании – много узлов, каждый сам по себе кластер. Партиционирование – “на уровне таблицы”, шардирование – “на уровне базы”. В партиционировании не возникает проблемы распределенных транзакций или сетевых задержек – все разделы доступны локально. В шардировании же приходится учитывать распределенность. С другой стороны, партиционирование не поможет, если одна таблица разрослась до 5 ТБ, а сервер умеет обрабатывать максимум 2 ТБ – просто разбив на 5×1 ТБ разделов, вы не выйдете за пределы 5 ТБ физически на диске этого сервера. Шардирование – решает (положив по 1 ТБ на 5 машин).
В некоторых случаях партиционирование может выступать альтернативой шардингу: например, если узким местом является одна очень большая таблица, а остальные данные небольшие, можно разнести эту таблицу по разделам и хранить разделы на разных tablespace (даже на сетевых дисках). Это сложнее, но иногда практикуется. Однако в целом, если речь о реальном масштабировании, приходится вводить именно шардинг с несколькими серверами.
Ниже приведена сводная таблица сравнения этих подходов:
Критерий | Шардирование | Партиционирование | Репликация |
---|---|---|---|
Где выполняется | На нескольких серверах (кластер, много инстансов) | В пределах одного сервера (один инстанс) | На нескольких серверах (ведущий + копии) |
Цель применения | Горизонтальный рост, больше данных и нагрузка | Производительность больших таблиц, удобство управления | Доступность, отказоустойчивость, масштабирование чтения |
Данные на узлах | Уникальная часть набора (разделены по ключу) | Уникальная часть набора (разделы таблицы) | Полная копия всей базы (синхр. или асинхр.) |
Масштабирует запись | Да (разные узлы обрабатывают разные записи) | Нет (запись все равно в один инстанс) | Нет (запись идет только на мастер) |
Масштабирует объем | Да (суммарный объем = сумме объемов узлов) | Нет (ограничен объемом одного сервера) | Нет (каждый узел хранит полный объем) |
Сложность для приложения | Высокая: нужен маршрутизатор, ограничения на транзакции | Низкая: прозрачно, если запросы учитывают секции | Низкая: почти прозрачно (роутинг чтения на реплики) |
Доступность | Отказ одного шарда теряет часть данных, но остальная система работает. Требуется резервирование каждого шарда. | Отказ сервера = падение всей БД (нет распределенности, хотя можно использовать реплики для сервера) | Отказ мастера требует фейловера, данные не теряются (если реплика синхронна), отказ реплики не влияет на работу мастера |
Пример СУБД | Citus (Postgres), MongoDB, Cassandra, Elasticsearch (под капотом), Spanner, etc. | PostgreSQL (секционирование), Oracle Partitioning, MySQL Partitioning | PostgreSQL Streaming Replication, MySQL Replication, Oracle Data Guard |
(Примечание: можно комбинировать методы – например, партиционированная таблица на мастер-базе, плюс реплики этой базы для чтения; или шардинг с репликацией.)
Архитектура решения с Apache ShardingSphere
Apache ShardingSphere – это открытая платформа для распределённых баз данных, предоставляющая набор инструментов для шардирования, репликации и других расширенных функций SQL. Существует два основных способа интеграции ShardingSphere в приложение: встраиваемый драйвер ShardingSphere-JDBC и прокси-сервер ShardingSphere-Proxy. В контексте Spring-приложения чаще используется ShardingSphere-JDBC, который подключается как обычный JDBC-драйвер и выполняет логику шардирования на стороне приложения. В отличие от прокси, ShardingSphere-JDBC поставляется в виде библиотеки (JAR) и не требует отдельного сервиса – приложение напрямую подключается к каждой базе данных через этот драйвер. Таким образом, ShardingSphere действует как «умный» DataSource, перехватывающий SQL-запросы и маршрутизирующий их на нужные шарды.

Рис. 6: Архитектура Spring Boot сервиса с Apache ShardingSphere. Один сервис использует ShardingSphere-JDBC как источник данных, взаимодействуя с несколькими узлами PostgreSQL. Реализована шардинг по двум физическим базам (Shard1
, Shard2
), каждая из которых имеет основную и реплицируемую БД для чтения.
На диаграмме выше показан пример архитектуры: один бэкенд-сервис (Spring Boot) включает драйвер ShardingSphere. В конфигурации ShardingSphere задаются логические данные источники, соответствующие шардам (например, Shard1
и Shard2
), каждый из которых может представлять кластер PostgreSQL с мастер-репликацией. Приложение выполняет запрос через ShardingSphere, а тот прозрачно распределяет запросы между физическими базами. Клиент (пользователь) обращается по HTTP к сервису, сервис формирует SQL-запрос к логической базе данных, ShardingSphere парсит запрос, определяет целевой шард(ы) и выполняет запрос на соответствующей физической БД. Результаты при необходимости агрегируются и возвращаются как обычный ответ. Такая схема позволяет масштабировать базу данных горизонтально, не меняя остальной код приложения.
Виды шардирования в Apache ShardingSphere
Apache ShardingSphere поддерживает несколько стратегий распределения данных и нагрузки:
- Автоматическое шардирование – маршрутизация осуществляется ShardingSphere на основании значений шардирующего ключа, извлечённых из SQL. Разработчик настраивает правило (алгоритм) шардирования, после чего платформа автоматически определяет, на какой шард отправить запрос. Например, при шардировании по полю
user_id
система сама вычисляет целевую базу/таблицу по алгоритму (хэш, диапазон и т.п.). Автоматическое шардирование избавляет от необходимости указывать шард вручную в коде – достаточно включить шардирующий ключ в запрос. - Ручное шардирование (Hint) – даёт возможность вручную задавать правила маршрутизации, независимо от содержимого SQL. ShardingSphere позволяет использовать Hint API или специальные комментарии в SQL, чтобы принудительно направлять запрос на конкретный шард, минуя парсинг SQL. Этот способ применим, когда запрос не содержит явного шардирующего ключа или нужна кастомная логика. Например, с помощью
HintManager
в Java-коде можно установить требуемое значение шарда перед выполнением запроса. Обычно ручное шардирование используется редко, для спецслучаев, тогда как стандартное (автоматическое) покрывает большинство сценариев. - Шардирование по диапазонам – распределение данных по диапазонам значений ключа. Например, все заказы за 2023 год хранятся в шарде A, за 2024 – в шарде B. В ShardingSphere это достигается с помощью реализации
RangeShardingAlgorithm
, которая обрабатывает условия типаBETWEEN ... AND
или сравнения (>=
,<=
) для шардирующего ключа. Разработчик может задать собственный алгоритм для сопоставления диапазонов значений шардам (например, диапазоны дат, ID или других числовых интервалов). Если алгоритм диапазона не настроен, запросы сBETWEEN
по умолчанию маршрутируются на все шарды (full scan), поэтому при активном использовании диапазонных условий рекомендуется реализовать явный Range-алгоритм. - Шардирование по хэшу (по модулю) – наиболее распространённый подход, когда вычисление шарда происходит по остаточному принципу или другому хэш-функции на ключе. Например, можно распределять записи по двум шардам на основе
user_id % 2
: чётныеuser_id
– в шард0, нечётные – в шард1. В YAML-конфигурации ShardingSphere подобное правило задаётся через Inline-алгоритм:algorithm-expression: ds${user_id % 2}
. Хэш-шардирование хорошо распределяет нагрузку равномерно, но усложняет range-запросы (например, выборку диапазона ID, которые окажутся на разных шардах). Поэтому часто комбинируются обе стратегии: модуль для равномерности распределения и отдельный механизм обработки диапазонов при необходимости. - Репликация данных (Master-Slave) – ShardingSphere позволяет работать с кластерами баз данных, где каждый шард имеет ведущую (master) и ведомые (slave) реплики. Мастер-сервер принимает операции записи (INSERT/UPDATE/DELETE), реплики асинхронно получают изменения и используются для разгрузки операций чтения. Например, в кластер PostgreSQL можно настроить streaming replication с одним ведущим узлом и несколькими репликами; ShardingSphere не выполняет саму репликацию, но знает о топологии и направляет запросы чтения/записи на нужные узлы. Важная особенность – репликация обычно асинхронная, поэтому данные на слейвах могут отставать на доли секунды от мастера. Это накладывает ограничения: сразу после записи чтение из реплики может не увидеть новую запись. В критичных случаях можно принудительно читать с мастера или использовать синхронные реплики.
- Балансировка чтения/записи – механизм, дополняющий репликацию. ShardingSphere автоматически разносит нагрузку чтения между replica-серверами по заданной стратегии балансировки. Поддерживаются встроенные алгоритмы – например, случайное распределение (Random), круговой (Round Robin) и т.д. или можно задать свои (с весами). Записи всегда идут на мастер, а чтения распределяются по репликам. Кроме того, внутри одной транзакции ShardingSphere по умолчанию направит все запросы (и чтение, и запись) на мастер, чтобы избежать проблемы устаревших данных (опция
transactionalReadQueryStrategy
). Таким образом достигается разгрузка мастера: массовые SELECT выполняются на отдельных узлах. Разработчик может настроить стратегию – например,transactionalReadQueryStrategy: PRIMARY
(всегда читать с мастера в транзакции) илиDYNAMIC
(чтения внутри транзакции могут идти на любые реплики, не рекомендуется для строгой консистентности).
В дополнение к вышеперечисленному, ShardingSphere поддерживает сложные сценарии шардирования:
- Композитное шардирование (Complex Keys) – когда используются сразу несколько полей в качестве ключа (например, и
region_id
, иuser_id
вместе). Разработчик может реализоватьComplexKeysShardingAlgorithm
для обработки такой логики. - Бродкаст-таблицы – таблицы, которые дублируются на все шарды (например, справочники) и не требуют шардирования. ShardingSphere умеет помечать такие таблицы, чтобы на всех шардах выполнялись DDL и учитывались SELECT без условий.
- Отсутствие шардирования – можно сочетать шардинг и не-шардинг в одной базе. Для неразбитых таблиц ShardingSphere применяет стратегию
None
(по сути, запрос просто идёт на все узлы или на один указанный основной).
YAML-конфигурация шардирования и репликации
ShardingSphere позволяет описывать правила шардирования декларативно в YAML. Конфигурация состоит из нескольких разделов: список физических источников данных, список правил (rules) и глобальные свойства. Ниже приведён упрощённый пример YAML, иллюстрирующий настройку шардирования таблицы и read/write репликации для PostgreSQL:
# Логическая база данных (имя)
databaseName: shardingsphere_db
dataSources:
# Источники данных: мастеры и реплики для двух шардов
shard0_master:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:postgresql://db-server-1:5432/app_db_0
username: appuser
password: secret
shard0_replica:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:postgresql://db-server-2:5432/app_db_0
username: appuser
password: secret
shard1_master:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:postgresql://db-server-3:5432/app_db_1
username: appuser
password: secret
shard1_replica:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
jdbcUrl: jdbc:postgresql://db-server-4:5432/app_db_1
username: appuser
password: secret
rules:
- !READWRITE_SPLITTING
dataSources:
shard0: # логический источник данных для шарда0 с репликацией
writeDataSourceName: shard0_master
readDataSourceNames: [ shard0_replica ]
loadBalancerName: round_robin # алгоритм балансировки между репликами
shard1:
writeDataSourceName: shard1_master
readDataSourceNames: [ shard1_replica ]
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
- !SHARDING
tables:
order_tbl: # имя логической таблицы
actualDataNodes: shard${0..1}.order_tbl_${0..1}
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_hash_algo
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db_hash_algo
bindingTables: [ order_tbl, order_item_tbl ] # пример связки (если две таблицы шардиируются совместно)
defaultDataSource: shard0 # куда отправлять данные без шардирования (необязательно)
shardingAlgorithms:
db_hash_algo:
type: INLINE
props:
algorithm-expression: shard${user_id % 2} # user_id mod 2 -> shard0 или shard1
order_hash_algo:
type: INLINE
props:
algorithm-expression: order_tbl_${order_id % 2} # order_id mod 2 -> суффикс таблицы
В этом YAML:
- dataSources определяет четыре физических подключения (две пары master-replica). Обратите внимание, это пример для наглядности – при запуске приложения реальные строки подключения, пользователи и пароли должны быть указаны корректно. Здесь используется пул HikariCP, но поддерживаются любые JDBC-пулы.
- Первый блок правил
!READWRITE_SPLITTING
объявляет две логических базыshard0
иshard1
. Для каждой указано, какой dataSource является главным (write) и какие – реплики (read). В нашем примере по одному реплике на шард. Настроен алгоритмROUND_ROBIN
– ShardingSphere будет чередовать обращения к replica при нескольких запросах чтения. - Второй блок
!SHARDING
описывает шардирование таблицыorder_tbl
. Параметр actualDataNodes задаёт шаблон физических таблиц: в примереshard${0..1}.order_tbl_${0..1}
означает, что существует 2 шарда (0 и 1) и на каждом две физические таблицыorder_tbl_0
иorder_tbl_1
. Таким образом, логическая таблица агрегирует 4 физические. Далее определены стратегии шардирования:- databaseStrategy – стандартная стратегия по колонке
user_id
с алгоритмомdb_hash_algo
. АлгоритмINLINE
с выражениемshard${user_id % 2}
будет возвращать имя логического dataSource (shard0
илиshard1
) по остатку от деленияuser_id
. То есть пользователи разделены по двум базам. - tableStrategy – стратегия разделения таблицы по колонке
order_id
с алгоритмомorder_hash_algo
. Он аналогично рассылает заказы по двум таблицам на каждом шарде (суффикс_0
или_1
в зависимости отorder_id mod 2
). В итоге полный ключ шардирования – сочетание user_id (выбор базы) и order_id (выбор таблицы внутри базы). - bindingTables (необязательно) позволяет указать связанные таблицы, которые должны одинаково шаридироваться (например, таблица заказов и позиции заказа
order_item_tbl
по одному и тому же ключу user_id), что гарантирует выполнение JOIN по ним на одном шарде. - defaultDataSource можно задать для запросов к неразбитым таблицам, в примере неразбитые данные идут на
shard0
.
- databaseStrategy – стандартная стратегия по колонке
Такая конфигурация может храниться как в стороннем YAML-файле, так и прямо в application.yml
Spring Boot (при использовании Spring Boot Starter). При старте ShardingSphere прочитает настройки и поднимет объединённый ShardingSphereDataSource, который приложению видится как единый DataSource. Включение шардирования не требует изменений в ORM или JDBC-коде – запросы можно писать как обычно (например, SELECT * FROM order_tbl WHERE user_id=123
), а ShardingSphere сам разложит его на запросы к физическим таблицам (в данном случае к нужному шарду и нужной партиции таблицы).
Интеграция с Spring Boot: конфигурация и аннотации
Интегрировать ShardingSphere в Spring (Boot) можно несколькими способами:
- Через Spring Boot Starter и YAML – разработчики могут добавить зависимость:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc</artifactId> <version>5.5.2</version> <!-- пример версии --> </dependency>
После этого вapplication.yml
достаточно указать специальный драйвер и путь до YAML-конфигурации шардирования:spring: datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:sharding-config.yaml
Здесьsharding-config.yaml
– наш YAML с правилами (как приведён выше) размещённый в classpath ресурса. ShardingSphereDriver – встроенный JDBC-драйвер ShardingSphere, перехватывающий все запросы. Этот способ наиболее прост: вся настройка сделана в YAML, приложение просто использует DataSource как обычно. ShardingSphere сам создаст пул соединений к каждой физической БД и объединит их под единым URL. - Через Java-конфиг Bean – альтернативно, можно программно создать объединённый DataSource. Библиотека предоставляет фабрику
YamlShardingSphereDataSourceFactory
, у которой есть методcreateDataSource(yamlFile)
для построения DataSource по YAML-конфигу. Например, в Spring можно объявить Bean:@Bean DataSource dataSource() throws IOException, SQLException { File yamlFile = new ClassPathResource("sharding-config.yaml").getFile(); return YamlShardingSphereDataSourceFactory.createDataSource(yamlFile); }
После чего этот DataSource можно использовать с любым ORM (JPA, MyBatis) как обычно. Такой подход удобен, если нужно программно манипулировать конфигурацией (но в большинстве случаев достаточно декларативного способа). - ShardingSphere-Proxy – хотя в Spring Boot обычно хватает embedded-режима, есть вариант вынести шардирование в отдельный процесс. Apache ShardingSphere-Proxy запускается как отдельный сервер (говорящий по протоколу PostgreSQL/MySQL), к которому приложение подключается как к обычной БД. Тогда конфигурация шардирования задаётся для прокси, а приложение не включает специальный драйвер – достаточно указывать JDBC URL прокси. В рамках монолитного приложения этот вариант даёт меньше преимуществ, но может быть полезен при вынесении управления БД в отдельный уровень.
После настройки DataSource вся остальная работа с БД в Spring остается привычной. Например, можно использовать Spring Data JPA или JdbcTemplate поверх ShardingSphere – он совместим со стандартом JDBC и прозрачно подменяет собой драйвер к реальной БД.
Аннотации транзакций. Для управления транзакциями используются стандартные средства Spring: аннотация @Transactional
на сервисных методах или вручную через PlatformTransactionManager
. ShardingSphere интегрируется с Spring Transaction Management: при начале транзакции Spring обращается к DataSource (ShardingSphere) за соединением, и ShardingSphere участвует в транзакции. Однако, помимо обычных локальных транзакций, ShardingSphere поддерживает распределённые транзакции (XA и BASE), о которых подробнее далее. Если требуется задействовать глобальную XA-транзакцию, ShardingSphere предоставляет аннотацию @ShardingTransactionType
для указания типа транзакции. Эту аннотацию следует использовать вместе с @Transactional
, например:
@Service
public class OrderService {
@Transactional
@ShardingTransactionType(TransactionType.XA)
public void createOrder(Order order) {
// ... сохранить заказ и обновить другие шарды ...
}
}
В приведённом примере метод помечен как транзакционный (Spring откроет транзакцию), а @ShardingTransactionType(TransactionType.XA)
указывает ShardingSphere, что транзакция должна быть XA (двухфазная). ShardingSphere перехватит создание соединений и вместо локальных начнёт XA-транзакции на каждом задействованном шарде. По завершении метода, Spring выполнит commit, ShardingSphere соберёт голоса от всех участников (prepare/commit на каждой БД) и гарантирует атомарность коммита на всех узлах или откат на всех при ошибке. Аннотация также поддерживает TransactionType.BASE
для включения BASE (??) транзакций. Если не указано явно, по умолчанию используется LOCAL
– локальные транзакции на отдельных БД без координации (что эквивалентно обычному поведению, но без гарантии атомарности между шардами).
Стоит отметить, что @ShardingTransactionType доступна начиная с ShardingSphere 4.x, и в новых версиях упрощает выбор режима транзакции. В более старых вариантах переключение типа делалось через явный вызов TransactionTypeHolder.set(TransactionType.XA)
до начала транзакции. Теперь всё инкапсулировано в AOP-аналоге @ShardingTransactional
, так что в коде достаточно расставить аннотации.
Транзакции и откаты при шардировании
Работа транзакций в шардированной базе имеет нюансы, поскольку единичная бизнес-операция может затрагивать несколько физических баз данных. Apache ShardingSphere предлагает три режима транзакций:
- Локальные транзакции (LOCAL) – это транзакции, ограниченные одним физическим узлом (одним DataSource). ShardingSphere в этом режиме фактически не координирует транзакции: каждый шард выполняет коммит/ролбек самостоятельно. Если приложение выполняет транзакцию, затрагивающую только один шард, то всё просто – будет использована обычная транзакционность СУБД. Однако если в рамках одного
@Transactional
метода были затронуты два и более шарда, то при LOCAL режиме не гарантируется атомарный коммит между ними. Например, если сервис записал в шард A и шард B и произошла ошибка после фиксации на A, то откат произойдёт только на B, а данные A останутся – возникнет неполный коммит. Поэтому локальный тип подходит только для операций, которые не изменяют сразу несколько шардов. Он выбран по умолчанию (наиболее быстрый, без накладных расходов) и безопасен, если модель данных спроектирована так, что каждая транзакция «живёт» внутри одного шарда (что часто и делают – например, все данные одной сессии или заказа хранятся на одном шарде). - XA-транзакции (двухфазный commit) – это жёсткие (ACID) распределённые транзакции. ShardingSphere выступает в роли координатора XA: при начале транзакции он открывает XA-сессии на всех задействованных источниках данных и генерирует XID. При фиксации – проводит фазу prepare на каждом узле, собирая подтверждения, и затем фазу commit (либо rollback при сбое). Такая схема гарантирует строгую консистентность: либо все узлы применят изменения, либо ни один. ShardingSphere реализует XA через встроенный менеджер
XAShardingSphereTransactionManager
, адаптирующийся к различным XA-драйверам СУБД. В современном ShardingSphere по умолчанию используется Atomikos или Narayana в качестве реализации XA, или можно интегрировать другие (через SPI). Использование XA-транзакций в Spring-приложении, как описано выше, сводится к аннотации или явному выбору типа. Ограничения XA – это относительно низкая производительность и сложность: двухфазный коммит вносит задержки, а также требует настроить журнал XA (например,xa_recovery.log
для восстановления после сбоев). Кроме того, не все SQL-операции идеально подходят для XA в распределённой среде (например, DDL может не поддерживаться транзакционно). Best Practice при использовании XA: ограничивать длительность транзакций, избегать пользовательского взаимодействия внутри транзакции, чтобы уменьшить вероятность конфликтов, и мониторить время выполнения. XA обеспечивают надёжность финансового уровня, но стоит применять их только когда действительно нужно атомарно обновлять несколько шардов. - BASE-транзакции (гибкие, в стиле Eventually Consistent) – альтернативный подход, жертвующий строгой моментальной консистентностью ради производительности и доступности. BASE расшифровывается как Basically Available, Soft state, Eventually consistent. В контексте ShardingSphere BASE-транзакции реализуются интеграцией с Seata (открытая система распределённых транзакций от Alibaba). Вместо блокирующего двухфазного коммита, Seata (в режиме AT – Automatic Transaction) фиксирует изменения локально на каждом шарде, а при неуспехе выполняет компенсацию (откат) с помощью автоматически сгенерированных обратных операций. По сути, это вариант паттерна Saga: если часть шагов не удалась, то ранее зафиксированные шаги компенсируются. ShardingSphere BASE обеспечивает финальную согласованность данных – через какое-то время (обычно мгновенно или за секунды) все узлы приходят к консистентному состоянию, но в моменте могли быть рассинхронизированы. Преимущество – быстродействие близкое к локальным транзакциям и отсутствие жёсткого блокирования всех участников. Недостаток – более сложная логика отката (Seata автоматически генерирует компенсационные SQL, но разработчику всё равно нужно учитывать возможные аномалии) и неподходящ для критичных инвариантов, требующих ACID. Использовать BASE имеет смысл, когда бизнес может потерпеть eventual consistency. Включается BASE также через
@ShardingTransactionType(TransactionType.BASE)
или глобально, и требует дополнительных зависимостей (Seata server или AT mode). Например, в микросервисных архитектурах с Saga-орchestration можно применить BASE для цепочки операций по разным шардам.
Откаты и подтверждения. При использовании XA ShardingSphere сам координирует откат: если любой участник транзакции возвращает ошибку на prepare, ShardingSphere отправит команду rollback всем ресурсам, где транзакция была открыта. Таким образом достигается all-or-nothing. При локальных транзакциях откат происходит как обычно – на том шарде, где вызван, другие шарды вообще не знают о транзакции. Это значит, что при сбое посреди распределённой операции разработчик должен сам реализовать компенсацию, либо избегать таких ситуаций, либо переключиться на XA/BASE. В случае BASE откат (точнее, компенсация) выполняется фреймворком Seata: он отслеживает изменения (например, с помощью undo-логов в отдельных служебных таблицах) и при командe rollback запускает обратные SQL на тех узлах, где изменения уже зафиксированы.
Лучшие практики:
- Разделяйте данные так, чтобы транзакции были локальными, где это возможно. Проектируя модель, старайтесь, чтобы одна бизнес-транзакция (например, оформление одного заказа) затрагивала только один шард – тогда можно работать в режиме Local, который самый простой и быстрый.
- Если нужна целостность – используйте XA лишь в необходимых сервисах. Например, денежные операции, перемещение средств между счетами на разных шардах – кандидат для XA. Менее критичные вещи (логирование, второстепенные обновления) лучше делать асинхронно или с eventual consistency, чтобы не блокировать основной поток.
- Следите за производительностью XA/BASE. Распределённая транзакция всегда медленнее. Ограничьте объём данных в таких транзакциях, и по возможности переходите на агрегирование результатов на уровне приложения (eventual consistency), чем делать один глобальный транзакционный запрос.
- Учитывайте задержку репликации. Это не про транзакции, но важно: при чтении из реплик (slave) возможна ситуация «чтение своих же некоммитнутых данных». Если сервис сначала пишет на мастер, а затем сразу читает, надо либо читать с мастера (через настройки
PRIMARY
для чтений в транзакции), либо в коде учитывать возможность отсутствия свежих данных на реплике и повторять попытку/ждать.
Масштабирование системы: добавление шардов и миграция данных
Одно из ключевых преимуществ шардирования – горизонтальное масштабирование, то есть возможность добавлять новые шарды по мере роста данных и нагрузки. Однако добавление нового шарда не сводится лишь к запуску новой базы – нужно перераспределить данные и перенастроить правила. Рассмотрим, как это может быть реализовано с ShardingSphere:
- Добавление нового шарда в конфигурацию. Предположим, изначально было 2 шарда, и мы хотим добавить третий. Первый шаг – добавить новый источник данных (базу) в конфиг. Например, дописать
shard2_master
(и при наличии реплик –shard2_replica
) в секциюdataSources
, а также обновить правила. Если использовалось шардирование по модулю, скажемuser_id % 2
, то при добавлении третьего шарда алгоритм придётся изменить, например, наuser_id % 3
. Внимание: просто изменить модуль недостаточно – старые данные сuser_id % 3 = 2
сейчас все находятся либо на shard0 либо shard1, и после смены правила ShardingSphere начнёт искать часть данных на новом shard2, где их нет. Поэтому без миграции данные станут недоступны. Альтернативный подход – вместо смены алгоритма на лету, можно ввести новый шард для новых данных: например, оставить правилоuser_id % 2
для старых диапазонов user_id, а user_id выше определённого нового диапазона отправлять на новый шард. Такой сценарий лучше реализуется через range-шардирование: изначально диапазоны ID распределены по двум шардам, затем добавляется диапазон для третьего. - Миграция данных (Resharding). Чтобы равномерно перераспределить существующие данные на новый шард, ShardingSphere предоставляет инструмент Scaling / Data Migration. В последних версиях (5.4.x и выше) это встроенная функция ShardingSphere-Proxy, позволяющая переливать данные из старого шарда на новый онлайн. Общий процесс:
- Развернуть новый пустой шард (база) и подключить его к ShardingSphere (добавить как новый
dataSource
, но пока не включать в правило шардинга для существующих таблиц). - Выполнить команду миграции данных. ShardingSphere использует механизм чтения binlog (для MySQL) или WAL (для PostgreSQL), чтобы перенести все записи заданных таблиц на новый шард, с минимальным даунтаймом. Он копирует все имеющиеся (stock) данные, параллельно отслеживая новые изменения (incremental). Таким образом, пока идёт миграция, приложение может продолжать работать.
- После завершения копирования и применения всех накопившихся изменений, переключить конфигурацию шардинга – теперь в правилах указываем новую топологию (например, изменить выражение
shard${0..2}
или диапазоны). С этого момента новые запросы будут учитывать shard2. - Опционально, можно отключить старое распределение или удалить (архивировать) данные, если они были перемещены. В ShardingSphere есть команды DistSQL для работы с миграцией:
REGISTER MIGRATION SOURCE STORAGE UNIT
,MIGRATE TABLE
,COMMIT MIGRATION
и др., которые выполняют эти шаги транзакционно.
- Развернуть новый пустой шард (база) и подключить его к ShardingSphere (добавить как новый
ShardingSphere-Scaling всё ещё развивается, но уже позволяет значительно упростить процесс, который иначе потребовал бы писать свой скрипт миграции или использовать ETL-инструменты. Например, блога ShardingSphere описывает подробный кейс миграции с одной БД на распределённую кластер из двух шардов без остановки сервиса.
- Добавление реплик (масштабирование по чтению). Аналогично, при возрастании нагрузки на чтение можно добавлять replica-серверы для существующих шардов. Это делается проще: новые реплики настраиваются на уровне СУБД (например, запуск новых PostgreSQL standby), затем их подключают в конфиг ShardingSphere (добавить в
readDataSourceNames
и перезапустить прокси или обновить конфигурацию в Registry Center, если используется Governance). ShardingSphere начнёт распределять чтения и на новые реплики тоже, увеличивая пропускную способность системы для SELECT-запросов. Здесь миграции данных не требуется, т.к. реплика сама скопирует состояние мастера. Однако рекомендуется следить за лагом репликации: при высокой нагрузке отставание может расти, что влияет на консистентность чтений. - Балансировка нагрузки и ребаланс шардов. После добавления новых узлов может встать задача перераспределить существующих пользователей/данные, чтобы устранить перекос. Например, если раньше shard0 и shard1 содержали по 50% данных, а теперь добавлен shard2, хорошо бы скинуть ~1/3 данных на него. Это достигается как раз миграцией с переключением алгоритма. В случае range-шардирования задача проще: достаточно переназначить некоторые диапазоны новому шарду и перенести эти диапазоны данных. В случае хэш-шардирования – приходится либо ре-хэшировать все данные (дорого), либо применять алгоритмы consistent hashing (ShardingSphere из коробки не поддерживает, но можно реализовать кастомно), либо постепенно разбивать горячие ключи на несколько шардов. В целом, горизонтальное масштабирование нужно планировать заранее, выбирая стратегию шардирования, которая допускает расширение (напр. диапазоны, где новые диапазоны идут на новые шарды).
- Высокая доступность шардов. Помимо производительного масштабирования, ShardingSphere может упрощать масштабирование для отказоустойчивости. Например, если один шард заполнен и нужен новый – описано выше. А если шард вышел из строя? В сочетании с Kubernetes или собственным оркестратором можно настроить автомасштабирование: новый инстанс БД поднимается и подхватывается. В ShardingSphere есть понятие репликации базы данных (DB Discovery) – отслеживание активного мастера при фейловере (например, в PostgreSQL patroni). Если настроен плагин DB Discovery, ShardingSphere-Proxy сам переключит роль новой ноды в случае падения основной, и приложение не заметит переключения. Для горизонтального фейловера (потеря целого шарда) обычно нужно настроить резервное копирование и при восстановлении – либо поднять шард с реплики, либо использовать резервную копию + перенаправление запросов. Вручную в ShardingSphere можно временно выключить проблемный шард (DistSQL
ALTER SHARDING TABLE RULE ... REMOVE ...
), тогда запросы с данными этого шарда будут падать (или идти на пусто), но остальные шарды продолжат работать. После восстановления шард можно вернуть. Таким образом достигается изоляция проблемы, хотя глобальная устойчивость требует дополнительных мер на уровне приложения.
Итак, масштабирование с ShardingSphere – сложный процесс, но значительно облегчён встроенными средствами. Главное – минимизировать простой: онлайн-миграция с помощью binlog/WAL позволяет добавить новый шард без остановки сервиса, последовательно перенеся данные и переключив маршрутизацию. Это преимущество перед самописными скриптами, которые, как правило, требуют остановить запись в базу. Рекомендуется заранее протестировать процесс миграции на стенде, т.к. для больших объёмов данных он может занять время и потребовать настройки параметров (количество потоков миграции, размер батчей, и т.д.).
Мониторинг и отладка распределённой БД
При внедрении шардирования и репликации возрастает сложность системы – необходимо отслеживать её состояние и уметь диагностировать проблемы. Рассмотрим, какие возможности есть для мониторинга и отладки ShardingSphere:
Метрики и интеграция с Prometheus. Apache ShardingSphere имеет модуль Observability (наблюдаемость), который включает сбор метрик работы запросов, трейсинг и логирование. В состав ShardingSphere входит Agent – javaagent, который можно подключить при запуске приложения или Proxy для сбора данных. Агент поддерживает экспорт метрик в Prometheus: достаточно подключить плагин shardingsphere-agent-metrics-prometheus.jar
и указать в конфиге agent’а host/port для прометеевского эндпоинта. После этого ShardingSphere будет экспонировать метрики – например, количество запросов, время выполнения, распределение по шардам, состояние транзакций и т.п. – в формате, готовом для сбора Prometheus. Можно подключить готовые дашборды Grafana, отображающие: QPS, среднее время ответа, процент ошибок SQL, трафик на шард и др. Мониторинг метрик позволяет оперативно видеть узкие места: например, если один шард перегружен или одна реплика отстаёт, это будет заметно. Кроме Prometheus, в агент встроена поддержка OpenTelemetry для трейсинга: можно настроить отправку трасс (span’ов) запросов в Jaeger или Zipkin. Тогда каждый запрос через ShardingSphere будет помечен trace-id, и можно в распределённом трейсинге видеть последовательность: запрос в сервис -> парсинг ShardingSphere -> запрос на конкретный шард -> ответ. Это очень помогает в отладке медленных запросов и в понимании, как именно запрос разбивается по узлам.
Логирование SQL и отладка запросов. ShardingSphere умеет логировать подробности выполнения запросов, что полезно при разработке. В настройках props можно включить параметр sql-show: true
. При этом каждый входящий запрос будет логироваться (на уровне INFO, категория ShardingSphere-SQL
) вместе с: исходным логическим SQL, списком реальных SQL запросов, отправленных на конкретные базы, и результатом парсинга (на какой шард пошёл и т.д.). Пример фрагмента лога при включённом sql-show:
[INFO] ShardingSphere-SQL: Logic SQL: SELECT * FROM order_tbl WHERE user_id=123
[INFO] ShardingSphere-SQL: Actual SQL: shard1 ::: SELECT * FROM order_tbl_1 WHERE user_id=123
Здесь видно, что логический запрос к order_tbl
был направлен на shard1
, и ShardingSphere переписал SQL на конкретную таблицу order_tbl_1
. Такие логи помогают удостовериться, что настройка шардов работает правильно (например, запросы разных пользователей реально идут на разные БД). Также по логам можно вылавливать запросы, которые по ошибке бьют сразу во все шарды (ShardingSphere помечает, когда выполняется broadcast или full route). В боевом режиме постоянно держать sql-show включённым не рекомендуется (большой объём логов), но на этапе отладки – очень ценная опция.
Кроме sql-show, у Proxy-сервера ShardingSphere есть собственные логи, куда он пишет запросы, ошибки и важные события. Их формат можно настроить (он совместим с логбэком). При интеграции с Spring Boot, ShardingSphere-JDBC будет использовать логгер приложения.
Диагностика и статистика запросов. ShardingSphere поддерживает DistSQL – набор команд, позволяющих запрашивать статус системы. Например, команда SHOW STATUS FROM READWRITE_SPLITTING RULE
покажет текущий статус балансировки (включена/выключена), SHOW SHARDING TABLE NODES order_tbl
отобразит, на каких физических нодах находятся данные логической таблицы, SHOW SHARDING RULES USED ALGORITHM
– какие алгоритмы шардирования используются и как настроены и т.п.. Эти команды можно выполнять либо через ShardingSphere-Proxy (они расширяют SQL-синтаксис), либо через API Governance (если настроен режим с Registry Center). Для отслеживания производительности запросов можно также включить профилирование на уровне СУБД (например, pg_stat_statements в PostgreSQL) – с учётом, что каждый шард – обычная база, на ней работают стандартные средства мониторинга, просто придётся агрегировать статистику с нескольких узлов.
Средства отладки распределённых транзакций. При использовании XA могут понадобиться инструменты для мониторинга состояния 2PC. ShardingSphere-Proxy пишет в лог события XA (например, XID и статус), также Atomikos/Narayana могут иметь консоль или логи по своим действиям. Если транзакция застряла (crash recovery), ShardingSphere по журналу xa_transaction
на диске (для Atomikos) может попытаться восстановить. Для BASE (Seata) – есть Server, который хранит состояние Saga и компенсирующих действий; его метрики тоже стоит мониторить.
Пример мониторинга с Prometheus: После интеграции agent и экспорта метрик, можно видеть, например, метрику shardingsphere_proxy_connections
– количество активных соединений к Proxy, shardingsphere_jdbc_execute_latency_millis
– гистограмму латентности запросов JDBC, shardingsphere_transaction_xa_commit_total
– число XA-коммитов, и многое другое. В Grafana Cloud даже есть готовый дашборд для ShardingSphere, визуализирующий основные показатели. Это существенно упрощает эксплуатацию, позволяя в едином месте видеть состояние всех шардов.
Отладочные диаграммы и профилирование запросов. Иногда понять поведение шардинга помогает построение последовательности вызовов. Выше мы приводили sequence-диаграмму, показывающую как запрос проходит через ShardingSphere. Ниже она приведена ещё раз для наглядности.

Рис. 7: Жизненный цикл выполнения запроса через ShardingSphere-JDBC (упрощённо). На диаграмме показано, как запрос от пользователя проходит через слои приложения и направляется на соответствующий шард. Цифры 1–6 соответствуют шагам: (1) клиент отправляет запрос в сервис; (2) приложение выполняет SQL через ShardingSphere; (3) ShardingSphere определяет нужный шард и направляет запрос в PostgreSQL; (4) БД возвращает результат; (5) ShardingSphere возвращает данные приложению; (6) сервис отдает ответ клиенту.
В сложных случаях, таких как неправильное распределение транзакций или внезапная нагрузка на один узел, полезно иметь глобальный обзор. C4-модели (как контейнерная диаграмма выше) и sequence-диаграммы помогают команде разработчиков и DevOps понимать, где искать проблему. Если, например, на одном шарде заметен рост времени ответа, по метрикам можно определить, какие запросы туда идут, и по логам – почему (возможно, не учли шардирующий ключ в WHERE, и ShardingSphere роутит на все базы).
Наконец, ShardingSphere предоставляет и ADM-интерфейс (UI) – ShardingSphere-UI, веб-консоль (в ранних версиях) для управления конфигурацией и просмотра статистики. В актуальных версиях упор делается на headless-режим с интеграцией Prometheus/Observability, поэтому часто достаточно подключить знакомые инструменты (Grafana, Kibana, Zipkin) к источникам, которые экспонирует ShardingSphere.
Заключение
В этой статье мы подробно рассмотрели горизонтальное масштабирование баз данных PostgreSQL с помощью шардирования — одного из наиболее эффективных и востребованных решений для работы с большими объёмами данных и высокими нагрузками. В первой части мы дали общий обзор архитектуры шардирования, рассмотрели различные алгоритмы распределения данных, подходы и стратегии, обсудили их преимущества и недостатки.
Затем мы перешли от теории к практике, детально разобрав использование Apache ShardingSphere в рамках реального приложения на платформе Spring Boot. ShardingSphere предлагает прозрачную интеграцию с PostgreSQL, позволяя минимизировать изменения в коде и максимально упростить работу с распределённой системой. Мы показали примеры конфигурации YAML, использования аннотаций Spring, работы с различными типами транзакций (LOCAL, XA, BASE), а также обсудили особенности масштабирования и мониторинга шардинга.
Однако стоит помнить, что шардирование — это не волшебное средство, которое решает все проблемы масштабирования автоматически. Это инструмент, который требует продуманного проектирования: правильный выбор ключей шардинга, стратегии распределения и учёт особенностей транзакционной работы напрямую влияют на успех реализации. Использование комплексного мониторинга и своевременное масштабирование позволят справиться с растущими нагрузками и поддерживать стабильную работу системы.
Apache ShardingSphere в связке с Spring Boot предоставляет удобный и гибкий способ реализации шардирования PostgreSQL, отлично подходящий как для высоконагруженных корпоративных сервисов, так и для активно растущих стартапов. При правильном подходе он позволяет получить надёжную, масштабируемую и отказоустойчивую систему, готовую к любым вызовам современного бизнеса.
You must be logged in to post a comment.