Привет, Хабр! Меня зовут Виктор Лучиц, я руководитель группы backend-разработки в департаменте рекламных технологий VK. В этой статье я расскажу вам про Tarantella — наше key-value хранилище, которое мы используем в рекламных технологиях. 

Из материала вы узнаете о том, как устроен этот «секретный ингредиент», без которого наша реклама не была бы такой эффективной.

Что такое Tarantella 

В рекламном департаменте хранятся огромные объёмы данных, которые мы используем для подбора релевантной рекламы при каждом показе баннера. В далёкие времена, когда рекламная система ещё была недостаточно зрелой, мы предпочитали простой подход для хранения и обработки этих данных: если появлялся новый признак или слой данных, то мы заводили для него отдельную инсталляцию Tarantool — в основном версии 1.5. На тот момент мы уже были самыми крупными пользователями этого продукта в тогда ещё Mail.ru Group. Однако у такого подхода со временем стали всё больше проявляться недостатки, ограничивающие рост. Среди них:

  • неравномерность заполнения инсталляций Tarantool;

  • вынужденный простой при изменении их размеров;

  • ручной перенос данных между хостами;

  • большой объём ручного труда и связанные с человеческим фактором риски.

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

Первоначальные требования

  • Нам хотелось минимизировать ручной труд и упростить работу с хранилищами. 

  • Мы не собирались отказываться от высокой производительности Tarantool. 

  • Кроме упомянутых выше репликации, шардирования и балансировки нам нужна была поддержка протокола memcache, чтобы сохранить обратную совместимость с уже существовавшим инструментом сопоставления идентификаторов. 

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

  • Всё это должно быть in-memory, потому что брать данные с дисков при жёстких ограничениях на время запроса в 2016 году было слишком медленно. 

Новую систему мы начали делать на основе Tarantool 1.6 (сейчас используем актуальный релиз 2.11) по следующим причинам:

  • Соблюдались требования по хранению в памяти и высокой производительности.

  • Не требовалось дорабатывать клиенты. Они уже умели работать со snapshot Tarantool, поэтому легко было адаптировать их для 1.6.

  • Если чего-то не хватало в Tarantool, мы могли обратиться к коллегам.

  • Можно работать с данными офлайн.

  • Можно реализовать на Lua недостающие фичи, например, подсчёт статистики, фоновое удаление, отслеживание состояния кластера, разграничение прав доступа.

Tarantella работает в качестве прослойки между клиентами (nginx и нашими внутренними продуктами) и узлами хранения. В качестве языка разработки мы выбрали Go, а в серверной части, работающей на узлах Tarantool, встречается также Lua и С. Система скрывает от клиентов подробности работы кластера, поддерживает актуальность топологии, реализует операции с данными. Она может работать как сетевой сервис, или её можно встраивать как Go-модуль в своё приложение. Также Tarantella контролирует доступ к ключам в хранилище.

Схема взаимодействия с Tarantella

Компоненты кластера Tarantella
Компоненты кластера Tarantella

Рассмотрим кратко компоненты кластера, показанных на схеме:

  • клиент — запрашивает данные по сети по поддерживаемым Tarantella протоколам (memcache, iproto, HTTP) или через встраивание;

  • Tarantella — middleware для прозрачной работы с кластером;

  • master Tarantool — хранилище топологии кластера;

  • data Tarantool (node) — инсталляция Tarantool с данными в формате ключ-значение;

  • Compositor — автомат для поддержания работоспособности кластера по заданным в конфигурации параметрам;

  • Choreograph — монитор состояния кластера и сборщик графиков.

Хранение данных

Мы решили разбивать данные на сегменты (бакеты) по хешу первичного ключа. Их должно быть столько, чтобы мы могли равномерно распределить их по кластеру и равномерно распределить данные по сегментам, но не слишком большим, иначе их обслуживание может оказаться очень дорогим. В итоге остановились на количестве сегментов в кластере — 64×1024. При этом каждый из них мы дублируем с учётом репликации. Целостность кластера поддерживается на уровне сегмента. 

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

Compositor

Ближайший аналог Compositor’a — Apache Helix. Во-первых, этот компонент работает по принципу конечного автомата, отслеживает состояние кластера по определённым правилам и меняет топологию, если она не соответствует нужным критериям:

  • следит за фактором репликации для сегментов;

  • ребалансирует данные между заполненными и свободными узлами;

  • обеспечивает доступность в пределах ЦОДов.

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

Choreograph

Это ещё один компонент для отслеживания «здоровья» кластера. Он брат-близнец Compositor’а, в том смысле, что они оба собираются из одинакового кода с той лишь разницей, что Choreograph — конечный автомат, работающий в «холостом» режиме. Он не меняет состояние кластера или данные, а лишь прогнозирует количество шагов для достижения «идеального» состояния кластера и тоже сигнализирует нам о возникающих проблемах.

Состояние узлов в топологии

При запуске узлы самостоятельно сообщают о себе в Master Tarantool и начинают регулярно отправлять ему свой heartbeat. Для хранения карт у нас есть Tarantool space с перечислением экземпляров Tarantool (далее просто «узлы кластера» или просто «узлы»). Узлы идентифицируются по своим UUID, который Tarantool сам генерирует при инициализации. 

В ещё одном space’е хранятся пары «сегмент-узел». Сегмент — это остаток от деления значения хеша от ключа на количество сегментов, и к нему может быть привязано от 1 до N (в зависимости от требуемой репликации) UUID узла. У каждой пары сегмент -UUID могут быть разные состояния:

  • New — узел только что добавили. Клиенты могут в этот сегмент писать данные, но не читать, потому что он ещё не синхронизирован с остальными внутри сегмента.

  • Online — «хороший» узел, можно читать и писать данные. 

  • Remove — узел необходимо «отвязать» от сегмента, вручную или по инициативе Compositor’а, если он посчитал, что узел требует замены. В этом состоянии данные можно читать, но не писать. От Compositor'а требуется скопировать данные на другие узлы и удалить текущий.

Условная схема хранения

В Master Tarantool у нас есть space с узлами и есть space с состояниями пар «сегмент-UUID». А в Data Tarantool есть общий space, в котором наряду с данными по ключам в кортежах присутствуют Segment_ID, mtime и другие данные.

Схема хранения данных в кластере
Схема хранения данных в кластере

Особенности и подводные камни

У архитектуры Tarantella есть особенности, которые приходится учитывать при эксплуатации. Например:

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

  • Внесение изменений в топологию — это точка синхронизации для всего кластера, потому что состояния New и Remove требуют от клиента определённого поведения, которое должно соблюдаться всеми. Когда узел добавляется в кластер, все клиенты должны подтвердить версию топологии и актуальное состояние, поэтому при большом количестве клиентов и значительном размере кластера перебалансировка может выполняться длительное время.

  • Размер самого большого кластера — 120 терабайтов. По мере его роста, нам приходилось дополнительно придумывать разные способы оптимизации процесса синхронизации данных в сегментах, при "наивной" реализации алгоритмов перебалансировка могла длиться месяцами.

Инструменты для управления

Теперь перечислим инструменты управления и диагностики, которые мы используем в Tarantella:

  • tarantellactl позволяет администраторам получить всю информацию о состоянии и поведении конкретного сегмента или узла. Один из инструментов позволяет полностью восстановить мастер Tarantool даже при одновременном падении всех его экземпляров.

  • swarmctl — настоящий «швейцарский нож» для кластеров Swarm (о нём позднее). Он запускается на самих узлах или на мастере и выполняет разные функции в зависимости от контекста. К примеру, с его помощью можно включать периодическое удаление данных по снимкам экземпляров Tarantool’а. Также инструмент может находить по ключу конкретную запись или список записей.

  • swarm-hadoop-export — позволяет экспортировать в HDFS срезы данных в формате Avro для офлайн-аналитики.

Дашборд

На дашборд в Grafana выводится разнообразная полезная информация о кластере. Например, сколько осталось времени до окончания синхронизации, нагрузка на вычислительные ресурсы и сеть, объём свободного места и т. д. Также есть графики по конкретным типам запросов, распределению места по ключам и многое другое.

Клиентские запросы

Tarantella поддерживает разные протоколы взаимодействия с клиентами. Выбор протокола и схема данных на узлах определяют список доступных операций над данными. Например:

  • Memcache: get, set, multi-get;

  • IProto: select, replace, delete, call;

  • RPC Protobuf HTTP: get, multi-get;

  • Basic/REST HTTP: get, put, delete;

  • Iproto over HTTP;

  • Swarm HTTP: get, multi-get, multi-key-get, put, delete, head.

Общая схема обработки запросов выглядит так:

Схема обработки запросов в кластере
Схема обработки запросов в кластере

Получив запрос, Tarantella берёт из него ключ и определяет нужный сегмент и затем находит все относящиеся к нему узлы. Система отправляет в них запросы по ключу, причём сразу во все доступные. Получив все ответы, либо по истечению таймаута, Tarantella объединяет их с учётом значения консенсуса для запроса и возвращает клиенту. Если была ошибка, то сообщает об этом клиенту.

Консенсус отвечает за порядок объединения данных с узлов. Можно выбрать один из двух вариантов, задав его либо в настройках, либо в параметрах запроса, если это позволяет протокол:

  • Tarantella выбирает в ответах запись с самой свежей mtime.

  • Когда важнее задержка при ответе, тогда можно возвращать первый непустой ответ целиком и не ждать более «медленные» узлы.

Tarantella swarm

Это наш самый большой кластер. Причина его появления похожа на причину, по которой мы изначально решили заняться Tarantella, так что можно считать это логическим ответвлением и развитием проекта.

До появления swarm при подборе рекламы для каждого пользователя приходилось запрашивать обезличенные данные, разбросанные по разным Tarantool- и Tarantella-хранилищам: сопоставления идентификаторов, серверные куки и т. д. Набор запрашиваемых данных отличался не только от сервиса к сервису, но даже и внутри одного сервиса в зависимости от параметров показа рекламы. Код баннерной системы и фронтенда получался сложным и громоздким из-за необходимости соблюдать сложную логику объединения ответов от разных хранилищ, обработки сценариев возникновения ошибок и т.д. 

Мы отказались от разрозненных хранилищ и собрали все данные в одном месте, сделав многосоставной первичный ключ. В первой его части идёт префикс и идентификатор, во второй — тип данных или, грубо говоря, то, что объединения могло считаться названием хранилища, а в третьей — либо пустая строка, либо дополнительный идентификатор. При этом шардирование осталось по первой части ключа. Теперь мы могли одним запросом по первой части составного ключа получать данные в виде списка и отфильтровывать ненужное. Если же требуется конкретный кортеж, то можно передать в запросе все три части ключа.

Также мы отказались от использования протокола iproto, потому что он позволял маршрутизировать только соединения, но не запросы, что негативно сказывалось на балансировке нагрузки. Вместо него написали свой протокол под цели swarm, добавив поддержку авторизации доступа к данным по ключам и типам данных. И заодно переписали узкие места в наших хранимых процедурах Tarantool с Lua на С. 

Структура записей в swarm сейчас выглядит следующим образом:

Принципиальная структура записей swarm
Принципиальная структура записей swarm
  • K1, K2 и K3 — составные элементы ключа, строковые данные:

    • K1 — префикс и ID через двоеточие;

    • K2 — тип данных;

    • K3 — ключ-значение;

  • MTime_Timestamp - время последнего обновления записи;

  • Value1, Value2, .. — поля с данными

Заключение

Сегодня в самом большом кластере Tarantella уже более 4 500 узлов Tarantool на 100 физических серверах. В нём хранится 90 Тб данных, а суммарный объём зарезервированных квот всех инсталляций — 120 Тб. Пиковая нагрузка на запись — 2 млн RPS, на чтение — 35 млн RPS.

Всего же в рекламных технологиях VK около 250 серверов, отведённых под хранение данных разных кластеров Tarantella, обслуживающих тысячи клиентов и соединений. Дорасти до такого масштаба при использовании старых бессистемных подходов мы бы точно не смогли. Достаточно вспомнить, что сложности при написании кода и администрировании мы начали испытывать при объёмах в десятки раз меньше.

Сейчас команда, отвечающая за развитие продукта, в основном занимается развитием инструментов администрирования, оптимизацией, постоянно борется за уменьшение времени ответа. Недавно завершили разработку решения для выборочной репликации данных между разрозненными кластерами Tarantella для целей CDN. Пока что планов выкладывать исходный код в open source нет, но в будущем это возможно.

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


  1. lolipop
    25.09.2023 14:45

    правильно ли я понимаю что в отличии от тарантульного vshard тут ничего не нужно переписывать в уже написанном софте?


    1. viciious Автор
      25.09.2023 14:45

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


  1. Hixon10
    25.09.2023 14:45

    Если бы вам нужно было построить подобную систему с нуля не в 2016 году, а сейчас (2023), стали бы вы повторять это решение с построением системы на основе Tarantool, или уже бы взяли одну из готовых БД (если да, то какую)?


    1. viciious Автор
      25.09.2023 14:45
      +2

      Вероятно, что мы бы стали смотреть в сторону NVME дисков и какого-то решения на основе LSM-деревьев, но там есть свои нюансы, которые тоже бы как-то пришлось решать с учётом нагрузок, рандомного доступа и требований, например, к аналитике, которую мы готовим по снапшотам данных. Подобные варианты мы и тогда рассматривали тоже, хотя бы ту же Cassandra.
      С другой стороны, у Tarantool есть полезные фичи, которые мы умеем и любим использовать, например, писать логику на Lua, составные ключи, встроенные процедуры на C и т.д.


  1. 0Bannon
    25.09.2023 14:45
    -1

    Тарантул, тарантелла. Дальше что? Брокер сообщений "Чик Пиба Рум"? Всё потому что это голос высокой травы - с той стороны.)


    1. ukko
      25.09.2023 14:45
      +5

      Tarantas ещё не занято