JRuby + Ratpack
JRuby + Ratpack = ❤️
Многие разработчики на Ruby знают как обстоят дела с асинхронным выполнением кода на имеющихся серверах. Либо вы используете что-то на EventMachine, либо колдуете с Ruby::Concurrent, Celluloid. В любом случае время это работает не сильно эффективно из-за GIL (ждем, надеемся и верим в Ruby 3). Но есть реализации свободные от этой проблемы, одна из них реализация поверх JVM - JRuby, где теже самые библиотеки будут чувствовать себя гораздо комфортней. Много расписывать не буду, думаю все как минимум слышали про него. Главной особенностью данной реализации является легкая интеграция с любой библиотекой на JVM. Это открывает нам большой простор в выборе библиотек и готовых инструментов.
Так в мире Java есть библиотека, избавляющая нас от использования стандартной конкурентной Java модели на Executor, реализуя ее на Акторах. Звать библиотеку Netty. Позже на ее основе были разработаны другие, например Ratpack.
Ratpack асинхронный веб сервер, под капотом находится Netty, следовательно достаточно эффективно работает с подключения и в целом с IO и содержит в себе все необходимое для построения производительного сервера.
Поэтому используя возможности Ratpack, гибкость и простоту Ruby (JRuby), сделаем простейший сервис, который будет разворачивать нам короткие ссылки. В интернете есть ряд примеров, но они заканчиваются на то как запустить все это и получить простой ответ. Есть еще пример с подключением метрик (ссылка в конце), способ из документации абсолютно не пригоден для JRuby, так как приводится только для Groovy.
В данном примере рассмотрим:
- подключение библиотек
- создание сервера
- подключение метрик
- асинхронное выполнение запросов к внешним ресурсам
- тестирование нашего сервиса
- и зальем все это на heroku
Подключение бибилиотек
Каждый Ruby программист пользуется bundler, жизнь без него была грустна и полна плясок, грабель и других приключений. В мире Java есть различные сборщики, которые подтянут указанные зависимости и соберут приложение, но это не Ruby way. Так появился jbundler. Выполняет туже самую функцию что и bundler, но для java библиотек, после чего при загрузке они становятся доступны из JRuby. Красота!
И так, нам нужно подключить ratpack к нашему приложению. Нам достаточно будет только core. Остальное мы пока не используем.
Gemfile:
Jarfile:
В консоли выполняем
В дальнейшем добавим еще пару библиотек, но пока остановимся на этом.
Создание сервера
Загрузив все зависимости, создаем базовый сервер, проверим что все работает. Так как у нас нет Rack, то маршрутизацию будем будем делать используя штатные средства.
Для начала импортируем необходимые Java классы.
И объявим наш класс сервера:
Для нашего сервиса создали endpoint: status. Первый позволит проверить жив ли сервер в принципе, а второй будет выполнять основную задачу, разворачивать ссылки. Метод handlers, принимает блок, в которой передается интерфейс Chain, определяющий маршрутизацию. Для объявления status испольуем метод get, эквивалентный HTTP методу. Вторым аргументом передается объект реализующий интерфейс Handler. В нашем случае это модуль, в котором объявлен метод handle, принимающий текущий контекст. Как видите все достаточно просто и понятно. Никаких трехэтажных фабрик, или чего-то подобного.
Собственно сам обработчик, просто ответим что все OK:
Так же в Ratpack есть своя собственная реализация health check, но для нашего примера она избыточна.
Подключение метрик
Отслеживать статус нашего сервиса мы теперь можем, но хорошо бы еще узнать что с ним внутри, время ответа, количество запросов и другие показатели. Для этого нам нужны метрики. Ratpack имеет интеграцию с Dropwizard, для этого нужно добавить в наш Jarfile пару пакетов и установить их
Далее подключим его к нашему серверу. Выполняется это достаточно просто, достаточно лишь модифицировать несколько участков.
Зарегистрируем модуль в нашем Registry:
И загрузим его конфигурацию:
А еще мы хотим получать наши метрики через WebSocket, добавим handler для этого:
Готово, так же можно подключить выгрузку метрик в консоль либо в StatsD. Так как для вывода у нас теперь есть WebSocket, добавим и страницу для отображения. Схема стандартная, папка public, содерщая всю статику. Для отдачи ее пропишем дополнитеный маршрут, указав имя папки и индекснового файла:
Асинхронное выполнение запросов к внешним ресурсам
Сервер у нас заводится, слушает указанный порт и отвечает на запросы. Далее добавим endpoint, который будет возвращать нам все url, через которые проходит наша короткая ссылка. Алгоритм простейший, на каждом redirect сохраняем новый Location в массив, после чего возвращаем его.
Добавленный endpoint будет принимать как POST, так и GET запросы.
Если бы у нас был только блокирующий API, каждый запрос обрабатывался в своем потоке, ну как обрабатывался, 90% времени он бы ждал ответа от сервера, т.к. полезных вычислений у нас минимум. Но к нашему счастью, Ratpack является асинхронным сервером и предоставляет полный набор компонентов, в том числе асинхронный http клиент и Promise, какая асинхронщина без них. И так, создадим для каждой исходной ссылки Promise, который при удачном завершении вернет нам массив Location. Внутри же, запускаем GET по нашему URL и вешаем callback на получение нового Location от сервера. Таким образом поместим в наш массив целевой URL и все промежуточные.
Создаем HttpClient, которым будем собирать наши ссылки
Собираем все URL, переданные нам и если ничего нет, то сразу возвращаем Promise с пустой мапой.
Создаем параллельные запросы по всем переданным ссылкам:
Дождавшись их выполнения собираем результат и возвращаем его:
В итоге получили цепочку из Promise, которая асинхронно выполнит наш код.
Тестирование нашего сервиса
Настало врем протестировать то, что мы написали. Тестировать будет через старый добрый rspec, но с нюансами. Т.к. мы используем Ratpack + Promise, то тестировать в отрыве от библиотеки не получится, как то эти самые Promise надо выполнять, т.е. запустить eventloop. Для этого подключим дополнительную JAR библиотеку из комплекта:
Данная библиотека позволяет организовать как тестирование запросов (создание тестового сервера), так и просто выполнение Promise. Для последнего используется класс ExecHarness, в документации он подробно описан и примеры легко переносятся на JRuby. Мы же будем тестировать как выполняется наш GET запрос и воспользуемся EmbeddedApp, который позволяет запустить тестовый сервер. Есть различные статические методы, для упрощения создания под определенные случаи. Мы тестировать будем только наш обработчик, независимо от пути, поэтому создадим его следующим образом:
И выполним проверку, что все работает как надо:
Метод test запускает выполнение и передает в блок экземпляр TestHTTPClient, с помощью которого выполняется запрос. Далее проверяем получаенный ответ. Как видите все достаточно просто. В отличие от ExecHarness, EmbeddedApp на кажду проверку пересоздает сервер, в то время как ExecHarness запускает EventLoop лишь раз. Поэтому лучше максимально отделять код от работы с Ratpack Context, чтобы можно было его независимо тестировать.
Запуск на Heroku
После того как все готово запустим наш проект на heroku. Данная процедура практически ничем не отличается от запуска обычного ruby сервиса. Единственное отличие связано с тем, что нам нужно установить наши JAR библиотеки, а heroku не выполнит данную операцию автоматически. Для этого делается маленький хак. В принципе он везде описан, но для целостности повторю его и здесь. В процессе сборки выполняется сборки статики в том числе, поэтому воспользуемся этим и добавим следующй rake task:
Все, теперь при сборке, также будут установлены и указанные в Jarfile библиотеки.
Заключение
Как видите использовать Ratpack в связке с JRuby не так сложно, в то же время дает доступ ко всем возможностям JVM и Netty в частности. На его основе можно собрать высокопроизводительный асинхронный сервер. Все это production ready, тестирование на Hello World показывает до 25k rps на EC2 c4.large в docker контейнере после прогрева. Для прогрева выполялось порядка 30К запросов, на старте время плавает, но к окончанию уже стабильно. При этом даже с достаточно сложной логикой время выполнения запроса выполняется в считанные милисекунды. Это конечно зависит от задач, но даже просто замена Puma на Ratpack (тестировали для оценки времени), дало прирост в несколько раз. После полного рефакторинга и переосмысления кода и плотной оптимизацией на JVM, время сократилось на порядки. Так что кто ищет производительность Java и гибкость, скорость разработки Ruby и есть наработанный код рекомендую посмотреть на эту пару.
Ссылочки
- Github https://github.com/fuCtor/jruby_ratpack_example/
- Demo : https://jruby-ratpack-example.herokuapp.com/
- Еще один пример, из которого была взята страница с метриками и пример того как эти метрики в принципе подключить: https://github.com/jkutner/ratpack-jruby-example
- Ну и сам виновник: https://ratpack.io/