---
# System prepended metadata

title: Roles / tiers

---

# Roles / tiers

Документ переехал:

- [RFC - Plugins]
- [RFC - Tiers]

[RFC - Plugins]: https://docs.google.com/document/d/1TSMmnxn_PqaUWjJhmckbkKEFkCzUrha_rhafKjQzVWI/edit
[RFC - Tiers]: https://docs.google.com/document/d/1B363AMhx912c_YtNQGkVCXl2-dd_zl0_R2Pwx-a2A5Y/edit

::: spoiler Исторический контент

## Группы хранения
Группы хранения (они же всем нам знакомые vshard groups) - это фича, которая позволяет иметь несколько кластеров vshard в рамках одного кластера Picodata.
Один узел (в смысле хранения данных) не может принадлежать к разным группам хранения.
Физически она выглядит так - в каждой группе хранения свое распределение бакетов по узлам хранения, а на роутерах есть несколько сущностей `vshard-router`, которые могут обращаться к своим группам хранения.

Группа хранения имеет собственную схему данных. Данные принадлежащие группе хранения находятся только в ней.
При этом пространство имен для схемы данных едино в рамках кластера.
Это значит, что нельзя создать таблицу `MY_TABLE` одновременно в группе хранения `A` и в группе хранения `B`.


## Тиры
Тир - это логическое объединение инстансов, образующее отдельную группу хранения ("группа хранения" == "vshard group"). Репликационный фактор является общим на весь тир.

Принадлежность инстанса к тиру задается при деплое инстанса и НЕ может быть изменена при эксплуатации.

Пример:
```bash=
picodata run --tier red
```

```console
$ picodata run --help
picodata-run
Run the picodata instance

USAGE:
    picodata run [OPTIONS]

OPTIONS:
        --tier <TIER> TODO: description
            [env: PICODATA_TIER=] [default: storage]
```
Каждый тир соответствует отдельной группе хранения. Тиры и группы хранения соотносятся 1 к 1. К каждому тиру привязана единственная и уникальная группа хранения.

### Требования к тирам
* Набор тиров определяется на этапе разработке
* Расположение тиров определяется на этапе эксплуатации
* В тиры можно добавлять инстансы
* Из тиров можно удалять инстансы и репликасеты
* В тире можно управлять фактором репликации


### Свойства тира

* Репликационный фактор.
* Набор включенных на нем ролей
* Возможно ли делать узлы из этого тира `raft-voter`
* Группа параметров для `box.cfg`

**Важно**: тир не хранит желаемое количество репликасетов. Это позволяет нам делать автоматическое масштабирование (и вверх, и вниз) развертыванием инстансов внешним оркестраторов. Это значит, что условный kubernetes может запустить еще 10 узлов Picodata с указанным тиром и Picodata автоматически его расширит. 

### Первоначальная конфигурация тиров
При первом запуске кластера инстансам указывается параметр `--init-cfg`, который описывает изначально существующие в кластере тиры и другие параметры кластера.
Этот параметр используется только узлом `bootstrap-leader`, который переносит эту конфигурацию в глобальные спейсы. 

```bash=
picodata run --init-cfg cfg.yml --tier red
```
```yml=
# init.cfg
# ========

tier:
  - name: red
    replication_factor: 3
    raft_voter: false
    memtx_memory: 5 * 1024 * 1024 * 1024
    # other_box_cfg_opts: ...
    role:
      - storage
      
  - name: blue
    replication_factor: 3
    raft_voter: false
    memtx_memory: 5 * 1024 * 1024 * 1024
    role:
      - storage
      
  - name: purple
    replication_factor: 1
    raft_voter: false
    memtx_memory: 128 * 1024 * 1024
    role:
      - api-gateway
      
  - name: yellow
    replication_factor: 2
    raft_voter: false
    role:
      - search-index
      
  - name: green
    replication_factor: 2
    raft_voter: false
    role:
      - kafka-consumer

  - name: raft-voters
    replication_factor: 1
    raft-voter: true
    role: []
```



### Пример деплоя кластера с набором тиров
1. У меня есть ansible роль, которая разложит Picodata на необходимых мне хостах
2. Эта же ansible роль запустит узлы Picodata, указав тиры узлов при запуске каждого узла. Без указания тира, каждый узел входит в тир по умолчанию - `storage`
4. Первоначальная конфигурация тиров указывается при создании группы (по аналогии с `--init-replication-factor`). Конфигурация тиров включает в себя имя и `replication-factor`
3. В join-запросе при добавлении инстанса в кластер указывается тир.
4. При формировании репликасетов `governor`, кроме `failure-domain` учитывает их тиры. Инстансы из разных тиров в репликасет не объединяются.


## Breaking changes
В интерфейс `create_space` будет добавлен параметр `tier`. 

Пример:
```lua
pico.create_space({
    tier = 'my_tier',
    name = 'my_space',
    format = ...,
})
```

## Picodata Roles
Роль - это набор кода, который исполняется в соответствии с некоторым жизненным циклом роли и позволяет выполнять пользовательскую логику внутри кластера Picodata

## Требования
* Возможость включать / выключать роли на уровне тиров
* Роли имеют доступ в сторадж напрямую (box.space.*)
* Роль экспортирует набор RPC хендлеров.
* Одну роль можно включить на множестве тиров
* На одном тире можно включить множество ролей
* Функционал ролей определяется на этапе разработки
* Расположение ролей определяется на этапе эксплуатации
* Роли должны иметь функционал зависимых ролей (своеобразных сайдкаров. Как пример - см. `metrics` в картридже)
* > Pavel Yudin: Слушай, нам в механизме ролей в пикодате обязательно нужна нативная поддержка синхры. То есть мы должны дожидаться выбора лидера до применения ролей, что бы не было как сейчас.

## Пример использования
Я хочу собрать кластер Picodata, который будет выполнять следующие функции:

Пользовательская логика:
* Считывать данные из Kafka и выполнять их обогащение
* Хранить эти данные шардированно, причем хранение должно быть разделено на горячее и холодное (memtx / vinyl)
* Иметь над этими данными распределенный вторичный индекс
* Предоставлять REST API поверх этих данных по HTTP

Технологическая часть:
* Предоставлять prometheus метрики (должно быть встроено в каждый узел Picodata с возможность расширения метрик из кода ролей, а не отдельной ролью)
* Предоставлять метрики для `zabbix`. Это будет реализовано через `dependency role`, которая будет сайдкаром прицеплена ко всем остальным.

Я хочу разнести весь это функционал по различным узлам, чтобы
* ошибка в программном коде в одной из часте приложения не приводила к выходу из строя всего приложения
* перегрузка одних частей приложения (например, необходимость считать миллион сообщений из кафки) не приводила к DoS всего приложения и увеличению latency из других частей

Таким образом у меня появляются 4 роли:
1. `kafka-consumer`
2. `storage`
3. `search-index`
4. `api-gateway`

Проецируемые на 5 тиров (каждый тир имеет независимую схему данных)
1. red: -- hot storage
    - storage
2. blue: -- cold storage
    - storage
3. yellow -- search-index
    - search-index
4. green:
    - kafka-consumer
5. purple:
    - api-gateway


При зачастую обращения к `search-index` будут идти по одному ключу, что приведет к высокой паразитной нагрузке и помешает производительности слоя storage [пример из проекта](https://git.picodata.io/ttq/ttq/-/issues/212).
Таким образом, я хочу иметь несколько групп хранения в одной кластере Picodata (`yellow`, `blue` и `red` в нашем примере).

При этом потребности в объединении ролей и групп хранения не возникает, так как на каждом узле Picodata запускается не только `vshard-storage`, но и набор из нескольких `vshard-router`, позволяющих получить доступ к любой из групп хранения.

При это на этапе разработки приложения я ничего не знаю о наборе физическом расположении групп хранения, но в RPC я должен уметь указывать необходимую мне группу хранения для обращения к данным / хранимым процедурам из этого тира.

Пример RPC вызова по тиру
```lua=
picodata> pico.rpc_call(
    'tier',
    bucket,
    'my_stored_procedure',
    {1},
    opts
)
---
true
...
```

Пример RPC вызова по имени роли
```lua=
picodata> pico.rpc_call_role(
    'role_name',
    'my_stored_procedure',
    {1},
    opts
)
```

Это значит, что при деплое кластера для работоспособности RPC вызовов по тиру я должен создать соответствующие тиры.

### Как происходит деплой приложения
1. У меня есть `ansible` роль, которая разложит Picodata и `so` файлы моего приложения по узлам Picodata на необходимых мне хостах
2. Эта же `ansible` роль запустит узлы Picodata, указав тиры узлов и `--init-cfg`, где будут описаны существующие в кластере тиры и назначенные им роли
3. Picodata соберется в кластер, выберет лидера Raft и `governor`. При этом `governor` будет запущен в соответствии с конфигурацией тиров, что позволит его выносить на отдельный узел Picodata и не позволит логике отдельной роли помешать его работе (например, заблокировав `event loop`). Это похоже на логику k8s и `master` нод в нём.
4. `governor` назначит соответствующие роли инстансам, в соответствии с содержимым глобальных спейсов

**Важно** отдельного `init.lua` как с Cartridge в качестве входной точки здесь нет. Его роль Picodata выполняет самостоятельно.

### Запуск роли Governor'ом
https://www.websequencediagrams.com/
```
title Initialize role
actor Governor
actor Worker

Note over Worker: dlopen "so" files
Governor->Worker: enable "storage" role
Note over Worker: call "storage.init" function from "so" opened earlier
Worker->Governor: ok
note over Governor: CaS("update", instance, status...)

Governor->Worker: enable "missing" role
Note over Worker: fail calling "missing.init" function
Worker->Governor: not ok
note over Governor: CaS("update", instance, status...)
```

TODO: описать параметр, который будет указывать Picodata, где искать `so` файлы с ролями

```bash=
$ picodata run --my-app-folder .
```
governor> call(my_role_init)

### Что такое роль

- Роль - это ...
- Роль на инстансе имеет статус включна / выключена (enabled / disabled)
- Роль имеет набор колбеков.
- Роль имеет набор RPC хендлеров 
- Апгрейд приложений делается в 2 этапа - сначала обновляется рантайм (инстансы рестартуют с новым кодом), потом обновляется схема, в конце можно заперсисть признак что все готово и новой версией можно пользоваться.

### Successful reload

- sequencediagram.org

```
title Reload cluster

participantgroup #red Governor tier
participant former governor
participant governor
end

participantgroup #lightgreen Replicaset
participant s-1-1
participant s-1-2
end 


note left of s-1-1: leader
note right of s-1-2: replica
[->former governor:Reload cluster
former governor->governor: reload
former governor->governor: become governor
former governor->former governor: reload

governor->s-1-2: reload
governor->s-1-2: ready check
s-1-2->governor: ready ok
governor->s-1-1: demote
governor->s-1-2: promote
governor->s-1-1: reload
governor->s-1-1: ready check
s-1-1->governor: ready ok
governor->s-1-1: promote
governor->s-1-2: denote
governor->s-1-1: apply DDL
```

### Errorneous reload


### Как происходит выключение роли
1. Gracefull
* Увидев, что текущая топология требует выключения роли на узле, `governor` зовет на инстансах синхронный метод `stop` соответствующей роли. Этот метод должен также вызывать методы `stop` у всех зависимых ролей

2. Force
* `Governor` обнаруживает, что инстанс с выбранной ролью не отвечает на `healthcheck`
* `Governor` исключает инстанс из кластера (в моем идеальном мире, кажется, что RPC фреймворк на всех узлах должен начать выкидывать exception при попытке обращения к этому инстансу)
* Должен ли вызываться `stop`?


### Role callbacks
1. `once_ddl` - упорядоченный набор DDL операций, который необходимо выполнить на кластере.
  - `governor` парсит это набор и строго упорядоченно вставляет эти операции в Raft
  - каждый новый узел получает и применяет эти операции локально
  
  Ex: 
  ```Lua
  {
      once_ddl = {
          [1] = "CREATE TABLE T",
          [2] = "ALTER TABLE T",
      }
  };
  ```

2. `async fn init()` - исполняется на узле, где запущена роль. Этот колбек дергает говернор по сети в процессе обработки `target_grade: Online`.
3. `cas_apply` - исполняется на узле, где запущена роль
4. `stop` - исполняется на узле, где запущена роль
5. `version` - отдает semver роли


### API предоставляемые коду в роли
1. HTTP sever
2. RPC server /  client
3. Background jobs container (должны ли мы повесить на отдельные коллбеки?)


Конфигурация ролей применяется через Governor, который и инициализирует роли


## Создание роли как пользователь

Для работы роли пользователю необходимо предоставить shared lib (so/dylib) файл 
который экспортирует необходимые роли. Каждая роль представляет собой rust структуру реализующую `trait picorole::Role`.
`picorole::Role` по сути набор callback'ов, реализацию которых мы ждем от пользователя. Кроме того, данная структура может хранить состояние (например, коннект к kafka, in-memory cache и т.д.).
Shared lib может экспортировать как одну так и несколько ролей. Кроме .so файла на вход к `picodata` поступает файл с конфигурацией (через CaS), описывающий какие роли необходимо запустить и зависимости ролей (зависимости возможно лучше описать статически в .so файле а не в конфигурации).

Пример проекта который реализует 2 роли в одном бинарном файле:

```rust=
// экспорт всех макросов и прочего
use picorole::prelude::*;

struct Role1 {
    _kafka_connection: Option<bool>,
    other_state: Vec<String>,
}

impl Role for Role1 {
    fn name(&self) -> &str {
        "role_1"
    }

    // init callback
    fn init(&mut self, _context: &PicoContext) {
        println!("hello (role 1)");
        println!("some state: {:?}", self.other_state);
    }

    // stop callback
    fn stop(&mut self, _context: &PicoContext) {
        println!("bye (role 1)");
    }
}

// функция-конструктор для роли 1
#[role_constructor]
fn role_1_ctor() -> Role1 {
    Role1 {
        _kafka_connection: None,
        other_state: vec!["hello".to_string(), "world".to_string()],
    }
}

struct Role2 {}

impl Role for Role2 {
    fn name(&self) -> &str {
        "role_2"
    }
    
    // init callback
    fn init(&mut self, _context: &PicoContext) {
        println!("hello (role 2)");
    }

    // stop callback
    fn stop(&mut self, _context: &PicoContext) {
        println!("bye (role 2)");
    }
}

// функция-конструктор для роли 2
#[role_constructor]
fn role_2_ctor() -> Role2 {
    Role2 {}
}
```

### Дефолтная реализация callback'ов.

Для некоторых callback'ов может быть стандартная реализация (обычно пустой callback) это решается через дефолтные методы trait'а.

## Детали реализации

### Picorole

Библиотека `picorole` предоставляет набор макросов и типов для создания role. Кроме того `picorole` экспортирует функции необходимые picodata для импорта ролей.

#### Rust to Rust ABI

По сути `.so` файл представляет набор плагинов для Picodata, каждый из которых - это роль. В настоящий момент Rust не имеет стабильного ABI, это значит 
что нет гарантии что Picodata "поймет" типы которые будут выгружены из .so файла. Варианты получить такую гарантию:
1) Lock версии rust которым компилируются плагины на тот же которым скомпилирована Picodata. Этот вариант нам не подходит т.к. сильно ограничивает пользователя и вносит дополнительную сложность при соблюдении совместимости между разными версиями picodata и .so файлами.
2) Использовать C ABI. C ABI является стабильным, поэтому мы можем его использовать. К сожалению C ABI заставляет использовать только простейшие типы, что не подходит для нас, т.к. на границе между picodata и .so мы хотим использовать `trait objects` (т.е. реализации trait'a Role).
3) Использовать сторонние крэйты предоставляющие стабильный ABI. Реализация ролей использует этот вариант, в частности крэйт https://github.com/rodrimati1992/abi_stable_crates/. Данный крэйт позволяет реализовать переносимые trait objects и предоставляет stable реализацию различных rust типов (Vec -> RVec, Result -> RResult, &str -> RStr и тд). Одна из важных задач реализации ролей - скрыть эти подробности для пользователя (разработчика роли) и позволить ему использовать привычные rust типы, делая необходимые преобразования прозрачно.

Итоговым выбран вариант №3 - `abi_stable_crates`

#### Trait Role

Trait role которые необходимо реализовать пользователю имеет вид:

```rust=
pub trait RoleFacade {
    fn name(&self) -> &str;
    fn init(&mut self, context: &PicoContext) {
    }
    fn stop(&mut self, context: &PicoContext) {
    }
}
```

Как можно заметить данный тип не является stable (его нельзя использовать при переходе границы Picodata -> .so file). Поэтому данный trait экспортируется для пользователя как `Role` и при запросе со стороны Picodata преобразуется в stable trait object (детали преобразования будут описаны ниже).

#### Role constuctor

Для того чтобы picodata знала о том что .so файл экспортирует конкретную роль пользователю необходимо создать функции-конструктор и пометить ее специальным макросом `#[role_constructor]`. Пример такой функции:

```rust=
#[role_constructor]
fn role_2_ctor() -> Role2 {
    Role2 {}
}
```

При вызове этого конструктора на стороне picodata происходит следующее:
1) Создается функция которая:
    1.1 Создает структуру Role2 имплементирующую trait Role. Данная структура преобзуется в TO: `Box<dyn Role>`.
    1.2 Создает структуру типа RoleProxy оборачивающая trait object типа `Box<dyn Role>`. Структура RoleProxy имплементирует trait `RoleStable`, данный trait является стабильным (trait object можно использовать при переходе границы picodata -> .so file). Тип trait object для `RoleStable` называется RoleBox.
2) С помощью библиотеки linkme эта функция на этапе линковки будет помещена в массив `CONSTRUCTORS` который имеет вид `CONSTRUCTORS: [extern "C" fn() -> RoleBox]`. В будущем picodata может обратится к этому массиву, выгрузить все конструкторы ролей и создать необходимую роль.

#### Экспортируемые функции picorole

Для того чтобы picodata могла получить доступ к массиву `CONSTRUCTORS` в .so файле `picorole` экспортирует функцию `constructors`. Пример:

### На стороне picodata

Picodata выполняет функцию загрузки роли, на входе есть shared lib файл (so/dylib) (в будущем кроме shared lib файла будет и файл с конфигурацией).
Пример импорта ролей из .so файла:

```rust=
    unsafe fn get_roles_fn<'a>(
        l: &'a Library,
        symbol: &str,
    ) -> Result<
        Symbol<'a, extern "C" fn() -> RSlice<'static, extern "C" fn() -> RoleBox>>,
        libloading::Error,
    > {
        l.get(symbol.as_bytes())
    }

    unsafe {
        let path_to_lib = "./target/debug/libproducer.so";

        let lib = Library::new(path_to_lib).unwrap();
        let func = get_roles_fn(&lib, "constructors").unwrap();
        let role_ctors = func();
        role_ctors.iter().for_each(|role_ctor| {
            let mut role = role_ctor();
            println!("role: {}", role.name());
            println!("init function execute result:");
            role.init(&PicoContext::empty());
            println!("stop function execute result:");
            role.stop(&PicoContext::empty());
            println!("--------------------------------------");
        });
    };
```

Вывод (пример файла [выше](#Создание-роли-как-пользователь)):
```
role: role_1
init function execute result:
hello (role 1)
some state: ["hello", "world"]
stop function execute result:
bye (role 1)
--------------------------------------
role: role_2
init function execute result:
hello (role 2)
stop function execute result:
bye (role 2)
--------------------------------------
```



## Вопросы:
1. Как роль storage будет отличать себя по `cold` и `hot`? Тиры должны быть описаны при разработке приложения, потому что tier - аналог vshard группы. Как роль `storage` будет отличать себя в кластерном конфиге? Определять свой тир и смотреть в соответствующие секции? А почему не определить их просто как две разные роли, которые будут переиспользовать свой код?
2. А можем мы вместе с конфигурацией тиров и конфигурацию ролей бахать при инициализации кластера? Наверное да, она просто запишется в global. А зачем?
3. Какие API будут доступны пользовательскому коду в ролях?
4. Прописать механизм импорта `so` файла ролей
5. Прописать механизм сборки ролей
6. Описать локальный запуск Picodata с ролями (вот бы еще просто механизм локального запуска Picodata увидеть)
7. Как будут выглядеть интеграционные тесты на роли? Нужно описать cluster-helpers на Rust / Lua для ролей
8. Описать обвязку OpenTelemetry для RPC фреймворка
9. Описать как мы будем шиппить готовые роли Picodata (стандартизированные kafka importer, например)
10. Как шиппить и как делать встраивание UI ролей
11. Где в UI и CLI посмотреть версии существующих ролей? Как версионировать роли? Видимо, при написании кода роль должна экспортировать свою версию
12. Неплохо бы иметь некий контейнер с глобальными "сервисами" время жизни которых привязано к времени жизни роли. В коде выглядит как то так:

```rust
fn my_fn() {
...
let worker = service_locator.role('my_role').get('worker_x');
worker.update_cfg(new_cfg);
...
}
```

### Заметки со встречи 2023-07-12 (d.koltsov, y.dynnikov)

```diff
--- 1.txt	2023-07-12 16:28:17.640478796 +0300
+++ 2.txt	2023-07-12 16:29:20.536662759 +0300
@@ -1,112 +1,141 @@
+
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_space
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
-- {'is_nullable': false, 'name': 'id', 'type': 'unsigned'}
-- {'is_nullable': false, 'name': 'name', 'type': 'string'}
-- {'is_nullable': false, 'name': 'distribution', 'type': 'array'}
-- {'is_nullable': false, 'name': 'format', 'type': 'array'}
-- {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'}
-- {'is_nullable': false, 'name': 'operable', 'type': 'boolean'}
+- {'name': 'id', 'type': 'unsigned', 'is_nullable': false}
+- {'name': 'name', 'type': 'string', 'is_nullable': false}
+- {'name': 'engine', 'type': 'string', 'is_nullable': false}
+- {'name': 'distribution', 'type': 'array', 'is_nullable': false}
+- {'name': 'format', 'type': 'array', 'is_nullable': false}
+- {'name': 'schema_version', 'type': 'unsigned', 'is_nullable': false}
+- {'name': 'operable', 'type': 'boolean', 'is_nullable': false}
+- {'name': 'tier', 'type': 'string', 'is_nullable': false}
+- {'name': 'doc', 'type': 'string', 'is_nullable': true}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_index
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'space_id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'name', 'type': 'string'}
 - {'is_nullable': false, 'name': 'local', 'type': 'boolean'}
 - {'is_nullable': false, 'name': 'parts', 'type': 'array'}
 - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'operable', 'type': 'boolean'}
 - {'is_nullable': false, 'name': 'unique', 'type': 'boolean'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_peer_address
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'raft_id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'address', 'type': 'string'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_instance
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'instance_id', 'type': 'string'}
-- {'is_nullable': false, 'doc': 'foobar', 'name': 'instance_uuid', 'type': 'string'}
+- {'is_nullable': false, 'name': 'instance_uuid', 'type': 'string', 'doc': 'foobar'}
 - {'is_nullable': false, 'name': 'raft_id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'replicaset_id', 'type': 'string'}
 - {'is_nullable': false, 'name': 'replicaset_uuid', 'type': 'string'}
 - {'is_nullable': false, 'name': 'current_grade', 'type': 'array'}
 - {'is_nullable': false, 'name': 'target_grade', 'type': 'array'}
 - {'is_nullable': false, 'name': 'failure_domain', 'type': 'map'}
 ...
 
+_pico_tier
+- {'name': 'tier_id', 'type': 'string', 'is_nullable': false,
+    doc: 'Unique alias, example: red / green / hot-storage / search-index'}
+- {'name': 'raft_voter', 'type': 'boolean', 'is_nullable': false}
+
+
+alt. 1
+- {'name': 'replication_factor', 'type': 'unsigned', 'is_nullable': false}
+- {'name': 'raft_voter', 'type': 'boolean', 'is_nullable': false}
+- {'name': 'roles', 'type': 'array', 'is_nullable': false}
+- {'name': 'doc', 'type': 'string', 'is_nullable': true}
+- {'name': 'box_cfg', 'type': 'map', 'is_nullable': false}
+- {'name': 'vshard_cfg', 'type': 'map', 'is_nullable': false}
+- {'name': 'vshard_bootstrapped', 'type': 'boolean', is_nullable: false}
+
+alt. 2 -- ни в коем случае
+- {'name': 'key', 'type': 'string', 'is_nullable': false}
+- {'name': 'value', 'type': 'any', 'is_nullable': false}
+- {'name': 'doc', 'type': 'string', 'is_nullable': true}
+
+alt. 3
+- {'name': 'properties', 'type': 'array', 'is_nullable': false}
+  - [{key: "replication_factor", value: 2, doc: ""}]
+
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_property
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'key', 'type': 'string'}
 - {'is_nullable': false, 'name': 'value', 'type': 'any'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_replicaset
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'replicaset_id', 'type': 'string'}
 - {'is_nullable': false, 'name': 'replicaset_uuid', 'type': 'string'}
 - {'is_nullable': false, 'name': 'master_id', 'type': 'string'}
 - {'is_nullable': false, 'name': 'weight', 'type': 'array'}
+- {'is_nullable': false, 'name': 'tier', 'type': 'string'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _raft_log
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'entry_type', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'index', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'term', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'data', 'type': 'any'}
 - {'is_nullable': false, 'name': 'context', 'type': 'any'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _raft_state
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'key', 'type': 'string'}
 - {'is_nullable': false, 'name': 'value', 'type': 'any'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_user
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'name', 'type': 'string'}
 - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'auth', 'type': 'array'}
 ...
 
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_privilege
 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> ---
 - {'is_nullable': false, 'name': 'user_id', 'type': 'unsigned'}
 - {'is_nullable': false, 'name': 'object_type', 'type': 'string'}
 - {'is_nullable': false, 'name': 'object_name', 'type': 'string'}
 - {'is_nullable': false, 'name': 'privilege', 'type': 'string'}
 - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'}
 ...
 """\
 +-----+----+-----+--------+
 |index|term| lc  |contents|
 +-----+----+-----+--------+
 |  1  | 1  |1.0.1|Insert({_pico_peer_address}, [1,"127.0.0.1:{p}"])|
+|  1  | 1  |1.0.1|Insert({_pico_tier}, ["red", {"replication_factor":1, "enabled_roles": [], ...}])| //
+|  9  | 2  |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","red"(tier),"i1"(leader_id),[0.0,"Auto","Initial"]])|
 |  2  | 1  |1.0.2|Insert({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Offline",0],{b}])|
-|  3  | 1  |1.0.3|Insert({_pico_property}, ["replication_factor",1])|
+|  3  | 1  |1.0.3|Insert({_pico_property}, ["replication_factor",1])| // Del
 |  4  | 1  |1.0.4|Insert({_pico_property}, ["global_schema_version",0])|
 |  5  | 1  |1.0.5|Insert({_pico_property}, ["next_schema_version",1])|
 |  6  | 1  |     |AddNode(1)|
 |  7  | 2  |     |-|
 |  8  | 2  |1.1.1|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Online",1],{b}])|
-|  9  | 2  |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1",[0.0,"Auto","Initial"]])|
 | 10  | 2  |1.1.3|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Replicated",1],["Online",1],{b}])|
 | 11  | 2  |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight[1]",1.0], ["=","weight[3]","UpToDate"]])|
 | 12  | 2  |1.1.5|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["ShardingInitialized",1],["Online",1],{b}])|
-| 13  | 2  |1.1.6|Replace({_pico_property}, ["vshard_bootstrapped",true])|
+| 13  | 2  |1.1.6|Replace({_pico_property}, ["vshard_bootstrapped",true])| // TODO
 | 14  | 2  |1.1.7|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Online",1],["Online",1],{b}])|
 +-----+----+-----+--------+
 """
```

:::