«Могу ли я выполнять 10, 100 или 1000 экземпляров процессов в секунду на этом кластере Camunda 8?»
Это типичный вопрос, который нам задают в последнее время, и на него можно ответить с помощью бенчмаркинга. Давайте разберёмся в этой увлекательной теме в сегодняшнем посте.
Наш путь в бенчмаркинге и немного технического бэкграунда
Внутри компании бенчмаркинг оказался интересным путешествием, которое я хочу кратко описать. С самого начала одной из инженерных целей Zeebe — движка процессов внутри Camunda 8 — было создание высокомасштабируемого движка, который может пойти гораздо дальше, чем существующие технологии. Поэтому измерение прогресса в таких областях, как пропускная способность и задержка, всегда было у нас в приоритете. Если вас интересует только то, как запустить собственный бенчмарк самым современным способом, можете пропустить этот раздел.
Когда мы впервые выпустили Zeebe в 2018 году, мы вручную собрали стенд для бенчмаркинга с использованием AWS, Terraform и Ansible. Эта настройка разворачивала кластер Zeebe вместе с простым генератором нагрузки, использующим Zeebe Java Client, и измеряла количество запускаемых экземпляров процессов в секунду. Хотя это было хорошим началом, до идеала было далеко.
Этот первый подход к бенчмаркингу дал нам два основных урока:
Мы смотрели не на те метрики. «Экземпляры процессов, запускаемые в секунду» — это легко измерить, но эта цифра не говорит о том, завершаются ли эти процессы, или вы просто создаёте большую волну экземпляров, которые накапливаются. Поэтому более интересной метрикой является «экземпляры процессов, завершаемые в секунду».
Нужно учитывать сервисные задачи. Для каждой сервисной задачи движок процессов должен создать задание, передать его обработчику и дождаться его выполнения. Эти вычисления конкурируют за ресурсы с другими вычислениями.
Давайте рассмотрим следующий пример процесса:

Чтобы завершить этот процесс, движку процессов необходимо:
Создать новый экземпляр процесса
Обработать шаг «Сделать A»
Обработать шаг «Сделать B»
Обработать шаг «Сделать C»
Завершить экземпляр процесса
Теперь вычисления, связанные с выполнением этих трёх сервисных задач, конкурируют за ресурсы с вычислениями, связанными с созданием экземпляра процесса. Это означает, что если вы запускаете экземпляры процессов слишком быстро, скорее всего, не все сервисные задачи будут завершены, а значит, экземпляры процессов не завершатся, а начнут накапливаться.
Именно поэтому движок Zeebe применяет механизм обратного давления (backpressure) к операциям запуска экземпляров процессов. Обратите внимание, что текущий дизайн отдаёт приоритет сервисным задачам по сравнению с запуском процессов, поскольку на завершение сервисных задач обратное давление не распространяется.
Это всё интересно, но что это значит для бенчмаркинга? По сути, нам нужно сбалансировать запуск экземпляров процессов и завершение сервисных задач. Наша первая попытка сделать это была примерно в 2019 году, когда мы начали создавать более реалистичные сценарии. Вкратце, мы решили:
Нужно создать приложение-starter, которое будет запускать экземпляры процессов с заданной частотой и передавать определённую нагрузку (payload).
Также нужно приложение-worker для завершения сервисных задач. Мы хотели, чтобы оно вело себя реалистично, поэтому смоделировали задержку при завершении задания. Мы использовали 100 мс — типичную задержку при завершении внешнего REST-запроса.
Мы упаковали оба компонента в виде Java-приложений и сделали возможной их настройку, развёртывание и масштабирование через Kubernetes. С тех пор мы регулярно используем этих бенчмарк-стартеров, и, например, вы можете увидеть их в действии здесь.

Но мы всё ещё сталкивались с двумя регулярными проблемами:
Наличие двух независимых приложений и контейнеров для стартера и воркера означало, что они действительно независимы друг от друга. Но на практике они должны быть сбалансированы (из-за конкуренции между запуском процессов и выполнением сервисных задач). Эту балансировку приходилось выполнять вручную. Это было сложно даже для опытных специалистов и часто приводило к ошибочным выводам.
Нам нужно было масштабировать эти стартеры и воркеры, чтобы полностью задействовать наши кластеры Zeebe. Архитектура стартеров и воркеров требовала огромного количества аппаратных ресурсов. Это делало бенчмарки дорогостоящими и мешало их широкому использованию, так как не каждый клиент может с лёгкостью развернуть крупный кластер Kubernetes.
Поэтому мы провели следующий раунд улучшений:
Мы объединили стартер и воркер в одно Java-приложение.
Мы добавили алгоритм балансировки, который пытается найти оптимальную скорость запуска экземпляров процессов. Под «оптимальной» мы понимаем такую, при которой все запущенные экземпляры процессов могут быть обработаны и завершены, при этом Zeebe используется на полную мощность. Алгоритм может использовать механизм обратного давления (backpressure) для корректировки. Хотя этот алгоритм, вероятно, ещё далёк от совершенства, он служит хорошей отправной точкой и позволяет вносить поэтапные улучшения, над чем мы сейчас и работаем.
Мы последовательно применили асинхронное/реактивное программирование и использовали возможности планирования задач платформы Java. Это значительно снизило потребность в аппаратных ресурсах для стартера и позволяет использовать даже небольшую машину для нагрузки на крупные кластеры Zeebe. Да здравствует реактивное программирование!
Похоже, это работает отлично. Теперь у нас есть инструмент, с помощью которого простое Java-приложение может проводить бенчмаркинг кластеров Zeebe без необходимости глубоко разбираться в том, как устроен сам бенчмарк. Алгоритм балансировки должен оптимизировать себя автоматически. Этот инструмент доступен как расширение сообщества Camunda (https://github.com/camunda-community-hub/camunda-8-benchmark) и может послужить отправной точкой для ваших собственных бенчмарков.

Бенчмаркинг vs. сайзинг и тюнинг
На этом этапе я хочу упомянуть ещё одну важную вещь, которую усвоил за последние годы, занимаясь бенчмаркингом: необходимо чётко понимать свою цель!
Существует несколько разных задач, которые можно решать:
Подбор конфигурации (sizing) — чтобы найти такую конфигурацию кластера, которая сможет достичь заданной цели.
Настройка производительности (performance tuning) — например, выяснение, даст ли прибавка в 2 vCPU улучшение, которое оправдает инвестиции и сделает конфигурацию кластера лучше.
Оптимизация эффективности (efficiency tuning) — например, выявление ресурсов, которые в текущем кластере используются недостаточно, что означает наличие других узких мест. Сокращая эти ресурсы, можно сэкономить деньги.
Нагрузочное тестирование (benchmarking) — чтобы понять максимальную пропускную способность текущей конфигурации кластера.
Чаще всего в прошлом мы делали всё это одновременно. Каждый раз, когда мы определяли конфигурацию кластера (например, для нашего облачного SaaS-сервиса), мы, разумеется, проводили бенчмаркинг. Запуск бенчмарка также может выявить проблемы с производительностью, что может привести к шагам по оптимизации — либо производительности, либо эффективности. Это приводит к улучшенной конфигурации кластера, для которой снова нужно запускать бенчмарк.
Это цикл оптимизации, который по сути можно выполнять бесконечно 🙂
Ниже представлена диаграмма, иллюстрирующая этот процесс:

Вы, возможно, уже догадываетесь, в чём главная хитрость: в какой-то момент нужно быть довольным конфигурацией кластера. Это может быть непросто, так как у инженеров хорошо развита интуиция, подсказывающая, что текущая конфигурация ещё не оптимальна (спойлер: возможно, она никогда не будет таковой). Тем не менее, необходимо остановиться и принять конфигурацию как «достаточно хорошую».
Также помните, что ни один бенчмарк не может быть на 100% реалистичным, а значит, любые цифры нужно воспринимать с долей скепсиса. Мой общий совет — прекращать настройку кластера скорее раньше, чем позже, мириться с некоторой неопределённостью в цифрах пропускной способности, но при этом заложить достаточный запас, чтобы вы могли справляться с её снижением по любой причине.
Типичное эмпирическое правило — размер кластера должен обеспечивать как минимум 200% от ожидаемой нагрузки. Если вы ожидаете пиковые нагрузки, это число должно быть ещё выше, чтобы гарантировать производительность в эти периоды. Дополнительные соображения по определению целей вы найдёте в нашем гиде по лучшим практикам “Sizing your environment”.
Метрики, которые нас интересуют
Подведём итог ключевых метрик, на которые мы смотрим в наших бенчмарках:
PI/s (Process Instances per second) — процессов в секунду: Важно управлять скоростью запуска процессов, но мы всегда измеряем число завершённых процессов в секунду.
Backpressure — обратное давление: Бенчмарк также фиксирует число запросов в секунду, которые не были выполнены из-за механизма обратного давления. Этот показатель даёт хорошее представление об уровне загрузки кластера. Наш опыт показывает, что кластер, который полностью использует аппаратные ресурсы, имеет уровень backpressure около 3–10%.
Tasks/s — задач в секунду: Как обсуждалось ранее, каждая сервисная задача создаёт накладные расходы. Обычно один процесс включает несколько задач — в стандартном бенчмарке их 10, как некий усреднённый случай. Это означает, что если мы хотим завершать 333 PI/s, нам нужно обрабатывать 3330 Tasks/s. В контексте Camunda вы также можете встретить метрику FNI/s — flow node instances per second. Flow node — это любой элемент BPMN-процесса: не только сервисная задача, но и стартовое событие, шлюз и так далее. Хотя это наиболее точная метрика, она также самая абстрактная. Поэтому мы чаще ориентируемся на Tasks/s или PI/s, так как они понятнее для большинства.
Cycle time — длительность цикла: Для некоторых процессов важна длительность выполнения. Например, в недавнем проекте с клиентом из сферы трейдинга нужно было гарантировать, что процесс завершается за 1 секунду.
Пример бенчмарка
Давайте рассмотрим пример.
Я буду запускать бенчмарк для SaaS-кластера Camunda 8 размера “S” с использованием нашего типичного процесса и типичной нагрузки.

Процесс содержит 10 сервисных задач, один шлюз и два таймера, каждый из которых ждёт одну минуту. Я задаю следующие параметры конфигурации для проекта camunda-8-benchmark:
benchmark.startPiPerSecond=25: мы начинаем со скорости 25 процессов в секунду, а затем бенчмарк автоматически подберёт оптимальную скорость. Хотя конкретное значение не критично, чем ближе оно к целевому, тем быстрее будет достигнута оптимальная нагрузка.
benchmark.taskCompletionDelay=200: симулируем задержку выполнения каждой сервисной задачи в 200 миллисекунд.
С учётом двух таймеров по одной минуте и 10 задач по 200 мс каждая, время полного прохождения процесса должно составлять не менее 2 минут и 2 секунд — по «бизнес-логике».
Запуская бенчмарк, можно следить за происходящим через предоставленную Grafana-дэшборду. Вначале система будет увеличивать скорость запуска процессов, поскольку кластер недозагружен и обратное давление (backpressure) ещё не зафиксировано.

Однако спустя некоторое время, особенно когда всё больше начинают выполняться сервисные задачи, мы увидим появление обратного давления (backpressure), и скорость запуска процессов начнёт постепенно снижаться. Теперь, по сути, нужно подождать несколько минут, пока система не найдёт хорошее оптимальное значение.
На изображении ниже показаны первые 30 минут работы бенчмарка. Приблизительно через 10 минут система вышла на относительно стабильное состояние.

Из этого запуска бенчмарка мы можем получить значения пропускной способности и времени цикла. В идеале на эти показатели стоит смотреть только после начальной фазы разогрева.

Результат таков:
Мы можем примерно завершать 17 процессов в секунду на этом кластере (как видно, количество запущенных и завершённых процессов примерно одинаково, что хорошо).
Задачи в секунду (Tasks/s) или работы в секунду (Jobs/s) примерно в 10 раз больше числа процессов в секунду, что логично, учитывая, что в нашем процессе 10 сервисных задач.
Время цикла составляет около 133 секунд, что недалеко от рассчитанного оптимума в 122 секунды. Для большинства случаев такое время цикла вполне приемлемо, но если для вашего сценария важна задержка, имеет смысл провести дополнительные исследования и оптимизацию — это тема для другого раза.
В этом видео также показан процесс запуска данного бенчмарка.
В качестве дополнительной заметки: конечно, вы можете использовать Camunda Optimize, чтобы просматривать данные, связанные с процессами (например, количество и время цикла), относящиеся к этому бенчмарку. Это менее технически сложно, но ещё удобнее в использовании :-)

Запуск собственного бенчмарка
Просто запустите свой бенчмарк, используя https://github.com/camunda-community-hub/camunda-8-benchmark. Как описано, одного приложения для бенчмарка может быть достаточно, чтобы нагрузить небольшие кластеры. Вы также можете запускать этот стартер через Docker или Kubernetes и масштабировать его. Обычно не нужно настраивать или изменять ничего, кроме эндпоинта Zeebe, но чаще всего хочется подстроить BPMN-модель процесса, полезную нагрузку (payload) и имитацию времени выполнения сервисной задачи. Пожалуйста, ознакомьтесь с readme проекта бенчмарка для подробностей.
«Наше продакшн-решение далеко не обрабатывает столько, сколько показывают результаты бенчмарка!» Как уже упоминалось, бенчмарк никогда не может быть на 100% реалистичным. Тем не менее, стоит стараться имитировать реалистичное поведение, чтобы получить ценные результаты и выводы. Главное для реалистичности:
Используйте модель процесса, близкую к той, что вы планируете использовать. Есть ли у вас много сервисных задач, шлюзов или таймеров? Создайте модель процесса с такими же элементами (быстрое предупреждение: пока в инструмент бенчмарка не встроена логика корреляции сообщений, это тема для будущего).
Используйте полезную нагрузку (переменные процесса), близкую к реальным данным. Если следовать нашим лучшим практикам работы с данными в процессах, переменных с большим объемом данных быть не должно. Но иногда пользователи кладут в переменную большой JSON, что сильно влияет на производительность.
При этом, я хочу отметить, что в любом случае рекомендуется следовать нашим лучшим практикам, чтобы строить процессы, которые будут работать стабильно в продакшне.
Запуск под высокой нагрузкой
В сегодняшнем посте я рассмотрел самый маленький кластер, который мы предлагаем в Camunda SaaS. Хотя 17 процессов в секунду достаточно для большинства случаев, это, конечно, не та большая цифра, которой хочется хвастаться. Сейчас я готовлю следующий пост, где опишу сценарий, который мой коллега Фалько протестировал для клиента из финансовой сферы. Там они успешно обрабатывают 6000 процессов в секунду, удерживая время цикла ниже одной секунды. Вау!
Дальнейшие шаги
Если внимательно посмотреть на процедуру бенчмарка, то видно, что она сосредоточена на тестировании самого движка Zeebe. Однако, если посмотреть, например, на архитектуру хранения истории Camunda 8, станет ясно, что есть и другие компоненты, которым тоже важно выдерживать такую нагрузку, в первую очередь Camunda Operate и Camunda Optimize. Сейчас между ними есть разрыв (поэтому официальные показатели Camunda 8 ниже указанных выше). Для решения этой задачи мы добавляем остальные компоненты в цепочку бенчмарка и снова корректируем метрики. Например, для учета завершённого процесса теперь важно, чтобы он был виден и в Operate, и в Optimize.
Заключение
Запустить бенчмарк, чтобы понять, на что способен ваш кластер Zeebe, просто с помощью расширения сообщества camunda-8-benchmark. Чтобы начать, вам может понадобиться всего лишь одна машина разработчика с подключением к интернету.

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