Замечание
Документация находится в процессе перевода и может отставать от английской версии.
Решение конфликтов репликации
Tarantool гарантирует, что все обновления применяются однократно на каждой реплике. Однако, поскольку репликация носит асинхронный характер, порядок обновлений не гарантируется. Сейчас мы проанализируем данную проблему более подробно с примерами рассинхронизации репликации и предложим соответствующие решения.
Кейс 1: у вас есть два экземпляра Тарантула. Например, вы пытаетесь сделать операцию замены одного и того же первичного ключа на обоих экземплярах одновременно. Случится конфликт из-за того, какой кортеж сохранить, а какой отбросить.
Триггер-функции Тарантула могут помочь в реализации правил разрешения конфликтов при определенных условиях. Например, если у вас есть метка времени, то можно указать, что сохранять нужно кортеж с большей меткой.
Во-первых, вам нужно повесить триггер before_replace() на спейс, в котором могут быть конфликты. В этом триггере вы можете сравнить старую и новую записи реплики и выбрать, какую из них использовать (или полностью пропустить обновление, или объединить две записи вместе).
Затем вам нужно установить триггер в нужное время, прежде чем спейс начнет получать обновления. Триггер before_replace
нужно устанавливать в тот момент, когда спейс создается, поэтому еще нужен триггер, чтобы установить другой триггер на системном спейсе _space
, чтобы поймать момент, когда ваш спейс создается, и установить триггер там. Для этого подходит триггер on_replace().
Разница между before_replace
и on_replace
заключается в том, что on_replace
вызывается после вставки строки в спейс, а before_replace
вызывается перед ней.
Устанавливать триггер _space:on_replace()
также нужно в определенный момент. Лучшее время для его использования – это когда только что создан _space
, что является триггером на box.ctl.on_schema_init().
Вам также нужно использовать box.on_commit
, чтобы получить доступ к создаваемому спейсу. В результате код будет выглядеть следующим образом:
local my_space_name = 'my_space'
local my_trigger = function(old, new) ... end -- ваша функция, устраняющая конфликт
box.ctl.on_schema_init(function()
box.space._space:on_replace(function(old_space, new_space)
if not old_space and new_space and new_space.name == my_space_name then
box.on_commit(function()
box.space[my_space_name]:before_replace(my_trigger)
end
end
end)
end)
Кейс 2: Предположим, что в наборе реплик с двумя мастерами мастер №1 пытается вставить кортеж с одинаковым уникальным ключом:
tarantool> box.space.tester:insert{1, 'data'}
Это вызовет сообщение об ошибке дубликата ключа (Duplicate key exists in unique index 'primary' in space 'tester'
), и репликация остановится. Такое поведение системы обеспечивается использованием рекомендуемого значения false
(по умолчанию) для конфигурационного параметра replication_skip_conflict.
$ # сообщения об ошибках от мастера №1
2017-06-26 21:17:03.233 [30444] main/104/applier/rep_user@100.96.166.1 I> can't read row
2017-06-26 21:17:03.233 [30444] main/104/applier/rep_user@100.96.166.1 memtx_hash.cc:226 E> ER_TUPLE_FOUND:
Duplicate key exists in unique index 'primary' in space 'tester'
2017-06-26 21:17:03.233 [30444] relay/[::ffff:100.96.166.178]/101/main I> the replica has closed its socket, exiting
2017-06-26 21:17:03.233 [30444] relay/[::ffff:100.96.166.178]/101/main C> exiting the relay loop
$ # сообщения об ошибках от мастера №2
2017-06-26 21:17:03.233 [30445] main/104/applier/rep_user@100.96.166.1 I> can't read row
2017-06-26 21:17:03.233 [30445] main/104/applier/rep_user@100.96.166.1 memtx_hash.cc:226 E> ER_TUPLE_FOUND:
Duplicate key exists in unique index 'primary' in space 'tester'
2017-06-26 21:17:03.234 [30445] relay/[::ffff:100.96.166.178]/101/main I> the replica has closed its socket, exiting
2017-06-26 21:17:03.234 [30445] relay/[::ffff:100.96.166.178]/101/main C> exiting the relay loop
Если мы проверим статус репликации с помощью box.info
, то увидим, что репликация на мастере №1 остановлена (1.upstream.status = stopped
). Кроме того, данные с этого мастера не реплицируются (группа 1.downstream
отсутствует в отчете), поскольку встречается та же ошибка:
# статусы репликации (отчет от мастера №3)
tarantool> box.info
---
- version: 1.7.4-52-g980d30092
id: 3
ro: false
vclock: {1: 9, 2: 1000000, 3: 3}
uptime: 557
lsn: 3
vinyl: []
cluster:
uuid: 34d13b1a-f851-45bb-8f57-57489d3b3c8b
pid: 30445
status: running
signature: 1000012
replication:
1:
id: 1
uuid: 7ab6dee7-dc0f-4477-af2b-0e63452573cf
lsn: 9
upstream:
peer: replicator@192.168.0.101:3301
lag: 0.00050592422485352
status: stopped
idle: 445.8626639843
message: Duplicate key exists in unique index 'primary' in space 'tester'
2:
id: 2
uuid: 9afbe2d9-db84-4d05-9a7b-e0cbbf861e28
lsn: 1000000
upstream:
status: follow
idle: 201.99915885925
peer: replicator@192.168.0.102:3301
lag: 0.0015020370483398
downstream:
vclock: {1: 8, 2: 1000000, 3: 3}
3:
id: 3
uuid: e826a667-eed7-48d5-a290-64299b159571
lsn: 3
uuid: e826a667-eed7-48d5-a290-64299b159571
...
Когда позднее репликация возобновлена вручную:
# возобновление остановленной репликации (на всех мастерах)
tarantool> original_value = box.cfg.replication
tarantool> box.cfg{replication={}}
tarantool> box.cfg{replication=original_value}
… запись с ошибкой в журнале упреждающей записи пропущена.
Решение #1: рассинхронизация репликации
Предположим, что мы выполняем следующую операцию в кластере из двух экземпляров с конфигурацией мастер-мастер:
tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
Когда эта операция применяется на обоих экземплярах в наборе реплик:
# на мастере #1
tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
# на мастере #2
tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
… можно получить следующие результаты в зависимости от порядка выполнения:
- каждая строка мастера содержит UUID из мастера №1,
- каждая строка мастера содержит UUID из мастера №2,
- у мастера №1 UUID мастера №2, и наоборот.
Решение #2: коммутативные изменения
Случаи, описанные в предыдущих абзацах, представляют собой примеры некоммутативных операций, т.е. операций, результат которых зависит от порядка их выполнения. Для коммутативных операций порядок выполнения значения не имеет.
Рассмотрим, например, следующую команду:
tarantool> box.space.tester:upsert{{1, 0}, {{'+', 2, 1)}
Эта операция коммутативна: получаем одинаковый результат, независимо от порядка, в котором обновление применяется на других мастерах.
Решение #3: использование триггера
Логика и установка триггера будет такой же, как в кейсе 1. Но сама триггер-функция будет отличаться:
local my_space_name = 'test'
local my_trigger = function(old, new, sp, op)
-- op: ‘INSERT’, ‘DELETE’, ‘UPDATE’, or ‘REPLACE’
if new == nil then
print("No new during "..op, old)
return -- удаление допустимо
end
if old == nil then
print("Insert new, no old", new)
return new -- вставка без старого значения допустима
end
print(op.." duplicate", old, new)
if op == 'INSERT' then
if new[2] > old[2] then
-- Создание нового кортежа сменит оператор на REPLACE
return box.tuple.new(new)
end
return old
end
if new[2] > old[2] then
return new
else
return old
end
return
end
box.ctl.on_schema_init(function()
box.space._space:on_replace(function(old_space, new_space)
if not old_space and new_space and new_space.name == my_space_name then
box.on_commit(function()
box.space[my_space_name]:before_replace(my_trigger)
end)
end
end)
end)