В современном мире данных нагрузка на базы данных стремительно растёт. Когда один сервер перестаёт справляться с объёмом запросов, встаёт вопрос о масштабировании: как эффективно распределить нагрузку, сохранив высокую производительность и доступность?
Можно конечно попытаться сделать наш единственный сервер идеальным и мощным - вертикально масштабировать его. Но сегодня мы поговорим о горизонтальном масштабировании - будем брать не мощностью сервера, а количеством узлов.
Существует множество стратегий решения указанной проблемы. Сегодня мы разберем самые популярные из них - репликацию, партициривание и шардирование. Рассмотрим их принципы, плюсы и минусы, а также лучшие практики применения. Понимание этих техник поможет разработчикам и архитекторам строить отказоустойчивые, масштабируемые и высокопроизводительные системы хранения данных.
Репликация (Replication)
Репликация базы данных — это процесс автоматического копирования и синхронизации данных между несколькими серверами (узлами). При репликации изменения, внесённые в одну копию базы (основной узел), автоматически передаются на другие узлы (реплики), которые могут использоваться для балансировки нагрузки, резервного копирования или аналитики.
Суть репликации заключается в существовании множества копий базы, имеющих весь набор данных. В случае выхода из строя или большой нагрузки на один узел, запросы перенаправляются на другой.
Не всегда все реплики имеют равные права. Существует 2 сценария: Master-Slave и Master-Master.
Главная СУБД (Master Database) — это оригинал. Имеет доступ ко всей информации в соответствии со всеми уровнями запросов
Подчиненная СУБД (Slave Database) — копия или реплика главной СУБД. Ее функциональность может отличаться от оригинальной
Master - Slave
Master-Slave репликация — это способ организации базы данных, где есть один главный сервер (Master) и один или несколько подчинённых (Slave). Master принимает все запросы на запись (добавление, изменение, удаление данных), а Slaves только читают данные и получают обновления от Master.
Как это работает?
Клиенты записывают данные только в Master.
Master автоматически передаёт изменения Slaves.
Клиенты могут читать данные как с Master, так и с Slaves (чаще читают со Slaves, чтобы снизить нагрузку на Master).
Плюсы:
Разгрузка Master – чтение можно распределить по нескольким Slaves, что повышает производительность.
Резервирование – если Master сломается, можно переключиться на один из Slaves.
Масштабируемость – можно добавлять новые Slaves без перегрузки основного сервера.
Минусы:
Задержка синхронизации – данные на Slaves могут немного отставать от Master.
Если Master падает, вручную или с помощью дополнительных механизмов нужно выбрать нового. Необходимо предусмотреть сценарий и алгоритм, при котором Slaves узлы самостоятельно выберут нового Master среди себя.
Не подходит для интенсивной записи – так как записи идут только в один узел, он может стать узким местом.
Master - Master
Master-Master репликация — это когда у базы данных есть два (или больше) главных сервера, и каждый из них может одновременно принимать и обрабатывать запросы на запись и чтение. Данные между серверами автоматически синхронизируются, чтобы оставаться одинаковыми.
Плюсы:
Высокая отказоустойчивость — если один сервер выходит из строя, другой продолжает работать.
Балансировка нагрузки — можно распределять запросы между серверами, снижая нагрузку на каждый из них.
Быстрее для географически распределенных систем — пользователи могут работать с ближайшим сервером, уменьшая задержки.
Минусы:
Конфликты данных — если два сервера изменяют одни и те же данные одновременно, могут возникнуть проблемы с их синхронизацией. Главной задачей разработчика системы станет гарантировать согласованность данных, что по CAP теореме повлечет снижение Доступности или Готовности к разрыву сети.
Сложность настройки и поддержки — требуется механизм разрешения конфликтов и контроль синхронизации.
Задержки репликации — хотя данные синхронизируются, это не всегда происходит мгновенно, что может привести к временным рассинхронизациям.
В некоторых сценариях, репликация не решит все ваши проблемы, а иногда только создаст дополнительные.
Второй популярной стратегией масштабирования БД является партицирование.
Партицирование (Partitioning)
Партицирование базы данных — это метод разбиения большой таблицы в рамках единой базы на более мелкие, логически связанные части (партиции). Каждая партиция хранится отдельно, но логически объединяется в одну таблицу, а база автоматически направляет запросы в нужные партиции.
Существует 2 способа партицировать таблицу - горизонтально и вертикально.
Вертикальное партицирование - не самый популярный, но заслуживающий упоминания вариант. При данном подходе таблица разделяется на партиции по принципу выбранных колонок. Например, если у вас есть таблица users с полями id, name, age, fav_sport, fav_food, то можно разделить её на партиции users_identity с полями id, name, age и users_preferences с полями id, fav_sport и fav_food.
Важно грамотно выбрать ключевые колонки дробления, чтобы равномерно распределить нагрузку. Ведь если программа 90% запросов будет направлять в партицию 1, а лишь оставшиеся 10% в партицию 2, то значит мы партицировали таблицу неправильно, и следовательно, теряем производительность.
Горизонтальное партицирование - более распространённый метод, заключающийся в делении таблицы по строкам. Например, таблицу consumers с полями city, name, и age можно разбить на таблицы по типу users_russia, users_ukraine, users_monaco. Однако опять же, важно помнить про равномерность распределения. Если предположить, что наш сервис распространен и в России и в Монако, то распределение на users_russia и users_monaco будет нерациональным, и не приведет к желаемому результату(потому что кол-во запросов из России очевидно будет много выше, так что проблема с большой нагрузкой от этой страны останется нерешенной). Кейс был приведен исключительно для наглядности, а конкретно в таком случае лучше будет выбрать другой принцип партицирования, чем регион.
P.S. методы шардирования Key-Based, Range-Based и Directory-Based о которых мы поговорим ниже, могут также применяться для выбора метода партицирования.
Шардирование (Sharding)
Шардирование — это метод горизонтального масштабирования базы данных, при котором данные разделяются на независимые части (шарды) и хранятся на разных серверах. Каждый шард содержит только часть общей информации, но вместе они образуют полную базу. Это позволяет распределять нагрузку и увеличивать производительность системы, так как запросы к данным могут выполняться параллельно на разных узлах.
Основная задача шардирования — определить, каким образом данные будут распределяться между шардами. Чаще всего используются хеш-функции, диапазоны значений или географическое разделение. Однако шардирование добавляет сложность в администрирование, так как требует балансировки данных, управления кросс-шардовыми запросами и обеспечения целостности транзакций.
Способы шардирования
1. Range-Based (Диапазонное)
В этом методе данные распределяются по шардам на основе определённых диапазонов значений. Например, если у нас есть база с пользователями, можно распределить их по шардам в зависимости от ID:
Шард 1: ID от 1 до 1000
Шард 2: ID от 1001 до 2000
Шард 3: ID от 2001 и выше
Плюсы:
Легкость реализации
Эффективные запросы, если запросы ограничены диапазоном
Минусы:
Возможен дисбаланс нагрузки, если некоторые диапазоны используются чаще
Трудности при изменении границ диапазонов
2. Key-Based (Хеширование)
В этом методе для распределения данных используется хеш-функция. Зачастую принцип такой - выбираем колонку таблицы, которую отдадим в хеш-функцию. Затем полученный хеш делим на количество шардов, и остаток от деления и будет номером шарда, на который стоит записать или с которого запросить данные. Если например как pivot поле взять user_id, то формула будет выглядеть так:
shard_id = hash(user_id) % num_shards
Плюсы:
Данные равномерно распределяются между шардами
Минусы:
Трудно добавить новые шарды без перераспределения данных (Resharding). Краткий пример: у нас есть пользователь Вася с id = 111. Отдав в хеш-функцию значение 111, мы получили результат 12. У нас 7 шардов. После деления мы получили остаток 5 и записали Васю на 5ый шард. После, мы решили добавить 1 шард для увеличения нагрузоустойчивости системы. Теперь, захотев узнать, в какой шард идти чтобы запросить данные о Васе, мы все также получаем от хеш-функции значение 12, но поделив его уже на 8 (новое кол-во шардов), остаток от деления = 4, и мы негадуем почему же на 4ом шарде данного пользователя не обнаружено.
3. Directory-Based (Каталожное)
В этом методе существует специальный каталог (Directory), который отслеживает, в каком шарде находятся данные. Мы условно "хардкодим" значения в некий список, по типу Вася - Шард1, Никита - Шард2, Ольга - Шард3. При запросе к данным сначала обращаются к каталогу, который указывает нужный шард.
Плюсы:
Гибкость в распределении данных
Можно менять логику шардирования без перераспределения всей базы
Минусы:
Каталог является единой точкой отказа
Дополнительные задержки при доступе к данным
Routing: Как понять, в какой шард отправить запрос?
1. Клиентский метод
Клиент сам знает, где лежат данные, и направляет запрос сразу в нужный шард.
Плюсы:
Отсутствие лишний узлов
Минусы:
Требует дополнительной сложной логики на клиенте
Трудно менять схему шардирования / схему хостов
2. Proxy
Все запросы идут через прокси-сервер, который определяет, куда их направлять. Он не содержит сложной логики, а просто знает, в какой шард направить какой запрос.
Плюсы:
Клиенты ничего не знают о шардировании
Упрощённое управление схемой шардирования
Минусы:
Дополнительный сетевой узел (увеличение задержки)
Потеря функциональности. Будет проблематично, а иногда невозможно выполнять сложные SQL запросы, агрегирующие данные из разных таблиц
Единая точка отказа
3. Coordinator
Координатор - это тот же прокси, но с логикой внутри. Мало того что он направляет каждый запрос в нужный шард, он также способен делать довольно сложные SQL запросы, декомпозировать на под-запросы к разным шардам с последующим объединением и т д.
Плюсы:
Кэширование
Приложение так же как и с прокси не знает о шардинге
Высокая функциональность
Минусы:
Дополнительный сетевой узел
Инфраструктурная сложность
Единая точка отказа
Узкая пропускная способность. При большом количестве запросов может происходить деградация, т.к. все запросы будут проходить через единый координатор
Перебалансировка данных: Как перенести данные из одного шарда на другой?
1. Только чтение. Отклонение запросов на запись
Допустим нам нужно перенести данные с шарда А на шард Б. Одним из возможных решений может быть временно отклонять все запросы на запись, обрабатывая только запросы на чтение. Параллельно тому, постепенно перекидывать данные на нужный шард. Данный метод хорошо подходит для использования ночью, если мы знаем что большинство наших пользователей находятся в одном часовом поясе.
Однако, если бизнес не потерпит недоступность нашего сервиса на любой промежуток времени, придется использовать альтернативный метод.
2. Неизменяемые данные
Если же данные, которыми мы управляем - неизменны(например логи, история покупок и т д), то можно организовать следующий флоу: запросы на запись мы отправляем исключительно на новый шард, а чтение адресуем в оба - старый и новый шард.
К слову, операцию update можно представить как последовательные операции delete и insert(именно по такому принципу и работают многие СУБД, например PostgreSQL).
3. Логическая репликация
Ждем полной синхронизации, работая только со сторым шардом. Затем отключаем старый шард и переключаемся на новый, адресуя все запросы исключительно ему.
4. Смешанный подход
Зачастую, на реальных проектах комбинируют различные подходы, беря лучшее от каждого из них.
Resharding: перераспределение данных
Resharding — это процесс перераспределения данных при добавлении или удалении серверов. Основные причины:
Добавление новых узлов
Исправление ошибок в стратегии шардирования
Дисбаланс нагрузки
Давайте вспомним вышеупомянутую проблему, которая возникает при решардинге, если мы выбрали способ шардирования - Key-Based:
Мы получили хеш 12. У нас 7 шардов. После деления мы получили остаток 5 и записали Васю на 5ый шард. После, мы решили добавить 1 шард для увеличения нагрузоустойчивости системы. Теперь, захотев узнать, в какой шард идти чтобы запросить данные о Васе, мы все также получаем от хеш-функции значение 12, но поделив его уже на 8 (новое кол-во шардов), остаток от деления = 4, и мы негадуем почему же на 4ом шарде данного пользователя не обнаружено.
Т.к. количество шардов может меняться хоть каждый день (сегодня мы хотим повысить производительность - докупили 5 узлов, завтра поняли, что излишние шарды наоборот замедляют нашу систему - сократили их до 3), делаем вывод, что обычного хеширования будет недостаточно. Нужна некая улучшенная версия данного подхода. Давайте рассморим наиболее популярные варианты.
Методы хеширования
Консистентное хеширование (Consistent Hashing)
Консистентное хеширование можно представить как круг, на котором равномерно распределены сервера (или шарды). Каждый ключ (например, ID пользователя) тоже хешируется и помещается на этот круг. Запросы направляются на ближайший сервер по часовой стрелке. Это позволяет легко добавлять или удалять сервера: вместо перераспределения всех данных, достаточно переместить только те, которые попадали на изменённый участок круга. Например, если добавить новый сервер, он возьмёт часть данных у ближайшего соседа, но остальные сервера останутся нетронутыми.
Как понять где на кругу разместить запись? - Принцип циферблата. Пусть наша хеш-функция возращает значения от 1 до 12. Это и станет указателем на кругу.
P.S. шарды и записи необязательно размещать на отрезке [1;12]. Если их много, то можно выставить другие рамки, например, [1;100], разделив круг на промежутку по 2Pi / 100 rad.
Если сервер выходит из строя, его нагрузку берут на себя ближайшие. Однако, при малом количестве серверов распределение может быть неравномерным, но это решается виртуальными узлами (виртуальные шарды — это искусственные копии одного физического сервера, которые распределяются по кольцу хеширования, имитируя большее количество узлов. Это помогает равномернее распределять нагрузку и избежать ситуации, когда один сервер получает слишком много запросов). Также такой подход сложнее классического модульного хеширования (key % N
), но его гибкость делает его незаменимым в больших распределённых системах.
Рандеву-хеширование (Rendezvous Hashing)
При данном подходе, наша хеш-функция принимает 2 аргумента - pivot поле и номер шарда. Комбинация, при которой хеш будет наибольшим и будет указывать нужный шард.
shard := 0 // assume that hash_func(any) > 0
for i := 0; i < len(shards); i++ {
shard = max(hash_func(user_id, i), shard)
}
После указанных команд, в переменной shard будет хранится номер нужного нам узла. Если сервер выходит из строя или добавляется новый, только те ключи, которые были привязаны к нему, перераспределяются, а все остальные остаются на месте. Это делает систему устойчивой к изменениям и снижает нагрузку при решардинге.
Главный плюс рандеву-хеширования — минимальные перестановки данных при изменениях в кластере. В отличие от консистентного хеширования, где ключи могут сильно «прыгать» при добавлении/удалении узлов, здесь перераспределяется только минимально необходимая часть данных. Это делает его удобным для распределённых систем, балансировки нагрузки и кэширующих прокси. Однако есть и минусы: алгоритм сложнее в реализации, чем обычное хеширование, и требует вычисления хешей для всех серверов перед выбором лучшего, что может немного замедлять процесс при большом количестве серверов.
Виртуальные бакеты (Virtual Buckets)
Чтобы лучше понять что такое виртуальные бакеты и чем они полезны, вспомним известную цитату:
Любую архитектурную проблему можно решить добавлением дополнительного слоя абстракции, кроме проблемы большого количества абстракций
Виртуальные бакеты решают проблему необходимости перераспределения данных при изменении количества шардов, добавляя дополнительный уровень абстракции. Вместо того, чтобы сразу привязывать данные к конкретным серверам, мы сначала распределяем их по виртуальным бакетам — маленьким контейнерам или группам. Эти бакеты, в свою очередь, уже привязываются к серверам. Если сервер выходит из строя, его бакеты просто перераспределяются между оставшимися серверами, а не все данные сразу. Это делает систему гибче и снижает нагрузку при изменениях.
Главное преимущество виртуальных бакетов — это равномерное распределение данных и минимальные перестановки при добавлении/удалении серверов. Однако есть и минусы: появляется дополнительный уровень управления, что требует памяти и вычислений. Но в большинстве случаев эта плата за стабильность и предсказуемость вполне оправдана.
Заключение
Масштабирование базы данных – ключевой аспект построения высоконагруженных систем. Репликация повышает отказоустойчивость и распределяет нагрузку на чтение, партиционирование и шардирование помогают эффективно управлять большими объемами данных, а выбор стратегии хеширования и методов перераспределения критически важен для минимизации даунтайма при решардинге.
Что читать дальше?
Типы репликации (логическая, физическая, асинхронная, синхронная) и их влияние на консистентность.
CAP-теорема и PACELC – компромиссы в распределенных системах.
Архитектуры распределенных баз данных (DynamoDB, Spanner, Cassandra, CockroachDB).
Техники уменьшения межузлового трафика (Bloom-фильтры, Merkle-деревья, компрессия).
Практикуйтесь, экспериментируйте с различными подходами и анализируйте их влияние на производительность системы.
Комментарии (5)
Akina
24.01.2025 05:44Вы хотели говорить о методах, которые масштабируют за счёт увеличения количества узлов. Очень хочется спросить - какое отношение к этому методу имеет секционирование (партиционирование)? Вся работа данного метода осуществляется в рамках одного инстанса сервера БД, ну максимум можно раскидать партиции по разным томам.
SkillMax999
24.01.2025 05:44Хорошая статья. Всё разложено по полочкам, структурно. Подходит для тех, кто готовится к собесам
economist75
Понравилось изложение. В статье не упомянуто, что партицирование (горизонтальное) на практике чаще всего делается по времени (по годам или же чаще по кварталам), т.к. 80% данных в большом бизнесе - это временные ряды (индексы таблиц чаще всего TimeStamp c наносекундами). Партицирование прекрасно реализовано даже в крохотных бессерверных аналитических движках типа DuckDB, без падения скорости выполнения запросов по всем партициям (и с ускорением в разы при запросах по последней партиции, самым частым, те же 80%).
Но в использовании оно все равно сложное, потому что "бухгалтерия в конце марта закрывает декабрь", а значит приходится обновлять не одну (4 кв.), а две партиции (4+1 кв.) А на практике часто все 5, потому что... Потому что изменения в марте часто вносятся и в 1-й квартал прошлого года, а значит придется изменить все 5-ть партиций.