Перейти к содержанию

Жизненный цикл инстанса

В контексте операционных систем каждый инстанс соответствует группе из двух процессов. Данный раздел высокоуровнево описывает основные этапы инициализации инстанса. Они изображены на следующей схеме.

main.rs

Красным показан родительский процесс, который запущен на всем протяжении жизненного цикла инстанса. Вся логика, начиная с присоединения (joining) к кластеру, и заканчивая обслуживанием клиентских запросов, происходит в дочернем процессе (голубой цвет). Единственное предназначение родительского процесса — иметь возможность сбросить состояние дочернего и инициализировать его повторно (сиреневый цвет).

Ниже описаны детали выполнения каждого этапа и соответствующей программной функции.

fn main()

На этом этапе происходит ветвление (форк) процесса. Родительский процесс, именуемый в дальнейшем "supervisor", ожидает от дочернего процесса сообщения по механизму IPC и при необходимости перезапускает его.

Выполнение дочернего процесса начинается с вызова функции start_discover() и далее следует алгоритму. При необходимости дочерний процесс может попросить родителя удалить все рабочие файлы инстанса. Это используется для того чтобы корректно учесть две особенности Tarantool:

  1. Принадлежность инстанса тому или иному репликасету определяется в момент первого вызова box.cfg(), когда создается первый снапшот. Впоследствии изменить принадлежность репликасету невозможно.
  2. Инициализация сервера iproto, реализующего бинарный сетевой протокол Tarantool, выполняется той же функцией box.cfg().

В совокупности эти две особенности создают проблему "курицы и яйца":

  • инстанс не может общаться по сети, пока не узнает принадлежность репликасету;
  • принадлежность репликасету невозможно узнать без общения по сети.

Чтобы эту проблему решить, инициализация на этапе start_discover() происходит со случайно сгенерированными идентификаторами инстанса, после чего supervisor очищает рабочую директорию и перезапускает дочерний процесс, начиная с функций start_boot() или start_join().

fn start_discover()

Дочерний процесс начинает свое существование с функции init_common(), в рамках которой в т.ч. инициализируется движок базы данных box.cfg().

Возможно, что при этом из БД станет ясно, что данный инстанса уже был добавлен в кластер — в таком случае этап discovery пропускается, инстанс регистрирует событие безопасности recover_local_db, после чего переходит к этапу postjoin().

В противном случае, если место инстанса в кластере еще не известно, алгоритм discovery позволяет найти адрес лидера или же определить, что им должен стать данный инстанс (i_am_bootstrap_leader).

В контексте инициализации кластера важно лишь то, что этот алгоритм позволяет выполнить инициализацию не более чем одному инстансу. Если таких инстансов было бы несколько, то и кластеров Picodata получилось бы несколько.

После discovery при отсутствии ошибок инстанс выполняет процедуру "rebootstrap" — сбрасывает свое состояние, чтобы повторно провести инициализацию box.cfg(), теперь уже с известными параметрами. Bootstrap-лидер выполняет start_boot(). Остальные инстансы переходят к start_join().

fn start_boot()

В функции start_boot() происходит инициализация системных глобальных таблиц Picodata — лидер генерирует и сохраняет в БД первые записи в raft-журнале. Эти записи описывают добавление первого инстанса в пустую raft-группу и создание начальной конфигурации кластера.

Сам raft-узел на данном этапе еще не создается. Это произойдет позже, на стадии postjoin().

fn start_join()

Вызову функции start_join() всегда предшествует rebootstrap (удаление всех данных и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля box, ни пространства хранения. Функция start_join() имеет простое устройство:

Инстанс отправляет join-запрос лидеру raft-группы (он известен после discovery), который в ответе присылает всю необходимую для инициализации информацию:

  • идентификатор raft_id;
  • данные системной таблицы _pico_peer_address;
  • идентификаторы instance_uuid, replicaset_uuid;
  • box.cfg.replication — список адресов для репликации.

Получив все настройки, инстанс использует их в box.cfg() (см. init_common()), и затем заполняет таблицу _pico_peer_address актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на сообщения от других членов raft-группы.

По завершении этих манипуляций инстанс регистрирует событие безопасности connect_local_db и также переходит к этапу postjoin().

fn postjoin()

Логика функции postjoin() одинакова для всех инстансов. К этому моменту для инстанса уже инициализированы корректные пространства хранения в БД и могут быть накоплены записи в журнале Raft.

Функция postjoin() выполняет следующие действия:

  • инициализирует HTTP-сервер в соответствии с параметром --http-listen;
  • запускает Lua-скрипт, указанный в аргументе --script;
  • инициализирует узел Raft, который начинает взаимодействовать с другими инстансами кластера;
  • в случае, если инстанс в кластере всего один, он тут же избирает себя лидером группы;
  • устанавливает триггер on_shutdown, который обеспечит корректное завершение работы инстанса.

Последним шагом инстанс оповещает кластер о том, что он готов проходить настройку необходимых подсистем (репликации, шардинга, и т.д.). Для этого лидеру отправляется запрос на обновление target_grade текущего инстанса до уровня Online, после чего за дальнейшие действия будет отвечать специальный поток управления topology governor.

Как только запись с обновленным target_grade будет применена, функция postjoin завершается. Сам процесс при этом остается запущен и продолжает исполнять файберы и обслуживать сетевые запросы.

fn init_common()

Функция init_common обобщает действия, необходимые для инициализации инстанса во всех трех вышеописанных сценариях — start_discover(), start_boot(), start_join().

Инициализация инстанса подразумевает следующие шаги:

  • создание директории с данными инстанса (из аргумента picodata run --data-dir);
  • первичный вызов box.cfg;
  • инициализация журнала событий безопасности;
  • инициализация Lua модулей vshard и http;
  • инициализация распределенного SQL;
  • инициализация хранимых процедур (box.schema.func.create);
  • создание локальных спейсов _raft_log и _raft_state;
  • создание системных таблиц.

Параметры первичного вызова box.cfg зависят от конкретного сценария:

param start_discover start_boot start_join
listen None None from args
read_only false false from join response
uuids random given from join response
replication None None from join response
data_dir from args from args from args
log_level from args from args from args