Skip to content
Snippets Groups Projects
Commit c2e18ae0 authored by Alexander Tolstoy's avatar Alexander Tolstoy
Browse files

docs: fix clustering.md

add roadmap-en
parent 8b4b4ba8
No related branches found
No related tags found
1 merge request!291docs: fix clustering.md
Pipeline #12753 passed
# Общая схема инициализации кластера # Общая схема инициализации кластера
Данный документ описывает высокоуровневый процесс инициализации кластера Picodata на основе нескольких отдельно запущенных экземпляров Picodata (инстансов). Данный раздел содержит описание архитектуры Picodata, в том числе высокоуровневый процесс инициализации кластера на основе нескольких отдельно запущенных экземпляров Picodata (инстансов).
Администратор запускает несколько инстансов, передавая в качестве аргументов необходимые параметры: Администратор запускает несколько инстансов, передавая в качестве аргументов необходимые параметры:
...@@ -11,18 +11,18 @@ picodata run --instance-id i3 --listen i3 --peer i1,i2,i3 ...@@ -11,18 +11,18 @@ picodata run --instance-id i3 --listen i3 --peer i1,i2,i3
picodata run --instance-id iN --listen iN --peer i1 picodata run --instance-id iN --listen iN --peer i1
``` ```
Независимо от количества запускаемых инстансов, в опции `--peer` у каждого из них следует указать один и тот же набор из нескольких инстансов - одного обычно достаточно, но для подстраховки можно взять три. Именно на их основе будет произведена инициализация кластера и поиск всех работающих инстансов для их включения в состав кластера (discovery). Независимо от количества запускаемых инстансов, в опции `--peer` у каждого из них следует указать один и тот же набор из нескольких инстансов одного обычно достаточно, но для подстраховки можно взять три. Именно на их основе будет произведена инициализация кластера и поиск всех работающих инстансов для их включения в состав кластера (discovery).
Подробности алгоритма discovery приведены в отдельном [документе](discovery.md). В контексте сборки кластера важно лишь понимать, что этот алгоритм позволяет не более чем одному инстансу (peer'у) создать Raft-группу, т.е. стать инстансом с raft_id=1. Если таких инстансов будет несколько, то и Raft-групп, а следовательно и кластеров Picodata получится несколько. Подробности алгоритма discovery приведены в отдельном [документе](discovery.md). В контексте сборки кластера важно лишь понимать, что этот алгоритм позволяет не более чем одному инстансу (peer'у) создать Raft-группу, т.е. стать инстансом с raft_id=1. Если таких инстансов будет несколько, то и Raft-групп, а следовательно и кластеров Picodata получится несколько.
Топологией Raft-группы управляет алгоритм Raft, реализованный в виде крейта `raft-rs`. Топологией Raft-группы управляет алгоритм Raft, реализованный в виде крейта `raft-rs`.
# Этапы инициализации кластера ## Этапы инициализации кластера
На схеме ниже показаны этапы жизненного цикла инстанса в контексте его присоединения к кластеру Picodata. На схеме ниже показаны этапы жизненного цикла инстанса в контексте его присоединения к кластеру Picodata.
![main.rs](clustering_curves.svg "main.rs control flow") ![main.rs](clustering_curves.svg "main.rs control flow")
Красным показан родительский процесс, который запущен на всем протяжении жизненного цикла инстанса. Вся логика, начиная с присоединения к кластеру, и заканчивая обслуживанием клиентсих запросов происходит в дочернем процессе (голубой цвет). Единственное предназначение родительского процесса - иметь возможность сбросить состояние дочернего (выполнить rebootstrap) и инициализировать его повторно (сиреневый цвет). Красным показан родительский процесс, который запущен на всем протяжении жизненного цикла инстанса. Вся логика, начиная с присоединения к кластеру, и заканчивая обслуживанием клиентских запросов происходит в дочернем процессе (голубой цвет). Единственное предназначение родительского процесса иметь возможность сбросить состояние дочернего (выполнить rebootstrap) и инициализировать его повторно (сиреневый цвет).
Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже описаны детали выполнения каждого этапа и соответствующей программной функции. Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже описаны детали выполнения каждого этапа и соответствующей программной функции.
...@@ -32,11 +32,11 @@ picodata run --instance-id iN --listen iN --peer i1 ...@@ -32,11 +32,11 @@ picodata run --instance-id iN --listen iN --peer i1
### fn start_discover() ### fn start_discover()
Дочерний процесс начинает свое существование с запуска модуля `box.cfg()` и вызова функции `start_discover()`. Возможно, что при этом из БД будет ясно, что bootstrap данного инстанса уже был произведен ранее и что Raft уже знает о вхождении этого инстанса в кластер - в таком случае никакого discovery не будет, инстанс сразу перейдет к этапу `postjoin()`. В противном случае, если место инстанса в кластере еще не известно, алгоритм discovery опредяет значение флага `i_am_bootstrap_leader` и адрес лидера Raft-группы. Далее инстанс сбрасывает свое состояние (этап rebootstrap), чтобы повторно провести инициализацию `box.cfg()`, теперь уже с известными параметрами. Сам лидер (единственный с `i_am_bootstrap_leader == true`) выполняет функцию `start_boot()`. Остальные инстансы переходят к функции `start_join()`. Дочерний процесс начинает свое существование с запуска модуля `box.cfg()` и вызова функции `start_discover()`. Возможно, что при этом из БД будет ясно, что bootstrap данного инстанса уже был произведен ранее и что Raft уже знает о вхождении этого инстанса в кластер в таком случае никакого discovery не будет, инстанс сразу перейдет к этапу `postjoin()`. В противном случае, если место инстанса в кластере еще не известно, алгоритм discovery определяет значение флага `i_am_bootstrap_leader` и адрес лидера Raft-группы. Далее инстанс сбрасывает свое состояние (этап rebootstrap), чтобы повторно провести инициализацию `box.cfg()`, теперь уже с известными параметрами. Сам лидер (единственный с `i_am_bootstrap_leader == true`) выполняет функцию `start_boot()`. Остальные инстансы переходят к функции `start_join()`.
### fn start_boot() ### fn start_boot()
В функции `start_boot` происходит инициализация Raft-группы - лидер генерирует и сохраняет в БД первые записи в журнале. Эти записи описывают добавление первого инстанса в пустую Raft-группу. Таким образом достигается однообразие кода, обрабатывающего эти записи. В функции `start_boot` происходит инициализация Raft-группы лидер генерирует и сохраняет в БД первые записи в журнале. Эти записи описывают добавление первого инстанса в пустую Raft-группу. Таким образом достигается однообразие кода, обрабатывающего эти записи.
Сам Raft-узел на данном этапе еще не создается. Это произойдет позже, на стадии `postjoin()`. Сам Raft-узел на данном этапе еще не создается. Это произойдет позже, на стадии `postjoin()`.
...@@ -45,7 +45,7 @@ picodata run --instance-id iN --listen iN --peer i1 ...@@ -45,7 +45,7 @@ picodata run --instance-id iN --listen iN --peer i1
Вызову функции `start_join()` всегда предшествует rebootstrap (удаление БД и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля box, ни пространства хранения. Функция `start_join()` имеет простое устройство: Вызову функции `start_join()` всегда предшествует rebootstrap (удаление БД и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля box, ни пространства хранения. Функция `start_join()` имеет простое устройство:
Инстанс-клиент отправляет запрос `raft_join` лидеру Raft-группы (он известен после discovery). После достижения консенсуса в Raft-группе лидер присылает в ответе необходимую информацию: Инстанс-клиент отправляет запрос `raft_join` лидеру Raft-группы (он известен после discovery). После достижения консенсуса в Raft-группе лидер присылает в ответе необходимую информацию:
- Идентификатор `raft_id` и данные таблицы `raft_group` - для инициализации Raft-узла; - Идентификатор `raft_id` и данные таблицы `raft_group` для инициализации Raft-узла;
- Идентификаторы `instance_uuid`, `replicaset_uuid` и параметры `replication`, `read_only` для `box.cfg`. - Идентификаторы `instance_uuid`, `replicaset_uuid` и параметры `replication`, `read_only` для `box.cfg`.
Получив все настройки, инстанс использует их в `box.cfg()`, и затем создает в БД группу `raft_group` с актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на сообщения от других членов Raft-группы. Для того чтобы записи в `raft_group` не были заменены на менее актуальные из журнала Raft, каждая запись маркируется значением `commit_index`. Получив все настройки, инстанс использует их в `box.cfg()`, и затем создает в БД группу `raft_group` с актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на сообщения от других членов Raft-группы. Для того чтобы записи в `raft_group` не были заменены на менее актуальные из журнала Raft, каждая запись маркируется значением `commit_index`.
...@@ -60,7 +60,7 @@ picodata run --instance-id iN --listen iN --peer i1 ...@@ -60,7 +60,7 @@ picodata run --instance-id iN --listen iN --peer i1
Теперь узел Raft готов к использованию. Теперь узел Raft готов к использованию.
# Обработка запросов ## Обработка запросов
### \#\[proc\] fn raft_join() ### \#\[proc\] fn raft_join()
...@@ -127,7 +127,7 @@ struct Peer { ...@@ -127,7 +127,7 @@ struct Peer {
- Помимо всевозможных идентификаторов, ответ содержит список голосующих членов Raft-группы. Они понадобятся новому инстансу чтобы знать адреса соседей и нормально с ними общаться. - Помимо всевозможных идентификаторов, ответ содержит список голосующих членов Raft-группы. Они понадобятся новому инстансу чтобы знать адреса соседей и нормально с ними общаться.
- Также ответ содержит параметр `box_replication`, который требуется для правильной настройки репликации. - Также ответ содержит параметр `box_replication`, который требуется для правильной настройки репликации.
# Логика raft_conf_change_loop ## Логика raft_conf_change_loop
Все узлы Raft в кластере делятся на два типа: голосующие (`voter`) и неголосующие (`learner`). На каждом инстансе кластера присутствует поток, управляющий конфигурацией Raft-группы (составом `voters` / `learners`). Реальные изменения тем не менее может генерировать только лидер, на остальных инстансах этот поток спит и ничего не делает. Все узлы Raft в кластере делятся на два типа: голосующие (`voter`) и неголосующие (`learner`). На каждом инстансе кластера присутствует поток, управляющий конфигурацией Raft-группы (составом `voters` / `learners`). Реальные изменения тем не менее может генерировать только лидер, на остальных инстансах этот поток спит и ничего не делает.
...@@ -140,7 +140,7 @@ struct Peer { ...@@ -140,7 +140,7 @@ struct Peer {
Количество голосующих узлов в кластере не настраивается и зависит только от общего количества инстансов. Если инстансов 1 или 2, то голосующий узел один. [TODO](## "Сейчас в кластере из 2 инстансов оба делаются голосующими. Надо проработать аргументацию и решить как правильно."). Если инстансов 3 или 4, то таких узлов три. Для кластера с 5 или более инстансами — пять голосующих узлов. Количество голосующих узлов в кластере не настраивается и зависит только от общего количества инстансов. Если инстансов 1 или 2, то голосующий узел один. [TODO](## "Сейчас в кластере из 2 инстансов оба делаются голосующими. Надо проработать аргументацию и решить как правильно."). Если инстансов 3 или 4, то таких узлов три. Для кластера с 5 или более инстансами — пять голосующих узлов.
# Пару слов об обработке записей Raft-журнала ## Пару слов об обработке записей Raft-журнала
Стейт-машину каждой отдельной записи в Raft-журнале можно описать так: Стейт-машину каждой отдельной записи в Raft-журнале можно описать так:
...@@ -148,19 +148,19 @@ struct Peer { ...@@ -148,19 +148,19 @@ struct Peer {
`Persisted``Committed``Applied` `Persisted``Committed``Applied`
``` ```
При добавлении в журнал (по правилам это делает лидер) запись получает статус `Persisted` и начинает реплицироваться (это асинхронно делает файбер `raft_main_loop` ). Когда кворум узлов подтвеждает персистентность записи, она считается закоммиченной. Важно понимать, что статус `Committed` присвается записи на основе совокупной полученной информации, а не какого-то конкретного действия. При добавлении в журнал (по правилам это делает лидер) запись получает статус `Persisted` и начинает реплицироваться (это асинхронно делает файбер `raft_main_loop` ). Когда кворум узлов подтверждает персистентность записи, она считается закоммиченной. Важно понимать, что статус `Committed` присваевается записи на основе совокупной полученной информации, а не какого-то конкретного действия.
Конкретные действия по оработке той или иной записи выполняет отдельный поток `raft_applier` ([TODO](## "Пока что отдельного потока нет, но лучше бы был")). Для каждой записи он выполняет обработчик `Op::on_commit()` и по завершении присваевает записи статус `Applied`. Важно помнить, что обновление статуса и сама операция могут выполняться не атоманро (если в `Op::on_commit()` происходит передача управления другому потоку - yield). В таком случае, следует позаботиться хотя бы об идемпотентности операции. Конкретные действия по оработке той или иной записи выполняет отдельный поток `raft_applier` ([TODO](## "Пока что отдельного потока нет, но лучше бы был")). Для каждой записи он выполняет обработчик `Op::on_commit()` и по завершении присваивает записи статус `Applied`. Важно помнить, что обновление статуса и сама операция могут выполняться не атоманро (если в `Op::on_commit()` происходит передача управления другому потоку yield). В таком случае, следует позаботиться хотя бы об идемпотентности операции.
Схема ниже поможет эту информацию переварить. Схема ниже поможет эту информацию переварить.
![Raft log](raft_log_curves.svg "Последовательность обработки записей в Raft-журнале") ![Raft log](raft_log_curves.svg "Последовательность обработки записей в Raft-журнале")
Стоит также помнить, что алгоритм Raft гарантирует лишь консистентность последовательности записей, но ничего не говорит о конкретных моментах времени. Смена статусов на разных инстансах так или иначе происходит в разные моменты времнени, и иногда эту очередность приходится учитывать в алгоритмах. Стоит также помнить, что алгоритм Raft гарантирует лишь консистентность последовательности записей, но ничего не говорит о конкретных моментах времени. Смена статусов на разных инстансах так или иначе происходит в разные моменты времени, и иногда эту очередность приходится учитывать в алгоритмах.
Была у нас однажды такая история - шла разработка graceful shutdown. Тест (`test_joining.py::test_deactivation`) останавливал один из двух инстансов и проверял, что тот (назовем его i2) перстал быть голосующим. Иногда тест проходил нормально, но иногда падал - `i2` завершал работу раньше, чем `i1` получал от него подтверждение. При этом критерий остановки включал в себя ожидание коммита, но только локально на `i2`, а не на `i1`. Из-за этого `i1` терял кворум. Была у нас однажды такая история шла разработка graceful shutdown. Тест (`test_joining.py::test_deactivation`) останавливал один из двух инстансов и проверял, что тот (назовем его i2) перстал быть голосующим. Иногда тест проходил нормально, но иногда падал `i2` завершал работу раньше, чем `i1` получал от него подтверждение. При этом критерий остановки включал в себя ожидание коммита, но только локально на `i2`, а не на `i1`. Из-за этого `i1` терял кворум.
# Graceful shutdown ## Graceful shutdown
Чтобы выключение прошло штатно и не имело негативных последствий необходимо следующее: Чтобы выключение прошло штатно и не имело негативных последствий необходимо следующее:
...@@ -169,15 +169,15 @@ struct Peer { ...@@ -169,15 +169,15 @@ struct Peer {
Чтобы этого добиться, каждый инстанс на `on_shutdown` триггер отправляет лидеру запрос `UpdatePeerRequest{ target_grade: 0_Offline, graceful: true }`. Непосредственно изменением роли `voter` -> `learner` занимается отдеьный поток на лидере (тот самый `raft_conf_change_loop`), инстанс только дожидается его применения. Чтобы этого добиться, каждый инстанс на `on_shutdown` триггер отправляет лидеру запрос `UpdatePeerRequest{ target_grade: 0_Offline, graceful: true }`. Непосредственно изменением роли `voter` -> `learner` занимается отдеьный поток на лидере (тот самый `raft_conf_change_loop`), инстанс только дожидается его применения.
# Описание состояний кластера ## Описание состояний кластера
В отличие от других кластерных решений (например, того же Tarantool Cartridge) Picodata не использует понятие "состояния" для отдельных инстансов. Вместо этого мы говорим об их "грейдах". Грейд инстанса — это лишь синоним слова "состояние", но измениться спонтанно он не может. Мы вводим два конкретных термина: `current_grade` и `target_grade`. В отличие от других кластерных решений (например, того же Tarantool Cartridge) Picodata не использует понятие "состояния" для отдельных инстансов. Вместо этого мы говорим об их "грейдах". Грейд инстанса — это лишь синоним слова "состояние", но измениться спонтанно он не может. Мы вводим два конкретных термина: `current_grade` и `target_grade`.
Инициировать изменение `current_grade` может только лидер при поддержке кворума, что гарантирует консистентность принятого решения (и внушает доверие по части отказоустойчивости всей системы). Инициировать изменение `current_grade` может только лидер при поддержке кворума, что гарантирует консистентность принятого решения (и внушает доверие по части отказоустойчивости всей системы).
Инициировать изменение `target_grade` может кто угодно — это может быть сам инстанс (при добавлении), или админ командой `picodata expel`, или нажав Crtl+C на клавиатуре. `target_grade` - это желаемое состояние инстанса, в которое тот должен прийти. Инициировать изменение `target_grade` может кто угодно — это может быть сам инстанс (при добавлении), или админ командой `picodata expel`, или нажав Crtl+C на клавиатуре. `target_grade` это желаемое состояние инстанса, в которое тот должен прийти.
Приведением действительного к желаемому занимается специальный файбер на лидере - `topology_governor` <!-- или лучше grade_manager? -->. Он управляет всеми инстансами сразу. Приведением действительного к желаемому занимается специальный файбер на лидере `topology_governor` <!-- или лучше grade_manager? -->. Он управляет всеми инстансами сразу.
На основе совокупности грейдов `topology_governor` на каждой итерации бесконечного цикла придумывает ~~дурацкие менеджерские~~ активности (activity) и пытается их организовать. Пока не организует, никаких других изменений в текущих грейдах не произойдет (но могут измениться целевые). Если активности сфейлятся, то на следующей итерации они будут перевычислены с учетом новых целей. На основе совокупности грейдов `topology_governor` на каждой итерации бесконечного цикла придумывает ~~дурацкие менеджерские~~ активности (activity) и пытается их организовать. Пока не организует, никаких других изменений в текущих грейдах не произойдет (но могут измениться целевые). Если активности сфейлятся, то на следующей итерации они будут перевычислены с учетом новых целей.
...@@ -187,7 +187,7 @@ struct Peer { ...@@ -187,7 +187,7 @@ struct Peer {
### 1. Обновить состав воутеров / лернеров ### 1. Обновить состав воутеров / лернеров
Надо сгенерировать `ConfChangeV2`. Если он пустой, переходим к следующиему шагу. Нет — отправляем его в рафт. Если не получилось - начинаем новую итерацию и перевычисляем активности. Получилось - ждем события `JointStateLeave`. Надо сгенерировать `ConfChangeV2`. Если он пустой, переходим к следующему шагу. Нет — отправляем его в рафт. Если не получилось начинаем новую итерацию и перевычисляем активности. Получилось ждем события `JointStateLeave`.
### 2. Обработать target_grade 0_Offline и 60_Expelled. ### 2. Обработать target_grade 0_Offline и 60_Expelled.
...@@ -203,7 +203,7 @@ struct Peer { ...@@ -203,7 +203,7 @@ struct Peer {
### 5. Запромоутить 20_BoxSynced в 30_VshardInitialized ### 5. Запромоутить 20_BoxSynced в 30_VshardInitialized
Это уже веселее. Эта активность выполняется на всем кластере. В первую очередь надо проверить, всем ли репликасетам назначено хоть какое-то значение vshard_weight. Если нет — простоавить 0 и дождаться коммита. По итогу надо убедиться, что все инстаны выполнили `vshard.router.cfg()` и `vshard.storage.cfg()`. Как и в предыдущем случае, грейд дается нескольким инстансам сразу. Это уже веселее. Эта активность выполняется на всем кластере. В первую очередь надо проверить, всем ли репликасетам назначено хоть какое-то значение vshard_weight. Если нет — проставить 0 и дождаться коммита. По итогу надо убедиться, что все инстансы выполнили `vshard.router.cfg()` и `vshard.storage.cfg()`. Как и в предыдущем случае, грейд дается нескольким инстансам сразу.
### 6. Запромоутить 30_VshardInitialized в 50_Online ### 6. Запромоутить 30_VshardInitialized в 50_Online
...@@ -211,4 +211,5 @@ struct Peer { ...@@ -211,4 +211,5 @@ struct Peer {
### 7. Возвращаясь к 60_Expelled ### 7. Возвращаясь к 60_Expelled
Наименьший приоритет имеет активность, связанная с ожиданием окончания ребалансироки. В конце концов инстанс должен быть удален из `box.space.cluster` на всех оставшихся репликах и из `vshard.router.cfg` и `vshard.storage.cfg` на всем кластере. Наименьший приоритет имеет активность, связанная с ожиданием окончания ребалансировки. В конце концов инстанс должен быть удален из `box.space.cluster` на всех оставшихся репликах и из `vshard.router.cfg` и `vshard.storage.cfg` на всем кластере.
# Picodata roadmap
This document describes the estimated development plan for the Picodata product for the upcoming quarters. In each case (for each major release), the goal is to achieve the stated functionality in the plan.
The Picodata Roadmap is aligned with the Release Policy to provide greater clarity on the development process and inform stakeholders about the plans and priorities of the Picodata development team.
A new versioning format is introduced, based on calendar versioning (CalVer).
So, the current release of Picodata is designated as 22.07.0, where:
22 - the number of the year in which the release took place;
07 - the number of the month in which the release took place;
0 is the number of the minor version, reflecting the presence and/or number of improvements made as part of the major release.
The release policy of Picodata LLC provides for the release of new major versions of Picodata 4 times a year, approximately once a quarter.
The following functionality is currently planned for future releases of Picodata:
Q4-2022 (22.10.0)
Adding the vshard module, which will ensure the distribution of data segments between different replicasets. Support for distributing data across cluster nodes in accordance with specified criteria. Access to data from any cluster node.
Q1-2023 (23.01.0)
Bringing the Picodata API to a functional state, providing the ability to create and delete tables (spaces) in the DBMS using the Picodata API.
Q2-2023 (23.04.0)
Implementation of an automatic data balancer in a cluster that moves data from more full cluster nodes to less full ones in order to maintain an even distribution of data across cluster nodes.
Implementation of partial support for the SQL:2016 standard within the entire cluster (support for distributed SQL), including elements from subsections: E011. Numeric data types, E011-05. Numerical Comparisons, E021. Character string types.
Q3-2023 (23.07.0)
Implementation of a distributed mechanism for managing the DBMS data schema (tables, stored procedures, users, privileges), which guarantees an identical data schema on all cluster nodes.
Cluster-wide extension of SQL:2016 support (distributed SQL support), including items from subsections: E031. Identifiers, E051. Base Request Specification, E061. Basic Predicates and Search Conditions, E071. Basic query expressions, E101. Basic data processing.
Q4-2023 (23.10.0)
Integration of Tarantool-Rust-module into the main Picodata application.
Implementation of the mechanism for executing tasks in a cluster in the semantics “exactly once”, “no more than once”.
Implementation of the mechanism of roles - the distribution of computing and application tasks among the nodes of the cluster based on their purpose (role), which provides centralized management of the program executed by the cluster.
Cluster-wide SQL:2016 support extension (distributed SQL support), including items from subsections: F041. Basic join of tables, F471. Subquery Scalar Values, T631. IN predicate with one list element.
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