Vladislav Shpilevoy PRO
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.
Шпилевой Владислав
Горизонтальное
масштабирование
Принятие решений
Ping
Ping
Ping
Ping
Ping
Вывод нерабочих узлов из кластера
Починка отказов
Ping – проверять лидера/мастера/...
Heartbeat – узел сам делает рассылку
узлов
сообщений в сети
"Все со всеми" не работает
узлов
сообщений в сети
Протоколы "слухов", "инфекций"
!
!
!
!
!
!
!
!
Распространяют событие все, а не один узел
Алгоритмы рандомизированы:
Сеть сломана
Ping
Ack
PingRequest
Ping
Ack
Ack
Сеть сломана
... или Я?
Жив
Мертв
VS
Критерии:
...
Событие
!
!
!
!
Запрос
!
Событие
!
!
!
!
!
!
!
Распространение:
Обнаружение:
Нагрузка на сеть:
Нагрузка на узел:
Lifeguard
Scalable
!
Weakly-Consistent
!
!
!
!
!
!
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
Ping Request
Ack
Ack
Подозрение отказа
!
Ping
События
UDP-пакет
T
T
Ping +
!
!
!
!
!
Каждое событие посылается раз
Подозрение не подтвердилось
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
UUID
URI1
URI2
URI3
Адрес можно менять
Heartbeat Rate
Модуль
Можно создавать много узлов
Конфигурируется почти все
Есть ручное управление таблицей узлов кластера
Можно обращаться к узлам
Асинхронная подписка на события
Аккуратный вывод из кластера с уведомлением всех
Случайное перемешивание
swim = require('swim')
s = swim.new([cfg])
s:quit()
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])
На двух Тарантулах запущены два 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()
Ping + UUID + IP:Port
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 узнают друг про друга – антиэнтропия шлется всегда
Как, подключившись к узлу, понять, куда подключаться по 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()
Плохой способ – удалить узел
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()
Быстро, можно реагировать не как на смерть
Инкарнации не всегда достаточно
A
B
A 1
Payload:
Два узла. B видит инкарнацию A = 1 и пейлоад "синий кружок".
A 1
Payload:
A 1
Payload:
A перезапущен, инкарнация снова 1, пейлоад другой.
Хей, у меня новый пейлоад с инкарнацией 1!
A1 vs A1? – Ничего нового. Пейлоад не изменился.
A сообщает B, что у него новый пейлоад, но инкарнация та же – B еще помнит A до перезапуска.
А выключен.
!
swim = require('swim') s = swim.new({generation = 1})
swim = require('swim') s = swim.new() saved_e = nil s:on_member_event(function(m, e) saved_e = e end) s:cfg({uri = 3334, uuid = uuid2})
s:probe_member(3333)
s:cfg({uri = 3333, uuid = uuid1})
Перезапуск
swim = require('swim') s = swim.new({generation = 2})
s:cfg({uri = 3333, uuid = uuid1})
tarantool> saved_e --- - is_new_version: true is_new_generation: true is_update: true is_new_incarnation: true ...
В Тарантуле инкарнация состоит из двух частей, одна из которых возрастает всегда
При создании можно указать generation – это число будет частью инкарнации. По умолчанию – текущее время
Указать надо до или во время первой конфигурации
Запускается второй узел. Они связываются.
Первый перезапускается
После старта указывается новый generation
Второй сразу об этом узнает, через событие
Дата-центр
Дата-центр
Задача:
Создать 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:payload().box)
end
box.cfg{
listen = box_uri,
replication = rep,
instance_uuid = uuid,
}
>
>
Один SWIM на узел
>
>
Таблица с узлами дата-центра
>
Обновляется автоматически
>
>
Рассказать всем свои ДЦ и порт
>
Периодически искать новые узлы
>
Для сборки репликасета надо найти 2 узла
>
Сборка и запуск репликации готовы!
Среди нескольких Тарантулов выбрать одного для принятия решений о конфигурации, о смене мастера
Находить отказавшие узлы, уведомлять администратора, перенаправлять нагрузку, начинать смену мастера
При рассылке сообщений раз в секунду
Размер кластера
Время распространения события, секунды
800
25
200
50
100
150
300
250
500
5
4
6
7
8
– реализация SWIM
– настоящий
SWIM
By Vladislav Shpilevoy
SWIM - это протокол обнаружения и мониторинга узлов кластера, распространения событий и данных между ними. Протокол особенен своей легковесностью, децентрализованностью и независимостью скорости работы от размера кластера. В докладе рассказывается о том, как устроен протокол SWIM, как и с какими расширениями он реализован в Тарантуле.
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.