×Закрыть

Разработка реактивных и распределенных систем с 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, провести несколько экспериментов с ним, а может даже разобраться детально, так как он может оказаться полезным уже в следующем вашем проекте.

LinkedIn

4 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Жаль, что в статье не было сравнения Vert.x с другими реактивными технологиями. Взять тот же Spring. Там есть и проект Reactor, и Spring WebFlux, и реактивные репозитории(Redis, MongoDB). И все это работает через EventLoop(как и в Vert.x).В чем же Vert.x лучше и предпочтительнее?

А де мінуси вертекса? А то виглядає як срібна куля, але взагалі не є такою.

1-ий мінус — треба спочатку переставити свою голову в режим думання асинхронщиною.
2-ий мінус — на Вертекс реально сильно вплинув Нод.ЖС, і тому ДжаваРІкс там не з коробки використовується. З коробки — все на колбеках. На щастя можна врятуватись лямбдами.
3-мінус — це бібліотека а не всемогутній Спрінг, де все автомагічно саме-собою робиться. Тут інколи треба подумати головою.
4-мінус — кластеризація і горизонтальне розширення 3-4 роки тому працювали так собі. Інколи шось не працювало, а шось десь дівалось в мережі, і більше не знаходилось., тобто — нет-спліти були проблемою, але цим сам вертекс не займався, тільки хейзлкаст чи шось інще, шо болу під капотом.

На самом деле- минусов у vert.x тоже хватает.
Я с этим vert.x — собаку съел. Но нужно понимать- что все описанные проблемы- это то что всплывает при реально очень больших нагрузках- порядка 1K rps.
Из того что сразу могу назвать:

1. Утечки памяти. Берем асинхронный клиент Redis идущий в поставке с vert.x. Шикарная вешь- отлично работает в асинхронном режиме с обычной кофигурацией Redis типа — vert.x клиент- редис. Но в случае- когда редис скофигурирован работать в Sentinel конфигурации, да и еще и под высокой нагрузкой — начинаются утечки памяти. Покопавшись в дебрях стектрейсов — понял что vertx redis client не сразу закрывает соединения- даже после того как код вышел за область исполнения redis related кода при возникновении таймаутов в запросах к редису. В свое время пришлось брать Jedis- так как он работает надежнее при любых нагрузках.
2. Rx обертки для vert.x тоже текут- или я совсем не умею их готовить
3. Писать код на колбеках — сложно. Еще сложнее потом это чинить и поддерживать/оптимизировать.
4. Hazelcast — что используется для distributed event bus- отлично ведет себя на тестовых примерах- но в на больших нагрузках- создаёт проблемы:
4.1. Надежность. Потерять сообщение? Легко.
4.2. Несвоевременно освобождение сокетов? Обязательно.
4.3. Конфигурация сокет пула- это отдельное приключение. Особенно — когда надо убедить Operations team- открыть порядка 1000 портов для общения между кластером vert.x- - та еще забава А это надо делать для каждой ноды в кластере.
5. Непонятки с рандомными ошибками «port in use» — я так и не понял куда копать. То ли проблема в Hazelcast, или OS закрывает сокеты недостаточно быстро, а приложение уже закрыло и думает- что сокет освобожден и можно переиспользовать. Вообщем- хз.
6. ЧАВО — нечаво. просто оставлю этот тут stackoverflow.com/questions/tagged/vert.x Создатель vert.x неподелил что то с stackoverflow и в итоге искать ответы на проблемы приходится в документации/исходниках и гугл группах но не на stackoverflow .
7 Идея distributed vertices в эпоху k8s & docker — слишком переусложнена. Горизонтально смаштабировать приложение намного легче средствами k8s, чем встраивать в само приложение.
8. Vert.x асихронные колбэки сложно/невозможно мониторить многими популярными системами мониторинга. Например я был к команде тех — кто потребовал от appdynamics реализовать поддержку Vert.x в их продукте года 3 назад, до этого- appdynamics java client ничего почти не видел в vert.x кластере.
9. C10k problem — всплывает если пишешь highload на vert.x без предварительной подготовки.

10. Vert.x поддерживат множество языков программирования. Например JavaScript. Но писать Vert.x приложение на EcmaScript 5 (на тот момент nashorn engine -поддерживал только 5 версию)- это очень малоприятное занятие- не говоря уже о том что документации и примеров- примерно «0» (не спрашивайте зачем мне это понадобилось- было такое требование)

Но хватит о проблемах. Vert.x действительно отличный фреймворк. Я большой фанат этой штуки и уже сделал множество решений на Vert.x.
Если надо сделать быстрое, малотребовательное, достаточно низкоуровневое (если это слово можно применить к java) — приложение— Vert.x это ваш выбор.

Подписаться на комментарии