Добро пожаловать в третий пост серии о Flowable Async Executor. После того, как в первой части были описаны базовые концепции, а во второй части рассмотрены различные компоненты и их настройки, теперь пришло время ответить на вопрос, который все ждут: насколько он быстр?

Настройка бенчмарка

Когда речь заходит о производительности Async Executor, главным показателем является пропускная способность заданий и таймеров. Проще говоря: чем больше заданий или таймеров выполняется за определённый промежуток времени, тем быстрее смогут продолжать выполнение процессы или кейсы, которые их используют.

Async Executor постоянно занимается обработкой всех новых асинхронных заданий или таймеров, срок которых наступил. Чтобы действительно протестировать систему, нужно привести её в состояние, когда большое количество заданий или таймеров «готовы к обработке», и затем дать Async Executor выполнить свою работу. Для этого мы написали простое приложение на Spring Boot + Flowable, которое делает именно это.

Исходный код доступен на Github, и его можно использовать, если вы хотите воспроизвести и/или проверить результаты самостоятельно или сравнить их с другими реализациями.

Конечно, когда речь идёт об асинхронных заданиях и таймерах, есть важное замечание: время, необходимое для их «обработки», зависит от фактической логики, которая выполняется, и того, что следует далее в BPMN или CMMN модели. Например, если асинхронное задание используется для вызова стороннего сервиса, который работает медленно, пропускная способность заданий снизится, потому что потоки будут заняты ожиданием ответа сервиса. Аналогично, если после таймера или асинхронной задачи идут один или несколько ресурсоёмких шагов, которые не помечены как асинхронные, все они будут выполняться в том же потоке и также негативно скажутся на пропускной способности.

Учитывая это, бенчмарк-приложение генерирует три различных типа данных. В каждом случае создаётся один миллион заданий определённого типа перед тем, как запустить Async Executor. Таким образом, у нас есть:

  • Один миллион асинхронных заданий с фиксированным временем выполнения (например, 100 миллисекунд). Это позволяет проверить, насколько близко Async Executor может подойти к теоретическому максимуму, так как фиксированное время и размер пула потоков позволяют рассчитать этот максимум;

  • Один миллион таймерных заданий, готовых к срабатыванию;

  • Один миллион асинхронных заданий, которые ничего не делают (no-op), а следующий шаг в BPMN-модели — это простое состояние ожидания. Это показывает чистые накладные расходы Async Executor и даёт представление о возможной максимальной пропускной способности.

Обратите внимание, что такая конфигурация проверяет как получение заданий, так и их выполнение. В корректно настроенной системе не должно оставаться миллиона заданий в очереди, поскольку это означает, что система не смогла обработать их своевременно (по крайней мере, асинхронные задания), так что приведённые здесь цифры на самом деле отражают худший сценарий.

Информация о системе

Тестирование производилось на Amazon Web Services (AWS) с использованием экземпляров Elastic Cloud Compute (EC2), на которых запускался Flowable с базой данных Amazon Relational Database Service (RDS).

Конфигурация EC2-инстансов была следующей:

  • Операционная система: Ubuntu 2020.04 LTS

  • Движок Flowable: 6.7.0-SNAPSHOT

  • Тип инстанса: C5.2xlarge (8 виртуальных CPU / 16 ГБ ОЗУ)

  • JDK: версия 11.0.10 (AdoptOpenJDK)

Для RDS использовалась следующая конфигурация PostgreSQL:

  • Версия: Postgres 13.1-R1

  • Тип инстанса: db.m6g.8xlarge (32 виртуальных CPU, 128 ГБ ОЗУ)

  • Выделенные IOPS: 30 000

Да, это значительные вычислительные ресурсы для базы данных, так как мы не хотели, чтобы она стала узким местом.

Мы также запускали тесты на Oracle RDS, но лицензия Oracle запрещает публиковать результаты бенчмарков.

Бенчмарк: задачи с фиксированным временем выполнения

Первый бенчмарк, который мы рассмотрим, — это ситуация, когда все миллион заданий (в модели BPMN: старт → асинхронное задание → состояние ожидания → конец) имеют фиксированное время выполнения 100 миллисекунд. Это означает, что каждый поток в пуле потоков будет заблокирован на 100 миллисекунд, что является значительным временем для современного процессора.

Результаты:

Если понятия размера выборки (acquire size) или потоков (threads) вам ни о чём не говорят, пожалуйста, посмотрите предыдущий пост, где мы подробно их описывали. В этой конфигурации мы также использовали размер очереди 8192. Мы используем пул потоков из 128 потоков. На современном оборудовании и операционных системах, при достаточном количестве операций ввода-вывода, это обычно не вызывает проблем.

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

Также очевидно, что добавление дополнительных экземпляров Flowable Async Executor увеличивает пропускную способность и масштабируемость, что как раз и ожидалось от реализации Global Acquire Lock. Интересно и то, что изменение размера выборки (сколько заданий выбирается за один сетевой запрос) здесь значительно повышает пропускную способность. Конечно, это зависит от конкретного сценария использования, но в данном случае эффект явно положительный.

Последний столбец также очень важен: он сравнивает теоретический максимум (число экземпляров x 10 в секунду, так как мы используем фиксированное время 100 мс и число потоков). В предыдущей архитектуре этот показатель быстро снижался при добавлении новых экземпляров. В новой архитектуре мы видим, что он остаётся стабильным и даже достигает 82%, если оптимизировать размер выборки. 82% от теоретического максимума — это отличный результат, особенно если учитывать задержки базы данных и сети.

Это доказывает, что стратегия Global Acquire Lock отлично подходит для распределения нагрузки между несколькими экземплярами Flowable.

Бенчмарк: задания-таймеры

Во втором бенчмарке было сгенерировано один миллион таймеров (использовался процесс: старт → задача с таймером → задача → конец).

Результаты:

Мы видим, что в оптимальной конфигурации Async Executor обрабатывает более 2,5 тысяч таймеров в секунду. Таймеры обрабатывать сложнее, чем асинхронные задания, поэтому мы ожидали, что их обработка будет медленнее. Тем не менее, более 2,5 тысяч таймеров в секунду — это впечатляющий результат. Если выразить этот показатель в пропускной способности за час, то речь идёт почти о 10 миллионах таймеров в час (точнее, 9,7 миллиона).

Бенчмарк: максимальная пропускная способность

В последнем бенчмарке мы хотели посмотреть, что произойдет, если задание выполняется максимально быстро, то есть логика задания — это пустая операция (не совсем на 100% пустая: логика задания выполняет простое выражение ${true}, то есть выражение всё равно парсится и исполняется). Фактически, этот тест измеряет накладные расходы Async Executor, то есть показывает, насколько эффективно происходит выборка и выполнение заданий.

Крайне важно убедиться, что стабильный поток заданий, проходящий через всю цепочку компонентов, не прерывается. В первом бенчмарке это было проще, потому что выполнение задания за 100 миллисекунд давало больше времени для заполнения внутренней очереди и выборки новых заданий. В случае с пустым заданием Async Executor должен постоянно работать над тем, чтобы быстро получать новые задания и поддерживать очередь заполненной, чтобы потоки не простаивали.

Давайте посмотрим на результаты:

Сравнивая результаты с первым бенчмарком, мы видим, что первые две строки находятся примерно на одном уровне. Это может означать, что мы упёрлись в пределы производительности оборудования в сочетании с выбранными настройками. Это также объясняет, почему результат в третьей строке примерно в четыре раза выше, чем в первой: настройки одинаковы, за исключением количества экземпляров Flowable. Похоже, в этом случае можно ускорить выполнение, если выбирать больше заданий за раз, поскольку, скорее всего, некоторые потоки простаивали.

Именно это и подтверждает четвёртая строка: при удвоении размера выборки мы видим значительный рост пропускной способности — более 6000 заданий в секунду.

Можно ли сделать ещё быстрее? Скорее всего, да. Быстрые тесты показали, что изменение некоторых параметров, например, уменьшение времени ожидания Global Acquire Lock, увеличивает пропускную способность (на несколько сотен заданий в секунду). Однако на этом этапе появляется риск чрезмерной нагрузки на базу данных, что никогда не является хорошей идеей.

Действительно, если посмотреть на загрузку CPU экземпляра RDS во время выполнения бенчмарка в этой конфигурации, она ни разу не превысила 55%.

Это означает, что запас для оптимизации всё ещё оставался. Однако, чтобы оценить приведённый выше результат: речь идёт о 365 тысячах заданий в минуту, почти 22 миллионах заданий в час и более полумиллиарда заданий в сутки.

Из любопытства мы провели тот же бенчмарк с отключённым глобальным блокировщиком выборки (global acquire lock). Это привело к следующему результату:

ТАБЛ

Это примерно 1/6 от пропускной способности по сравнению с запуском при включённом Global Acquire Lock. Важно отметить, что это уже не тот же самый Async Executor, который был в Flowable 6.6.0, поскольку мы внесли оптимизации во весь связанный код, получив ценный опыт в процессе. В результате даже «старая» архитектура также получит прирост производительности в предстоящем релизе движка 6.7.0.

Заключение

Мы очень довольны впечатляющими показателями пропускной способности — тысячи заданий или таймеров в секунду — которые достигаются в таких конфигурациях с новой архитектурой Async Executor. Это подтверждает наши предположения и эксперименты: новая архитектура действительно является серьёзным улучшением по сравнению с предыдущей реализацией. Как описано во второй части, Flowable очень гибок в плане настройки различных компонентов Async Executor. Это доказывает, что понимание этих конфигураций и особенностей среды, в которой они работают, действительно приносит отличные результаты.

Следующее поколение Async Executor войдёт в предстоящий открытый релиз 6.7.0. Если вы хотите попробовать его уже сейчас, вы можете получить и собрать исходный код из основной ветки на Github. Для корпоративных клиентов поддерживаемая версия уже доступна в недавнем релизе 3.9.0.

На этом этапе мы изложили практически всё, что хотели рассказать миру. Само собой разумеется, что мы чрезвычайно гордимся проделанной работой и достигнутыми результатами. Мы уверены, что каждый пользователь Flowable будет рад этим изменениям. Ведь кому не нравятся более быстрые процессы и кейсы или меньшее количество серверов при той же пропускной способности ;-)?

Однако впереди осталась ещё одна публикация в этой серии: как мы пришли к этой последней итерации Async Executor. Ведь если посмотреть на историческую эволюцию Async Executor, можно выделить три поколения архитектуры, а Global Acquire Lock стал четвёртым. И на каждом этапе мы делали шаг вперёд в понимании и расширяли возможности Async Executor.

Об авторе:

Joram Barrez Principal Software Architect

Ключевой разработчик Flowable с более чем десятилетним опытом работы с open source и построения масштабируемых процессных движков. Сооснователь проекта Activiti (на базе которого создан Flowable), а до этого был участником команды JBoss jBPM.

Jmix.ru — платформа быстрой разработки B2B и B2G веб-приложений на Java.
BPM Developers — про бизнес-процессы: новости, гайды, полезная информация и юмор.

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