В течение последнего года мы готовились к выпуску первого стабильного релиза Hibernate Reactive и пытались ответить на некоторые вопросы, которые у нас возникли.

Анализ производительности и масштабируемости

Нам было интересно узнать, можем ли мы действительно показать, когда стоит переходить на Hibernate Reactive при работе с реляционными базами данных: как для нашего собственного понимания, так и для того, чтобы помочь каждому из вас принять разумное решение.

Выяснилось, что ответить на такие вопросы непросто, поскольку базы данных рассчитаны на классическое взаимодействие; это отражается на разных уровнях - от низкоуровневого проектирования протоколов до более высокоуровневых конструкций, таких, как крайне желательная необходимость инкапсулировать юниты работы (units of work) в ACID-транзакции, которые моделируются путем определения явных границ выполняемой "работы": транзакция начинается в момент времени A, заканчивается в момент времени B, и все операции, поставленные в очередь на одно и то же соединение в пределах этих маркеров, рассматриваются как участвующие в процессах этого диапазона.

Это означает, например, что начало такого "диапазона" часто связывает физическое соединение, делая его недоступным для мультиплексирования с другими реактивными потоками, как это обычно делают реактивные технологии.

Другим следствием является то, что такие "временные маркеры" не позволяют использовать интерактивное взаимодействие для работы с непрерывными потоками: да, вы можете работать с потоками и курсорами, но при этом все равно нужно отмечать начало и конец рабочих областей.

Я слишком упрощаю, чтобы дать представление о трудностях, поскольку детали протоколов и возможностей баз данных значительно отличаются в разных имплементациях (и я сам все еще изучаю их) — но это также часть нашей проблемы: мы работаем сейчас на таком низком уровне, где подобные вещи имеют значение, тогда как при работе со стандартным JDBC с установленными паттернами взаимодействия они уже не так важны.

Таким образом, хотя Hibernate Reactive выигрывает от полностью реактивного дизайна клиентских библиотек Vertx SQL, некоторые интерактивные паттерны все еще ограничены требованиями базового протокола; это означает, что не все случаи использования на самом деле работают лучше или вообще отличаются от использования традиционного стека на основе Hibernate ORM, драйверов JDBC и пулов соединений JDBC, поскольку существуют ограничения ресурсов, которые могут одинаково влиять как на императивные, так и на реактивные стеки.

Итак, когда стоит использовать Hibernate Reactive?

Возникает вопрос, способны ли наши усилия в этой области действительно продемонстрировать значительные преимущества по сравнению с использованием классического Hibernate ORM.

Но прежде чем перейти к обсуждению производительности, есть прямой ответ: если ваше приложение уже является реактивным по своей сути, то, разумеется, интегрировать его с таким реактивным фреймворком, как Hibernate Reactive, будет проще, чем пытаться сделать это с традиционным блокирующим Hibernate ORM.

Так что если вы создаете приложение Vert.x или используете Quarkus с другими предоставляемыми им реактивными компонентами, такими как RESTEasy Reactive, то на данный вопрос есть простой ответ: несомненно, предпочтительнее использовать Hibernate Reactive.

Теперь давайте рассмотрим метрики производительности, поскольку мы также хотим понять, может ли использование этой парадигмы быть более эффективным.

Тесты Techempower

В этом бенчмарке из популярного пакета Techempower конечная точка веб-сайта многократно подвергается высокой нагрузке, и при этом производится измерение пропускной способности.

Так как нам нужен веб-сервер, потребуется упаковать полное приложение; мы выбрали Quarkus, так как хорошо знакомы с ним, а также потому, что он имеет встроенные расширения интеграции для Hibernate ORM "классический" и Hibernate Reactive, что позволяет сравнивать переменные, имеющие наибольшее значение для нас в данном контексте.

Quarkus также способен работать в "чисто реактивном" режиме — по крайней мере, в сочетании с Hibernate Reactive — так что это должно позволить реактивному компоненту использовать отсутствие пулов исполнителей и избежать накладных расходов на диспетчеризацию потоков.

N.B.  Хотя пакет Techempower ориентирован на пропускную способность, мы больше заинтересованы в достижении разумной задержки ответа и изучении того, как задержка меняется по мере увеличения нагрузки.

Пожалуйста, помните об этой разнице в методологии измерений при сравнении с другими результатами, включая цифры на самом сайте Techempower: они суммируют результаты в баллах (пропускная способность), что полезно для сравнения различных систем (что и является их целью), но обычно нас больше интересует возможность применения технологии в реальном мире, где важнее всего оценить, сколько нужно заплатить, чтобы достичь определенного SLA (Service Level Agreement), выраженного в терминах приемлемого времени отклика, при ожидаемой нагрузке, которой будет подвергаться система.

Тесты Techempower: Множественные запросы

В сценарии "запросы" бенчмарка Techempower, при каждом веб-запросе нам необходимо загрузить 20 объектов; не допускается загрузка 20 объектов с помощью одного оператора select: должны быть выполнены 20 отдельных, индивидуальных операций загрузки.

Такое правило представляется спорным в отрыве от контекста, поскольку в реальном приложении загрузка 20 элементов с помощью одного цикла будет намного лучше; тем не менее, интересно отметить именно такой сценарий, так как данный подход можно использовать для моделирования нетривиального паттерна доступа к данным при написании обычного кода для бенчмаркинга (что также представляется сомнительным, но давайте будем прагматичны): в вашем реальном приложении все последующие операции загрузки могут быть следствием некоторой бизнес-логики, которая зависит от предшествующих загрузок. Например, можно и не знать, какую вторую сущность нужно загрузить, пока не будет обработана первая, и так далее. Это приводит к "цепочке" взаимодействий IO с базой данных, которые необходимо разрешить, чтобы удовлетворить один веб-запрос; поэтому, хотя сценарий создан искусственно, он все же может предоставить интересное понимание и возможность сравнения с другими фреймворками и подходами.

На оси x показана нагрузка, выраженная в запросах в секунду; мы видим, как время отклика двух стеков становится медленнее по мере увеличения нагрузки:


Рисунок 1. Бенчмарк Techempower, множественные запросы. На этом графике показана задержка: более высокие цифры — хуже.
Рисунок 1. Бенчмарк Techempower, множественные запросы. На этом графике показана задержка: более высокие цифры — хуже.

Итак, предположим, что у вас есть SLA, которое требует доставки ответов в течение 10 мс; отсюда приходим к выводу, что "классический" Hibernate ORM способен выполнять требования SLA до тех пор, пока нагрузка на эту единственную машину не превышает предела 20 000 запросов в секунду — это довольно прилично, но после этого задержка ответов начинает резко возрастать, что приводит к ухудшению работы.

С другой стороны, используя Hibernate Reactive, мы можем получить ответы в пределах того же SLA в 10 мс даже при нагрузке свыше 35 000 запросов в секунду. Это явно лучше — и с существенным преимуществом.

Данный конкретный результат был достигнут на одном из наших настольных компьютеров; специфика оборудования не имеет значения, как и абсолютные цифры: давайте сосредоточимся на сравнении двух стеков, которые были нагружены на одном и том же оборудовании и работали в одинаковых условиях, с одной и той же базой данных PostgreSQL; мы получили аналогичные результаты в Red Hat Performance Lab: конечно, абсолютные цифры будут отличаться на разном оборудовании, но форма графика сопоставима и показывает, что подход эффективен, по крайней мере, в этом конкретном сценарии.

Для выполнения этих тестов мы использовали последнюю версию Quarkus: v. 2.4.0.CR1.

Тесты Techempower: Одиночный запрос к БД

В очередном сценарии от Techempower нам нужно загрузить только одну сущность из базы данных, а затем приступить к кодированию ответа.

Рисунок 2. Бенчмарк Techempower, одиночные запросы. На этом графике показана задержка: более высокие цифры — хуже.
Рисунок 2. Бенчмарк Techempower, одиночные запросы. На этом графике показана задержка: более высокие цифры — хуже.

Как видите, польза от того, что данное приложение было написано с использованием реактивного стека Quarkus, не столь очевидна.

Можно сделать вывод, что, хотя реактивное программирование обладает некоторыми неоспоримыми достоинствами, оно не способно волшебным образом повысить производительность каждого приложения.

Однако у реактивного программирования есть и другие преимущества, помимо производительности; например, систему легче настроить, поскольку не нужно выяснять, как конфигурировать пулы исполнителей, и система способна лучше реагировать на ситуации, когда нагрузка чрезмерна: в традиционной модели блокирования ошибка в таких настройках или внезапный неожиданный всплеск нагрузки могут привести к тому, что система перестанет реагировать.

Кроме того, хоть сегодня различия незначительны, мы оптимистично настроены на то, что дальнейшие улучшения в этом молодом проекте в будущем могут сделать их более заметными.

Рисунок 3. Бенчмарк Techempower, пропускная способность. Этот график показывает пропускную способность: более высокие цифры лучше. Горизонтальная ось представляет собой количество клиентов/потоков.
Рисунок 3. Бенчмарк Techempower, пропускная способность. Этот график показывает пропускную способность: более высокие цифры лучше. Горизонтальная ось представляет собой количество клиентов/потоков.

Улучшения за год

И напоследок давайте посмотрим, как улучшился Hibernate Reactive с момента его первого включения в Quarkus и Techempower, примерно год назад:

Рисунок 4. Бенчмарк Techempower, улучшения с момента выхода бета-версии Earfly. Этот график показывает пропускную способность: более высокие цифры лучше. Горизонтальная ось представляет количество клиентов/потоков.
Рисунок 4. Бенчмарк Techempower, улучшения с момента выхода бета-версии Earfly. Этот график показывает пропускную способность: более высокие цифры лучше. Горизонтальная ось представляет количество клиентов/потоков.

На момент написания этой статьи репозиторий Techempower все еще зависит от этой довольно старой версии; нам нужно будет обновить ее в ближайшее время, чтобы отчеты показывали реальные цифры Hibernate Reactive 1.0.0.Final.

N.B.  Мы не можем присвоить себе все заслуги в повышении производительности: хотя операции, связанные с базой данных, являются основным приоритетом в этом конкретном бенчмарке, достигнутый результат также является свидетельством улучшения Quarkus v2 по сравнению с Quarkus v1, а также использования Vert.x v4 вместо Vert.x v3. Оба этих фреймворка также значительно развились, и более высокие показатели являются результатом комбинации усовершенствований в трех стеках и их интеграции.

Будущие улучшения

На этом мы не закончили: Hibernate ORM работает замечательно, и, будучи устоявшейся технологией, в нем легко найти множество вариантов интеграции, с которыми можно экспериментировать, существуют различные инструменты диагностики производительности, которые интегрируются с ним; кроме того, мы накопили многолетний опыт настройки и можем оптимизировать его достаточно быстро.

Наш опыт работы с Hibernate Reactive и реактивным программированием в целом гораздо более ограничен, поскольку это относительно молодой проект; мы получили фантастическую помощь и руководство от команды Vert.x, но есть еще много тестов, которые можно провести, и каждый новый бенчмарк потенциально может привести нас к еще большему улучшению молодого проекта Hibernate Reactive.

Уже сейчас мы уверены, что есть, по крайней мере, несколько сценариев, в которых Hibernate Reactive действительно может быть лучшим выбором, и различия кажутся достаточно очевидными, чтобы понять, что вам стоит познакомиться с новым стеком, а нам - продолжать совершенствовать его дальше.

Получение Hibernate Reactive 1.0.0.Final

Все подробности и документация доступны и обновляются на специальной странице на hibernate.org.


Материал подготовлен в рамках специализации "Java Developer"

Комментарии (0)