Разработка реактивных и распределенных систем с 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) приложений. Эта модель называется Verticle. Что же это такое? Еще одна адаптация какого-то классического паттерна? Не поверите, но почти да. Verticle — это контейнер (не 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 кода, часть вашего приложения. Чуть далее мы узнаем, зачем нужно такое извращение. А пока давайте разберемся, как эта штука работает и как она вообще деплоится.

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

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 уже позволяет делать все описанное. И более того, в ней еще есть:

  • свой интерфейс для работы с файловой системой (синхронно и асинхронно);
  • свой интерфейс для получения доступа к распределенным структурам данных (Map, Lock, Counter);
  • интерфейс для разработки TCP, HTTP и UDP серверов и клиентов;
  • DNS-клиент;
  • Launcher, который позволяет создавать так называемые fat-jar, где точкой входа будет Main Verticle.

Как видите, это уже немало. И это только одна библиотека. А ведь Vert.x — это целая экосистема. Далее в статье я приведу краткий обзор других библиотек, а детальнее о них можно прочитать в официальной документации.

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

Авторизацию и аутентификацию позволит сделать подключаемый Vert.x Auth. Он умеет работать с 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, провести несколько экспериментов с ним, а может даже разобраться детально, так как он может оказаться полезным уже в следующем вашем проекте.

Похожие статьи:
Привіт, мої любі сішники! Сьогодні випуск буде присвячено відладці у Visual Studio та Visual Studio Code. Тож почнімо? :) Debug у Visual Studio та Visual Studio Code Visual...
У травні воєнний стан та мобілізацію в Україні продовжили ще на три місяці, тобто до 18 серпня 2023 року. Нині триває поповнення лав ЗСУ...
С наступившим Вас! Курс «Python/Django с нуля» стартует 15 января 2016, цена первого (обзорного) занятия — 100 грн, если Вам понравится...
Український онлайн-банк monobank із 18 квітня 2022 року закриває рахунки понад 11 тисяч клієнтів, які нині перебувають у росії,...
Привіт! Ми Юля і Юра, попередні два роки жили в Харкові та працювали Test Automation інженерами у компанії SoftServe. Пів року...
Яндекс.Метрика