Всем привет! Меня зовут Николай Первухин, я Senior Java Developer в Райффайзенбанке. В последнее время я активно занимаюсь BPM-системами Camunda и Zeebe (основа Camunda-cloud). Если вы, как и я, с ходу не можете ответить на вопрос, кто быстрее — Camunda или Zeebe, насколько, и в каких случаях они могут тормозить, — то вам будет очень интересно прочитать эту статью. В этом материале я попытаюсь оценить производительность систем Camunda и Zeebe с различными параметрами, коснусь классических систем по workflow — Apache Camel и Spring Integration, а также постараюсь предложить гибридное решение для повышения производительности. Часто со стороны бизнеса можно услышать вопрос, что будет, если поток данных возрастет — справится ли наша система с этим. Вот и мне стало интересно, так что давайте немного поэкспериментируем на простом workflow: сделаем несколько действий и осуществим rest-вызов в каждом из них. Вызывать будем статический тестовый файл test.json с содержимым: {}, который будет выдавать локальный nginx. Немного об оборудовании: в моем распоряжении 4х ядерный Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz, SSD диск и 24Gb памяти. Итак, наши кандидаты: Признаюсь, это вообще не BPM. Но этот фреймворк настолько популярен, что стоит начать рассказ для человека, который не в теме BPMN, и его глаза вспыхивают пониманием — это же про Apache Camel! Действительно, задачу он решает похожую, поэтому давайте рассмотрим его с должным уважением. Вот воркфлоу, который будем исполнять: Будем производить тестовый REST-вызов: random.nextLong поможет препятствовать кешированию запросов. Ссылка проекта на gitlab С ростом популярности Spring / Spring boot значимость Apache Camel снижается в сторону Spring Integration. Фреймворк тоже не BPM, но нам важны референтные значения для сравнения производительности. Для лучшей схожести с BPMN мы будем использовать описание воркфлоу (контекста) в виде xml, сделаем два канала и два сервис активатора: Ссылка проекта на gitlab Пока рассмотрим самый быстрый вариант Camunda — мы отключим историю вообще, и будем использовать in-memory h2 базу данных. Наш «сложный» процесс выглядит так: Код для RestDelegate достаточно прост: Ссылка проекта на gitlab. Для работы активируем профиль h2mem. Zeebe появился относительно недавно: первый стабильный релиз, пригодный для использования, вышел в 2019. В отличие от Camunda, он использует не транзакционную, а распределенную базу rockdb под капотом.  По заявлению Бернда Рюкера, увеличение производительности в ней достигается путем создания дополнительных инстансов в кластере. Интересное было выступление, кстати. Судя по графику, при одном инcтансе результаты должны быть в пределе 1 тыс. процессов в секунду. Будем использовать данный процесс: Наш воркер: Запуск инстанса будем производить через docker: Если вам потребуется вынести папку с данными, используйте параметр: Небольшой тюнинг (в рамках 1 иснтанса): ZEEBE_BROKER_CLUSTER_PARTITIONSCOUNT — число партиций кластера (по умолчанию это 1) определяет, на какое количество частей или папок разбивать rockdb для дальнейшей синхронизации. В зависимости от скорости работы SSD или жесткого диска, можно подобрать лучший параметр. В данном случае устанавливается 24, потому что дальнейшее увеличение дает скорее ухудшение. При этом два дополнительных параметра не оказывают большую роль, так как процесс относительно простой: ZEEBE_BROKER_THREADS_CPUTHREADCOUNT (2) — количество потоков на вычисления. ZEEBE_BROKER_THREADS_IOTHREADCOUNT (2) — количество потоков для ввода/вывода. Мы ставим их по умолчанию. Клиент для Zeebe с воркером можно найти на gitlab. Для замера нагрузки будет произведен запуск 100 тыс. процессов, при этом с интервалом 10 тыс. мы планируем замерять время выполнения. Прошу простить меня за неточность, но начало отсчета будем фиксировать не с 0, а с 1 тыс. процессов. Это важно, чтобы оценить, насколько быстро происходит прогрев системы. Результат нагрузки: Объясню полученные результаты: Apache Camel и Spring Integration вне конкуренции, когда сложность процесса небольшая и нет изменчивости. Так что +100 очков команде Apache Camel, они даже немного обошли Spring Integration. Здесь было важно показать, что условные 4,2 тыс. процессов — наилучший возможный результат на 1 инстансе. Camunda без истории и in-memory показывает достаточно хорошую производительность.  Zeebe даже без экспорта на одном инстансе более чем скромен — как и ожидалось. Все вышеописанные результаты получены при полном отсутствии логирования — это как раз тот случай, когда нам не требуется контролировать результаты. Теперь нам требуется посмотреть, как проходил процесс, какие таски запускались и какие были переменные. В этом случае мы должны включить логирование процесса — или экспортирование, говоря в терминах Zeebe. Camunda поддерживает транзакционные SQL-базы. Канонической для Camunda базой является PostgreSQL — это наиболее частый случай использования, которую будем использовать в нашем эксперименте. Базу данных подключаем в виде готового докер контейнера, тут все стандартно: Если требуется вынести папку с данными, используйте параметр: Дальше мы переключаем профиль в pure-camunda на PostgreSQL. В Zeebe подключаем готовый экспортер для ElasticSearch. Данные из Elastic потом можно увидеть в Zeebe:Operate (аналог Camunda Cockpit). Подключаем ElasticSearch тоже через docker-контейнер. Для простоты мы будем использовать сеть рабочей машины, чтобы Zeebe и ElasticSearch могли общаться напрямую. Если требуется вынести папку с данными, используйте параметр: В самом Zeebe подключаем стандартный экспортер в ElasticSearch: Из новых параметров среды: ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_CLASSNAME — тип экспортера ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_URL — адрес ElasticSearch ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_BULK_SIZE — размер пачки экспортируемых сообщений А вот и результаты: Объясню полученные результаты: Результат с Camunda изменился сильно, ухудшение почти в три раза. Более того, идет деградирование с увеличением данных в базе. Это связано, в основном, с ребилдом индексов, которые Camunda активно использует. А вот у Zeebe значения почти не изменились — экспорт оказал минимальное влияние на время исполнения. Результат скромный, но стабильный.  Экспортируемые данные накапливаются и отправляются в систему хранения пачками по таймеру, либо по мере накопления такой пачки. Здесь достигается двойной эффект: Экспорт вынесен отдельно от процессинга. Если данные вдруг перестанут экспортироваться — процессинг все равно идет своим чередом. Добавление данных большими объемами (batch mode) почти в любых системах хранения эффективнее, чем по одной записи. Давайте подумаем, как создать гибридную систему, чтобы она была: Такая же масштабируемая, как Zeebe Быстрая как in-memory Camunda. Лучше быстрее, чем Apache Camel, но это мечты :) С возможностью экспорта данных, чтобы визуально было видно, как отработал процесс. Помните, как в фильме «Марсианин» снимали с ракеты все ненужное? Избавляемся от тяжелых компонентов, оставляем только движок Camunda. В состав pom включаем компоненты: Spring boot, spring data и web-services Только модуль движка Camunda: Драйверы баз данных h2, clickhouse Средство развертывания базы flyway Из интересного (надеюсь): Конфигурация движка Camunda. Используем StandaloneInMemProcessEngineConfiguration — думаю, название говорит само за себя: Разрабатываем свой собственный регистратор исторических событий CustomHistoryHandler: Аналогично перехватываем начало и завершение задач в процессе, а также создание и изменение переменных. Здесь очень важным моментом является, что метод полностью подменяет сохранение исторических данных — это значит, что в памяти исторические данные мы больше не храним. Идею с экспортером данных честно позаимствуем у Zeebe. Будем накапливать события в LinkedBlockingQueue. Метод *.export вынесен как интерфейс для поддержки различных систем хранения. Так было сделано в Zeebe, мы же просто позаимствуем этот принцип. Переключать систему можно профилем spring-boot. Для эксперимента я сделал три различных экспортера: В каноническую базу данных H2. На ней легко тестировать, не требует установки В ElasticSearch. Наверное, это один из самых быстрых индексаторов json-данных, который можно рассматривать как быстрое и масштабируемое хранилище В Clickhouse. Эта база данных прекрасно себя зарекомендовала при быстрой вставке больших массивов данных, по описанию — то, что нам нужно. Рассмотрим на примере h2: Сам же метод execute добавляет параметры в jdbc-запрос в зависимости от типа и выполняет запрос: Фронт у системы тоже должен быть. Для этого создадим rest-api и различные методы извлечения данных для каждой системы хранения. Фронт сделан минималистично, он позволяет: Просматривать и устанавливать новые bpmn-процессы Смотреть активные и завершенные инстансы Видеть путь прохождения инстанса, все переменные и тайминги по каждой из задач. Проект фронта использует jsf primefaces и bpmn.io для рендеринга самих процессов. Если интересно, то можно ознакомиться с исходным кодом здесь: Код серверной части Код фронт части Вернемся к нагрузке и дадим такую же нагрузку на этот проект для каждого из методов хранения: Результаты: На удивление прекрасно показал себя Elastic, неожиданно обогнав Clickhouse. Результаты h2 тоже впечатлили бы, если бы не одно большое но: без индексов на такой базе выборки начинают тупить уже после 5 тыс. процессов, да и фронт уже перестает отвечать. В общем, это хорошая база для тестов, но не для реальной жизни. Важной отличительной частью такого подхода является то, что такой гибрид можно запустить в большом количестве инстансов и поставить за распределителем нагрузки (eureka, nginx и тп). Получится, что вы сможете без каких-либо ограничений горизонтально масштабировать решение. Главный минус — неперсистентность активных инстансов. Сами процессы идут в оперативной памяти. В случае аварийной перезагрузки исторические данные по работающим экземплярам могут не успеть экспортироваться в хранилище данных. Итак, пришло время ответить на вопросы в начале статьи: Кто быстрее — Camunda или Zeebe? При равных условиях выигрывает Camunda. Но при марафоне с нарастанием базы рано или поздно Camunda упирается в деградацию и начинает замедляться. Zeebe из коробки с дефолтными настройками показывает очень скромные результаты, зато практически не имеет пределов для масштабирования.  А насколько, и в каких случаях? При равных условиях в виде отсутствия экспорта и истории, Camunda почти в 15 раз быстрее, чем Zeebe. При подключении истории производительность Camunda резко падает — почти в три раза, а эффективный метод экспортирования данных у Zeebe такой просадки не дает. Для совсем небольших процессов без необходимости хранить историю шагов даже Camunda не является целесообразной. В этих случаях лучше выбрать Apache Camel или Spring Integration — такое решение получится как минимум в два раза быстрее. Zeebe нужна для совсем крупных задач и высоконагруженных процессов (коротких и нагруженных вычислениями), чтобы быть экономически целесообразным. Для обеспечения высокой производительности требуется кластер с большим количеством инстансов и быстрыми дисками. В случае, когда бюджет ограничен, но есть желание получить скорость и масштабируемость, лучше создать аналогичный гибрид — отучить Camunda от использования классической базы данных. За счет чего может тормозить? Camunda тормозит из-за своей архитектуры — транзакционная запись каждого действия в базу данных. Это крутая фича, она позволяет при возникновении ошибок процессу откатываться на нужный этап, но это совсем не про hi-load. Zeebe тормозит из-за своего ядра — распределенной базы данных rockdb. Для нее нужны очень быстрые диски и большое количество партиций и инстансов. Ссылка на код группы проектов Коллеги, а как вы оптимизируете производительность Camunda и Zeebe? Поделитесь в комментариях! Программист Java