База данных — это сердце системы. И в какой-то момент это сердце начинает давать сбои. Не от объема данных, а от их разнородности. Таблица users разрастается до 200 колонок. Одни нужны для логина каждую секунду, другие — для годового отчета раз в год. В итоге, чтобы прочитать два горячих поля, база тащит с диска целый блок с холодными данными. Это неэффективно.
Вертикальное шардирование — это не про то, как распилить одну гигантскую таблицу на миллион строк. Это про то, как навести порядок в одной жирной строке. Это функциональная декомпозиция на уровне данных.
#include <iostream>
#include <string>
#include <vector>
class MonolithicUserTable {
public:
void getUserProfile(long userId) {
std::cout << "SELECT * FROM users WHERE id = " << userId << std::endl;
}
void updateUserPassword(long userId) {
std::cout << "UPDATE users SET password_hash = '...' WHERE id = " << userId << std::endl;
}
private:
// id, username, email, password_hash, registration_date, last_seen_at,
// avatar_url, bio_text, user_settings_json, phone_number,
// login_history_blob, all_user_posts_json, ...
};
int main() {
MonolithicUserTable db;
db.getUserProfile(123);
db.updateUserPassword(123);
}
Проблема №1 – Как и где провести границу?
Это первое и самое главное решение. Ошибка здесь стоит дороже всего. Неправильно проведенная граница создаст больше проблем, чем решит.
-
Решение А: Разделение по функциональным группам (Table Splitting)
Суть: Таблицы, относящиеся к разным бизнес-функциям, разносятся по разным базам данных. Таблицы профиля пользователя — в profile_db. Таблицы, связанные с финансами, — в billing_db.
-
Плюсы:
Логичность. Такое разделение интуитивно понятно и часто совпадает с границами микросервисов.
Изоляция нагрузки. Высокая нагрузка на профили не повлияет на производительность финансовых транзакций.
-
Минусы:
Теряются JOIN-ы. Вы больше не можете одним запросом соединить таблицу пользователей и их платежей.
Предполагает знание будущего. Вы должны быть уверены, что эти функциональные области не станут тесно связаны в будущем.
-
Решение Б: Разделение одной таблицы по колонкам (Column Splitting)
Суть: Одна жирная таблица (например, users) разрезается на несколько. В users_core остаются только часто используемые данные (id, email, password_hash). В users_profile — редко используемые (bio, avatar_blob).
-
Плюсы:
Высокая производительность. Запросы к горячим данным становятся очень быстрыми и компактными.
Точечная оптимизация. Можно использовать разные движки хранения для разных таблиц.
-
Минусы:
Усложнение кода. Приложению теперь нужно делать JOIN или два отдельных запроса, чтобы собрать полный объект.
Поддержание консистентности. Удаление пользователя теперь требует удаления записей из нескольких таблиц.
-
Решение В: Гибридный подход
Суть: Комбинация. Сначала таблицы разносятся по функциональным базам, а затем, внутри каждой базы, самые жирные таблицы дополнительно разрезаются по колонкам.
-
Плюсы:
Максимальная гранулярность. Позволяет очень тонко настроить производительность и изоляцию.
-
Минусы:
Максимальная сложность. Количество баз данных и таблиц растет, что превращает управление схемой в кошмар.
Проблема №2 – Прощайте, JOIN-ы
Как только вы разнесли таблицы users и orders по разным физическим серверам, команда SELECT ... FROM users JOIN orders ... перестает работать.
-
Решение А: JOIN-ы на стороне приложения (Application-Side Joins)
Суть: Приложение сначала делает запрос в users_db, а затем второй запрос в orders_db (WHERE user_id IN (...)).
-
Плюсы:
Простота логики. Это самый прямолинейный способ.
Гибкость. Можно соединять данные из любых источников.
-
Минусы:
Низкая производительность. Требует нескольких сетевых походов.
Нагрузка на приложение. Приложение тратит свои CPU и память на то, что раньше делала база данных.
-
Решение Б: Денормализация и дублирование данных
Суть: Признать, что некоторые данные придется дублировать. Например, при создании заказа в orders_db можно сохранить не только user_id, но и user_name.
-
Плюсы:
Высокая скорость чтения. Чтобы показать список заказов с именами пользователей, нужен всего один запрос к orders_db.
Независимость. Сервис заказов может работать, даже если сервис пользователей недоступен.
-
Минусы:
Проблемы с консистентностью. Что произойдет, если пользователь изменит свое имя? Нужно реализовать механизм (обычно через события), который обновит user_name во всех заказах.
Избыточность. Увеличивает объем хранимых данных.
-
Решение В: Создание материализованных представлений
Суть: Создать отдельную, денормализованную базу данных, оптимизированную для чтения. Специальный процесс (ETL) в фоне собирает данные из всех шардов.
-
Плюсы:
Идеально для аналитики. Позволяет выполнять сложные запросы, не нагружая основные, транзакционные базы.
Разделение нагрузки. OLTP- и OLAP-нагрузки полностью изолированы.
-
Минусы:
Данные неактуальны. Всегда будет задержка между обновлением в основной базе и появлением данных в представлении.
Сложность. Требует разработки и поддержки отдельного конвейера данных.
Проблема №3 – Транзакционная целостность
Регистрация пользователя может требовать создания записи в auth_db и в profile_db. В монолите это одна ACID-транзакция. В распределенной системе — нет.
-
Решение А: Распределенные транзакции (XA / 2PC)
Суть: Использовать протокол двухфазного коммита (Two-Phase Commit), который координирует транзакции между несколькими базами.
-
Плюсы:
Гарантия ACID. Обеспечивает строгую консистентность.
-
Минусы:
Очень медленно. Протокол очень болтливый и требует блокировок, что убивает производительность.
Хрупкость. Координатор транзакций становится единой точкой отказа.
-
Решение Б: Паттерн Сага
Суть: Заменить одну распределенную транзакцию на последовательность локальных транзакций и событий. Если шаг N не удается, запускаются компенсирующие операции для шагов N-1, N-2 и т.д.
-
Плюсы:
Отказоустойчивость. Не требует блокировок и работает в асинхронном режиме.
Слабая связанность. Каждый сервис отвечает только за свою локальную транзакцию.
-
Минусы:
Сложность. Требует продуманной системы отката для каждой операции.
Итоговая консистентность (Eventual Consistency). Данные не всегда находятся в целостном состоянии.
Проблема №4 – Логика доступа к данным в приложении
Приложение больше не может просто использовать один DataSource. Оно должно знать, в какую базу идти за профилем, а в какую — за балансом.
-
Решение А: Логика в каждом сервисе
Суть: Каждый сервис напрямую в коде или конфигурации знает адреса нужных ему баз данных.
-
Плюсы:
Простота на старте. Не требует никаких дополнительных слоев.
-
Минусы:
Дублирование и жесткая связь. Конфигурация подключения размазана по всей системе.
Трудно управлять. Со временем становится невозможно отследить, кто куда ходит.
-
Решение Б: Единый слой доступа к данным (DAL)
Суть: Создать общую библиотеку или сервис-репозиторий, который инкапсулирует всю логику маршрутизации. Приложение просто говорит: Дай мне пользователя с ID 123", а DAL уже сам решает, в какую базу пойти.
#include <iostream>
#include <string>
#include <memory>
#include <map>
class DbConnection { public: void query(const std::string& q) { std::cout << "Executing: " << q << std::endl; } };
struct UserCore { long id; std::string email; };
struct UserProfile { long id; std::string bio; };
struct FullUser { UserCore core; UserProfile profile; };
class UserRepository {
public:
UserRepository(std::shared_ptr<DbConnection> core_db, std::shared_ptr<DbConnection> profile_db)
: core_db_(core_db), profile_db_(profile_db) {}
FullUser findById(long user_id) {
std::cout << "Fetching user " << user_id << "..." << std::endl;
core_db_->query("SELECT id, email FROM users_core WHERE id = " + std::to_string(user_id));
UserCore core = {user_id, "user@example.com"};
profile_db_->query("SELECT bio FROM users_profile WHERE user_id = " + std::to_string(user_id));
UserProfile profile = {user_id, "Some bio text."};
return {core, profile};
}
private:
std::shared_ptr<DbConnection> core_db_;
std::shared_ptr<DbConnection> profile_db_;
};
int main() {
auto core_connection = std::make_shared<DbConnection>();
auto profile_connection = std::make_shared<DbConnection>();
UserRepository user_repo(core_connection, profile_connection);
FullUser user = user_repo.findById(123);
std::cout << "Found user: " << user.core.email << " with bio: " << user.profile.bio << std::endl;
}
-
Решение В: Прокси-сервер баз данных
Суть: Использовать специализированный прокси (ProxySQL, Vitess), который стоит между приложением и базами. Приложение подключается к прокси, как к обычной базе.
-
Плюсы:
Прозрачность для приложения. Приложение вообще не знает о шардировании.
Мощный функционал. Прокси могут обеспечивать балансировку, отказоустойчивость, кэширование.
-
Минусы:
Дополнительный компонент. Еще одна система, которую нужно развертывать, настраивать и мониторить.
Ограниченность. Может не справиться с очень сложной логикой маршрутизации.
Проблема №5 – Миграция данных
У вас есть одна огромная таблица products на 1 ТБ. Как безопасно и без простоя перенести из нее колонки с картинками в отдельную базу products_content_db?
-
Решение А: Миграция с простоем (Downtime)
-
Плюсы:
Простота и надежность. Никаких гонок данных и проблем с консистентностью.
-
Минусы:
Неприемлемо для большинства современных систем. Простои стоят денег и репутации.
-
-
Решение Б: Поэтапная миграция без простоя
-
Суть:
Приложение начинает писать и в старую, и в новую таблицу.
Запускается фоновый процесс, который копирует старые данные в новую таблицу.
Когда процесс догонит текущие данные, приложение переключается на чтение из новой таблицы.
После периода проверки, когда все работает стабильно, запись в старую таблицу отключается.
-
Плюсы:
Нулевой простой. Пользователи не замечают миграции.
-
Минусы:
Огромная сложность. Требует тщательного планирования, написания временного кода и постоянного контроля.
Риск. Много этапов, на которых что-то может пойти не так.
-
Проблема №6 – Мониторинг и отладка
Раньше у вас была одна база, один набор метрик. Теперь их десять. Запрос тормозит. Где именно? На auth_db, который перегружен запросами на логин, или на analytics_db, где крутится тяжелый отчет?
-
Решение А: Разрозненный мониторинг
Суть: Настроить стандартный мониторинг для каждого экземпляра базы данных отдельно.
-
Плюсы:
Простота. Используются стандартные инструменты (Zabbix, Prometheus Node Exporter).
-
Минусы:
Отсутствие общей картины. Вы видите, что один сервер нагружен, но не понимаете, почему и как это влияет на всю систему.
Сложность корреляции. Трудно связать всплеск нагрузки на profile_db с медленным ответом API регистрации.
-
Решение Б: Единая платформа наблюдаемости (Observability)
Суть: Собирать метрики, логи и трейсы со всех баз данных и приложений в единую систему (Prometheus + Grafana, Datadog, New Relic).
-
Плюсы:
Целостное представление. Позволяет строить дашборды, которые показывают здоровье всей системы, а не отдельных ее частей.
Быстрая диагностика. С помощью распределенной трассировки можно увидеть, что медленный ответ API на 90% состоит из медленного запроса к inventory_db.
-
Минусы:
Требует инвестиций. Внедрение и поддержка такой платформы — это отдельный серьезный проект.
Инструментация. Все приложения должны быть инструментированы для отправки метрик и трейсов в едином формате.
Проблема №7 – Резервное копирование и восстановление
Бэкап одной базы — понятная задача. Бэкап десяти баз, которые должны быть консистентны между собой на определенный момент времени, — это кошмар.
-
Решение А: Независимые бэкапы
Суть: Каждая база бэкапится по своему собственному расписанию.
-
Плюсы:
Простота. Легко настроить.
-
Минусы:
Неконсистентность. Бэкап users_db сделан в 02:00, а orders_db — в 02:05. За эти 5 минут могли быть созданы заказы для пользователей, которых еще нет в бэкапе. Восстановление из таких бэкапов приведет к нарушению целостности.
-
Решение Б: Скоординированные снимки (Coordinated Snapshots)
Суть: Вы используете технологии, которые фотографируют состояние дисков всех серверов в один и тот же момент. Это могут быть LVM-снимки на уровне файловой системы или функции вашего облачного провайдера.
-
Плюсы:
Целостность данных. Вы получаете гарантию, что все восстановленные данные будут на один и тот же момент времени.
-
Минусы:
Сложность. Требует поддержки на уровне инфраструктуры.
Заморозка I/O. Создание снимка может на короткое время приостановить операции записи в базу.
-
Решение В: Резервное копирование на уровне логики
Суть: Смириться с тем, что идеальной консистентности не будет, и заранее подготовиться к этому. После восстановления из независимых бэкапов запускается специальный скрипт, который причесывает данные: находит и исправляет осиротевшие записи.
-
Плюсы:
Гибкость. Не зависит от конкретной инфраструктуры.
-
Минусы:
Сложность и риски. Скрипты для исправления данных должны быть идеально написаны и протестированы. Ошибка в них может привести к еще большей потере данных.
Проблема №8 – Управление схемой и миграции
Вам нужно добавить новую колонку в таблицу users_profile. А через неделю — переименовать колонку в billing_info. Управлять изменениями схемы в распределенной системе гораздо сложнее.
-
Решение А: Ручное управление
Суть: DBA или разработчик вручную подключается к каждой базе и применяет SQL-скрипты.
-
Плюсы:
Полный контроль.
-
Минусы:
Человеческий фактор. Огромный риск забыть применить скрипт к одной из баз.
Немасштабируемо. Работает для двух баз, но не для двадцати.
-
Решение Б: Инструменты для миграций (Flyway, Liquibase)
Суть: Вы используете инструменты, которые превращают изменения схемы в код. Они сами отслеживают, какие миграции уже применены к каждой базе, и накатывают только новые при запуске приложения.
-
Плюсы:
Автоматизация и надежность. Исключает человеческий фактор. Состояние схемы БД становится частью кода.
Воспроизводимость. Легко поднять новую базу и накатить на нее все миграции.
-
Минусы:
Координация. Если миграция затрагивает несколько сервисов и баз, ее нужно выкатывать скоординированно.
-
Решение В: Расширяемость без миграций (Schema-on-read)
Суть: Использовать NoSQL базы данных (MongoDB, Cassandra), которые не требуют строгой схемы.
-
Плюсы:
Гибкость. Идеально для быстро меняющихся данных.
-
Минусы:
Потеря контроля. Вся ответственность за консистентность данных ложится на код приложения.
Скрытая сложность. Миграция все равно происходит, но уже в коде.
Проблема №9 – Масштабирование отдельных шардов
Вы идеально разделили систему. Но внезапно одна часть — например, social_graph_db — начинает расти в 10 раз быстрее остальных и становится новым узким местом.
-
Решение А: Вертикальное масштабирование
Суть: Просто докупить для сервера social_graph_db больше памяти, CPU и более быстрые диски.
-
Плюсы:
Простота. Не требует никаких изменений в коде или архитектуре.
-
Минусы:
Есть предел. Рано или поздно вы упретесь в потолок самого мощного доступного железа.
Стоимость. Большое железо стоит очень дорого.
-
Решение Б: Ре-шардирование (горизонтальное)
Суть: Признать, что social_graph_db сама стала слишком большой, и начать применять к ней уже горизонтальное шардирование — разрезать ее данные по user_id.
-
Плюсы:
Безграничное масштабирование. Можно добавлять новые серверы практически до бесконечности.
-
Минусы:
Максимальная сложность. Горизонтальное шардирование — это на порядок более сложная задача, чем вертикальное. Она порождает все те же проблемы, но в еще большем масштабе.
Проблема №10 – Сложность локальной разработки
Чтобы запустить свою фичу, разработчику теперь нужно поднять на своей машине не одно приложение и одну базу, а пять сервисов и три разных базы данных.
-
Решение А: Поднять все (Docker Compose)
Суть: Использовать Docker Compose, чтобы одной командой развернуть все необходимое окружение локально.
-
Плюсы:
Максимальная достоверность. Разработчик работает с полной копией системы.
-
Минусы:
Требования к ресурсам. Современные ноутбуки могут не справиться с запуском 10-20 контейнеров.
Медленный старт. Поднятие всего окружения может занимать минуты.
-
Решение Б: Моки и стабы
Суть: Разработчик запускает только свой сервис и его базу. Все внешние зависимости заменяются на заглушки.
-
Плюсы:
Скорость и легковесность. Запускается быстро и не требует много ресурсов.
-
Минусы:
Риск рассинхронизации. Мок может вести себя не так, как реальный сервис.
Трудоемкость. Поддержание моков в актуальном состоянии — это отдельная работа.
-
Решение В: Общее облачное dev-окружение
Суть: Вместо локального запуска использовать общее для команды, постоянно работающее тестовое окружение в облаке.
-
Плюсы:
Экономия локальных ресурсов.
Стабильность. Окружение всегда доступно и настроено.
-
Минусы:
Эффект соседа. Один разработчик может сломать окружение для всех остальных.
Стоимость. Поддержание такого стенда стоит денег.
Проблема №11 – Управление соединениями
Приложение теперь должно управлять пулами соединений не к одной базе данных, а к десятку. Это создает оверхед и точки отказа.
-
Решение А: Пул соединений в каждом приложении
Суть: Каждый экземпляр приложения держит свой собственный пул соединений к каждой базе данных, с которой он работает.
-
Плюсы:
Простота. Это стандартный подход, поддерживаемый всеми фреймворками.
-
Минусы:
Connection storm. Если у вас 100 экземпляров приложения, и каждый должен говорить с 5 базами, вы можете легко исчерпать лимит соединений на стороне БД.
Дублирование конфигурации.
-
Решение Б: Внешний пулер соединений (PgBouncer, MaxScale)
Суть: Развертывается отдельный сервис-пулер. Приложения подключаются к нему, а он уже управляет ограниченным количеством реальных соединений к базам данных.
-
Плюсы:
Эффективность. Значительно снижает количество одновременных соединений к БД.
Централизация. Управление соединениями вынесено в один компонент.
-
Минусы:
Дополнительный компонент. Еще одна система, которую нужно сделать отказоустойчивой.
Добавляет сетевую задержку.
Выбор правильного инструмента для задачи
Жирная таблица с разнородной нагрузкой. Классический случай для разделения по колонкам. Выносите тяжелые BLOB-ы и редко используемые JSON-ы в отдельную таблицу.
Разделение монолита на микросервисы. Здесь естественным выбором является разделение по функциональным группам. Границы сервисов должны совпадать с границами баз данных.
Система с очень высокой нагрузкой на запись. Тут стоит подумать о разделении данных на горячие и холодные. Например, текущие транзакции хранить в быстрой OLTP-базе, а всю историю — в системе, оптимизированной для архивов.
Практические рекомендации
Не начинайте с шардирования. Это решение для проблем масштаба, которых у вас, скорее всего, еще нет.
Проводите границу там, где меньше всего JOIN-ов. Анализируйте ваши запросы.
Денормализация — ваш друг. В распределенном мире попытка сохранить полную нормализацию приводит к неработающей системе.
Думайте о данных с точки зрения их жизненного цикла. Какие данные горячие, а какие холодные?
Внедрите механизм асинхронных событий для поддержания консистентности между шардами.
ID должны быть глобально уникальными. Забудьте про автоинкрементные int. Используйте UUID.
Автоматизируйте миграции схемы с самого начала.
Инвестируйте в observability. Без единой системы мониторинга вы будете слепы.
Продумайте стратегию резервного копирования и восстановления до того, как записали первый байт в прод.
Локальная разработка должна быть простой.
Используйте централизованное хранилище секретов. Не храните пароли в коде или конфигах.
Спланируйте управление пулами соединений. Не допустите шторма соединений.
Вертикальное шардирование — это мощный, но опасный инструмент. Это не просто техническое упражнение, это глубокое архитектурное решение, которое затрагивает все аспекты системы: от кода приложения до процедур резервного копирования.
Это всегда компромисс. Вы меняете простоту и строгую консистентность монолитной базы на производительность, масштабируемость и изоляцию. Этот шаг оправдан только тогда, когда боль от текущих проблем становится невыносимой, а выгода от разделения — очевидной. В противном случае, вы рискуете создать себе сложного, хрупкого и дорогого в поддержке монстра. И как всегда в архитектуре, главное — не знать решение, а точно понимать проблему.
Комментарии (2)

eigrad
25.10.2025 03:34Спасибо за статью, хорошо собрали все связанные проблемы вместе. Вступление возможно стоит поправить, из него вообще не понятно что речь идёт о разнесении таблиц по разным базам - после прочтения вступления и первой секции возникла мысль написать "автор, ты чо, реально про нормальные формы не слышал?"
Gapon65
Вы совершенно забыли о такой вещи как РЕПЛИКАЦИЯ. Эта технология определенно поможет Вам решить часть перечисленных проблем: https://www.mydbops.com/blog/postgresql-replication. Технология (репликации) также поддерживается MySQL/MariaDB.