Skip to content
Snippets Groups Projects
Commit 28e242f9 authored by Alexander Tolstoy's avatar Alexander Tolstoy Committed by Yaroslav Dynnikov
Browse files

Update clustering.md

parent ffbbed0d
No related branches found
No related tags found
1 merge request!77Updated clustering.md
Pipeline #4055 passed
# Инициализация кластера. Высокоуровнево. # Общая схема инициализации кластера
Данный документ описывает высокоуровневый процесс инициализации кластера Picodata на основе нескольких отдельно запущенных экземпляров Picodata (инстансов).
Админ запускает пачку инстансов: Администратор запускает несколько инстансов, передавая в качестве аргументов необходимые параметры:
```sh ```sh
picodata run --instance-id i1 --listen i1 --peer i1,i2 picodata run --instance-id i1 --listen i1 --peer i1,i2
...@@ -10,85 +11,70 @@ picodata run --instance-id i3 --listen i3 --peer i1,i2 ...@@ -10,85 +11,70 @@ picodata run --instance-id i3 --listen i3 --peer i1,i2
picodata run --instance-id iN --listen iN --peer i1,i2 picodata run --instance-id iN --listen iN --peer i1,i2
``` ```
Сколько бы инстансов ни было, в опции `--peer` у каждого следует указать один и тот же набор из нескольких "первых". На них возлагается особая миссия по инициализации кластера (дискавери). Независимо от количества запускаемых инстансов, в опции `--peer` у каждого из них следует указать один и тот же набор из нескольких инстансов - обычно первых двух. Именно на их основе будет произведена инициализация кластера и поиск всех работающих инстансов для их включения в состав кластера (discovery).
Подробный алгоритм дискавери в этой истории роли не играет, но описан в соседнем файле `discovery.md`. Пока об алгоритме дискавери достаточно знать лишь то, что, следуя этому алгоритму, один и только один из этих пиров возьмет на себя смелость создать рафт группу. Иначе рафт групп получилось бы неколько. Подробности алгоритма discovery приведены в отдельном [документе](discover.md). В контексте сборки кластера важно лишь понимать, что этот алгоритм позволяет лишь одному инстансу/peer'у создать Raft-группу, т.е. стать инстансом с raft_id=1. Если таких инстансов будет несколько, то и Raft-групп, а следовательно и кластеров Picodata получится несколько.
Всё управление топологией рафт группы по сути возлагается на сам алгоритм рафт. И на его конкретную имплементацию - крейт `raft-rs`. Топологией Raft-группы управляет алгоритм Raft, реализованный в виде крейта `raft-rs`.
# Инициализация кластера. Подробнее. # Этапы инициализации кластера
На схеме ниже показаны этапы жизненного цикла инстанса в контексте его присоединения ко кластеру Picodata.
[https://yuml.me/edit/15c7c2d0] ![main.rs](clustering_curves.svg "main.rs control flow")
![main.rs](main_run.svg "main.rs control flow") Красным показан родительский процесс, который запущен на всём протяжении жизненного цикла инстанса. Вся логика поиска лидера Raft-группы и присоединения к ней происходит в дочернем процессе (голубой цвет). При сбросе состояния инстанса и rebootstrap происходит повторная инициализация форка (сиреневый цвет).
Эта устрашающая схема максимально точно изображает логику кода в `main.rs`. Ниже объясняется подробнее, что происходит на каждом этапе. Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже описаны детали выполнения каждого этапа и соответствующей программной функции.
### fn main() ### fn main()
Сначала процесс пикодаты форкается. Родитель (supervisor) ждет по механизму IPC сообщения от дочернего процесса и при необходимости рестартит его. Опционально дочерний процесс может попросить родителя дропнуть все файлы БД. Это будет нужно для т.н. ребутстрапа. На этом этапе происходит ветвление (форк) процесса `picodata`. Родительский процесс (supervisor) ожидает от дочернего процесса сообщения по механизму IPC и при необходимости перезапускает дочерний процесс. При необходимости дочерний процесс может попросить родителя удалить все файлы БД, т.е. вызвать функцию `drop_db`. Это может понадобиться для повторной инициализации кластера когда, например, у инстанса изначально имеется устаревший или некорректный `replicaset_id`.
### fn start_discover() ### fn start_discover()
Дочерний процесс начинает своё существование с вызова `box.cfg()` и вызова функции `start_discover`. Дочерний процесс начинает своё существование с запуска модуля `box.cfg()` и вызова функции `start_discover`. Возможно, что при этом из постоянно хранимых данных будет ясно, что bootstrap данного инстанса уже был произведён ранее и что Raft уже знает о вхождении этого инстанса в кластер - в таком случае никакого discovery не будет, инстанс сразу перейдёт к этапу `postjoin()`. Однако, если это новый инстанс, то алгоритм discovery выдаст ему флаг `i_am_bootstrap_leader == false` и сообщит адрес лидера Raft-группы. Сам лидер (единственный с `i_am_bootstrap_leader == true`) выполняет функцию `start_boot` и затем переходит к функции `postjoin()`. Остальные инстансы (уже запущенные и все будущие) сбрасывают своё состояние (этап rebootstrap) и переходят к функции `start_join`.
Если вдруг из спейсов обнаруживается, что нода уже была забутстрапленна, то никакой алгоритм дискавери делаь и не надо, и инстанс сразу переходит на этап `postjoin()`. В противном случае, если это первый запуск, из алгоритма дискавери мы получаем флаг `its_me` и адрес лидера. Сам лидер (единственный `its_me == true`) выполняет `start_boot`, после чего выполняет `postjoin()`. Остальные инстансы (не только проигравшие пиры, но и все будущие) ребутстрапятся и идут делать `start_join`.
### fn start_boot() ### fn start_boot()
В функции `start_boot` происходит инициализация рафт группы - лидер генерирует и персистит первую запись в журнале. В этой записи будет лежать операция добавления ноды, что позволит всем остальным инстансам инициализироваться с пустой рафт группой. В функции `start_boot` происходит инициализация Raft-группы - лидер генерирует и сохраняет в БД первую запись в журнале. В этой записи будет лежать операция добавления Raft-узла; все остальные инстансы будут инициализированы с пустой Raft-группой.
Саму рафт ноду инстанс на этом этапе не создаёт. Это произойдет позже, на стадии `postjoin()`. Сам Raft-узел на данном этапе ещё не создаётся. Это произойдет позже, на стадии `postjoin()`.
### fn start_join() ### fn start_join()
Вызову `start_join` всегда предшествует ребутстрап (удаление БД и рестарт процесса), поэтому ни бокса, ни спейсов на этом этапе снова нет. Сама функция достаточно примитивная. Вызову функции `start_join` всегда предшествует rebootstrap (удаление БД и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля box, ни пространства хранения. Функция start_join() имеет простое устройство:
Инстанс отправляет запрос `join` на лидера (лидер известен после дискавери). Лидер шушукается с группой, и если всё хорошо, в ответ присылает необходимую информацию: Инстанс-клиент отправляет запрос `join` лидеру Raft-группы (он известен после discovery). После достижения консенсуса в Raft-группе лидер присылает в ответе необходимую информацию:
- `raft_id` и `raft_group` - для инициализации рафт ноды; - Идентификатор `raft_id` и данные таблицы `raft_group` - для инициализации Raft-узла;
- `insance_uuid`, `replicaset_uuid`, `replication`, `read_only` - для `box.cfg`. - Идентификаторы `instance_uuid`, `replicaset_uuid` и параметры `replication`, `read_only` для `box.cfg`.
Получив все настройки, инстанс засовывает их в `box.cfg()`, и после этого персистит `raft_group` с актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на рафт сообщения. А чтобы записи в `raft_group` не были потёрты менее актуальными из рафт лога, каждая маркируется значением `commit_index`. Получив все настройки, инстанс использует их в `box.cfg()`, и затем создает в БД группу `raft_group` с актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на сообщения от Raft. Для того чтобы записи в `raft_group` не были затем заменены на менее актуальные из журнала Raft, каждая запись маркируется значением `commit_index`.
После всех этих манипуляций, также идёт `postjoin()`. По завершении этих манипуляций инстанс также переходит к этапу `postjoin()`.
### fn postjoin() ### fn postjoin()
Логика `postjoin()` для всех инстансов одинакова. К этому моменту на инстансе уже инициализированы правильные спейсы и возможно даже существует предыстория рафт журнала. Инстанс инициализирует рафт ноду и получает read barrier (это позволяет убедиться, что рафт лог актуален). Из рафт лога становятся известны параметры репликации, и инстанс синхронизируется с репликами. Логика функции `postjoin()` одинакова для всех инстансов. К этому моменту для инстанса уже инициализированы корректные пространства хранения в БД и могут быть накоплены записи в журнале Raft. Инстанс инициализирует узел Raft и проверяет, что данные синхронизированы (`read barrier` получен) и журнал Raft актуален. Из журнала становятся известны параметры репликации, и инстанс начинает синхронизацию данных уровне репликационных групп Tarantool.
Остаётся один маленький штришок - проверить свой статус voter / learner, при необходимости кинуть запрос на промоут до воутера (всё тот же `join`, лидер известен после получения read barrier), и дождаться его применения. На данном этапе инстансу остаётся проверить свой статус voter / learner, при необходимости запросить повышение до статуса voter (повторение функции `join`, лидер известен после получения `read barrier`), и дождаться применения статуса.
Всё, нода готова к использованию. Теперь узел Raft готов к использованию.
# Обработка запросов # Обработка запросов
### extern "C" fn join() ### extern "C" fn join()
Львиная доля всей логики по управлению топологией кроется в хранимке `join`. Её назначение достаточно простое - закоммитить ConfChange, но за этими словами кроется несколько нюансов. Значительная часть всей логики по управлению топологией находится в хранимой процедуре `join`. Её назначение состоит в обработке запросов на добавление нового инстанса (клиента) в Raft-группу с учётом следующих обстоятельств:
Во-первых, если этот `instance_id` уже есть в группе (закоммиченый) и никакая информация не обновилась (например флаг voter / learner), то можно сразу отвечать клиенту не задействуя рафт.
Если это первое появление инстанса в группе, то он всегда добавляется в роли learner. В роли voter его добавлять нельзя, иначе появится проблема курицы и яйца. Чтобы ConfChange с воутером закоммитился, этот voter должен участвовать в кворуме. А он не может - он ещё ждёт ответа на запрос `join`.
Во-вторых, рафт не позволяет делать ConfChange, если предыдущий ConfChange не был закоммичен. Поэтому запросы `join` на лидере придётся обрабатывать батчами в отдельном файбере. Пачку запросов накопили - обработали - каждому послали индивидуальный ответ.
В-третьих, прежде чем отвечать клиенту, надо дождаться, пока ConfChange закоммитится. Или, более формально, пока лидер не выйдет из т.н. joint state (см. рафт диссер §4.3). После этого можно отвечать клиенту `raft_id`, `raft_group`, `insance_uuid`, `replicaset__uuid`, `replication`, `read_only`. В будущем состав ответа может дополняться новыми параметрами по мере необходимости.
- `raft_id` генерит лидер, и делает это строго последовательно и атомарно на весь батч.
- `raft_group` представляет собой дамп всего спейса с топологией кластера. Он понадобится новому инстансу чтобы знать адреса соседей и нормально с ними общаться.
И, наконец, где-то здесь же надо будет убедиться, что нода является лидером, когда генерит `raft_id`. У остальных нет на это права.
# TODO Во-первых, если такой `instance_id` уже имеется в кластере (и его данные сохранены в БД) и никакая другая информация не обновлялась (например флаг `voter` / `learner`), то можно сразу отвечать клиенту, не задействуя Raft.
Q: провести эксперимент, может ли ConfChange пролезть в MsgPropose? Если это первое появление инстанса в группе, то он всегда добавляется в роли `learner`. В роли `voter` его добавлять нельзя, так как для достижения консенсуса в Raft-группе требуется участие добавляемого инстанса, но он не сможет участвовать, так как сам ещё ждёт ответа на запрос `join`.
A: Может
Q: провести эксперимент, можно ли параллельно отравлять simple_conf_change, или это только v2 касается? Во-вторых, Raft не позволяет изменять конфигурацию, пока предыдущее изменение не было применено. Поэтому запросы `join` на лидере Raft-группы необходимо группировать и обрабатывать за один приём в отдельном потоке. Сначала накапливается пакет запросов, затем он обрабатывается, и после этого каждому инстансу возвращается индивидуальный ответ.
A: Пофиг, simple у нас только один в логе - самый первый.
Q: правда ли, что пропоуз сразу после коммита не потеряется, и не задублируется? В-третьих, прежде чем отвечать инстансу-клиенту, надо дождаться применения изменений конфигурации. Это произойдёт после того как лидер Raft-группы выйдет из состояния `joint state` ([подробнее](https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf), §4.3). Только после этого лидер сможет вернуть клиенту данные `raft_id`, `raft_group`, `insance_uuid`, `replicaset_uuid`, `replication`, `read_only`.
A: нет, коммит на фоловерах может прийти, а пропоуз потеряется. тогда у нового лидера не будет подходящего момента, чтобы восполнить эту утрату. Именно поэтому за промоутом до воутера следит сам фоловер.
Q: может ли фоловер (не лернер) слать другому фоловеру MgsAppend закомиченных энтрей? - Значение `raft_id` генерируется лидером Raft-группы, причём строго последовательно и атомарно в рамках всего пакета запросов.
A: ??? - Данные `raft_group` представляют собой копию таблицы с топологией кластера. Они понадобятся новому инстансу чтобы знать адреса соседей и нормально с ними общаться.
- Генерировать значения `raft_id` может только лидер Raft-группы. У любого другого узла Raft нет на это права.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
(start)-|fork|
|fork|[child]->(start_discover)->[summary\n{leader}]-><c>
<c>-[its me]>(start_boot)->(postjoin)
<c>->(drop_db)[rebootstrap]->|reboot|->(start_join)
(start_join)->(postjoin)
(start_discover)->(postjoin)
(postjoin)->|b|
|fork|-[supervisor]>(wait_pid)->|b|
|b|->(end)
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment