Тarantool Cartridge: шардування Lua-бекенду в три рядки

Тarantool Cartridge: шардування Lua-бекенду в три рядки

У нас в Mail.ru Group є Tarantool - це такий сервер додатків на Lua, який за сумісництвом ще й база даних (або навпаки?). Він швидкий та класний, але можливості одного сервера все одно не безмежні. Вертикальне масштабування теж не панацея, тому в Tarantool є інструменти для горизонтального масштабування - модуль vshard [1]. Він дозволяє шардувати дані по кількох серверах, але доведеться повозитись, щоб його налаштувати і прикрутити бізнес-логіку.

Хороші новини: ми зібрали шишок (наприклад [2], [3]) та запилили черговий фреймворк, який помітно спростить вирішення цієї проблеми.

Тarantool Cartridge це новий фреймворк для розробки складних розподілених систем. Він дає змогу сфокусуватися на написанні бізнес-логіки замість вирішення інфраструктурних проблем. Під катом я розповім, як цей фреймворк влаштований і як за його допомогою писати розподілені сервіси.

А в чому, власне, проблема?

У нас є тарантул, є vshard - чого ще побажати?

По-перше, справа у зручності. Конфігурація vshard налаштовується через Lua-таблиці. Щоб розподілена система з кількох процесів Tarantool працювала правильно, конфігурація має бути скрізь однаковою. Ніхто не хоче цим займатися вручну. Тому в хід йдуть усілякі скрипти, Ansible, системи розгортання.

Cartridge сам керує конфігурацією vshard, він робить це на основі своєї своєї розподіленої конфігурації. По суті це простий YAML-файл, копія якого зберігається в кожному екземплярі Tarantool. Спрощення полягає в тому, що фреймворк сам стежить за своєю конфігурацією і тим, щоб вона скрізь була однакова.

По-друге, справа знову у зручності. Конфігурація вшард не має жодного відношення до розробки бізнес-логіки і лише відволікає програміста від роботи. Коли ми обговорюємо архітектуру того чи іншого проекту, то найчастіше йдеться про окремі компоненти та їхню взаємодію. Про викочування кластера на 3 датацентри думати рано.

Ми вирішували ці проблеми щоразу, і в якийсь момент у нас вдалося виробити підхід, що дозволяє спростити роботу з додатком на всьому його життєвому циклі: створення, розробка, тестування, CI/CD, супровід.

Cartridge вводить поняття ролі кожного процесу Tarantool. Ролі - це та концепція, яка дозволяє сфокусуватися розробнику на написанні коду. Усі наявні в проекті ролі можна запустити на одному екземплярі Tarantool, і для тестів цього буде достатньо.

Основні можливості Tarantool Cartridge:

  • автоматизоване оркестрування кластера;
  • розширення функціональності програми за допомогою нових ролей;
  • шаблон програми для розробки та розгортання;
  • вбудоване автоматичне шардування;
  • інтеграція з тестовим фреймворком Luatest;
  • управління кластером за допомогою WebUI та API;
  • інструменти упаковки та деплою.

Привіт Світ!

Мені не терпиться показати сам фреймворк, тому розповідь про архітектуру залишимо на потім і почнемо з простого. Якщо припустити, що сам Tarantool вже встановлений, залишається зробити тільки

$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH

Ці дві команди встановлять утиліти командного рядка і дозволять створити свій перший додаток із шаблону:

$ cartridge create --name myapp

І ось що ми отримаємо:

myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│   ├── helper
│   │   ├── integration.lua
│   │   └── unit.lua
│   ├── helper.lua
│   ├── integration/api_test.lua
│   └── unit/sample_test.lua
└── tmp/

Це git-репозиторій із готовим «Hello, World!» додатком. Давайте одразу спробуємо його запустити, попередньо встановивши залежності (в т.ч. сам фреймворк):

$ tarantoolctl rocks make
$ ./init.lua --http-port 8080

Отже, у нас запущено одну ноду майбутнього шардованого додатка. Допитливий обиватель може відразу відкрити веб-інтерфейс, мишкою сконфігурувати кластер з одного вузла і насолоджуватися результатом, але радіти поки що рано. Поки що програма не вміє робити нічого корисного, тому про деплою я розповім потім, а зараз настає час писати код.

Розробка додатків

Ось уявіть, що ми проектуємо проект, який повинен приймати дані, зберігати їх і раз на добу будувати звіт.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Ми починаємо малювати схему, і поміщаємо на неї три компоненти: gateway, storage і scheduler. Опрацьовуємо архітектуру далі. Якщо ми використовуємо як сховища vshard, то додаємо в схему vshard-router і vshard-storage. Ні gateway, ні scheduler звертатися до сховища безпосередньо не будуть, для цього є роутер, він для того й створений.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Ця схема все ще не зовсім точно відображає те, що ми створюватимемо в проекті, тому що компоненти виглядають абстрактно. Потрібно ще подивитися, як це спроектується на реальний Tarantool – згрупуємо наші компоненти у процесах.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Тримати vshard-router та gateway на окремих примірниках сенсу мало. Навіщо нам зайвий раз ходити мережею, якщо це і так входить в обов'язки роутера? Вони мають бути запущені всередині одного процесу. Тобто в одному процесі ініціалізуються і gateway, і vshard.router.cfg, і нехай вони взаємодіють локально.

На етапі проектування працювати з трьома компонентами було зручно, але я, як розробник, поки пишу код, не хочу замислюватися про запуск трьох екземплярів Tarnatool. Мені потрібно запустити тести та перевірити, що я правильно написав gateway. Чи, може, я хочу продемонструвати колегам фічу. Навіщо мені мучитися з розгортанням трьох екземплярів? Саме так народилася концепція ролей. Роль - це звичайний луашний модуль, життєвим циклом якого управляє Cartridge. У цьому прикладі їх чотири - gateway, router, storage, scheduler. В іншому проекті їх може бути більше. Усі ролі можна запустити в одному процесі, і цього буде достатньо.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

А коли мова піде про розгортання в staging або експлуатацію, тоді ми призначимо кожному процесу Tarantool свій набір ролей залежно від апаратних можливостей:

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Управління топологією

Інформацію про те, де якісь ролі запущені, треба десь зберігати. І це десь — розподілена конфігурація, про яку я вже згадував вище. Найголовніше в ній це топологія кластера. Тут зображено 3 реплікаційні групи з 5 процесів Tarantool:

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Ми не хочемо втратити дані, тому дбайливо ставимося до інформації про запущені процеси. Cartridge слідкує за конфігурацією за допомогою двофазного комміту. Як тільки ми хочемо оновити конфігурацію, він спочатку перевіряє доступність всіх екземплярів та їхню готовність прийняти нову конфігурацію. Після цього другий фазою застосовується конфіг. Таким чином, навіть якщо один екземпляр виявився тимчасово недоступним, то нічого страшного не станеться. Конфігурація просто не застосовується, і ви заздалегідь побачите помилку.

Також у секції топології зазначений такий важливий параметр, як лідер кожної групи реплікації. Зазвичай, це той екземпляр, на який йде запис. Інші найчастіше є read-only, хоча тут можуть бути винятки. Іноді сміливі розробники не бояться конфліктів і можуть писати дані на кілька реплік паралельно, але є деякі операції, які, незважаючи ні на що, не повинні виконуватися двічі. Для цього є ознакою лідера.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Життя ролей

Щоб абстрактна роль могла існувати у такій архітектурі, фреймворк має ними якось керувати. Звичайно, управління відбувається без перезапуску процесу Tarantool. Для керування ролями існує 4 колбеки. Cartridge сам буде викликати їх залежно від цього, що він написано у розподіленої конфігурації, цим застосовуючи конфігурацію до конкретним ролям.

function init()
function validate_config()
function apply_config()
function stop()

Кожна роль має функцію init. Вона викликається один раз або при включенні ролі або при перезапуску Tarantool'а. Там зручно, наприклад, ініціалізувати box.space.create, або scheduler може запустити якийсь фоновий fiber, який виконуватиме роботу через певні інтервали часу.

Однією функції init може бути недостатньо. Cartridge дозволяє ролям користуватись тією розподіленою конфігурацією, яку він використовує для зберігання топології. Ми можемо в цій конфігурації оголосити нову секцію і зберігати в ній фрагмент бізнес-конфігурації. У моєму прикладі може бути схема даних, або налаштування розкладу для ролі scheduler.

Кластер викликає validate_config и apply_config при кожній зміні розподіленої конфігурації. Коли конфігурація застосовується двофазним коммітом, кластер перевіряє, що кожна роль готова прийняти цю нову конфігурацію, і за необхідності повідомляє про помилку. Коли всі погодилися, що конфігурація нормальна, то виконується apply_config.

Також у ролей є метод stop, який необхідний очищення результатів життєдіяльності участі. Якщо ми говоримо, що scheduler на цьому сервері більше не потрібний, він може зупинити ті файбери, які запускав за допомогою init.

Ролі можуть взаємодіяти між собою. Ми звикли писати виклики функцій на Lua, але може статися так, що в цьому процесі немає потрібної ролі. Щоб полегшити звернення по мережі, ми використовуємо допоміжний модуль RPC (remote procedure call), який побудований на основі стандартного netbox, вбудованого в Tarantool. Це може стати в нагоді, якщо, наприклад, ваш gateway захоче прямо попросити scheduler зробити роботу прямо зараз, а не чекати на добу.

Ще один важливий момент – забезпечення стійкості до відмови. Для моніторингу здоров'я у Cartridge використовується протокол SWIM [4]. Якщо говорити коротенько, процеси обмінюються один з одним «чутками» по UDP — кожен процес розповідає своїм сусідам останні новини, і вони відповідають. Якщо раптом відповідь не надійшла, Tarantool починає підозрювати щось недобре, а через деякий час декламує смерть і починає розповідати всім навколишнім цю новину.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

На основі цього протоколу Cartridge організує автоматичну обробку відмов. Кожен процес стежить за своїм оточенням, і якщо лідер раптом перестав відповідати, то репліка може взяти його на себе, а Cartridge відповідним чином конфігурує запущені ролі.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Тут треба бути обережним, тому що часте перемикання туди-сюди може призвести до конфліктів даних при реплікації. Включати автоматичний failover навмання, звичайно, не варто. Потрібно чітко розуміти, що відбувається, і бути впевненими, що реплікація не зламається після того, як лідер відновиться і повернуть йому корону.

Зі всього сказаного може скластися відчуття, що ролі схожі на мікросервіси. В якомусь сенсі вони ними і є лише як модулі всередині процесів Tarantool. Але є й низка принципових відмінностей. По-перше, всі ролі проекту мають жити в одній кодовій базі. І всі процеси Tarantool повинні запускатися з однієї кодової бази, щоб не було сюрпризів на зразок тих, коли ми намагаємося ініціалізувати scheduler, а його просто немає. Також не варто допускати відмінностей у версіях коду, тому що поведінка системи в такій ситуації дуже складно передбачати та налагоджувати.

На відміну від Docker ми не можемо просто взяти «образ» ролі, віднести його на іншу машину і там запустити. Наші ролі не настільки ізольовані як Docker-контейнери. Також ми не можемо запустити на одному примірнику дві однакові ролі. Роль або є, або її немає, в якомусь сенсі це єдинийтон. Ну і по-третє, всередині всієї реплікаційної групи ролі мають бути однаковими, бо інакше було б безглуздо дані однакові, а конфігурація різна.

Інструменти деплою

Я обіцяв показати, як Cartridge допомагає деплоїти програми. Щоб полегшити життя оточуючим, фреймворк пакує RPM-пакети:

$ cartridge pack rpm myapp -- упакует для нас ./myapp-0.1.0-1.rpm
$ sudo yum install ./myapp-0.1.0-1.rpm

Встановлений пакет несе в собі майже все необхідне: і додаток, і встановлені луашні залежності. Tarantool на сервер теж приїде як залежність RPM-пакету і наш сервіс готовий до запуску. Робиться це через systemd, але насамперед необхідно написати трохи конфігурації. Як мінімум, вказати URI кожного процесу. Трьох для прикладу вистачить.

$ sudo tee /etc/tarantool/conf.d/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIG

Тут є цікавий нюанс. Замість того, щоб вказати лише порт бінарного протоколу, ми вказуємо публічну адресу процесу повністю, включаючи hostname. Це потрібно для того, щоб вузли кластера знали, як з'єднатися один з одним. Погана ідея використовувати як advertise_uri адресу 0.0.0.0, це має бути зовнішня IP-адреса, а не bind сокета. Без нього нічого працювати не буде, тому Cartridge просто не дасть запустити вузол із неправильним advertise_uri.

Тепер, коли готова конфігурація, можна запустити процеси. Так як звичайний systemd-юніт не дозволяє стартувати більше одного процесу, програми на Cartridge встановлює т.зв. instantiated-юніти, які працюють так:

$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_B

У конфігурації ми вказали HTTP-порт, на якому Cartridge обслуговує веб-інтерфейс - 8080. Зайдемо на нього і подивимося:

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Ми бачимо, що процеси хоч і запущені, але поки що не налаштовані. Картридж поки не знає, хто з ким має реплікуватися і не може прийняти рішення самостійно, тому чекає на наші дії. А у нас вибір невеликий: життя нового кластера починається з конфігурації першого вузла. Потім додамо до кластера інші, призначимо їм ролі, і цьому деплой вважатимуться успішно завершеним.

Наллємо кухоль улюбленого напою і розслабимося після довгого робочого тижня. Програму можна експлуатувати.

Тarantool Cartridge: шардування Lua-бекенду в три рядки

Підсумки

А що результати? Пробуйте, користуйтеся, залишайте зворотний зв'язок, заводьте тікети на гітхабі.

Посилання

[1] Tarantool » 2.2 » Reference » Rocks reference » Module vshard

[2] Як ми запроваджували ядро ​​інвестиційного бізнесу Альфа-Банку на базі Tarantool

[3] Архітектура білінгу нового покоління: трансформація з переходом на Tarantool

[4] SWIM - протокол побудови кластера

[5] GitHub - tarantool/cartridge-cli

[6] GitHub - tarantool/cartridge

Джерело: habr.com

Додати коментар або відгук