Решение конфликтов репликации
Tarantool гарантирует, что каждое обновление применяется на каждой реплике только один раз. Однако из-за асинхронной природы репликации порядок обновлений не гарантируется. В этом разделе описывается решение проблем при репликации типа мастер-мастер.
Случай 1: Есть два экземпляра Tarantool. Например, выполняется операция replace с одним и тем же первичным ключом на обоих
экземплярах одновременно. Это вызывает конфликт: необходимо решить, какой кортеж сохранить, а какой отбросить.
Триггер-функции Tarantool могут помочь в реализации правил разрешения конфликтов при определенных условиях. Например, если у вас есть метка времени, то можно указать, что сохранять нужно кортеж с большей меткой.
Сначала нужен триггер 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 thenbox.on_commit(function()box.space[my_space_name]:before_replace(my_trigger)endendend)end)
Случай 2: В наборе реплик из двух мастеров оба пытаются вставить данные с одним и тем же уникальным ключом:
tarantool> box.space.tester:insert{1, 'data'}
Это вызовет сообщение об ошибке дубликата ключа (Duplicate key exists in unique index 'primary' in space 'tester'), и
репликация остановится. Такое поведение системы обеспечивается использованием рекомендуемого значения false (по умолчанию) для
конфигурационного параметра
replication_skip_conflict.
$ # error messages from master #12017-06-26 21:17:03.233 [30444] main/104/applier/rep_user@100.96.166.1 I> can't read row2017-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, exiting2017-06-26 21:17:03.233 [30444] relay/[::ffff:100.96.166.178]/101/main C> exiting the relay loop$ # error messages from master #22017-06-26 21:17:03.233 [30445] main/104/applier/rep_user@100.96.166.1 I> can't read row2017-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, exiting2017-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-g980d30092id: 3ro: falsevclock: {1: 9, 2: 1000000, 3: 3}uptime: 557lsn: 3vinyl:cluster:uuid: 34d13b1a-f851-45bb-8f57-57489d3b3c8bpid: 30445status: runningsignature: 1000012replication:1:id: 1uuid: 7ab6dee7-dc0f-4477-af2b-0e63452573cflsn: 9upstream:peer: replicator@192.168.0.101:3301lag: 0.00050592422485352status: stoppedidle: 445.8626639843message: Duplicate key exists in unique index 'primary' in space 'tester'2:id: 2uuid: 9afbe2d9-db84-4d05-9a7b-e0cbbf861e28lsn: 1000000upstream:status: followidle: 201.99915885925peer: replicator@192.168.0.102:3301lag: 0.0015020370483398downstream:vclock: {1: 8, 2: 1000000, 3: 3}3:id: 3uuid: e826a667-eed7-48d5-a290-64299b159571lsn: 3uuid: e826a667-eed7-48d5-a290-64299b159571...
О том, как разрешить конфликт репликации путем пересоздания реплики, см. в разделе Разрешение конфликтов репликации.
Пример описывает выполнение следующей операции в кластере из двух экземпляров с конфигурацией мастер-мастер:
tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
Когда эта операция применяется на обоих экземплярах в наборе реплик:
# на мастере #1tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})# на мастере #2tarantool> box.space.tester:upsert({1}, {{'=', 2, box.info.uuid}})
... можно получить следующие результаты в зависимости от порядка выполнения:
- В строке каждого мастера содержится UUID мастера №1.
- В строке каждого мастера содержится UUID мастера №2.
- На мастере №1 содержится UUID мастера №2, и наоборот.
Случаи, описанные в предыдущих раздела - примеры некоммутативных операций, то есть операций, результат которых зависит от порядка выполнения. Напротив, для коммутативных операций порядок выполнения не имеет значения.
Эта команда представляет собой коммутативную операцию:
tarantool> box.space.tester:upsert{{1, 0}, {{'+', 2, 1)}
Ее выполнение приводит к одинаковому результату, независимо от порядка, в котором обновление применяется на других мастерах.
Логика и фрагмент кода для установки триггера здесь будут теми же, что и в случае 1. Однако функция триггера будет отличаться. Обратите внимание, что приведенный ниже триггер предполагает наличие метки времени во втором поле кортежа.
local my_space_name = 'test'local my_trigger = function(old, new, sp, op)-- op: ‘INSERT’, ‘DELETE’, ‘UPDATE’, or ‘REPLACE’if new == nil thenprint("No new during "..op, old)return -- deletes are okendif old == nil thenprint("Insert new, no old", new)return new -- insert without old value: okendprint(op.." duplicate", old, new)if op == 'INSERT' thenif new[2] > old[2] then-- Creating new tuple will change op to ‘REPLACE’return box.tuple.new(new)endreturn oldendif new[2] > old[2] thenreturn newelsereturn oldendreturnendbox.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 thenbox.on_commit(function()box.space[my_space_name]:before_replace(my_trigger)end)endend)end)