Масштабируем кластеры без лишних усилий

Kate

Administrator
Команда форума
Каждый, кто работал с большими кластерами, знает: данные все время растут. Рано или поздно перед разработчиками распределенных систем встает задача масштабирования. Сейчас найти место для хранения данных не проблема, но как быть с доработкой и настройкой приложений? Доработки можно избежать, если заранее заложить в систему возможность масштабирования. Можно разделить узлы приложения по типу выполняемой функциональности и развёртывать только то, что необходимо.

Меня зовут Игорь, я работаю в команде Tarantool. У нас большой опыт разработки высоконагруженных продуктов, например, систем хранения данных для крупных ритейлеров или операторов сотовой связи. Сегодня я расскажу о принципах масштабирования наших кластеров и покажу типовой пример. Будет интересно всем, кто работает с большими данными и задумывается о масштабировании.

Это дополненная версия доклада, который я рассказывал на SaintHighload++ 2021.

Задача​

При разработке высоконагруженных приложений с данными нам нужно решить три задачи:

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

Архитектура кластера​

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

b27d8ad96a4cae60257351f62aca2569.png

Роутер — первое, что проходят клиентские запросы, попадая в кластер. Это узел, который перенаправляет входящие запросы на другие узлы. Именно роутер связывает клиентов с их данными.

5f6112125f870ff8bb9e456a0b478cf2.png

Далее запросы от клиентов попадают с помощью роутера в другую категорию элементов кластера — хранилища. Они хранят данные, записывают и отдают их по запросу от роутера.

d0ee7909c246822463fd80616885a160.png

Каждый узел кластера выполняет свою роль, то есть определенную логику. При масштабировании кластера мы можем добавить в него новый узел приложения и назначить ему одну из доступных ролей. Например, если нам понадобится увеличить место для хранения данных, мы можем добавить узел с ролью «хранилище». Другие хранилища сами позаботятся о том, как перераспределить данные на новый узел, а роутер начнет отправлять к нему запросы.

00fe8f3eeda33406e05ed59e1a5b71f4.png

Аналогично и с роутерами. Если нам потребуется увеличить количество узлов, на которые клиенты будут отправлять запросы, мы должны добавить в кластер новый узел с ролью «роутер» и позаботиться о том, чтобы запросы проходили через него тоже. Роутеры при этом являются равнозначными и будут пересылать запросы на все хранилища.

968f2b2dd1797bee8b87f8d2a0bc12d6.png

Следующим важным элементом кластера будет failover-coordinator. Это узел кластера, который заботится о доступности других узлов на запись. Состояние кластера при этом он будет записывать во внешнее хранилище, которое называется state-provider. Оно должно быть независимым от остального кластера и удовлетворять требованиям к надежности.

f43f281437a9bc87daf4c7478cabc017.png

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

a03abf20171cbd2bef44b7d5cab39f1e.png

Для создания кластера мы можем воспользоваться Cartridge.

Cartridge​

Это фреймворк для разработки кластерных приложений. Cartridge сам управляет кластером и заботится о шардировании, позволяет описывать бизнес-логику и схемы данных с помощью механизма ролей, имеет failover, который позаботится о доступности экземпляров приложения на запись. Для Cartridge создано множество дополнительных инструментов, которые можно встраивать в CI/CD-конвейеры и использовать для администрирования запущенного кластера. Также для Cartridge написана Ansible-роль, имеются модуль для интеграционного тестирования, утилита для создания приложения из шаблона и локального запуска кластера, есть модуль для сбора метрик и инфопанель для Grafana.

Давайте теперь посмотрим, что лежит в основе Cartridge.

Tarantool​

Cartridge — это фреймворк на базе нескольких экземпляров Tarantool, платформы для in-memory вычислений. Она одновременно является и in-memory базой данных, и сервером приложений, написанным на Lua. Tarantool очень быстрый за счет хранения данных в оперативной памяти, но при этом надежный. Он обеспечивает сброс снепшотов с данными на диск, позволяет настроить репликацию и шардирование.

Репликация​

Экземпляры Tarantool можно объединить в набор реплик (replica set) — это узлы, связанные между собой асинхронной репликацией. При настройке можно указать, на каком экземпляре данные будут доступны только на чтение. Те экземпляры, которые могут писать данные, мы будем называть «мастерами», а остальные — «репликами».

9b499a3da73d3dc63cda2b7edb813a8c.png

Пример настройки репликации на мастере:

box.cfg{
listen = 3301,
read_only = false,
replication = {
'127.0.0.1:3301',
'127.0.0.1:3302',
},
}
И на реплике:

box.cfg{
listen = 3302,
read_only = true,
replication = {
'127.0.0.1:3301',
'127.0.0.1:3302',
},
}

Шардирование​

В Tarantool есть специальный модуль для шардирования данных — vshard. Он позволяет разделить ваши данные на куски, которые будут храниться на разных узлах приложения.

Vshard оперирует двумя понятиями — vshard.storage и vshard.router. В хранилищах находятся бакеты с данными, также они обеспечивают ребалансировку данных в случае появления новых экземпляров. А роутеры используются для отправки запросов к узлам хранения данных.

В vshard нужно на каждом узле настраивать роутеры и хранилища. При расширении кластера на vshard нужно либо изменять код приложения, либо писать обёртку вокруг vshard, она будет сама настраивать экземпляры при каждом добавлении узлов.

4226514763c534d8f64a48e1f74f46e6.png

Пример конфигурирования vshard на экземплярах Tarantool:

local sharding = {
['aaaaaaaa-0000-4000-a000-000000000000'] = {
replicas = {
['aaaaaaaa-0000-4000-a000-000000000011'] = {
name = 'storage',
master = true,
uri = "sharding:pass@127.0.0.1:30011"
},
}
}
}

vshard.storage.cfg(
{
bucket_count = 3000,
sharding = sharding,
...
},
'aaaaaaaa-0000-4000-a000-000000000011'
)

vshard.router.cfg({sharding = sharding,})

Собираем всё вместе​

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

  • Фейловер.
  • Автоматическая настройка шардирования при добавлении новых узлов.
  • WebUI для мониторинга кластера.
2516ef702ccdd91f152c7dfff02ee90c.png

Роли экземпляров в Cartridge​

До этого я уже показал, что каждый экземпляр в кластере выполняет свою роль. Роли в Cartridge — это Lua-модули со специальным API, которые служат для описания бизнес-логики приложения.

Какая может быть функциональность у ролей?

  • Хранилище, в котором будут создаваться таблицы для данных.
  • HTTP-API для клиентов.
  • Репликация данных из внешнего источника.
  • Сбор метрик или цепочек вызовов, отслеживание работоспособности экземпляров.
  • Исполнение любой другой кастомной логики, написанной на Lua.
Роль назначается для набора реплик и включается на каждом экземпляре в наборе. Роль всегда знает, запущена она на мастере или на реплике. Её поведением можно управлять через конфигурацию, не меняя код роли.

API​

return {
role_name = 'custom-role',

init = init,
validate_config = validate_config,
apply_config = apply_config,
stop = stop,

dependencies = {
'another-role',
},
}
Каждая роль имеет стандартный API:

  • В init-функции производятся действия для начальной настройки роли.
  • validate_config проверяет валидность конфигурации.
  • apply_config применяет конфигурацию.
  • В stop производятся действия для отключении роли.
У роли есть список зависимостей dependencies, и она будет запущена только после того, как инициализируется весь этот список.

Готовые роли​

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

Встроенные роли:

  • vshard—router. Направляет запросы к узлам хранения данных.
  • vshard—storage. Хранит и контролирует бакеты с данными.
  • failover—coordinator. Управляет фейловером в кластере.
Другие полезные роли:

  • metrics. Собирает метрики приложений.
  • crud—storage и crud—router. Упрощают взаимодействие с шардированными спейсами.

Зависимости между ролями​

Зависимости между ролями — важнейший механизм, который лежит в основе всех приложений на Cartridge. Роль будет запущена только после успешной инициализации других ролей, от которых она зависит. Это означает, что каждая роль может использовать функции из других ролей, если они будут указаны в списке dependencies.

Рассмотрим пример. Пусть мы написали свою роль custom-storage, в которой описано создание таблиц данных. Она будет зависеть от crud-storage, чтобы мы могли воспользоваться упрощенными интерфейсами для получения и записи данных в наше хранилище. При этом crud-storage уже зависит от роли vshard-storage, что позволяет нам не заботиться о том, как наши данные шардируются. В коде это выглядит вот так:

app/roles/custom-storage.lua

return {
role_name = 'custom-storage',

dependencies = {
'crud-storage',
},
}
cartridge/roles/crud-storage.lua

return {
role_name = 'crud-storage',

dependencies = {
'vshard-storage',
},
}
Cartridge гарантирует порядок запуска ролей. Значит в роли custom-storage мы можем пользоваться всеми возможностями ролей vshard-storage и crud-storage и не бояться, что они еще не проинициализированы.

abc0de71f4ce01e4088e77f96df192bf.png

Entrypoint​

Входной точкой каждого экземпляра в кластере Cartridge является файл init.lua. В нем обязательно должна быть вызвана функция cartridge.cfg — в нее передается список ролей, доступных в кластере, а также дефолтные настройки Cartridge и Tarantool. init-файл может быть запущен как из консоли с помощью Tarantool, так и с помощью cartridge-cli или systemd/supervisord.

tarantool ./init.lua

local ok, err = cartridge.cfg({
roles = {
'cartridge.roles.vshard-storage',
'cartridge.roles.vshard-router',
'cartridge.roles.metrics',
'app.roles.custom’,
},
... -- cartridge opts
}, {
... -- tarantool opts
})

Масштабирование с помощью ролей​

Рассмотрим типовой сценарий масштабирования. Пусть у нас есть кластер из двух экземпляров: у одного роль «роутер», у другого «хранилище».

0e60ff25f25e2e889a6aac1121fa57ec.png

Новые реплики добавляем, когда нужно дополнительное резервирование, например, появился новый резервный ЦОД. Или когда хотим увеличить доступность наборов реплик на чтение, например, если большая часть запросов на чтение отправляется на реплику.

a33f31ec82829a9b073209e0a23268f2.png

Добавляем новые шарды, когда на экземплярах память заполняется на 60-80 %. Можно и раньше, если видим на графиках потребления памяти постоянный рост и можем прогнозировать время, когда место закончится.

d4369271b0173a7ae3fd60017a427d37.png

Роутеры добавляем, если видим пиковую нагрузку CPU около 80-100 %, а также по бизнес-метрикам. Например, увеличилось число пользователей у кластера, выросла нагрузка.

b9c8fd4bf7fb542c3ae909114d4f117c.jpeg

Добавляем новые роли, если меняется или дополняется бизнес-логика приложений, или если вдруг нужен дополнительный мониторинг. Например, можно включить в кластер модули metrics или tracing.

5920e96bf221de27926afc61daf07235.jpeg

Нужно отметить, что добавлять в кластер новые экземпляры можно без изменения кода, как и включать существующие роли на имеющихся наборах реплик. Но при добавлении новых ролей, которые раньше отсутствовали в кластере, нужно добавить их в init.lua и перезапустить приложение.

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

Кластерная конфигурация​

Кластерная конфигурация — основной способ управления кластером, там хранится топология, настройки vshard, которые передаются в vshard.storage.cfg и vshard.router.cfg, а также настройки кастомных ролей. Полная конфигурация хранится на каждом экземпляре приложения. Cartridge гарантирует, что на всех узлах в любой момент времени конфигурация будет идентична.

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

271fbbbe74d4959fe7370c48f53f29c5.png

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

73bfd970bb07654270a2a467c219c7a9.png

Если же конфигурация была валидной, то Cartridge дожидается, пока каждый из инстансов в кластере скажет, что проверка validate_config завершилась успешно и конфигурацию можно применять.

4ed0032e1c9dbd164858764679bae52a.png

Теперь Cartridge выполнит все функции apply_config и применит новую конфигурацию на каждом экземпляре приложения. Она будет сохранена на диске взамен старой конфигурации.

73d4bf3815a225c16204f1404dc0c210.png

Но если вдруг во время применения конфигурации что-то пошло не так и стадия apply_config выполнилась с ошибкой, Cartridge выведет в WebUI информацию об этом и предложит переприменить конфигурацию.

Failover​

У нас есть репликация и мы знаем, что наши данные никуда не пропадут, но также нам нужно, чтобы приложения всё время были доступны на запись. В Cartridge для этого существует фейловер. Для его работы в кластере должны быть экземпляры с ролью failover-coordinator, которые будут управлять переключением мастеров. Еще нужен state-provider, в котором хранится список текущих мастеров. Сейчас поддерживаются два режима state-provider’a: etcd или отдельный экземпляр Tarantool, который называется stateboard.

Failover-coordinator следит за тем, чтобы в наборе реплик был доступен хотя бы один мастер.

e15a841a3cbc227ec799bd21a4f784e3.png

Если вдруг мастер в наборе упадет или перестанет отвечать на запросы, failover-coordinator узнает об этом и назначит одну из реплик в том же наборе мастером.

ddca8dc41470279ee02335dd97e2381c.png

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

ab27c5a12570062b599193cc9518dd67.png

Инструменты​

А как вообще поддерживать Cartridge? Для этого есть богатый инструментарий:

  • cartridge-cli для создания приложения из шаблона, локального запуска, подключения к экземплярам и вызова функций администрирования.
  • Для тестирования есть cartridge.test-helpers, который позволяет создавать в интеграционных тестах кластер любого состава.
  • Для мониторинга есть WebUI, который позволяет узнать о некоторых проблемах с помощью Cartridge Issues и починить часть из них с помощью Suggestions; существует пакет metrics и дашборд для Grafana.
  • Развёртывание выполняется с помощью ansible-cartridge, она позволяет добавлять новые экземпляры в кластер, обновлять существующие и многое другое.

Cartridge в проде​

Cartridge хорошо зарекомендовал себя в эксплуатации. Наши заказчики пользуются им уже как минимум три года. Вот немного статистики по использованию Cartridge:

  • > 100 витрин в проде: кеши и мастер-хранилища.
  • От нескольких МБ до нескольких ТБ.
  • От 4 до 500 экземпляров в кластере.
  • Мастеры и реплики в разных ЦОДах.
  • CI/CD-конвейер, общий для всех приложений в рамках одного контура.
  • Общие модули и роли в проектах.

Итоги​

Я рассказал о принципах масштабирования кластеров, а также про:

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

 
Сверху