В качестве примера мастер-мастер кластера Tarantool я предлагаю сделать небольшую текстовую мультиплеер-игру, где каждый участник стремится набрать большее число очков. Каждый игрок будет некоторым узлом, который меняет данные в игровом мире. Эти данные реплицируются между узлами. Таким образом, репликация Tarantool будет являться своего рода транспортом для игрового процесса. Но что будет, если два игрока одновременно создадут или поменяют какой-то объект в мире и создадут соответствующие транзакции? Если этого не предусмотреть, то, или данные на разных узлах «разъедутся», и у каждого игрока сложится своя картина мира, или репликация «сломается», и, как следствие, игровой процесс остановится. Есть разные способы решения таких конфликтов. Я выбрал схему данных и распределил операции над данными по узлам так, чтобы конфликтов в кластере не возникало. Чуть позже я объясню это подробнее. Игра будет с ascii-графикой, и такое отображение репликации позволяет сразу видеть картину происходящего на каждом инстансе, не требуя дополнительных запросов к данным. Кроме этого, перезапуская узлы, можно будет визуально проследить процесс запуска базы, загрузки данных, подключения репликации. Игра чем-то похожа на bomberman. Игровое поле 80x40. Каждый игрок управляет своим персонажем. Игроки должны собирать фрукты, которые добавляют жизней. Порции жизней можно потратить на создание бомб. Бомбы взрываются и небольшой волной забирают жизни тех, кто оказался рядом. Установить Tarantool 2-ой версии по инструкции. Взять исходники игры: Если что-то случилось во время запуска, я подготовил небольшой список ситуаций и решений.
Эти рекомендации применимы только к этой игре, в случае проблемных ситуаций на проде, я, конечно же, рекомендую более детальное исследование ситуации.  Если у вас ошибка не такая как из списка, или вы делаете что-то ещё и возникают вопросы, то у нас есть русскоязычный чат в телеграме. Все объекты игры будут содержаться в одном спейсе (таблице), который будет реплицироваться между узлами. Вот как она будет выглядеть: Первичным ключом будет является поле ID. Для каждого объекта в том числе персонажей это поле будет уникальным. Icon содержит текстовый спрайт объекта. X, Y содержит текущие координаты объекта. Type тип объекта: Health жизни объекта: Все действия над объектами будут производится с помощью обновления соответствующих полей. Индексов на спейсе будет несколько: Конфликт транзакций возникает в случае, когда два узла тарантула вставляют новые данные по одному и тому же уникальному ключу. В этому случае репликация останавливается. Для таких случаев тарантул позволяет написать некоторую логику, которая в случае конфликтов будет выбирать из двух транзакций одну, а другую отбрасывать. Но в рамках моей задачи, мне показалось это избыточным, и я распределил создание объектов так, что любой узел создавая игровые объекты генерировал для них уникальный для всего кластера ключ. Я воспользовался генерацией uuid. Теперь представим, что свойство одного игрового объекта меняется на двух узлах одновременно. Первый узел назначит свойство в значение X, второй узел — в значение Y. Во время репликации первый узел получит транзакцию со значением Y и применит её у себя, а второй узел — со значением X, и тоже применит её у себя. В результате данные «разъедутся». Чтобы такого не происходило, я воспользовался аддитивными операциями. В этом случае, в какой бы последовательности не применялись транзакции, результат окажется одинаковым. Например: Или, другими словами, операция присваивания значения неаддитивна, а операции сложения и вычитания аддитивны.  В топологии игры будет один узел-координатор, ответственный за геймплей, и некоторое количество узлов игроков. Максимальное количество активных реплик может достигать 32, это ограничение репликации Tarantool. Узлы, которые работают только на чтение, называются анонимными репликами, и их может быть сколько угодно. Репликасет в Tarantool — это группа серверов, которые реплицируют данные между собой. У каждого узла есть свой уникальный идентификатор instance uuid. И одновременно с этим у узлов репликасета есть одинаковое для всех поле replicaset uuid. Чтобы все эти идентификаторы узлов правильно сошлись, создавать репликасет лучше последовательно. Я предлагаю сначала запустить координатор, который выполнит все первоначальные настройки, и затем к нему подключать игроков. Их можно будет подключать как одновременно, так и последовательно. Вот как это будет выглядеть:     Tarantool, с одной стороны, это база данных с возможностью репликации, а с другой — полноценный сервер приложений. Для создания приложений в Tarantool используется язык Lua с JIT-компиляцией. Сама база данных также конфигурируется с помощью Lua. Основное конфигурирование базы данных происходит с помощью функции box.cfg. На координаторе функция должна будет сделать первоначальную настройку репликасета. Для настройки репликации используются параметры: В случае координатора я точно знаю, что кворум не нужен, так как это самые первый инстанс, который логически не нуждается в остальных. Соответственно, параметры примут значения: Конфигурирование игрока заключается в том, чтобы прежде всего подключиться к координатору с репликацией, а затем настроить обратную репликацию с игрока на координатор. Координатор, с одной стороны, знает о том, кто к нему подключен по репликации, но, с другой стороны, не знает, как ему самому подключиться к новому игроку. Я решил это следующим образом: Координатор создает функцию add_player. Игрок удаленно вызывает эту функцию на координаторе со своим адресом. В случае перезагрузки координатора игрок перенастраивает репликацию, когда тот вернется. Функция на координаторе выглядит так: Игрок сохраняет соединение в глобальном неймспейсе, чтобы его не остановил сборщик мусора. Схема данных задается на координаторе сразу после конфигурирования базы данных. Она может создаваться только на одном узле, остальные получают её по репликации. В процессе разработки я постоянно перезапускал и дорабатывал детали на уже инициализированной базе. Чтобы повторное применение схемы данных не вызывало ошибки, я пользовался флагом if_not_exists=true. Он позволяет игнорировать DDL-команды, когда спейсы, индексы и другие объекты уже существуют. Краткий обзор DDL-операций, которые я использую: Часть логики приложения может быть запущена до инициализации базы данных. В этом случае я использую цикл, ожидающий появления таблицы в БД. Триггеры в Tarantool являются частью сервера приложений и не сохраняются в базе данных. Чтобы создать триггер, я: Инициализация базы данных — процесс из нескольких стадий, поэтому установка триггера, на первый взгляд, может показаться сложной. На второй взгляд — скорее всего, тоже :)  Итак, чтобы установить триггер в спейс, предлагается такая схема: Часть логики запускается на координаторе, часть — на узлах игроков. Запуск логики происходит либо в отдельном файбере, либо из триггера. Файберы — легковесные потоки исполнения (сопрограмма, зеленый тред, корутина, горутина). Они используют кооперативную многозадачность. То есть, в один момент времени запущен только один файбер. Когда он выполнил свою логику, то должен явно отдать управление через fiber.sleep(N) или fiber.yield(), либо вызвав некоторую io-операцию. Вся логика выполняется с помощью изменения данных в спейсе. Узел игрока при первом запуске создаёт своего персонажа. ID персонажа применяется из значения instance uuid. Далее узел игрока слушает события клавиатуры и меняет позицию персонажа. Чтобы события от клавиатуры приходили как есть, я использую функции tcgetattr и tcsetattr через LuaJIT FFI. Для создания транзакции с несколькими действиями я пользуюсь паттерном. Для обработки событий клавиатуры запущен отдельный файбер. Рендерер отображает любые графически значимые изменения в текстовую консоль. Он запускается из триггера как на узлах игроков, так и на координаторе. На узлах игроков рендерер также отображает информацию о жизнях. Генератор продуктов запускается на координаторе и раз в N секунд создает объект. Генератор продуктов работает в отдельном файбере. Чтобы быстро увидеть, идет ли репликация от координатора к игроку, необходим цикл, который создает анимацию поезда и бесконечного индикатора прогресса. Цикл запускается на координаторе в отдельном файбере. Обработка столкновений состоит из двух частей: «детектора» и «обработчика». «Детектор» запускается из триггера в случае, когда произошли значимые для этого изменения. Например, позиция игрока поменялась, сгенерировался новый фрукт и т.п.  «Детектор» через межфайберный канал отправляет «обработчику» информацию о столкнувшихся объектах. «Обработчик» запускается на координаторе в отдельном файбере, в цикле читает сообщения из канала, и, в зависмости от столкновений объектов, меняет значения в полях с жизнями. На координаторе запущен файбер, который раз в секунду прокручивает жизненный цикл бомбы. Этот файбер таким же образом следит за ударной волной. Чтобы игровой процесс чуть больше мотивировал двигаться, на координаторе запущен файбер, который сдувает всех игроков в правый нижний угол. Я предлагаю самый простой путь для создания таблицы игроков, а именно — сделать анонимную реплику. Она будет подключена к координатору и станет в триггере отображать список игроков с сортировкой начиная с лидеров. Для подключения анонимной реплики предназначен параметр replication_anon. Вот так с помощью нехитрых приспособлений буханку хлеба можно превратить в троллейбус Таким приложением я хочу: Если вам хотелось бы рассмотреть более практичное приложение на Tarantool, то есть отличная статья от codesign про создание очереди.