Розробка реактивних та розподілених систем з Vert.x

У цій статті я хочу розповісти про інструмент для розробки високопродуктивних JVM-додатків, що не вимагає вивчення складних архітектурних моделей.

Пошук альтернатив

Я давно і з задоволенням користуюся такими інструментами, як Spring, а також Akka і модель акторів. Проте і у них є недоліки. Spring при своїй зручності і широких можливостях може іноді витрачати трохи більше ресурсів, ніж хотілося б. Akka ґрунтується на моделі акторів, яку не кожна команда може легко, швидко і головне ефективно впровадити. І я почав думати про можливі альтернативи.

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

У процесі вивчення я провів свої бенчмарки і отримав приємні результати. Загалом, Vert.x досить мало навантажує процесор, в тому числі економний за витратами пам'яті. Пропускна спроможність (запитів в секунду) теж радує. До того ж Vert.x виявився напрочуд простий у вивченні. Для мене в моєму маленькому тесті він виявився лідером. Зауважу, що мова йде тільки про мої враження, так як я не люблю холиварить і розумію, що кожен може провести свої тести і отримати свої результати. Давайте подивимося, які можливості відкриває перед нами Vert.x.

Основи Vert.x

В першу чергу мені захотілося розібратися в архітектурі ядра Vert.x, у тому як він влаштований. Це, в свою чергу, допомогло б зрозуміти, де його краще застосовувати. Я вирішив почати вивчення з простого Hello World програми. Перше, що кинулося в очі, це те, що Vert.x — це бібліотека. Точніше, набір бібліотек, які разом складають цілу екосистему. Це не фреймворк, тобто в ньому немає інверсії управління. Для ін'єкції залежностей можна підключити будь-який бажаний інструмент. Давайте розглянемо маленкий фрагментом коду, написаний з використанням Vert.x.

Vert.x Vert.x = Vert.x.Vert.x();
Router router = Router.router(Vert.x);

JsonObject mySQLClientConfig = new JsonObject().put("host", "localhost").put("database", "test");
SQLClient sqlClient = MySQLClient.createShared(Vert.x, mySQLClientConfig);

router.get("/hello/:name").produces("text/plain").handler(routingContext -> {
 String name = routingContext.pathParam("name");
 HttpServerResponse response = routingContext.response();

 sqlClient.updateWithParams("INSERT INTO names (name) VALUES (?)", new JsonArray().add(name), event -> {
 if (event.succeeded()) {
 response.end("Hello" + name);
 } else {
 System.out.println("Could not INSERT to database with cause:" + event.cause());
response.setStatusCode(500);
response.end();
}
});
});

HttpServer server = Vert.x.createHttpServer();
server.requestHandler(router::accept).listen(8080);

Відразу помітно наявність глобального об'єкта Vert.x. Далі використовується якийсь роутер, який входить в бібліотеку Vert.x Web. Він допомагає розробляти веб-сервіси нагадує Node.js манері. Зупинимося на тому, що роутер дозволяє створювати HTTP эндпоинты. Далі ми підключаємося до MySQL, використовуючи реактивний клієнт, який входить в постачання. Потім пишемо обробники подій, які передаються як callback-функції. Отже, ми створили обробник для HTTP-эндпоинта і для отримання результату виконання SQL-запиту. Ну і в кінці стартуємо наш веб-сервіс, запускаючи HttpServer на порту 8080.

З одного боку, код виглядає незвично як для Java-програміста, з іншого боку дуже нагадує JavaScript/Node.js-додаток. Насправді так і є. Як я встиг зрозуміти, в свій час Node.js зіграв велику роль у створенні Vert.x. Це, звичайно, не сама приємна новина для більшості Java-розробників. Однак, будучи людиною, яка активно бавиться JavaScript/TypeScript, я вирішив тимчасово закрити на це очі і розібратися далі. Як виявилося, Vert.x побудований як імплементація вже класичного патерну Reactor з маленькою модифікацією, яку розробники назвали Multi-Reactor.

Патерн Reactor

Щоб зрозуміти патерн Multi-Reactor, достатньо знати відомий патерн Reactor. Класичний Reactor говорить про те, що є якийсь Event Loop, як правило однопотоковий, який відповідає за обробку подій. Всі клієнтські запити заходять події. Далі виконується обробник, Handler, який підписаний на відповідні події. При цьому буде недобре, якщо обробник заблокує Event Loop надовго. Тому довгограючі завдання делегуються Worker-потоків і виконуються, не блокуючи Event Loop. На них висів якийсь Callback, який буде викликаний, як тільки завдання буде виконано (або перерветься зі звітом про помилку).

У свою чергу, Multi-Reactor розширює цей шаблон (патерн), додаючи ще декілька потоків (додаткові Event Loop-и). Таким чином, формується шина подій (Event Bus) яка вміє масштабуватися під ресурси конкретної машини. Як правило, кількість потоків Event Loop визначається за формулою «кількість ядер процесора * 2». Отже, весь Vert.x — це один великий Event Bus, з яким ми спілкуємося за допомогою Callback-ів.

Структура програми

Розібравшись з тим, як писати код на Vert.x і як це все працює всередині, я задумався про те, як же структурувати такий додаток. Адже це можна зробити по-різному. Але повинен бути якийсь шаблонний варіант, якийсь best practice, який пропонують розробники Vert.x. Як виявилося, вони запропонували не тільки підхід, але ще і його реалізацію.

Виявилося, Vert.x надає цілу екосистему, з якої потрібно було розібратися. Крім реактивної архітектури, він також пропонує свою модель розгортання (deployment) додатків. Ця модель називається Вертикальними. Що ж це таке? Ще одна адаптація якогось класичного патерну? Не повірите, але майже так. Вертикальними — це контейнер (не Docker, звичайно, це не контейнер програми). Це переносимий контейнер для Vert.x. І ось, як він виглядає:

public class MyVerticle extends AbstractVerticle {

 private HttpServer server;
@Override
 public void start(Future<Void> startFuture) {
 server = Vert.x.createHttpServer().requestHandler(req -> {
req.response()
 .putHeader("content-type", "text/plain")
 .end("Hello from Vert.x!");
});

 server.listen(8080, res -> {
 if (res.succeeded()) {
startFuture.complete();
 } else {
startFuture.fail(res.cause());
}
});
}

@Override
 public void stop(Future<Void> stopFuture) {
//...
}
}

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

По суті, Вертикальними — це контейнер для обробників подій (handler). Так як весь код нагадує набір безлічі callback-ів, їх можна логічно зібрати в вертиклы і тим самим структурувати додаток. Насправді, вертиклы бувають трьох типів: Standard, Worker і Multi-Threaded Worker. Стандартний вертикл, точніше код всередині нього, виконується в потоці Event Loop, блокуючи його на час виконання. Worker-вертиклы виконуються на Worker-потоках. Але справа в тому, що одноразово один Worker-вертикл може виконуватися тільки на одному потоці. Якщо вам потрібна можливість виконати вертикл паралельно в декількох потоках, тоді вам потрібен Multi-Threaded Worker Вертикальними. Створюються всі ці вертиклы дуже просто: потрібно вказати лише тип, наприклад:

DeploymentOptions options = new DeploymentOptions().setWorker(true);
Vert.x.deployVerticle("io.orkhan.MyFirstVerticle", options);

Таким чином, можна сказати, що базова структура нашої програми має наступну форму:

Кластеризація

Що якщо, нам недостатньо одного додатка? Що якщо нам потрібно масштабувати наш додаток на кілька серверів у мережі? Це, звичайно, можна зробити стандартними підходами. Однак у випадку Vert.x цю задачу також можуть вирішити вертиклы. Насправді, вертиклы є чимось більшим, ніж просто інструментом для структурування програми. З допомогою вертиклов можна масштабувати додаток шляхом кластерирования.

В екосистемі Vert.x кластер є надбудовою над готовими рішеннями, такими як Hazelcast, Infinispan, Ignite, Zookeeper, Atomix та інші. Точніше Vert.x використовує вищезгадані підсистеми для синхронізації і організації свого кластера. За замовчуванням використовується Hazelcast. Інші можна підключити з поставки, крім Atomix, який потрібно підключати окремо (так як він є 3rd party-імплементацією і не входить в Vert.x). У тому числі можуть бути доступні інші варіанти Cluster Manager, що надаються сторонніми постачальниками. Налаштування самого кластер-менеджера, наприклад, Hazelcast, доступна в документації Vert.x. Важливо розуміти, що кластер складається з безлічі екземплярів Vert.x-додатків, тобто це JVM-додатки, в яких запущений Vert.x.

Найголовніше, це не те, що все це можна зробити з коду, а те, що це також можна зробити з командного рядка. Це дозволяє спростити автоматизацію процесу розгортки. Наприклад, командою $ vertx run MyVerticle можна просто розгорнути і запустити вертикл. З ключем -cluster можна вказати, що запускається примірник буде частиною кластеру (файл конфігурації cluster.xml можна покласти в ту ж папку або передати параметром при запуску). З ключем -ha можна включити режим High Availability, в якому впали вертиклы будуть автоматом розгортатися на інших примірниках у кластері. Цей режим особливо цікавий з додатковим ключем –hagroup, який дозволяє розділяти вертиклы на групи. Наприклад, якщо різні дата-центри виділити різні групи, вертиклы в одному дата-центрі будуть розгортатися тільки на інстансах цього дата-центру.

Зауважу, що можна навіть запускати порожні примірники командою $ vertx run -ha -hagroup my-group. Ну і наостанок, мені дуже подобається опція -quorum, яка дозволяє вказати мінімальну кількість примірників у кластері, необхідну для успішної роботи системи. Якщо буде доступно менше примірників, всі вертиклы будуть прибиті (undeploy) і розгорнуться назад, як тільки кількість кворуму відновиться.

Балансування навантаження

Щоб підсумувати тему моделі вертиклов, додам ще один маленький, але важливий коментар про балансування навантаження. Один вертикл можна розгортати (deploy) багато разів незалежно від того, запущений він у кластері або локально. В обох випадках навантаження буде ділитися між запущеними копіями вертикла за алгоритмом Round-Robin (такий спрощений load balancing).

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

Розширені можливості

Отже, лише одна бібліотека, Vert.x Core вже дозволяє робити все описане. І більше того, в ній є:

Як бачите, це вже немало. І це лише одна бібліотека. А адже Vert.x — це ціла екосистема. Далі в статті я наведу короткий огляд інших бібліотек, а детальніше про них можна прочитати в офіційній документації .

Перша і, на мій погляд, обов'язкова для розгляду бібліотека — це Vert.x Web, яка надає той самий роутер, використаний у прикладі вище. Справа в тому, що Vert.x Core дає можливість розробляти галузеві HTTP-сервери і клієнти. А ось роутер вже надає можливість розробки веб-сервісів на зручному високому рівні з надбудовами, які полегшують завдання. Наприклад, якщо для розробки HTTP-сервера достатньо одного методу, в коді якого треба буде парсити запит і розуміти, що з ним далі робити, то за допомогою роутера ми можемо розділити GET, POST, PUT та інші запити. У тому числі в Web доступний ще й WebClient, який дозволить досить зручно консьюмить інші веб-сервіси, дозволяючи встановити таймаут, парсити в обидві сторони JSON (під капотом старий добрий jackson) і багато іншого.

Авторизацію та аутентифікацію дозволить зробити підключається Vert.x Аутентифікації. Він вміє працювати з OAuth2, Shiro, JWT і багатьом іншим. По суті, Vert.x Auth інтегрується з роутером з Vert.x Web, що дуже зручно.

Далі з допомогою Vert.x Microservices в додаток можна додати розширений service discovery, скористатися вбудованим circuit breaker-му і отримувати конфігурацію з безлічі доступних джерел. Що мені дуже сподобалося, це те, що з одного боку Vert.x уміє інтегруватися з зовнішнім discovery-сервером, наприклад, Consul. З іншого боку, в Vert.x сервісом можна назвати будь-handler, доступний (підписаний) на event bus, що дозволяє паблишить і дискаверить все, що завгодно.

Тобто нам не обов'язково поверх функції доступу до даних вішати на неї ще й якийсь API для того, щоб достукатися до неї по мережі. Достатньо знати назву цієї функції (як сервіс в service discovery), знайти її і просто користуватися. Vert.x за вас вже все зробив. Всі дані (в обидві сторони) будуть пересилатися по TCP (якщо потрібно захистити дані від чужих очей, можна включити TLS). Насправді в тому, щоб перетворити будь-яку функцію в сервіс, доступний по діскавері, є нюанси. Наприклад, вам знадобляться service proxy. На цю тему можна довго говорити, але краще один раз прочитати в офіційній документації з прикладами.

Крім усього іншого, в Vert.x ще доступні широкі можливості інтеграції з зовнішніми системами через безліч каналів, інтеграція з RxJava, щоб писати реактивно виглядає код замість коллбэков, інтеграція з Micrometer і багато інших приємних дрібниць.

Підсумки

Загалом, я розглядаю Vert.x як надійний інструмент для розробки систем, де важлива висока продуктивність. Він дуже спритний, споживає мало ресурсів і дуже стабільний, хоч і трохи незвичний. Також потрібно відзначити ризики, пов'язані з підтримкою і подальшим розвитком цього інструменту. Команда розробки Vert.x і спільнота не дуже великі, хоч релізи і досить часті.

При цьому всі проекти, що використовують Vert.x, про яких я чув і з якими перетинався, виявилися дуже вдалими (як мінімум з технічної точки зору). Тому раджу спробувати Vert.x, провести декілька експериментів з ним, а може навіть розібратися детально, так як він може виявитися корисним вже в наступному вашому проекті.

Опубліковано: 30/01/19 @ 12:32
Розділ Різне

Рекомендуємо:

Токсичний HR: дії, які отруюють команду
Три основні проблеми розумних будинків і як їх можна вирішити
PM дайджест #16: розбираємося з OKR, як повернути Scrum-команді натхнення, метрики в розробці
Ще 13 консенсус-протоколів для розподілених систем. Частина 2
Як побудувати посилальну стратегію сайту