SWIM - Протокол построения кластера

Шпилевой Владислав

План доклада

  1. Failure Detection
  2. Gossip протоколы
  3. Протокол SWIM
  4. Схема работы SWIM
  5. Реализация SWIM в Тарантуле
  6. Расширения SWIM
  7. Примеры применения
  8. Планы

Горизонтальное

масштабирование

Failure detection

Принятие решений

Ping

Ping

Ping

Ping

Ping

Вывод нерабочих узлов из кластера

Починка отказов

Ping - проверять лидера/мастера/...

Heartbeat - узел сам делает рассылку

Failure detection

узлов

сообщений в сети

O(N^2)
N

"Все со всеми" не работает

Gossip

узлов

сообщений в сети

O(N)
N

Протоколы "слухов", "инфекций"

Gossip

!

!

!

!

!

!

!

!

Распространяют событие все, а не один узел

Алгоритмы рандомизированы:

  • нет синхронизации;
  • нагрузка равномерна

Gossip. Обнаружение отказов

Прямой ping

Непрямой ping

Сеть сломана

Ping

Ack

PingRequest

Ping

Ack

Ack

Gossip. Гранулярность отказов

Непрерывное состояние

Жив

Мертв

VS

Дискретное состояние

Критерии:

  • Количество без-ACK-ов
  • Задержка
  • Непрямая доступность

...

Gossip. Направление слухов

Push

Ping

!

!

!

Pull

!

Ping +

Meta

!

Ack

!

Push-Pull

!

!

Ping +

Meta

!

Ack

!

!

!

Распространение:

O(log(N))
O(log(N))
O(log(log(N)))

Обнаружение:

O(1)
O(1)
O(1)

Gossip. Применения

Lifeguard

SWIM

SWIM

Scalable

!

O(1)

Weakly-Consistent

!

!

!

!

!

!

O(log(N))

A

B

C

A

A

Infection-Style Process

!

!

!

!

Group Membership

D

E

H

G

F

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

A, B, C, D, E, F, G, H

SWIM

Обнаружение отказов

Распространение событий

Ping Request

Ack

Ack

Подозрение отказа

!

Ping

События

UDP пакет

T

T

Ping +

!

!

!

!

!

Каждое событие посылается всем по разу

Подозрение не подтвердилось

SWIM

Инкарнация

C

A

B

B

B

B жив

?

B

B мертв

?

B

B

B

Предполагай худшее

Но как B очистит свое имя?

Вводится понятие "инкарнация" для опровержения слухов

B 1

B 1

B 1

Знаешь? Как бы сказать ... ты мертв

?!

B 2

B 2

?

B 2

Предполагай худшее, но больше инкарнация - новее информация

B 2

Итог:

  • Опровержение ложных слухов
  • Защита от проблем UDP
  • Если равны - предполагай худшее

Tarantool SWIM

Раунды

Адресация

UUID

URI1

URI2

URI3

Адрес можно менять

Heartbeat Rate

Модуль

Можно создавать много узлов

Конфигурируется почти все

Есть ручное управление таблицей узлов кластера

Можно обращаться к узлам

Асинхронная подписка на события

Аккуратный вывод из кластера с уведомлением всех

Случайное перемешивание

swim = require('swim')

s = swim.new([cfg])

s:cfg({heartbeat_rate = ..., ack_timeout = ...,
       gc_mode = ..., uri = ..., uuid = ...})

s:probe_member(uri)

s:add_member({uuid = ..., uri = ...})

s:remove_member(uuid)

s:broadcast([port])

s:size()

s:member_by_uuid(uuid)

s:pairs()

s:on_member_event(new_trigger[, old_trigger])

s:quit()

s:delete()

SWIM - Пример

На двух Тарантулах запущены два SWIM узла

Первый отправляет Ping запрос второму

Теперь узлы друг друга знают, пингуют, делятся информацией

При помощи триггеров можно узнавать момент и детали обновления таблицы

Если долго нет ответа от кого-либо, то он будет помечен мертвым спустя несколько пингов

swim = require('swim')

s = swim.new({uri = 3333, uuid = uuid1,
              ack_timeout = 0.1})

local event
s:on_member_event(function(m, ev, ctx)
    event = {m, ev, ctx}
end)
swim = require('swim')

s = swim.new({uri = 3334, uuid = uuid2,
              ack_timeout = 0.1})
s:probe_member(3334)
tarantool> s:size()
--
- 2
...
tarantool> event
---
- - uri: 127.0.0.1:3334
    status: alive
    incarnation: 0
    uuid: 00000000-0000-1000-8000-000000000002
  - is_new: true
...

tarantool> member = event[1]
---
...

tarantool> member:status()
---
- dead
...
tarantool> s:size()
--
- 2
...
s:delete()

 

 

 

 

Расширения - Антиэнтропия

A

B

A, B

A, B

Узлы A и B знают друг про друга

С

Появляется третий, знающий B. Как он узнает про А?

B, C

B разошлет событие "новый узел", но оно теряется

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

Ping

События

UDP пакет

Антиэнтропия

A, B, C

A, B, C

A, B, C

Новый узел С!

Моя таблица - A, B, C

Моя таблица - A, B, C

Рано или поздно A и  C  узнают друг про друга - антиэнтропия шлется всегда

Работает само, не нужно настраивать

Расширения - Payload

Как, подключившись к узлу, понять, куда подключаться по TCP, или как распространять свои данные?

swim = require('swim')

s = swim.new({uri = 3333, uuid = uuid1})
swim = require('swim')

s = swim.new({uri = 3335, uuid = uuid2})
box.cfg{listen = 3336}
s:set_payload({tport = 3336,
               any_other_data = value

Метод set_payload позволяет распространить произвольную информацию

s:broadcast(3335)

Достаточно любым образом соединить узлы SWIM

p = s:member_by_uuid(uuid2):payload()

tarantool> p
---
- {'tport': 3336, 'any_other_data': value}
...

На всех узлах payload будет доступен

tarantool> require('net.box').connect(p.tport)
---
- peer_uuid: 1262ec51-46f4-488e-8925-853f55367e61
  schema_version: 73
  protocol: Binary
  state: active
  peer_version_id: 131584
  port: '3336'
...

Можно подключаться!

tarantool> s:size()
--
- 2
...

Расширения - Шифрование

SWIM может быть использован в открытой сети

Сеть подвержена атакам

В Tarantool SWIM есть встроенное шифрование

swim = require('swim')

s = swim.new()

s:set_codec({algo = 'aes192', mode = 'cbc', key = private_key})

Алгоритмы:

DES, AES128/192/256

 

Режимы:

ECB, CBC, CFB, OFB

Расширения - Выход из кластера

A

B

C

D

Узел A хочет выйти из кластера

swim_a:delete()

Плохой способ - удалить узел

  1. Дождаться, пока кто-то отправит Ping A
  2. Дождаться неответов на 5 Ping-ов
  3. Дождаться распространения смерти A - логарифм размера кластера

Ping

A

A

A

Очень долго и неотличимо от смерти

Расширения - Выход из кластера

A

B

C

D

Узел A хочет выйти из кластера

swim_a:quit()

Хороший способ - уведомить всех

A

A

A

A сам разошлет всем уведомление

swim = require('swim')

s = swim.new({uri = 3333, uuid = uuid1})
swim = require('swim')

s = swim.new({uri = 3334, uuid = uuid2})
s:probe_member(3334)
tarantool> s:member_by_uuid(uuid2):status()
---
- left
...
s:quit()

Быстро, можно реагировать не как на смерть

Практика - Построение репликации

Дата-центр

Дата-центр

Задача:

Создать N репликасетов по 2 реплики каждый, сгруппированных по дата-центрам.

Практика - Построение репликации - Код

datacenter = arg[1]
swim_uri = arg[2]
box_uri = arg[3]
uuid = arg[4]















Характеристики одного узла

fiber = require('fiber')
fiber.create(function()
    while true do
        for port in pairs(swim_ports) do
            swim:broadcast(port)
        end
	fiber.sleep(60)
    end
end)












swim = require('swim').new()
nodes = {}
function on_event(m, e)
    if e:is_drop() then
        nodes[m:uuid()] = nil
    elseif e:is_new_payload() and
           m:payload() then
        local dc = m:payload().dc
        if dc == datacenter then
            nodes[m:uuid()] = m
        end
    end
end
swim:on_member_event(on_event)
swim:cfg({uri = swim_uri, uuid = swim_uuid})
swim:set_payload({dc = datacenter,
                  box = box_uri})















while map_size(nodes) ~= 2 do
    fiber.sleep(5)
end

local rep = {}
for uuid, n in pairs(nodes) do
    table.insert(rep, n:uri())
end

box.cfg{
    listen = box_uri,
    replication = rep,
    instance_uuid = uuid,
}

>

>

Один SWIM на узел

>

>

Таблица с узлами датацентра

>

Обновляется автоматически

>

>

Рассказать всем свои ДЦ и порт

>

Периодически искать новые узлы

>

Для сборки репликасета надо найти 2 узла

>

Сборка и запуск репликации готовы!

Планы - Уменьшение False Positive

Проблема

Решение

A

B

C

D

Есть кластер с проблемной сетью B-D

Спустя время B начинает подозревать D

D 1

Успевает это распространить

D 1

D 1

Теперь D начинает отвечать, но не знает, что его подозревают ...

...

...

D не менял инкарнацию, а значит B не сможет распространять обновление

>

Придется ждать, пока до D дойдут слухи

A

B

C

D

D 1

D 1

D 1

...

...

Ping + D 

D 2

Ack + D 2

D 2

D 2

D 2

Ping

Ack + D 1

Каждый Ping будет содержать статус получателя с точки зрения отправителя

Получатель немедленно реинкарнируется в случае статуса != живой

Слух опровергнут сразу после восстановления сети

Планы - Ускорение распространения

Классический SWIM

Push-Pull протоколы

A

B

Ping +

!

!

!

!

Ack

  • События посылаются только с Ping сообщениями - скорость
     
  • Пакет с Ack почти пустой - трата ресурсов сети
O(log(N))

A

B

Ping +

!

!

!

!

Ack + 

!

!

  • События посылаются всегда - скорость потенциально

     
  • Пакет с Ack набит полезными данными
O(log(log(N)))

Планы - Детектор перезапуска

swim = require('swim')

s = swim.new({
    uri = 3333,
    uuid = uuid1,
    generation = number
})

function on_event(m, event)
    if event:is_restart() then
        ...
    end
end

Generation - счетчик перезапусков, можно сохранить на диск и при перезапуске увеличить

Обновление от 23.06,2019 - это уже сделано

Планы - Интеграция с репликацией

  • Raft для автосмены лидера кластера
  • Автосборка кластера из коробки
  • Обнаружение топологии кластера