---
# System prepended metadata

title: Синхронная репликация

---

# Синхронная репликация

## Введение

Транзакции делятся на **локальные**, то есть инициированны на самом узле, и **удаленные** (реплицированные) - то есть полученные с другой ноды. Они могут быть как **синхронными**, так и **асинхронными**.

Транзакция состоит из операций над спейсом. Атрибут "синхронный спейс" определяет, синхронной ли будет транзакция или нет. Более того, транзакция может оперировать над синхронными и асинхронными спейсами одновременно. Такая транзакция называется _композитной_. С точки зрения работы движка, это частный случай синхронной транзакции.

Асинхронные транзакции просто пишутся в WAL-журнал по мере их поступления.

Синхронные же транзакции отслеживаются через очередь `txn_limbo`, в которую добавляются транзакции через оберточную структуру `txn_limbo_entry`.

```language:c
struct txn_limbo_entry {
    struct rlist    in_queue;     // <-+ entry
    struct txn      *txn;         //   |
    int64_t         lsn;          //   |
    int             ack_count;    //   |
    bool            is_commit;    //   |
    bool            is_rollback;  //   |
};                                //   |
                                  //   |
struct txn_limbo {                //   |
    struct rlist    queue;        // <-+ queue
    uint32_t        owner_id;
    struct vclock   vclock;
    struct vclock   promote_term_map;
    uint64_t        promote_greatest_term;
    int64_t         confirmed_lsn;
};
```

Одновременно только **один** узел в кластере должен быть владельцем очереди, что отслеживается через `txn_limbo::owner_id`.

Все элементы очереди упорядоченны по возрастанию номера `txn_libo_entry::lsn`. Если транзакция локальная, то LSN изначально выставляется в `-1`, а затем присваивается WAL-движком, перед записью транзакции в журнал. Если же транзакция удаленная, то она приходит вместе с уже назначенным LSN (поскольку отсылается она из WAL-журнала реплики).

Cуществуют следующие виды пакетов для манипуляцией синхронной репликацией:

 1) `PROMOTE` и `DEMOTE` - для управления владельцем очереди
 2) `CONFIRM` и `ROLLBACK` - для управления синхронной транзакцией

## Управление очередью синхронных транзакций

Чтобы использовать синхронную репликацию, помимо самого синхронного спейса, администратор должен сделать узел владельцем очереди. Для этого существует команда `box.ctl.promote()`. Для простоты можно считать, что она вызывается на пустом узле, то есть данные еще не писались.

Во время вызова `box.ctl.promote()` записывается следующий пакет

``` language:c
struct synchro_request req = {
    .type       = IPROTO_RAFT_PROMOTE,
    .replica_id = limbo->owner_id,
    .lsn        = lsn,
    .term       = term,
};
```

Поскольку очередь была пуста, то `limbo->owner_id = 0` и `lsn = 0`. Именно эти данные осядут в журнале. Затем отработает

``` lanugage:c
static void
txn_limbo_read_promote(struct txn_limbo *limbo,
                       uint32_t replica_id, int64_t lsn)
{
    txn_limbo_read_confirm(limbo, lsn);
    txn_limbo_read_rollback(limbo, lsn + 1);

    limbo->owner_id = replica_id;
    box_update_ro_summary();
    limbo->confirmed_lsn = 0;
}
```

В результате чего владельцем очереди станет сам узел `limbo->owner_id = replica_id`. Например

```language:bash
s = box.schema.space.create("sync", {is_sync = true})
s:create_index('primary', { type = 'hash' })
box.ctl.promote()
---
...

tarantool> s:insert{3}
---
- [3]
...

```

Чтобы отдать владение очередью какому-то другому узлу, надо вызвать команду `box.ctl.demote()`. Она действует аналогично `PROMOTE`, за исключением того, что `limbo::owner_id` выставляется в `0`.

Как только владение очередью сбрасывается, любая попытка локальной записи синхронной репликации будет выдавать ошибку, покуда узел не вернет себе статус владельца очереди.

``` language:bash
tarantool> s:insert{2}
---
- error: The synchronous transaction queue doesn't belong to any instance
...
```

Обе команды (`promote` и `demote`) вычищают очередь по окончанию своей работы, выставляя `confirmed_lsn` в `0`.

## Путешествие синхронных транзакций

Основная идея синхронной репликации - записать данные только после того, как они были успешно отреплецированы на `n` узлах.
Фактически, инициатор транзакции должен выслать данные репликам, дождаться от них подтверждения, и только потом считать транзакцию завершенной (commit).

### Протокол Qsync

Протокол синхронной репликации базируется на двух специальных записях: `CONFIRM` и `ROLLBACK`. Запись `CONFIRM` подразумевает, что транзакция была успешно отреплицирована, в то время как `ROLLBACK` указывает, что транзакция не смогла отреплицироваться и не должна приниматься во внимание. 

Поскольку в Tarantool журнал только дописывается, то именно `ROLLBACK` запись обеспечивает откат неудачной транзакции (после ее исполнения транзакция выкидывается из памяти, хотя и остается в журнале до следующей операции сборки мусора, это отдельный механизм, который мы не рассматриваем).

### Локальные транзакции

Под локальными транзакциями мы будем понимать те, которые инициируются узлом. Такой узел должен быть владельцем очереди, т.е. предварительно вызван `box.ctl.promote()`. Как только транзакция создана и аллоцирована, она добавляется в конец очереди. В этот момент ее `lsn = -1`.

Тут же вызывается `journal_write`, который записывает данную транзакцию в локальный WAL, причем назначает записи `lsn`.

Как указывалось выше, синхронные транзакции сами по себе бесмысленны, поэтому они должны быть отреплицированы на `n` узлов, причем сам узел инициатор транзакции тоже является частью кластера. Таким образом, запись в представляет собой последовательность

``` language:c
txn_commit(struct txn *txn) {
    limbo_entry = txn_limbo_append(&txn_limbo, origin_id, txn);
    journal_write(req);
    
    lsn = req->rows[req->n_rows - 1]->lsn;
    txn_limbo_assign_local_lsn(&txn_limbo, limbo_entry, lsn);
    txn_limbo_ack(&txn_limbo, txn_limbo.owner_id, lsn);
    
    txn_limbo_wait_complete(&txn_limbo, limbo_entry)
}
```

После записи в WAL номер `LSN` копируется в `txn_limbo_entry::lsn`, тут же инкрементится `txn_limbo_entry::ack_count` и начинается ожидание, пока оставшиеся `n-1` узлы в кластере подтвердят репликацию.

Как только узлы подтвердят репликацию, такой элемент удаляется из очереди. Подтверждение высылается `relay` потоком, которому реплика отсылает свой последний записанный `LSN`.

``` language:c
void
txn_limbo_ack(struct txn_limbo *limbo, uint32_t replica_id,
              int64_t lsn)
{
    if (rlist_empty(&limbo->queue))
        return;
        
    struct txn_limbo_entry *e;
    int64_t confirm_lsn = -1;

    int64_t prev_lsn = vclock_get(&limbo->vclock, replica_id);
    
    rlist_foreach_entry(e, &limbo->queue, in_queue) {
        if (e->lsn > lsn)
            break;
        /*
         * Sync transactions need to collect acks. Async
         * transactions are automatically committed right
         * after all the previous sync transactions are.
         */
        if (!txn_has_flag(e->txn, TXN_WAIT_ACK)) {
            continue;
        } else if (e->lsn <= prev_lsn) {
            continue;
        } else if (++e->ack_count < replication_synchro_quorum) {
            continue;
        } else {
            confirm_lsn = e->lsn;
        }
    }
    
    if (confirm_lsn == -1 ||
        confirm_lsn <= limbo->confirmed_lsn)
        return;

    txn_limbo_write_confirm(limbo, confirm_lsn);
    txn_limbo_read_confirm(limbo, confirm_lsn);
}
```