Автор: Сергей Петренко
Меня зовут Сергей Петренко, и я работаю в команде кластерных технологий в Tarantool. В прошлом году я рассказывал о том, как Tarantool добавил синхронную репликацию и поддержку автоматического выбора лидера на базе Raft. Теперь предлагаю углубиться в реализацию репликации в Tarantool’е. Я расскажу о том, как работает репликация, на чем она основана в Tarantool и почему очевидные решения не всегда являются самыми оптимальными.
Если вы всегда хотели узнать больше об этой теме и понять, как работает репликация на реальных примерах, эта статья для вас.
Немного о хранении данных
Tarantool — это платформа для вычислений в памяти, в которой данные хранятся в оперативной памяти. Для большей безопасности данные также сохраняются в виде полных моментальных снимков состояния сервера и в файлах журнала с упреждающей записью.
Снапшоты (файлы .snap) ускоряют восстановление данных с диска после перезагрузки сервера. Обычно для этого достаточно полной копии данных. Таким образом, снимок не содержит информации обо всех произошедших изменениях. Например, у нас есть поле, указывающее, занят ли туалет. Может принимать значения ЗАНЯТО и СВОБОДНО. Снимок будет хранить только текущее состояние (например, ЗАНЯТО).
Создание полного моментального снимка данных — дорогостоящая операция. Помимо прочего, для обновления моментальных снимков при каждом изменении потребовалось бы много места на диске. Вот почему моментальные снимки дополняются файлами журнала упреждающей записи (WAL) в формате .xlog. Снапшоты обычно делаются на регулярной основе, например, раз в час, при этом журнал постоянно обновляется при каждом изменении в базе данных. В нашем примере с заполненностью туалета в журнале будет храниться полная история изменения значений: ЗАНЯТО, СВОБОДНО, ЗАНЯТО и так далее.
Как только создается новый снимок, все ранее созданные файлы устаревают и могут быть удалены. Это оптимизирует дисковое пространство, сохраняя только один полный моментальный снимок данных и ограниченное количество файлов журнала на сервере.
Tarantool реализует транзакционную репликацию, которую иногда также называют логической (например, в PostgreSQL). Транзакционная репликация, в отличие от других типов, обеспечивает согласованность. Это естественно отражает восстановление из журнала: смотрите на это как на восстановление реплики из журнала, хранящегося на другом диске сервера. Таким образом, реплицируются не сами данные, а операции над данными и даже целые транзакции, состоящие из операций.
Есть несколько настроек ведения журнала:
Box.cfg{wal_mode = ‘write’}
Box.cfg{wal_mode = ‘fsync’}
Box.cfg{wal_mode = ‘none’}
В режимах write
и fsync
мастер подтверждает транзакцию только после сохранения данных в лог — системный вызов write()
должен вернуть OK. В обоих случаях есть гарантии, что операция записи будет успешной. Эти режимы отличаются способом открытия файлов журналов.
Параметр write
используется по умолчанию. В этом режиме лог-файлы синхронизируются только при их закрытии: каждый write
вызов возвращается сразу после записи данных в выделенный буфер ОЗУ, но не обязательно на диск.
В режиме fsync
файлы журналов открываются с флагом O_SYNC
. Это означает, что write()
возвращает OK только после того, как операционная система отправила данные на диск.
Если выбран режим none
, в журнал ничего не записывается. Единственный способ сохранить данные на диск в этом режиме — сделать снимок. Все изменения, сделанные после снимка, будут храниться только в памяти, и они исчезнут после перезагрузки. Репликация не работает в режиме none
, так как нет лога, который является источником данных для репликации.
Что происходит при создании нового сервера?
Любой сервер должен быть определенным образом инициализирован, прежде чем он сможет начать обработку клиентских запросов. Все системные пространства (таблицы) должны быть созданы и заполнены исходными данными:
_cluster
— список всех серверов, зарегистрированных в кластере_user
и_priv
— системные пространства, содержащие информацию о пользователях и их правах._space
и_index
— системные пространства, содержащие информацию о созданных пространствах и их индексах
Кроме того, каждый сервер идентифицируется уникальным значением instance_uuid
, а каждый кластер — уникальным значением cluster_uuid
.
Изначально ни одна из нод не принадлежит кластеру, так как они запускаются впервые, и кто-то должен указать, где они находятся. лидер начальной загрузки — это сервер инициализации кластера, используемый в таком случае. Лидер Bootstrap создает начальный снимок данных. Он содержит системные данные, перечисленные выше, размером около пяти килобайт. Как только бутстрап-лидер закончит создание начального снапшота, он готов к работе с другими узлами: зарегистрировать их в кластере и отправить им начальный снэпшот. Таким образом, в момент первоначальной настройки сервер либо генерирует исходный снимок данных, либо получает его от одного из уже инициализированных узлов. Читайте дальше, чтобы узнать больше о том, как новый сервер решает, от кого он должен получить моментальный снимок.
Независимо от того, как был инициализирован сервер, исходный снимок является автономным: новый сервер также включен в него. Это означает, что пространство _cluster
в исходном снимке (и во всех последующих) всегда содержит запись {id, instance_uuid}
, соответствующую серверу. Единственным исключением являются анонимные узлы, о них мы поговорим позже.
ВКлок
Vclock — это массив порядковых номеров журнала, определяющий версию набора данных, хранящегося на сервере, т. е. количество логических операций, выполненных на текущем сервере. Каждый узел хранит данные, соответствующие не конкретному номеру LSN, а массиву номеров LSN с одним компонентом для каждого члена кластера. Это достигается повторной отправкой операций с каждого зарегистрированного узла кластера (кроме анонимных реплик) на другие узлы с использованием репликации транзакций.
Tarantool поддерживает репликацию «мастер-мастер», что означает, что независимые изменения могут происходить на разных узлах одновременно. Вот почему мы используем vclock вместо одного монотонно возрастающего LSN. Используя эти знания, давайте разберемся, зачем нам нужно пространство _cluster
.
Что это за _cluster?
Выше я упоминал, что пространство _cluster
содержит список всех серверов, зарегистрированных в кластере. Но какой в этом смысл? Зачем нам хранить информацию о членах кластера? Эти данные важны для сопоставления server_uuid : id
, которое используется для определения того, на каком сервере были сделаны изменения. «Почему бы нам не подписать изменения с помощью идентификатора сервера (instance_uuid
)?», — спросите вы. Конечно, это будет намного проще сделать, но это потребует отправки 18 дополнительных байтов UUID (+ несколько байтов для кодирования в msgpack) в дополнение к каждой операции. Вместо этого используется байт id
типа int. Его значения ограничены константой VCLOCK_MAX = 32
, поэтому id
всегда кодируется в msgpack одним байтом.
Этапы репликации
В Tarantool связь с мастером всегда инициируется репликами. С точки зрения сервера все удаленные узлы, к которым он подключен, считаются ведущими. «Настоящий» мастер назначается пользователем, который делает один из серверов rw, а остальные — ro, или, в случае репликации master-master, оставляет все серверы rw. Для этого вам нужно установить box.cfg{read_only = true
и box.cfg{read_only = false}
соответственно.
В дальнейшем мы будем называть сервер, который принимает соединения, мастером, а сервер, который подключается к удаленному хосту, — репликой. Например, при рекомендуемом в Tarantool полносвязном соединении (все подключаются ко всем) каждый сервер будет одновременно и мастером, потому что он обслуживает входящие соединения и отправляет на них изменения, и репликой, так как подключается ко всем остальным. Список основных URI для получения изменений передается в box.cfg.replication
.
Соединение с каждым из мастеров обрабатывается выделенным волокном, называемым applier. Логика приложения может быть очень грубо описана следующим циклом:
while (true) {
try {
applier_connect();
if (!joined)
applier_join();
applier_subscribe();
} catch (RecoverableError) {
goto Reconnect;
} catch (UnrecoverableError) {
goto LogErrorAndExit;
}
Reconnect:
sleep(replication_timeout);
}
LogErrorAndExit:
…
Идея состоит в том, что приложение все время пытается поддерживать соединение с мастером, и если мастер отключен или возникла другая решаемая проблема, приложение пытается восстановить соединение после короткого тайм-аута.
На стороне мастера создается отдельный поток, называемый ретранслятором, для связи с каждой репликой. Основная и в то же время самая простая задача реле — прочитать лог из нужного места и отправить его содержимое на подключенную реплику.
Статус репликации легче отслеживать из реплики. Его приложение последовательно проходит следующие состояния: ПОДКЛЮЧЕНИЕ, АУТЕНТИФИКАЦИЯ, ПРИСОЕДИНЕНИЕ (НАЧАЛЬНОЕ + ОКОНЧАТЕЛЬНОЕ), ПОДПИСКА (СИНХРОНИЗАЦИЯ + ПОДПИСКА). Давайте разберемся, что это за состояния и как они работают.
СОЕДИНЯТЬ
Когда реплика подключена, ее приложение переходит в состояние CONNECT. При новом подключении мастер сразу выдает текстовое приветствие, которое выглядит так:
“Tarantool 2.10.0 (Binary)” “68387c01-c9be-4dc5-842a-a2bee66f5b4d” “<salt>”
Идентификатор во второй строке — это UUID сервера, а <salt>
— соль пароля, которая будет использоваться для аутентификации (состояние AUTH).
АВТОРИЗАЦИЯ
AUTH — это необязательный шаг, который выполняется только в том случае, если информация аутентификации передается вместе с URI. Например, box.cfg{replication="127.0.0.1:3301"}
или box.cfg{replication="user:[email protected]:3301"}
можно передать в box.cfg.replication
. В первом случае AUTH выполняться не будет, а во втором случае будет. В противном случае соединение устанавливается от имени пользователя guest
.
Чтобы репликация работала, реплики должны подключаться как пользователь с правами на чтение юниверса, что позволяет им читать всю базу данных и записывать в пространство _cluster
. Такие (или более сильные) права доступа могут быть даны явно, или вы можете дать пользователю роль replication
, которая будет иметь тот же эффект. Необходимые разрешения проверяются мастером при обработке запросов JOIN и SUBSCRIBE.
Как избирается лидер начальной загрузки?
После прохождения или провала аутентификации мы переходим к выбору лидера начальной загрузки. В этом разделе я объясню, как выбирается лучший узел для регистрации.
Примечание: здесь мы не будем рассматривать алгоритм выбора лидера Raft. Эти два понятия часто путают. Лидер начальной загрузки — это узел, который выполняет первоначальную настройку кластера. После того, как первоначальная настройка выполнена, не имеет значения, кто был лидером начальной загрузки.
Как только реплика получила приветствие и проверила его (то есть мы действительно подключились к Tarantool), она отправляет мастеру запрос VOTE.
В ответ на это мастер присылает ответы на вопросы, на основании которых реплика может сделать выбор:
- Сервер настроен?
- Сервер настроен как НЕ
read_only
? - Сервер НЕ
read_only
в данный момент? - Настроен ли сервер с
election_mode manual
илиcandidate
?
После сбора ответов от каждого сервера, указанного в box.cfg.replication
, узел переходит к выбору лидера начальной загрузки. Каждый утвердительный ответ добавляет очки серверу, и в итоге побеждает сильнейший.
В случае, если несколько серверов дали одинаковые ответы на все вопросы, выбирается сервер с наибольшим vclock. Если vclock тоже совпадают, то побеждает узел с наименьшим instance_uuid
.
Вы также можете явно указать реплику в box.cfg.replication
. Затем, при соблюдении вышеперечисленных условий, эта реплика может стать лидером начальной загрузки. Явное указание реплики имеет смысл почти во всех случаях. В противном случае может возникнуть ситуация, когда все узлы считают лидером определенный сервер, а этот узел считает своим лидером какой-то другой узел, что приводит к зависанию бутстрапа.
Если узел оказался для себя бутстреп-лидером, то он переходит к заполнению начального снапшота. В других случаях узел отправляет запрос JOIN выбранному лидеру начальной загрузки, чтобы получить от него снимок. Каждый узел выбирает независимо на основе полученных ответов VOTE.
Стоит запомнить
- Если ни один из серверов в кластере не инициализирован, лидером становится узел с наименьшим
instance_uuid
. - Вы можете явно указать лидер начальной загрузки, передав серверу параметр
box.cfg.instance_uuid
. - Если кластер содержит серверы, сконфигурированные как
election_mode=’candidate’
или‘manual’
, один из них становится лидером начальной загрузки. Этот же сервер станет лидером Raft в первый срок после инициализации кластера. - Если есть уже инициализированные серверы, среди них будет выбран бутстрап-лидер.
Распространенные ошибки настройки
Пример №1. Давайте попробуем инициализировать двухузловой кластер. В конфигурации каждого узла укажем только URI его соседа, например, box.cfg{listen=3301, replication=3302}
. Оба узла будут зависать без создания моментального снимка. Причина в том, что первая нода выбрала вторую в качестве бутстреп-лидера, а вторая нода выбрала первую. Каждый из них ждет, пока другой инициализирует кластер и отправит начальный снимок.
Сервер ищет лидер начальной загрузки среди узлов, перечисленных в box.cfg.replication
. Поэтому на каждом из серверов нужно указать и текущую ноду, и другую, чтобы они оба могли выбирать из одного и того же набора опций. Правильный способ настройки обоих узлов таков: box.cfg{listen=..., replication={3301,3302}}
.
Пример №2. Предположим, что в кластере уже настроены две ноды, прослушивающие порты 3301 и 3302. Попробуем добавить еще две ноды и передать им разные настройки репликации: box.cfg{replication=3301}
и box.cfg{replication=3302}
. В результате репликация между узлами 3301 и 3302 остановится с такой ошибкой:
main/111/applier/ memtx_tree.cc:870 E> ER_TUPLE_FOUND: Duplicate key exists in unique index "primary" in space "_cluster" with old tuple - [3, "3af63a1e-9e35-4ce8-b76c-5c51100c36e8"] and new tuple - [3, "c611e878-4869-47d9-80b4-7ed9859e2403"]
Каждая из нод зарегистрировала новую реплику, которая подключилась к ней под одним и тем же id — 3. Чтобы этого не произошло, нужно указать идентичный список box.cfg.replication
при загрузке нескольких серверов одновременно, для каждого из них, включая текущий один.
Параметры replication_connect_timeout
и replication_connect_quorum
также помогают предотвратить одновременную регистрацию одного и того же идентификатора на разных узлах. При начальной загрузке replication_connect_timeout
работает следующим образом: перед выбором лидера начальной загрузки сервер пытается подключиться ко всем серверам из box.cfg.replication
в течение replication_connect_timeout
. Если не удалось подключиться ко всем, то серверу достаточно подключиться хотя бы к replication_connect_quorum
серверам, после чего он переходит к выборам.
По умолчанию replication_connect_timeout
равно 30 секундам, а replication_connect_quorum
равно количеству серверов, перечисленных в box.cfg.replication
. Эти опции будут работать правильно, только если вы передадите одинаковые box.cfg.replication
списки на стартовые серверы.
ПРИСОЕДИНИТЬСЯ
Определившись с бутстрап-лидером, реплика отправляет запрос на присоединение к кластеру — JOIN. Он состоит из двух этапов: INITIAL JOIN и FINAL JOIN (это внутреннее разделение). И реплика, и мастер переходят ко второму этапу сразу после первого, не отправляя никаких дополнительных запросов.
Вместе с запросом JOIN реплика отправляет свой файл instance_uuid
. При получении этого запроса мастер сначала проверяет разрешения, а затем создает снимок текущего состояния в памяти (который не записывается на диск). Началом ответа на запрос JOIN является vclock мастера на момент JOIN — vclock только что созданного снапшота. Далее отправляется состояние Raft (текущий срок мастера) и очереди синхронных транзакций (его владелец и срок, в котором он им завладел). Затем мастер шаг за шагом проходит весь моментальный снимок данных. Он выполняет итерацию по пространствам в порядке возрастания их id, а внутри каждого пространства по первичному ключу, отправляя реплике поток данных INSERT, соответствующий содержимому пространств.
После отправки данных снимка (фаза INITIAL_JOIN) мастер регистрирует подключающуюся реплику в кластере, вставляя пару {id, replica_uuid}
в свободный слот пространства _cluster
. Начинается этап FINAL_JOIN. Мастер информирует об этом реплику, снова отправляя обновленный vclock. Этот vclock будет соответствовать начальному снимку, сделанному самой репликой на основе всех данных, полученных от мастера. На этом этапе мы назовем его stop_vclock.
Этап FINAL_JOIN состоит из отправки операций, накопленных в логе за время INITIAL_JOIN. Поток данных FINAL_JOIN выглядит так же, как поток SUBSCRIBE, который мы обсудим ниже. Единственное отличие состоит в том, что FINAL_JOIN, в отличие от SUBSCRIBE, является конечным и завершается, как только мастер отправляет запись в журнал, соответствующую stop_vclock.
Вы спросите, а зачем вообще нужен этап FINAL_JOIN? Почему бы не перейти на этап ПОДПИСКА сразу после отправки снимка? Более того, FINAL_JOIN — это, по сути, усеченный этап SUBSCRIBE. Проблема в том, что снимок, полученный от мастера во время INITIAL_JOIN, еще не имеет записи о новом узле. Его регистрация происходит только после успешной отправки снапшота. Поэтому перед сохранением начального снимка на диск реплика должна быть зарегистрирована мастером. Чтобы «прокрутить» поток репликации до его регистрации, реплике необходимо дождаться записи, соответствующей stop_vclock.
Итак, реплика нуждается в фазе FINAL_JOIN по единственной причине: моментальный снимок реплики должен иметь регистрацию в пространстве _cluster
. Вместе со своей регистрацией реплика получает все изменения, произошедшие на мастере на момент отправки снапшота. От них никуда не деться.
ПОДПИСАТЬСЯ
ПОДПИСКА — это подписка на все изменения, происходящие на мастере. Реплика, подключенная к кластеру, отправляет запрос одновременно всем узлам, перечисленным в box.cfg.replication
. С точки зрения реплики SUBSCRIBE делится на две части: SYNC и FOLLOW.
Явных признаков окончания одного этапа и начала следующего в потоке репликации нет. Реплика сама переключается из состояния SYNC в состояние FOLLOW в соответствии с определенным правилом.
Состояние SYNC необходимо для предотвращения перехода отстающей и вновь запущенной реплики в состояние записи до того, как она догонит все изменения в кластере. Пока соединения со всеми членами кластера не перейдут в состояние FOLLOW, реплика доступна только для чтения. Его box.info.status
будет установлено на orphan
.
Вы также можете изменить параметр box.cfg.replication_connect_quorum
. Он контролирует количество соединений, которые должны переключиться в состояние FOLLOW, прежде чем узел выйдет из состояния orphan
и станет writeable
.
Вместе с SUBSCRIBE реплика отправляет свои instance_vclock
(мастер использует vclock для поиска первого требуемого лог-файла), instance_uuid
и cluster_uuid
для проверки регистрации в кластере. При обработке SUBSCRIBE мастер сначала проверяет разрешения, а затем регистрацию реплики в кластере. Далее мастер проверяет, что файлы журналов (.xlog), содержащие данные из instance_vclock
, еще не были удалены сборщиком мусора, о котором я расскажу ниже.
Если все проверки пройдены, то мастер отвечает реплике своим текущим vclock. Мы назовем его start_vclock. За start_vclock следует бесконечный поток репликации, пока соединение не разорвется или не произойдет другая ошибка. Поток репликации содержит все операции, которые выполнялись и выполняются на мастере (начиная с instance_vclock). На мастере этот поток генерирует отдельный поток — relay — который читает все лог-файлы из нужного места и построчно отправляет их на реплику.
Чтобы иметь возможность понять, какая транзакция относится к операции в логе и потоке репликации, мы присваиваем каждой операции LSN, который я описал в главе о хранении данных, и TSN — порядковый номер транзакции. TSN для каждой операции в транзакции одинаков и равен LSN первой операции. Реплика, руководствуясь TSN операций, поступающих по потоку репликации, собирает их обратно в транзакции, применяет их и записывает в свой журнал. Таким образом, журнал реплики становится почти точной копией журнала мастера (при условии, что не используются триггеры репликации).
Теперь поговорим о правиле, по которому соединение с мастером переключается из состояния SYNC в состояние FOLLOW. Одной из основных характеристик соединения является отставание реплики от мастера. Этот лаг — это время между попаданием записи в журнал мастера и получением этой записи репликой. Мы называем это задержкой репликации.
Переход соединения из SYNC в FOLLOW происходит, как только выполняются два условия:
- Задержка репликации не превышает
box.cfg.replication_sync_lag
(по умолчанию 10 секунд). - Реплика получила все данные вплоть до start_vclock — то есть до того vclock, который был у мастера на момент установления соединения.
Как упоминалось выше, процесс SUBSCRIBE бесконечен, его можно прервать только из-за ошибки или потери мастера/реплики. И мастер, и реплика используют тайм-ауты, чтобы определить, когда другая сторона соединения отсутствует. Тайм-аут репликации настраивается с помощью параметра box.cfg.replication_timeout
, который по умолчанию равен одной секунде.
При отсутствии новых изменений для отправки мастер отправляет пинги реплике с частотой, указанной в box.cfg.replication_timeout.
. Реплика отвечает на пинг так же, как и на получение обычной транзакции — пакетом ACK, содержащим ее текущий vclock.
Используя vclock в пакете ACK реплики, мастер отслеживает, какие данные реплика фактически получила и применила, управляет сбором старых файлов журнала и подсчитывает количество узлов, которые применили синхронную транзакцию (ожидание кворума).
Если нет новостей от мастера или реплики в течение четырех replication_timeout
раз, то другая сторона разрывает соединение. Кто бы ни отвечал за это, после каждого дисконнекта реплика пытается переподключиться к мастеру один раз за replication_timeout
.
Как реализован мониторинг репликации
Таблица box.info.replication
помогает отслеживать репликацию. Он содержит информацию обо всех узлах, зарегистрированных в пространстве _cluster
. Вся доступная информация о реплике с определенным id содержится в box.info.replication[id]
. Это выглядит так:
> box.info.replication[3]
—--
id: 3
uuid: 67d66a88-59ad-4348-881f-09ebb6eb119f
lsn: 5
upstream:
status: follow
idle: 0.73756800000046
peer: localhost:3303
lag: 0.0002129077911377
downstream:
status: follow
idle: 0.064674000001105
vclock: {1: 3, 3: 5}
lag: 0
...
Поля id
, uuid
и lsn
означают именно то, на что они похожи. Что касается upstream
и downstream
, я находил их немного запутанными, пока не выработал «эмпирическое правило»:
Вверх по течению — что-то приходит к нам вверх по течению. Это состояние входящего потока репликации (с удаленного сервера к нам), который обрабатывается приложением.
Поля вверх по течению:
status
— повторяет состояние соединения (например,connect
,auth
,sync
,follow
)Idle
— время (в секундах) с момента получения последней посылкиpeer
— соответствующая строка изbox.cfg.replication
lag
— отставание репликации
Вниз по течению — что-то движется от нас вниз по течению. Это состояние исходящего потока репликации (от нас к удаленному серверу), который генерируется ретранслятором.
Нисходящие поля:
status
— либоfollow
, либоstopped
, в зависимости от того, есть соединение онлайн или нетIdle
— то же, что и вupstream
: время с момента получения последнего пакета от репликvclock
— последний vclock, отправленный репликойlag
— задержка, рассчитанная с точки зрения мастера. Это время, прошедшее между последней операцией, занесенной в журнал мастера, и получением подтверждения этой операции от реплики. Если представить, что часы на реплике и мастере идеально синхронизированы и посмотреть на upstream.lag реплики и downstream.lag мастера, то окажется, что downstream.lag равен upstream.lag + время доставки пакета ACK от реплики к мастеру.
Если соединение разрывается, то соответствующие восходящие или нисходящие потоки получают новые поля message
и system_message
, предоставляя информацию о возникшей ошибке. Например:
upstream:
peer: localhost:3303
lag: 9.8943710327148e-05
status: disconnected
idle: 4.1842909999978
message: 'connect, called on fd 19, aka 127.0.0.1:65001: Connection refused'
system_message: Connection refused
Or
downstream:
status: stopped
message: 'unexpected EOF when reading from socket, called on fd 26, aka [::1]:3302,
peer of [::1]:64955: Broken pipe'
system_message: Broken pipe
Сборка мусора XLOG/SNAP
Количество снимков, хранящихся на диске, контролируется параметром box.cfg.checkpoint_count
, а частота создания снимков — параметром box.cfg.checkpoint_interval
. Новый снимок создается каждые checkpoint_interval
секунд, и как только количество снимков достигает checkpoint_count
, создание каждого нового снимка приводит к удалению самого старого снимка.
В начале статьи я сказал, что Tarantool хранит все лог-файлы после самого старого снапшота. Это необходимо для того, чтобы вы могли восстановиться из любого из снимков checkpoint_count
. Файлы, которые необходимы хотя бы одной из реплик, также сохраняются.
Как мастер понимает, какие файлы нужны репликам?
Каждая реплика хранит информацию о последнем полученном vclock. Эту информацию сохраняет vclock, который изначально получает реплика в момент JOIN, а также в момент SUBSCRIBE vclock, отправляемый репликой. Позже vclock, необходимый для каждой реплики, обновляется по мере поступления от нее пакетов ACK.
Информация о том, какие лог-файлы еще нужны репликам, не хранится на диске, поэтому после перезагрузки сервера он может удалить все ненужные, по его мнению, .xlog-файлы. В этом случае вернуть реплику в кластер можно только с помощью re-JOIN: нужно удалить все ее данные с диска и запустить заново. Параметр box.cfg.wal_cleanup_delay
помогает избежать подобных ситуаций. Измеряется в секундах и определяет время простоя сборщика мусора после перезагрузки сервера. По умолчанию box.cfg.wal_cleanup_delay
равно 4 часам, и в течение первых 4 часов после запуска мастер не будет удалять старые файлы .xlog, чтобы дать репликам возможность повторно подключиться и сообщить, какие файлы им действительно нужны. Вы можете посмотреть состояние сборки мусора в box.info.gc()
— там перечислены все реплики и требуемые им виртуальные часы, а также все моментальные снимки.
Анонимные реплики
Исторически кластер не может иметь более 31 зарегистрированного узла. Это связано с тем, что каждая нода получает уникальный идентификатор и занимает место в vclock. Vclock не может расти бесконечно, потому что он регулярно обменивается между узлами кластера: он содержится в каждом ACK-пакете. Слишком длинные виртуальные часы могут занимать больше пропускной способности, чем полезная нагрузка, поэтому размер виртуальных часов ограничен 31 «ячейкой» для сервера, что означает, что в кластере может быть не более 31 зарегистрированного члена.
Анонимные реплики помогают обойти это ограничение. Они используются как подписчики на изменения и не занимают места в _cluster
. Для работы они не требуют регистрации, но и не могут генерировать никаких изменений — только в локальные и временные пространства, об этом в следующей главе. В кластере может быть любое количество анонимных реплик. Каждый из них доступен только для чтения, имеет полный набор данных и может обрабатывать запросы на чтение. Можно строить разные сложные топологии: прицеплять одну анонимную реплику к другой, а ту к основной части кластера.
Для создания анонимной реплики необходимо передать ей параметр replication_anon = true
при первоначальной настройке. Тогда вместо JOIN реплика отправит мастеру запрос FETCH_SNAPSHOT. Он работает так же, как и JOIN, но без регистрации реплики в кластере, поэтому он ограничен только начальным этапом. После FETCH_SNAPSHOT последует запрос SUBSCRIBE. Отличие от обычного только в том, что нет проверки, зарегистрирована ли нода в кластере.
Снапшот, созданный на анонимной реплике, не является самодостаточным: в нем нет записи о реплике в пространстве _cluster
. Из такого моментального снимка можно восстановить только анонимную реплику.
Локальные и временные пространства
Не все данные нужно реплицировать: иногда нужно хранить данные только на одном сервере без репликации. Для этого используются локальные и реплика-локальные пространства.
Все данные, хранящиеся в таком пространстве, регистрируются, но не реплицируются.
Поскольку данные в локальных пространствах влияют только на состояние сервера, а не всего кластера, операции с локальными пространствами не увеличивают LSN сервера и не изменяют его компонент vclock. Вместо этого операции с локальным пространством увеличивают выделенный номер LSN, хранящийся в vclock[0]
и не назначенный ни одному узлу. В результате нулевой компонент vclock отличается на каждом сервере и не соответствует vclock[0]
других серверов.
Есть также временные пробелы. В этих пространствах данные не только не реплицируются, но и не попадают в журнал или моментальные снимки. Вы можете писать только во временные пространства на сервере только для чтения. Сохраненные данные будут храниться в пространстве, пока сервер работает. После перезагрузки сервера временное пространство будет пустым.
Поскольку данные в локальном или временном пространстве не влияют на его соседние узлы, даже сервер в режиме только для чтения может заполнить локальное пространство. Это означает, что анонимные реплики могут использовать такие пространства. Есть только одна проблема: создать локальное или временное пространство можно только на записываемом сервере, а после его создания оно появится на всех серверах. Каждый сервер сможет наполнять его независимо от других.
Триггеры
Триггеры предоставляют множество интересных возможностей. Их проще продемонстрировать на некоторых примерах. Не все из них пригодятся вам в ближайшем будущем, но всегда полезно знать свои варианты.
Пример №1.
Заменим космический движок huge_space
с memtx (в памяти) на Vinyl (диск) на реплике:
function _space_before_replace(old, new) if old == nil and new ~= nil and new[3] == 'huge_space' and new[4] == 'memtx' then return new:update{{'=', 4, 'vinyl'}} end end
box.ctl.on_schema_init(function() box.space._space:before_replace(_space_before_replace) end)
Триггер on_schema_init
работает во время инициализации узла. Вы можете поместить этот триггер в систему _space
, которая хранит информацию обо всех пространствах в базе данных. Он позволяет заменить запись движка нужного пространства прямо в тот момент, когда запись о создании пространства достигает реплики.
Пример №2.
На реплике мы хотим игнорировать все изменения unneeded_space
, поступающие от мастера, и подсчитывать количество проигнорированных изменений в переменной skipped_changes
.
local skipped_changes
function empty_space_trig(old, new) if box.session.type() == 'applier' then skipped_changes = skipped_changes + 1 return old end return new end
function make_space_empty(old, new) if old == nil and new[3] == 'unneeded_space' then box.on_commit(function() box.space.unneeded_space:before_replace(empty_space_trig) end) end end
box.ctl.on_schema_init(function() box.space._space:on_replace(make_space_empty) end)
В триггере empty_space_trig
мы будем проверять, откуда пришло изменение. Если оно пришло от мастера, мы его игнорируем. Чтобы убедиться, что мы не пропустим никаких изменений от мастера, мы помещаем триггер на unneeded_space
прямо в момент создания этого пространства. Об этом позаботится функция make_space_empty
. Как только создание пространства зафиксировано (см. box.on_commit()
в примере кода), к пространству будет добавлен триггер before_replace
. Как мы знаем из первого примера, чтобы зафиксировать момент создания пространства, нам нужно поставить триггер на пространство _space
.
Вы можете скачать Tarantool на нашем официальном сайте и получить помощь в Telegram-чате.