Привет, Хабр! Меня зовут Сергей Скубач, я работаю в VK и разрабатываю облачное хранилище WorkDisk. Мы используем Tarantool в своём продукте, так как его разрабатывают и развивают наши коллеги. Я впервые столкнулся с Tarantool именно в VK, и для более глубокого понимания решил попробовать использовать его в своём pet-проекте.

Постараюсь рассказать о сложностях, с которыми столкнулся при внедрении Tarantool-а с позиции разработчика, впервые работающего с этой базой данных.

Tarantool — это решение класса middleware для ускорения ИТ-систем и обработки данных. Оно включает в себя in‑memory базу данных, которая умеет хранить реляционные данные, и сервер приложений на Lua. Имеет готовые коннекторы для популярных языков (Python, PHP, Go, C++).

Для начала разберёмся с постановкой задачи pet-проекта. Идея заключается в том, чтобы получать через Telegram-бота уведомления об актуальных объявлениях с сайтов, с небольшой задержкой после их публикации. Заранее можно спрогнозировать, что количество таких популярных сайтов изначально будет небольшим, но по мере роста проекта будут добавляться новые профили. Поэтому необходима архитектура, которая позволит масштабироваться по мере роста объёмов данных. Забегая вперёд, отмечу, что Tarantool поддерживает шардирование и репликацию из коробки.

Архитектура

Сервисы напрямую друг с другом не обмениваются данными, что позволяет снизить зависимости между ними. Основным узлом обмена является Tarantool. Очередь обработки данных и событийная модель построены через Tarantool, поэтому ниже разберём эти части подробнее.

Код микросервисов проекта написан на Go и Lua, его можно посмотреть в репозиториях.

Инициализация

Tarantool как платформа включает в себя сервер приложений на языке Lua. Для запуска базы данных нужно создать файл инициализации, в котором необходимо описать конфигурацию.

И здесь сталкиваемся с первой сложностью — отсутствием готовых шаблонов файла инициализации для запуска Tarantool-а в режиме репликации либо шардирования. Вместо этого есть достаточно подробная инструкция с описанием каждого параметра конфигурации, которая в какой-то степени позволяет написать такой файл вручную.

Можно посмотреть пример файла init.lua, который у меня получился для инициализации модели взаимодействия «мастер‑реплика», где мастер работает в режиме read/write, а реплика в read only. Для запуска в Dockerfile указываем официальный образ tarantool/tarantool. Команда для запуска должна быть:

CMD ["tarantool", "/usr/share/tarantool/ad/init.lua"]

Иначе можно столкнуться с проблемами применения переменных окружения. Для запуска в docker-compose указываем переменные окружения отдельно для мастера и реплики.

LuaRocks — менеджер пакетов для модулей Lua

Lua — скриптовый язык программирования, чем-то похожий на JavaScript. Для поддержания модульности используется менеджер пакетов LuaRocks. Те, кто никогда не работал с языком Lua, скорее всего столкнутся с необходимостью разобраться в подключении rocks-модулей. Есть отдельные серверы, на которых хранятся эти модули, и необходимо указать адреса этих серверов, чтобы менеджер пакетов смог скачать зависимости. При этом у Tarantool-а есть собственный сервер с модулями для работы с сетью, JSON, метриками и т. п.

Вот примерный список серверов, модули которых можно использовать для разработки:

http://moonlibs.github.io/rocks
http://rocks.tarantool.org
http://luarocks.org/repositories/rocks
https://rocks.moonscript.org

Затем в файле .rockspec указываем необходимые для нас модули, и менеджер пакетов пройдёт по серверам и скачает те, которые сможет найти в списке зависимостей. Для запуска менеджера пакетов используем команду:

tarantoolctl rocks install --tree=./.rocks --only-deps .rocks/ad-tnt-scm-3.rockspec

Миграции

Поскольку мы будем работать с базой данных, необходимо с самого начала продумать механизм миграций. Для этого есть готовый модуль spacer, который позволяет накатывать и откатывать ревизии миграций. При инициализации необходимо указать папку с файлами миграций, после чего можно вызывать методы для их применения:

box.spacer = require 'spacer'.new {
    migrations = fio.pathjoin(app_root, 'migrations'),
}
...
box.spacer:migrate_up()

Пример файла миграции можно посмотреть здесь, в нём нужно указать создание таблицы и форматирование полей, описать индексы либо заполнить таблицу демо данными.

В Tarantool-е есть свои наименования сущностей: space — таблица, tuple — запись в таблице (кортеж); но мы будем называть всё привычными именами.

Для работы со spacer также необходимо описать модель данных, пример можно посмотреть в файле model.lua. Подробнее с возможностями модуля миграций можно ознакомиться в readme репозитория tarantool-spacer.

Очередь

Для формирования очереди существуют различные реализации модулей, в том числе и официальный tarantool/queue. Но я решил попробовать стороннее решение moonlibs/xqueue для обработки очереди, возникающей при добавлении в таблицу новых объявлений от сервиса parser. Преимущество решения в том, что очередь можно сделать из любой существующей таблицы, достаточно лишь добавить поле для хранения статуса.

При подключении модуля необходимо указать название таблицы, пометить, какое поле является статусом, и указать настройки формирования очереди:

local xq = require('xqueue')
xq(
      box.space.ad,
      {
          fields = {
              status = 'nq_status',
          },
          features = {
              id = function()
                  return box.sequence.ad_seq:next()
              end,
              buried = true,
              delayed = false,
              keep = true,
              ttl = false,
              ttr = false,
          },
      }
)

Далее можно использовать привычные для работы с очередью команды: put, take, ack, release и bury. Важно знать, что при работе с очередью нельзя использовать insert вместо put, поскольку статус в поле тогда не будет проставлен автоматически.

Событийная модель watch / broadcast

Как было сказано выше, низкая зависимость между сервисами достигается с помощью архитектуры обмена данными через Tarantool. Это возможно благодаря событийной модели, которую поддерживает Tarantool через функции box.broadcast и box.watch. Подробнее можно почитать в документации.

Сервис parser, который складывает данные в очередь, отправляет broadcast-запрос с определенным названием события event_new_ad в Tarantool.

ad.conn.Do(tarantool.NewBroadcastRequest("event_new_ad").Value(true), pool.RO)

С другой стороны, сервис notifications подписывается на получение всех запросов с таким же названием события.

ad.conn.NewWatcher("event_new_ad", callback, pool.RO)

В результате при каждом добавлении новой записи в таблицу из сервиса parser будет вызываться callback сервиса notifications (там обрабатывается очередь) с отправкой уведомления в Telegram-бот.

Важно, что для возможности работы с watch / broadcast при подключении к пулу инстансов Tarantool-а необходимо добавить протокол WatchersFeature:

// init tarantool
conn, err := pool.Connect(cfg.Tarantool.Servers, tarantool.Opts{
    ...
    RequiredProtocolInfo: tarantool.ProtocolInfo{
        Features: []tarantool.ProtocolFeature{tarantool.WatchersFeature},
    },
})

SQL-запросы

Tarantool поддерживает выполнение SQL-запросов, в том числе и объединение результатов таблиц через JOIN. Но такие конструкции, как правило, работают через полное сканирование таблиц (full scan), и при большом количестве данных их лучше не использовать.

Для примера в проекте SQL-запрос используется для получения статистики и передачи результатов в метрику Prometheus:

var usersCountQuery = fmt.Sprint(
		`SELECT COUNT(DISTINCT "tg_id") as "users_count", COUNT("id") as "sub_count" FROM "subscription"`)
var usersStat []*model.UsersStat
req := tarantool.NewExecuteRequest(usersCountQuery).Context(ctx)
err := ad.conn.Do(req, pool.PreferRO).GetTyped(&usersStat)

В заключение

Реализация pet-проектов часто помогает лучше понять продукт, так как приходится проходить весь путь от инициализации до развёртывания на сервере. В результате внедрения Tarantool-а в свой проект я заметил ускорение работы с данными (благодаря хранению в оперативной памяти), но в то же время требуются определённые усилия для интеграции этого инструмента.

Буду рад, если поделитесь со мной своими соображениями по проекту, в том числе об использовании и интеграции Tarantool-а.

Комментарии (0)