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

docs: review clustering.md and update glossary

parent 77b97ee7
No related branches found
No related tags found
1 merge request!419docs: review clustering.md + glossary update
Pipeline #14238 passed
# Общая схема инициализации кластера
Данный раздел содержит описание архитектуры Picodata, в том числе высокоуровневый процесс инициализации кластера на основе нескольких отдельно запущенных экземпляров Picodata (инстансов).
Данный раздел содержит описание архитектуры Picodata, в том числе
высокоуровневый процесс инициализации кластера на основе нескольких
отдельно запущенных экземпляров Picodata (инстансов).
Администратор запускает несколько инстансов, передавая в качестве аргументов необходимые параметры:
Администратор запускает несколько инстансов, передавая в качестве
аргументов необходимые параметры:
```sh
picodata run --instance-id i1 --listen i1 --peer i1,i2,i3
......@@ -11,67 +14,133 @@ picodata run --instance-id i3 --listen i3 --peer i1,i2,i3
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")
Красным показан родительский процесс, который запущен на всем протяжении жизненного цикла инстанса. Вся логика, начиная с присоединения к кластеру, и заканчивая обслуживанием клиентских запросов происходит в дочернем процессе (голубой цвет). Единственное предназначение родительского процесса — иметь возможность сбросить состояние дочернего (выполнить rebootstrap) и инициализировать его повторно (сиреневый цвет).
Красным показан родительский процесс, который запущен на всем протяжении
жизненного цикла инстанса. Вся логика, начиная с присоединения к
кластеру, и заканчивая обслуживанием клиентских запросов, происходит в
дочернем процессе (голубой цвет). Единственное предназначение
родительского процесса — иметь возможность сбросить состояние дочернего
(выполнить rebootstrap) и инициализировать его повторно (сиреневый
цвет).
Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже описаны детали выполнения каждого этапа и соответствующей программной функции.
Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже
описаны детали выполнения каждого этапа и соответствующей программной
функции.
### fn main()
На этом этапе происходит ветвление (форк) процесса `picodata`. Родительский процесс (supervisor) ожидает от дочернего процесса сообщения по механизму IPC и при необходимости перезапускает дочерний процесс. При необходимости дочерний процесс может попросить родителя удалить все файлы БД, т.е. вызвать функцию `drop_db()`. Это может понадобиться для повторной инициализации кластера когда, например, у инстанса изначально имеется временный, рандомно сгенерированный `replicaset_id`.
На этом этапе происходит ветвление (форк) процесса `picodata`.
Родительский процесс (supervisor) ожидает от дочернего процесса
сообщения по механизму IPC и при необходимости перезапускает дочерний
процесс. Также, при необходимости дочерний процесс может попросить
родителя удалить все файлы БД, т.е. вызвать функцию `drop_db()`. Это
может понадобиться для повторной инициализации кластера когда, например,
у инстанса изначально имеется временный, случайно сгенерированный
`replicaset_id`.
### 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()
В функции `start_boot` происходит инициализация Raft-группы — лидер генерирует и сохраняет в БД первые записи в журнале. Эти записи описывают добавление первого инстанса в пустую Raft-группу и создание начальной cluster-wide конфигурации. Таким образом достигается однообразие кода, обрабатывающего эти записи.
В функции `start_boot` происходит инициализация Raft-группы — лидер
генерирует и сохраняет в БД первые записи в журнале. Эти записи
описывают добавление первого инстанса в пустую Raft-группу и создание
начальной clusterwide-конфигурации. Таким образом достигается
однообразие кода, обрабатывающего эти записи.
Сам Raft-узел на данном этапе еще не создается. Это произойдет позже, на стадии `postjoin()`.
Сам Raft-узел на данном этапе еще не создается. Это произойдет позже, на
стадии `postjoin()`.
### fn start_join()
Вызову функции `start_join()` всегда предшествует rebootstrap (удаление БД и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля box, ни пространства хранения. Функция `start_join()` имеет простое устройство:
Инстанс-клиент отправляет запрос `raft_join` лидеру Raft-группы (он известен после discovery). После достижения консенсуса в Raft-группе лидер присылает в ответе необходимую информацию:
- Идентификатор `raft_id` и данные таблицы `raft_group` — для инициализации Raft-узла;
- Идентификаторы `instance_uuid`, `replicaset_uuid` и параметры `replication`, `read_only` для `box.cfg`.
Получив все настройки, инстанс использует их в `box.cfg()`, и затем создает в БД группу `raft_group` с актуальными адресами других инстансов. Без этого инстанс не сможет отвечать на сообщения от других членов Raft-группы. Для того чтобы записи в `raft_group` не были заменены на менее актуальные из журнала Raft, каждая запись маркируется значением `commit_index`.
По завершении этих манипуляций инстанс также переходит к этапу `postjoin()`.
Вызову функции `start_join()` всегда предшествует rebootstrap (удаление
БД и перезапуск процесса), поэтому на данном этапе в БД нет ни модуля
box, ни пространства хранения. Функция `start_join()` имеет простое
устройство:
Инстанс-клиент отправляет запрос `raft_join` лидеру Raft-группы (он
известен после discovery). После достижения консенсуса в Raft-группе
лидер присылает в ответе необходимую информацию:
- Идентификатор `raft_id` и данные таблицы `raft_group` — для
инициализации Raft-узла;
- Идентификаторы `instance_uuid`, `replicaset_uuid` и параметры
`replication`, `read_only` для `box.cfg`.
Получив все настройки, инстанс использует их в `box.cfg()`, и затем
создает в БД группу `raft_group` с актуальными адресами других
инстансов. Без этого инстанс не сможет отвечать на сообщения от других
членов Raft-группы. Для того чтобы записи в `raft_group` не были
заменены на менее актуальные из журнала Raft, каждая запись маркируется
значением `commit_index`.
По завершении этих манипуляций инстанс также переходит к этапу
`postjoin()`.
### fn postjoin()
Логика функции `postjoin()` одинакова для всех инстансов. К этому моменту для инстанса уже инициализированы корректные пространства хранения в БД и могут быть накоплены записи в журнале Raft. Инстанс инициализирует узел Raft, который начинает взаимодействовать с Raft-группой. В случае, если других кандидатов нет, инстанс тут же избирает себя лидером группы.
Логика функции `postjoin()` одинакова для всех инстансов. К этому
моменту для инстанса уже инициализированы корректные пространства
хранения в БД и могут быть накоплены записи в журнале Raft. Инстанс
инициализирует узел Raft, который начинает взаимодействовать с
Raft-группой. В случае, если других кандидатов нет, инстанс тут же
избирает себя лидером группы.
В этом месте также устанавливает on_shutdown колбек, который обеспечит [graceful shutdown](#Graceful-shutdown).
В этом месте также устанавливается `on_shutdown` callback, который
обеспечит [корректное завершение работы инстанса](#Graceful-shutdown).
Следующим шагом инстанс оповещает кластер о том, что он готов проходить
настройку необходимых подсистем (репликации, шардинга, и т.д.). Для этого лидеру
отправляется запрос на обновление `target_grade` текущего инстанса до `Online`,
после чего за дальнейшие действия будет отвечать специальный поток управления
[topology governor](#Topology-governor) (aka `governor_loop`).
настройку необходимых подсистем (репликации, шардинга, и т.д.). Для
этого лидеру отправляется запрос на обновление `target_grade` текущего
инстанса до уровня `Online`, после чего за дальнейшие действия будет
отвечать специальный поток управления [topology
governor](#Topology-governor), также называемый `governor_loop`.
Как только запись с обновленным грейдом будет зафиксирована в Raft, узел
готов к использованию.
Как только запись с обновлённым грейдом будет закоммиченна в Raft, узел готов к
использованию.
## Обработка запросов
### \#\[proc\] fn raft_join()
Значительная часть всей логики по управлению топологией находится в хранимой процедуре `raft_join`. Аргументом для нее является следующая структура:
Значительная часть всей логики по управлению топологией находится в
хранимой процедуре `raft_join`. Аргументом для нее является следующая
структура:
```rust
struct join::Request {
......@@ -86,10 +155,10 @@ struct join::Request {
Ответом служит структура:
```rust
struct join::Response {
/// Добавленный инстанс (чтобы знать все айдишники)
struct JoinResponse {
/// Добавленный инстанс (чтобы знать все ID)
instance: Instance,
/// Адреса остальных инстансев кластера (чтобы добавляемый инстанс мог наладить контакт)
/// Голосующие узлы (чтобы добавляемый инстанс мог наладить контакт)
peer_addresses: Vec<PeerAddress>,
/// Настройки репликации (чтобы инициализировать репликацию)
box_replication: Vec<String>,
......@@ -107,202 +176,272 @@ struct Instance {
peer_address: String,
failure_domain: FailureDomain,
// целевая и текущая оценки состояния инстанса с точки зрения лидера кластера
// целевая и текущая оценки уровня инстанса с точки зрения лидера кластера
target_grade: CurrentGrade,
current_grade: CurrentGrade,
/// Индекс записи в Raft-журнале. Препятствует затиранию
/// более старыми записями, по мере применения Raft-журнала.
/// более старыми записями по мере применения Raft-журнала.
commit_index: RaftIndex,
}
```
Цель такого запроса сводится к добавление нового инстанса в Raft-группу. Для этого алгоритма справедливы следующие тезисы:
Цель такого запроса сводится к добавлению нового инстанса в Raft-группу.
Для этого алгоритма справедливы следующие тезисы:
- `join::Request` отправляет всегда неинициализированный инстанс.
- В зависимости от того, содержится ли в запросе `instance_id`, проводится анализ его корректности (уникальности).
- В процессе обработки запроса в Raft-журнал добавляется запись `op::PersistPeer { peer }`, которая помимо всевозможных айдишников содержит поля `current_grade: Offline`, `target_grade: Offline`, играющие важную роль в обеспечении надежности кластера (подробнее о них в разделе [topology governor](#Topology-governor)).
- В ответ выдаётся всегда новый `raft_id`, никому другому ранее не принадлежавший.
- В зависимости от того, содержится ли в запросе `instance_id`,
проводится анализ его корректности (уникальности).
- В процессе обработки запроса в Raft-журнал добавляется запись
`op::PersistPeer { peer }`, которая помимо всевозможных ID содержит
поля `current_grade: Offline`, `target_grade: Offline`, играющие
важную роль в обеспечении надежности кластера (подробнее о них в
разделе [topology governor](#Topology-governor)).
- В ответ выдается всегда новый `raft_id`, никому другому ранее не
принадлежавший.
- Генерировать значение `raft_id` может только лидер Raft-группы.
- Помимо всевозможных идентификаторов, ответ содержит список голосующих членов Raft-группы. Они понадобятся новому инстансу чтобы знать адреса соседей и нормально с ними общаться.
- Также ответ содержит параметр `box_replication`, который требуется для правильной настройки репликации.
- Помимо всевозможных ID, ответ содержит список голосующих членов
Raft-группы. Они понадобятся новому инстансу для того чтобы знать
адреса соседей и общаться с ними.
- Также ответ содержит параметр `box_replication`, который требуется для
правильной настройки репликации.
## Пару слов об обработке записей Raft-журнала
## Обработка записей Raft-журнала
Стейт-машину каждой отдельной записи в Raft-журнале можно описать так:
Последовательность состояний каждой отдельной записи в Raft-журнале
можно описать так:
```md
`Persisted``Committed``Applied`
```
При добавлении в журнал (по правилам это делает лидер) запись получает статус `Persisted` и начинает реплицироваться (это асинхронно делает файбер `raft_main_loop` ). Когда кворум узлов подтверждает персистентность записи, она считается закоммиченной. Важно понимать, что статус `Committed` присваевается записи на основе совокупной полученной информации, а не какого-то конкретного действия.
Конкретные действия по оработке той или иной записи выполняет отдельный поток `raft_applier` ([TODO](## "Пока что отдельного потока нет, но лучше бы был")). Для каждой записи он выполняет обработчик `Op::on_commit()` и по завершении присваивает записи статус `Applied`. Важно помнить, что обновление статуса и сама операция могут выполняться не атоманро (если в `Op::on_commit()` происходит передача управления другому потоку — yield). В таком случае, следует позаботиться хотя бы об идемпотентности операции.
При добавлении в журнал (по правилам это делает лидер) запись получает
статус `Persisted` и начинает реплицироваться (это асинхронно делает
файбер `raft_main_loop` ). Когда кворум узлов подтверждает
персистентность записи, она считается зафиксированной. Важно понимать,
что статус `Committed` присваивается записи на основе совокупности
полученной информации, а не какого-то конкретного действия.
Схема ниже поможет эту информацию переварить.
Конкретные действия по обработке той или иной записи выполняет
отдельный поток `raft_applier`<!--([TODO](## "Пока что отдельного
потока нет, но лучше бы был"))-->. Для каждой записи он выполняет
обработчик `Op::on_commit()` и по завершении присваивает записи статус
`Applied`. Важно помнить, что обновление статуса и сама операция могут
выполняться не атомарно (если в `Op::on_commit()` происходит передача
управления другому потоку — yield). В таком случае, следует
позаботиться хотя бы об идемпотентности операции.
![Raft log](raft_log_curves.svg "Последовательность обработки записей в Raft-журнале")
Схема ниже иллюстрирует эту информацию.
Стоит также помнить, что алгоритм Raft гарантирует лишь консистентность последовательности записей, но ничего не говорит о конкретных моментах времени. Смена статусов на разных инстансах так или иначе происходит в разные моменты времени, и иногда эту очередность приходится учитывать в алгоритмах.
![Raft log](raft_log_curves.svg "Последовательность обработки записей в
Raft-журнале")
Была у нас однажды такая история — шла разработка graceful shutdown. Тест (`test_joining.py::test_deactivation`) останавливал один из двух инстансов и проверял, что тот (назовем его i2) перестал быть голосующим. Иногда тест проходил нормально, но иногда падал — `i2` завершал работу раньше, чем `i1` получал от него подтверждение. При этом критерий остановки включал в себя ожидание коммита, но только локально на `i2`, а не на `i1`. Из-за этого `i1` терял кворум.
Стоит также помнить, что алгоритм Raft гарантирует лишь консистентность
последовательности записей, но не конкретные сроки выполнения. Смена
статусов на разных инстансах так или иначе происходит в разные моменты
времени, и иногда эту очередность приходится учитывать в алгоритмах.
Например, при корректном запланированном выводе инстанса из эксплуатации
(graceful shutdown) может возникнуть ситуация, когда инстанс завершится
слишком быстро, и его соседи могут ошибочно продолжать считать его
голосующим. Причиной такой ситуации может быть критерий остановки,
включающий ожидание коммита лишь локально на завершаемом инстансе, но не
на других — это может стать причиной потери кворума в кластере.
## Graceful shutdown
Чтобы выключение прошло штатно и не имело негативных последствий необходимо следующее:
<!-- Ниже данная ситуация рассмотрена подробнее. -->
- Инстанс не должен оставаться воутером, пока есть другие онлайн кандидаты.
- Инстанс не должен оставаться лидером.
<!-- Была у нас однажды такая история — шла разработка graceful shutdown. Тест (`test_joining.py::test_deactivation`) останавливал один из двух инстансов и проверял, что тот (назовем его i2) перестал быть голосующим. Иногда тест проходил нормально, но иногда падал — `i2` завершал работу раньше, чем `i1` получал от него подтверждение. При этом критерий остановки включал в себя ожидание коммита, но только локально на `i2`, а не на `i1`. Из-за этого `i1` терял кворум. -->
Чтобы этого добиться, каждый инстанс на `on_shutdown` триггер отправляет лидеру
запрос `UpdatePeerRequest { target_grade: Offline }`, обработкой которого
займётся вышеупомянутый `governor_loop`. После этого инстанс пытается дождаться
применения записи о смене своего `current_grade` на `Offline` (о том почему так
произойдет читай ниже).
По некоторым причинам коммит записи может не успеть дойти до инстанса в срок
отведённый на выполнение `on_shtudown` триггера (например в кластере может быть
потерян кворум) в таком случае graceful shutdown невозможен.
## Topology governor
## Graceful shutdown
В отличие от других кластерных решений (например, того же Tarantool Cartridge) Picodata не использует понятие "состояния" для отдельных инстансов. Вместо этого мы говорим об их "грейдах". Грейд инстанса — это лишь синоним слова "состояние", но измениться спонтанно он не может. Мы вводим два конкретных термина: `current_grade` и `target_grade`.
Чтобы выключение прошло штатно и не имело негативных последствий,
необходимо следить за соблюдением следующих условий:
Инициировать изменение `current_grade` может только лидер при поддержке кворума, что гарантирует консистентность принятого решения (и внушает доверие по части отказоустойчивости всей системы).
- Инстанс не должен оставаться голосующим, пока есть другие кандидаты в
состоянии `Online`.
- Инстанс не должен оставаться лидером.
Инициировать изменение `target_grade` может кто угодно — это может быть сам инстанс (при добавлении), или админ командой `picodata expel`, или нажав Crtl+C на клавиатуре. `target_grade` — это желаемое состояние инстанса, в которое тот должен прийти.
Чтобы этого добиться, каждый инстанс при срабатывании триггера
`on_shutdown` отправляет лидеру запрос `UpdatePeerRequest {
target_grade: Offline }`, обработкой которого займется вышеупомянутый
`governor_loop`. После этого инстанс пытается дождаться применения
записи о смене своего `current_grade` на `Offline` (о том, почему так
произойдет см. ниже).
Приведением действительного к желаемому занимается специальный файбер на лидере — `governor_loop`. Он управляет всеми инстансами сразу.
С грейдом (как с текущим так и с целевым) также всегда ассоциирована инкарнация
(`incarnation`) - порядковое число, подсчитывающее количество раз когда governor
начинал процесс обработки данного инстанса. Это позволяет нам замечать ситуации,
когда инстансы выходят из строя на какой-то период времени, после чего их
необходимо снова привести в актуальное состояние.
## Описание уровней (grades) кластера
По некоторым причинам коммит записи может не успеть дойти до инстанса в
срок, отведенный на выполнение триггера `on_shutdown` триггера (например
в кластере может быть потерян кворум). В таком случае корректное
завершение работы инстанса (`graceful shutdown`) невозможно.
На основе совокупности грейдов и их инкарнаций `governor_loop` на каждой итерации бесконечного цикла придумывает ~~дурацкие менеджерские~~ активности (activity) и пытается их организовать. Пока не организует, никаких других изменений в текущих грейдах не произойдет (но могут измениться целевые). Если активности сфейлятся, то на следующей итерации они будут перевычислены с учетом новых целей.
## Topology governor
![Instance states](fsm.svg "Возможные переходы состояний инстанса")
В отличие от других кластерных решений (например, того же Tarantool
Cartridge) Picodata не использует понятие "состояния" для описания
отдельных инстансов. Вместо этого теперь применяется новое понятие
«грейд» (grade). Данный термин отражает не состояние самого инстанса, а
конфигурацию остальных участников кластера по отношению к нему.
Существуют две разновидности грейдов: текущий (`current_grade`) и
целевой (`target_grade`). Инициировать изменение `current_grade` может
только лидер при поддержке кворума, что гарантирует консистентность
принятого решения (и поддерживает доверие к системе в плане
отказоустойчивости).
Инициировать изменение `target_grade` может кто угодно — это может быть
сам инстанс (при его добавлении), или администратор кластера командой
`picodata expel` либо нажатием Ctrl+C на клавиатуре. `target_grade`
это желаемое состояние инстанса, в которое тот должен прийти.
Приведением действительного к желаемому занимается специальный файбер на
лидере — `governor_loop`. Он управляет всеми инстансами сразу.
С грейдом (как с текущим, так и с целевым) также всегда ассоциирована
инкарнация (`incarnation`) — порядковое число, отражающее число попыток
обработать данный инстанс со стороны файбера `governor_loop`. Это
позволяет реагировать на ситуации, когда инстансы выходят из строя на
какой-то период времени, после чего их необходимо снова привести в
актуальное состояние.
На основе совокупности грейдов и их инкарнаций `governor_loop` на каждой
итерации бесконечного цикла генерирует активности (activity) и пытается
их организовать. Пока не организует, никаких других изменений в текущих
грейдах не произойдет (но могут измениться целевые). Если активности
завершатся ошибкой, то на следующей итерации они будут перевычислены с
учетом новых целей.
Инкарнации грейдов вычисляются по следующему принципу.
- Каждый раз когда `target_grade` инстанса получает значение `Online`,
его инкарнация увеличивается на 1.
- Все остальные изменения грейдов копируют инкарнацию с противоположного грейда,
то есть при изменении `target_grade` инкарнация копируется с
`current_grade`, при изменении `current_grade` - с `target_grade`.
- Все остальные изменения грейдов копируют инкарнацию с противоположного
грейда, то есть при изменении `target_grade` инкарнация копируется с
`current_grade`, при изменении `current_grade` с `target_grade`.
Дальше перечислены активности, которыми занимается governor, в том же порядке, в
котором он к ним приступает.
Дальше перечислены активности, которыми занимается `governor_loop`, в
том же порядке, в котором он к ним приступает.
### 1. Обновить состав воутеров / лернеров
Первым делом проверяем нужно ли внести изменения в конфигурацию Raft группы -
изменить состав воутеров и лернеров.
![Instance states](fsm.svg "Возможные переходы состояний инстанса")
Правила выбора новой конфигурации описаны в
`picodata::governor::cc::raft_conf_change` и (на момент написание этого текста)
заключаются примерно в следующем:
- Любые инстансы переходящие в грейд `Expelled` удаляем из Raft группы;
- Воутеры переходящие в грейд `Offline` делаем лернерами и находим им замена;
- Среди свеже-добавленных инстансов с текущим грейдом `Online` добираем
необходимое количество воутеров, а остальных добавляем лернерами;
Ниже перечислены существующие варианты активностей, которые создает
`topology_governor`.
### 1. Обновить состав голосующих / неголосующих инстансов
[TODO](#Предстоит сделать) Новые воутеры должны выбираться с учётом failure
domain'ов.
Сначала нужно проверить необходимость менять конфигурацию Raft-группы, а
именно — состав голосующих / неголосующих узлов (`voters` и `learners`).
По этим правилам генерируем `ConfChangeV2`, и отправляем его в Raft, если он не
пустой конечно.
После этого дожидаемся события `TopologyChanged`, которое будет послано в ответ
на успешное применение новой конфигурации.
Правила выбора новой конфигурации описаны в
`picodata::governor::cc::raft_conf_change` и заключаются в следующем:
- Любые инстансы, переходящие в грейд `Expelled`, удаляются из
Raft-группы;
- Голосущие инстансы, переходящие в грейд `Offline`, перестают быть
голосующими (становятся `learners`) и для них находится замена;
- Среди свежедобавленных инстансов с текущим грейдом `Online`
подбирается необходимое количество голосующих инстансов (`voters`),
остальные добавляются как `learners`;
<!-- [TODO](#Предстоит сделать) Новые воутеры должны выбираться с учетом failure
domain'ов. -->
По этим правилам создается `ConfChangeV2`, и, если он не пуст,
отправляется в Raft. Далее нужно дождаться события `TopologyChanged`,
которое будет послано в ответ на успешное применение новой конфигурации.
### 2. target_grade Offline / Expelled.
Следующим шагом берём первый попавшийся инстанс, который нужно вывести из строя
временно (`target_grade = Offline`) или навсегда (`target_grade = Expelled`).
Перед тем как выключить инстанс, нужно убедиться, что кластер сможет продолжить
функционировать без него.
Ниже рассмотрены два варианта вывода инстанса из строя: временный
(`target_grade = Offline`) и постоянный (`target_grade = Expelled`).
Перед тем как выключить инстанс, нужно убедиться, что кластер сможет
продолжить функционировать без него.
Если уходит лидер Raft группы, то есть инстанс на котором в данный момент
крутится `governor_loop`, то он снимает с себя полномочия (делает
`transfer_leadership`) и ждёт смены Raft статуса, дальше действовать будет
кто-то другой.
Если уходит лидер Raft-группы, то есть инстанс, на котором в данный
момент выполняется `governor_loop`, то он снимает с себя полномочия
(делает `transfer_leadership`) и ждет смены Raft-статуса, дальше
действовать будет кто-то другой.
Если уходит лидер своего репликасета, выбираем нового и дожидаемся
соответствующей записи в спейс с репликасетами.
Если уходит лидер своего репликасета, то происходят новые выборы такого
лидера, после чего нужно дождаться соответствующей записи в спейс с
репликасетами.
Обновляем конфигурацию шардинга (`vhsard`) на всех стораджах и роутерах в
кластере, чтобы оповестить их об изменениях в топологии.
Далее следует обновить конфигурацию шардирования (`vshard`) на всех
инстансах с ролями хранения данных (`storage`) и маршрутизации
(`routers`), чтобы оповестить их об изменениях в топологии. Если это
последний узел хранения в репликасете, ему будет выставлен вес 0.
[TODO](#Пока не сделано) Также, если это последний сторадж в репликасете, ему надо выставить вес в 0. Дожидаться ребалансировки на этом шаге не требуется (да и не получится — слишком долгая блокировка), для этого есть отдельный пункт.
<!-- [TODO](#Пока не сделано)
Также, если это последний узел хранения в репликасете, ему надо выставить вес в 0. Дожидаться ребалансировки на этом шаге не требуется (да и не получится — слишком долгая блокировка), для этого есть отдельный пункт. -->
После этого, наконец присваиваем инстансу `current_grade` соответствующей его
целевому.
Наконец, инстансу присваивается `current_grade`, соответствующей его
целевому уровню.
### 3. target_grade: Online, current_grade: * -> RaftSynced
Дальше начинается обработка инстансов, которых нужно привести в актуальное
состояние. Это либо свеже-добавленные инстансы, либо инстансы, которые были
какое-то время неактивны.
Дальше начинается обработка инстансов, которых нужно привести в
актуальное состояние. Это либо свежедобавленные инстансы, либо инстансы,
которые были какое-то время неактивны.
Выбираем инстанс либо имеющий `current_grade: Offline`, либо имеющий
инкарнация текущего грейда меньше, чем инкарнацию целевого.
Выбираем инстанс, либо имеющий `current_grade: Offline`, либо имеющий
инкарнацию текущего грейда меньше, чем инкарнацию целевого.
На этом этапе мы синхронизируем Raft журнал выбранных инстансов. Берём текущий
`commit_index` лидера и дожидаемся, пока `commit_index` пира его не догонит.
После этого присваиваем инстансу `current_grade = RaftSynced`.
На этом этапе мы синхронизируем Raft-журнал выбранных инстансов. Берем
текущий `commit_index` лидера и дожидаемся, пока `commit_index` пира его
не догонит. После этого присваиваем инстансу `current_grade =
RaftSynced`.
[TODO](#Пока не сделано) Этот шаг можно распараллелить, отправив запрос сразу
нескольким подходящим пирам.
<!-- [TODO](#Пока не сделано) Этот шаг можно распараллелить, отправив запрос сразу
нескольким подходящим пирам. -->
### 4. target_grade: Online, current_grade: RaftSynced -> Replicated
Этот этап отвечает за настройку репликации внутри одного репликасета, к которому
относится выбранный инстанс.
Этот этап отвечает за настройку репликации внутри одного репликасета, к
которому относится выбранный инстанс.
Первым делом мы сообщаем всем инстансам репликасета, что необходимо применить
новую конфигурацию репликации через `box.cfg { replication = ... }`. Однако, так
как конфигурация кластера (в том числе и конфигурация репликасетов)
распространяется между инстансами через Raft журнал, необходимо убедиться
что журнал у всех свежий. Для этого в запросе также передаём `commit_index`,
которого пиры должны дождаться прежде чем выполнять сам запрос.
Первым делом мы сообщаем всем инстансам репликасета, что необходимо
применить новую конфигурацию репликации через `box.cfg { replication =
... }`. Однако, так как конфигурация кластера (в том числе и
конфигурация репликасетов) распространяется между инстансами через
Raft-журнал, необходимо убедиться что журнал у всех свежий. Для этого в
запросе также передаем `commit_index`, которого пиры должны дождаться
прежде чем выполнять сам запрос.
После этого инстансу, инициировавшему активность, присваевается `current_grade:
Replicated`.
После этого инстансу, инициировавшему активность, присваивается
`current_grade: Replicated`.
[TODO](#Пока не сделано) Можно обновлять грейд сразу всем инстансам в
репликасете, которым это нужно.
<!-- [TODO](#Пока не сделано) Можно обновлять грейд сразу всем инстансам в
репликасете, которым это нужно. -->
На этом же добавляем запись в спейс с репликасетами, если её там ещё нет. При
этом, его вес шардирования устанавливается в 0, если только это не первый
репликасет в кластере.
На этом же этапе добавляем запись в спейс с репликасетами, если ее там
еще нет. При этом вес шардирования устанавливается в 0, если только это
не первый репликасет в кластере.
Последнее что нужно сделать на этом этапе это обновить обновить
`box.cfg { read_only }` конфигурацию лидера затронутого репликасета
([TODO](#Пока не сделано) это не обязательно делать каждый раз).
Последнее что нужно сделать на этом этапе, это обновить значение
`box.cfg { read_only }` в конфигурации лидера затронутого репликасета.
<!-- ([TODO](#Пока не сделано) это не обязательно делать каждый раз). -->
### 5. target_grade: Online, current_grade: Replicated -> ShardingInitialized
На данном этапе настраивается шардирование всего кластера, по этому запросы
отправляются сразу всем инстансам.
На данном этапе настраивается шардирование всего кластера, поэтому
запросы отправляются сразу всем инстансам.
Рассылаем всем запрос на обновление конфигурации шардирования
(`vshard.router.cfg()` и `vshard.storage.cfg()`) опять вместе с `commit_index`,
чтобы инстансы получили последние данные.
(`vshard.router.cfg()` и `vshard.storage.cfg()`) опять вместе с
`commit_index`, чтобы инстансы получили последние данные.
На этом этапе первый репликасет наполненный до фактора репликации запускает
начальное распределение бакетов (`vshard.router.bootstrap`)
([TODO](#Пока не сделано) пока что это делает первый инстанс в кластере).
На этом этапе первый репликасет, наполненный до фактора репликации,
запускает начальное распределение бакетов (`vshard.router.bootstrap`)
<!-- ([TODO](#Пока не сделано) пока что это делает первый инстанс в кластере). -->
В конце этого этапа подсистема шардирования данных (vshard) на всех инстансах
знает о топологии всего кластера, но на некоторых репликасетах вес всё ещё
проставлен в 0, поэтому данные на него ребалансироваться ещё не будут.
В конце этого этапа подсистема шардирования данных (`vshard`) на всех
инстансах знает о топологии всего кластера, но на некоторых репликасетах
вес все еще проставлен вес 0, поэтому данные на них ребалансироваться
еще не будут.
### 6. target_grade: Online, current_grade: ShardingInitialized -> Online
Этот этап нужен для того, чтобы запустить ребалансировку данных на новые
репликасеты. Для этого проверяем, есть ли у нас репликасеты с весом 0
наполненные до фактора репликации. Если есть, то обновляем их вес и
повторно обновляем конфигурацию шардирования на всём кластере, чтобы данные
начали ребалансироваться.
В обоих случаях после этого шага все инстансы могут переходить в грейд `Online`.
Этот этап нужен для того чтобы запустить ребалансировку данных на новые
репликасеты. Для этого проверяем, есть ли у нас репликасеты с весом 0 и
достигнутым фактором репликации. Если есть, то обновляем их вес и
повторно обновляем конфигурацию шардирования на всем кластере, чтобы
данные начали ребалансироваться.
......@@ -20,12 +20,18 @@
**Фактор репликации** — число инстансов в репликасете.
**Репликасет** — буквально «набор реплик», экземпляров приложений, в которых хранится один и тот же набор реплицированных данных. В зависимости от роли реплик в Picodata есть реплики _active (RW)_ и _standby (RO)_.
**Репликасет** — буквально «набор реплик», экземпляров приложений, в которых хранится один и тот же набор реплицированных данных. В зависимости от роли реплик, в Picodata есть реплики _active (RW)_ и _standby (RO)_.
**Failure domain** — букв. _"домен отказа"_. Термин обозначает зону доступности инстанса Picodata, т.е. признак физического размещения сервера, на котором запущен инстанс (географический регион, датацентр, стойка и т.д.). Зона доступности используется для того чтобы в один репликасет по возможности попадали инстансы с разным размещением, повышая таким образом отказоустойчивость как отдельного репликасета, так и кластера в целом.
**Bucket** — виртуальная неделимая единица хранения данных, обеспечивающая локальность данных (т. е. их нахождение на каком-то одном репликасете).
**Bucket** (бакет) — виртуальная неделимая единица хранения данных, обеспечивающая локальность данных (т. е. их нахождение на каком-то одном репликасете).
**Горизонтальное масштабирование** — шардинг, т.е. распределение bucket'ов между различными репликасетами, находящихся на разных серверах. Каждый такое репликасет называется шардом.
**Discovery** — алгоритм взаимного обнаружения инстансами друг друга во время объединения в кластер.
**Space** (спейс) — пространство хранения данных в СУБД. В резидентных СУБД спейс является синонимом _таблицы_ из реляционных СУБД.
**Grade** (грейд) — специфичный для Picodata способ обозначения состояния инстанса. Грейд отражает то, как инстанс сконфигурирован его соседями. Существуют _текущий_ (current) и _целевой_ (target) типы грейдов. За приведение первого ко второму отвечает _governor_ (губернатор).
**Governor** (губернатор) — внутренняя централизованная сущность, управляющая конфигурациями и жизненными циклами инстансов в соответствие с изменениями их грейдов.
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