
Привет, Хабр! Мы — команда разработки кредитных продуктов для физических лиц в Т-Банке. Делимся опытом нагрузочного тестирования workflow-движка Zeebe (основы Camunda 8), который проводили, когда искали альтернативу Camunda 7.
О Zeebe сложилось немало стереотипов: медленный, сложный в настройке, нестабильный. Но большинство мнений основаны на данных, актуальных для устаревших версий. Мы приняли во внимание развитие продукта и провели собственное исследование.
Для Proof of Concept (PoC) использовали относительно свежие версии Camunda 8.5 и 8.6, протестировав движок в различных конфигурациях. Цели были просты, но амбициозны:
Оценить производительность Zeebe под высокой нагрузкой.
Изучить особенности горизонтального и вертикального масштабирования.
Выявить потенциальные узкие места при эксплуатации.
Нам было важно получить объективные метрики, чтобы понять, насколько обоснованны опасения по поводу перехода на Camunda 8. А еще — сформулировать практические рекомендации по настройке и оптимизации инфраструктуры для стабильной работы движка в нагруженных сценариях.
Архитектура Zeebe
Процесс запускается через Zeebe Client, который отправляет команду на Gateway по протоколу gRPC. Gateway маршрутизирует запрос брокеру — лидеру нужной партиции. Тот сначала записывает команду в event log, затем реплицирует ее на фолловеров. Только после подтверждения от большинства реплик команда считается зафиксированной.

Далее команду обрабатывает Stream Processor: он считывает запись из event log, применяет ее к внутренней стейт-машине и генерирует соответствующее событие. Это событие записывается в event log и реплицируется на фолловеры.
Важно, что для каждого экспортера в Stream Processor создается отдельный экземпляр, который последовательно обрабатывает записи из лога и передает их соответствующим внешним системам.
Состояние для каждой партиции хранится в RocksDB — встроенной LSM-базе, которая обеспечивает хранение как в оперативной памяти, так и на диске. Для ускорения восстановления состояния после перезапусков периодически фоновым процессом создаются снэпшоты.
Worker в Zeebe: не просто делегат, а архитектурный паттерн. В Camunda 7 мы привыкли к делегатам — логике, встроенной прямо в процесс. В Zeebe от этого подхода отказались: вместо него — worker, внешний исполнитель задач.
На первый взгляд, воркер похож на делегат: он получает задачу и выполняет бизнес-логику. Но это уже не часть движка, а независимый сервис, который можно масштабировать, переиспользовать и развертывать отдельно. Концептуально воркер можно представить как External Task в Camunda 7.

Потенциальное узкое место: общий пул потоков. Одна из ключевых проблем — использование общего thread pool’а для всех компонентов воркера. Планировщик, опрос и выполнение логики — все работает в рамках одного пула потоков. Если один элемент блокируется (например, из-за долгой операции), это может заблокировать весь воркер.
Есть два основных параметра, которые позволяют настроить поведение воркера:
max-jobs-active — сколько задач воркер может обрабатывать одновременно. Увеличение этого значения позволяет параллельно выполнять больше задач, но требует больше ресурсов.
Размер thread pool’а — можно выделить больше потоков, чтобы снизить риск блокировки. Хотя это не панацея: слишком большое количество потоков может привести к перегрузке CPU и деградации.
Идемпотентность — не опция, а обязательное требование. Zeebe гарантирует доставку задач at least once. Если воркер не успел отправить результат в течение заданного таймаута, задача снова становится доступной.
На практике мы сталкивались с кейсами, когда под нагрузкой воркер начинал повторно обрабатывать задачу, но Zeebe уже не находил ее в списке активных — из-за рассинхронизации состояния.
Воркер в Zeebe — не просто замена делегату, а ключевой элемент распределенной архитектуры.
Чтобы использовать воркер в Zeebe эффективно, нужно:
разделять ответственность по доменам;
обеспечивать идемпотентность обработчиков;
настроить max-jobs-active и thread pool под нагрузку;
помнить, что общий пул потоков — потенциальное узкое место.
Zeebe дает гибкость, но требует вдумчивого подхода к настройке и эксплуатации.
Компоненты тестирования, анализ производительности и стабильности
Цель тестирования — анализ поведения системы в экстремальных условиях и выявление узких мест. Оценку проводили по трем направлениям:
Воркеры — мониторинг времени ответа, пропускной способности, ошибок и метрик JVM. В процессе тестирования мы уделили внимание идемпотентности и корректной реакции на backpressure.
Zeebe — анализ Pi/s, Ti/s (среднее количество завершенных задач в секунду), задержек обработки команд, нагрузки на RocksDB, баланса между выполнением процессов и экспортом истории. Контролировались backpressure, системные ошибки и стабильность экспорта в Elasticsearch.
Инфраструктура — учет нагрузки на CPU, память, диск, сеть и I/O для Zeebe и Elasticsearch. Особое внимание — свободному месту, latency индексации и запросов, а также стабильности JVM и кластерных соединений.
Комплексный подход позволил оценить устойчивость системы в продакшен-сценариях и обосновать решения по ее масштабированию и настройке.
Для нагрузочного тестирования мы использовали конфигурации на основе версий Camunda 8.5/8.6. Изначально мы взяли простую конфигурацию в одну виртуальную машину на базе Camunda 8.6.

Gatling-сервис — сервис, на котором размещены сценарии нагрузочного тестирования на базе Gatling. Именно отсюда осуществлялась подача нагрузки на систему.
PoC-сервис — основной сервис, реализующий логику запуска процессов. Через REST-контроллер инициируются запуски процессов. В этом же сервисе реализованы воркеры, обрабатывающие задачи.
Инфраструктура Zeebe:
Виртуальная машина: одна ВМ с SSD-дисками 8000 IOPS и пропускной способностью 55 МБ/с (в burst — 15 000 IOPS/100 МБ/с).
Брокер и встроенный Gateway: размещены на одной ВМ.
RocksDB: используется на уровне брокера для хранения runtime-состояния процессов.
Elasticsearch: исторические данные асинхронно выгружаются в Elasticsearch через REST API. Кластер Elasticsearch разворачивался на отдельной виртуальной машине (4 CPU / 8 RAM / 50 ГБ диск). На этапе подготовки стенда были увеличены ресурсы ВМ для обеспечения стабильной работы (8 CPU / 16 RAM / 100 ГБ диск).
Operate: инструмент для визуализации и мониторинга истории выполнения процессов.
Дополнительные компоненты: на начальном этапе в тестировании участвовали Mockingbird, а также развернутые KaaS и DBaaS для реализации внутренней бизнес-логики воркеров. Однако в дальнейшем от них решили отказаться, чтобы упростить процесс нагрузочного тестирования и исключить влияние сторонних факторов на результаты тестирования.
Проблемы тестирования бизнес-процесса
Опишем типовые проблемы, с которыми мы столкнулись во время нагрузочного тестирования.
Как обойти кейс с Conditional Event в Camunda 8. В Camunda 8.6 и 8.8 нет поддержки Conditional Event — ключевого элемента для асинхронных сценариев. В Camunda 7 он позволял ждать изменения переменной, не блокируя процесс.


Альтернативы элемента conditional event (таймеры, циклы, polling) усложняют схему, увеличивают нагрузку и не обеспечивают настоящей асинхронности. Разработчикам приходится идти на компромиссы, что подчеркивает важность продуманного проектирования.

-
Как продолжить процесс нагрузочного тестирования после Conditional Event. На базе инфраструктуры банка мы рассмотрели несколько возможных решений:
1. Mockingbird + stub-коллбэк + Kafka REST Proxy — использование заглушки для перехвата сообщений и их последующей отправки через REST-интерфейс Kafka.
2. Отправка события внутри воркера POC-сервиса — генерация нужного события (например, сообщения в Kafka) в коде одного из сервисных воркеров.
3. Gatling с Kafka-плагином — прямая отправка сообщений в Kafka из нагрузочного сценария для имитации внешних триггеров.
Ни одно решение не дало предсказуемого и безопасного способа продолжения процесса. Основные барьеры:
Гонка между сохранением данных и отправкой событий.
Ограниченная поддержка инфраструктурных компонентов.
Отсутствие точного контроля за порядком выполнения.
Без устранения барьеров тест проверяет не движок, а стабильность обходных решений.
Message buffering как решение. В Camunda 8 на основе Zeebe доступен встроенный механизм — буферизация сообщений, который позволяет эффективно решать проблему несвоевременного прихода событий. Если сообщение приходит раньше, чем процесс достигает точки ожидания, Zeebe сохраняет его во внутреннем буфере на время TTL. Как только процесс доходит до нужного элемента — происходит автоматическая корреляция.
Ключевым параметром является время жизни сообщения (time-to-live, TTL), которое задается при публикации события. Именно оно определяет, как долго сообщение будет храниться в буфере в ожидании подписки. Но при высокой нагрузке это может привести к росту буфера и нагрузки на систему.

Для предотвращения роста буфера требуется:
Установить разумное значение TTL на уровне клиента (параметр zeebe.client.message.timeToLive), соответствующее максимальному ожидаемому времени обработки.
Мониторить состояние буфера с помощью метрик — как на примере ниже.

При одной партиции все сообщения обрабатываются на одном узле — это создает узкое место, особенно при высокой нагрузке и концентрации на одном корреляционном ключе.
Метрики. Мы столкнулись с расхождением в метрике «среднее количество завершенных процессов в секунду». Разница — почти в 2 раза.

Изначально у нас было два процесса: родительский и дочерний. Первая метрика учитывала оба, вторая — только родительский. При этом в лейблах метрик нет указания на тип процесса.
Нужно быть внимательным при анализе метрик, особенно во вложенных сценариях.
Бизнес-логика. Один из воркеров выполнял громоздкую логику: сбор данных, запись в БД, внешние вызовы. Это мешало тестировать движок. К тому же сложная логика — риск нарушения идемпотентности. Мы упростили процесс до happy path, чтобы фокусироваться на производительности Zeebe, а не на бизнес-логике.
На выходе получили процесс, который мы тестируем по happy-сценарию.

Мы получили простой процесс, от которого можем отталкиваться для первичного анализа результатов нагрузочного тестирования. Так как мы избавились от conditional event, это не значит, что мы избавились от самого тяжелого элемента. Убрав conditional event, мы получили преимущества:
Отказ от поллинга — в Camunda 7 движок постоянно проверял БД на условия для корреляции. В Camunda 8 Zeebe просто ждет входящий пуш, не тратя ресурсы CPU и диска на пустые проверки.
Мгновенная корреляция — сообщения в Camunda 8 буферизуются и сопоставляются в памяти по ключу. Это исключает тяжелые SQL-запросы JOIN и блокировки таблиц, которые тормозили Camunda 7 при росте нагрузки.
Линейная масштабируемость. Благодаря аппенд-логу Zeebe может обрабатывать тысячи входящих сообщений в секунду, распределяя их по партициям. Это превращает «узкое место» базы данных в быстрый поток последовательной записи.
Результаты: от 20 до 100 Pi/s
Базовая конфигурация:
1 VM;
1 брокер;
1 партиция.
Первые результаты:
22 процесса;
167 задач в секунду.
В качестве одного из ключевых показателей использовали intensity в gatling — максимальную нагрузку на PoC-сервис, который не всегда соотносится один к одному с RPS или средним количеством завершенных процессов на последней полке подаваемой нагрузки.

При достижении сервисом показателя в 200 intensity движок медленно переходит в режим стабильности через несколько часов работы. В этом состоянии, называемом последней полкой, количество завершенных процессов в секунду (RPS) становится равным показателю intensity при условии, что система выдерживает нагрузку, движок полностью прогрет и трафик корректно распределяется по партициям.
22 процесса и 167 Ti/s демонстрируют базовую производительность Camunda 8.6 в минимальной конфигурации.
Увидели на графиках, что CPU движка не утилизируется по полной, а в логах сыплются ошибки, запросы отбрасываются backpressure и дальше пропускная способность не растет.

Zeebe по умолчанию использует около двух потоков на партицию. Одной партиции недостаточно, даже если узел имеет много ядер.

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

При стабильной нагрузке и достаточном времени теста после прогрева движка Zeebe способен выйти на среднюю производительность (Pi/s), близкую к заданному значению intensity. Во втором тесте при intensity 100 система показала лишь 35 Pi/s — из-за короткой длительности этапов, не хватило времени на прогрев. Нужно увеличить время нагрузочного теста, и результат приблизится к показателям теста с 8 партициями.
При этом рост числа партиций до 8 может быть избыточным: уже при 4 активных партициях задействуются все ядра CPU. К этому моменту еще вернемся, а пока построим графики.

Эксперименты показали, что производительность Zeebe масштабируется почти линейно с увеличением числа партиций. Критически важно при проектировании высоконагруженных систем не только стабильно работать под нагрузкой, но и предсказуемо расти при расширении инфраструктуры.
С увеличением числа партиций CPU и RAM близки к пределу: утилизация достигает 80—90%.

Стресс-тестирование: каскадный сбой. Даже при конфигурации 1 VM, 8 партиций максимально движок можно разогнать до 70 процессов и 503 задач в секунду.

В условиях неоптимальной нагрузки на движок в метриках видим, что:
Все партиции упали к концу теста на последней ступени.
Ресурсы брокера и эластика (CPU, RAM, диск) перегружены и заполнены.

Вместо graceful recovery мы получили классический cascading failure: кластер не смог самостоятельно восстановить работоспособность, процессы застряли, а ручное вмешательство стало единственным способом вернуть систему к жизни. Архитектура, позиционируемая как отказоустойчивая, в этом сценарии проявила себя как крайне уязвимая к перегрузкам.
Горизонтальный скейлинг. Мы планировали масштабирование: перейти с одной ноды (8 vCPU / 16 ГБ RAM) на кластер из трех виртуальных машин по 4 vCPU / 8 ГБ RAM каждая — суммарно 12 vCPU / 24 ГБ RAM. Цель — проверить производительность при распределенной нагрузке с небольшим запасом по ресурсам.
Реализовать задуманное на текущей инфраструктуре не получилось из-за ограниченной доступности ресурсов и технических ограничений платформы.
Расширить конфигурацию оказалось невозможно, а поддержание существующего уровня стало затруднительным: ресурсов едва хватало для стабильного запуска нагрузочного теста.
Мы были вынуждены перейти к развертыванию нового кластера на базе OpenStack 3.0 — не просто как технического обновления, а как необходимого условия для дальнейших экспериментов. На результаты НТ это никак не повлияло.
Изменились и характеристики хранилища: если раньше мы использовали SSD-2 с пропускной способностью до 8K IOPS / 55 МБ/с (burst — до 15K / 100 МБ/с), то в новой инфраструктуре получили уровень silver с гарантированными 1.5K IOPS / 25 МБ/с — что влияет на производительность и стабильность Zeebe.
Изначально мы получили цифры в 53 процесса и 380 задач в секунду. Более менее сходимо к результатам конфигурации с одной нодой, причем без каких-либо доработок.

Mockingbird. На этом этапе мы приняли решение отказаться от Mockingbird — универсального мок-сервиса, использовавшегося для нескольких оркестраторов, включая Temporal и Zeebe. С ростом нагрузки он стал узким местом: увеличилась латентность обработки моков, а стабильность НТ пошатнулась.
Попытки масштабировать его инфраструктуру вплоть до настройки connection pool к БД лишь усложняли эксплуатацию. Приходилось держать под контролем ресурсы еще одной системы, что отвлекало от основной цели.
После отключения Mockingbird производительность выросла до ~60 процессов и 400 задач в секунду. Прирост скромный, но главное — прибавка в стабильности. Исчезла синхронизация с внешним сервисом, сократились накладные расходы на поддержку и тюнинг зависимостей.

Важно понимать, что проблема не в Mockingbird как таковом. Аналогичные сложности могут возникнуть и с другими мок-решениями, например с Wiremock. Для тестирования микросервисов такие инструменты незаменимы, но при нагрузочном тестировании инфраструктурных систем вроде Zeebe они вносят шум и искажают картину. Лишние зависимости мешают оценить реальные возможности движка.
Оптимизация воркеров. На этом рост производительности остановился. Ресурсы виртуальных машин простаивали, и мы приняли решение оптимизировать клиентскую часть — воркеры.
Увеличили количество потоков для воркеров и максимальное число активных задач:
zeebe.client.worker.threads — 8;
zeebe.client.worker.max-jobs-active — 64.
И это дало ощутимый прирост: нагрузка выросла до ~70 процессов и 500 задач в секунду — максимум для текущей конфигурации. При этом кластер оставался стабильным: падений партиций не было, деградации показателей тоже.

Intensity на предыдущем шаге меньше, но длительность тестов одинаковая, поэтому здесь это особой роли не сыграло. Далее мы подтвердим это на других сценариях.
Дальнейшее увеличение потоков или количества задач результата не приносило. Пришло время детальнее погрузиться в метрики.
Очередь. Первоначально мы заметили неравномерное распределение запросов по партициям. Количество партиций (всего — 9) мы корректировали в поиске баланса между масштабируемостью и нагрузкой.
Cluster size: 3 Partitions count: 9 Replication factor: 2 Gateway version: 8.5.19 Brokers: Broker 0 - 10.125.144.159:26501 Version: 8.5.19 Partition 1 : Leader, Healthy Partition 3 : Follower, Healthy Partition 4 : Leader, Healthy Partition 6 : Follower, Healthy Partition 7 : Follower, Healthy Partition 9 : Follower, Healthy Broker 1 - 10.125.144.189:26501 Version: 8.5.19 Partition 1 : Follower, Healthy Partition 2 : Leader, Healthy Partition 4 : Follower, Healthy Partition 5 : Leader, Healthy Partition 7 : Leader, Healthy Partition 8 : Leader, Healthy Broker 2 - 10.125.144.46:26501 Version: 8.5.19 Partition 2 : Follower, Healthy Partition 3 : Leader, Healthy Partition 5 : Follower, Healthy Partition 6 : Leader, Healthy Partition 8 : Follower, Healthy Partition 9 : Leader, Healthy
В определенные моменты наблюдался значительный рост очереди обработки задач. В условиях высокой и продолжительной нагрузки размер этой очереди может достигать тысяч, а в отдельных случаях — миллионов событий. Такое поведение указывает на наличие узких мест в архитектуре кластера.

Анализ был сфокусирован на трех потенциальных источниках проблемы:
Встроенный Gateway — поскольку шлюз интегрирован непосредственно в брокер и изначально весь входящий трафик направлялся на одну виртуальную машину, возникло предположение о недостаточной производительности компонента под высокой нагрузкой.
Распределение партиций-лидеров — в одной из конфигураций один узел имел 2 лидера, второй 4, третий — 3. Такой дисбаланс мог вызывать перегрузку отдельных нод и способствовать росту очереди.
Механизм backpressure — предполагалось, что отбрасывание запросов и увеличение количества inflight-запросов могут быть следствием активации backpressure, что в свою очередь влияет на стабильность обработки.
Для устранения выявленных проблем мы внедрили LBaaS, провели ручной ребалансинг лидеров партиций и поменяли конфигурацию RTT.
LBaaS внедрили для распределения нагрузки между тремя виртуальными машинами. Первоначально балансировщик был настроен на уровне L4, но в ходе эксплуатации возникли сбои и потребовалось переключение на уровень L7. После настройки L7-балансировщика и повторного тестирования проблема не исчезла. Так мы исключили LBaaS из списка основных причин.
Ручной ребалансинг лидеров партиций проводили для более равномерного распределения нагрузки. Хотя это привело к частичному сглаживанию очереди, периодические всплески («выпады») сохранялись, что свидетельствует о неполном решении проблемы.
Что касается backpressure, прямое тестирование его влияния не проводилось, поскольку отключение этого механизма под высокой нагрузкой считается рискованным. Мы отметили, что настройка RTT может оказывать влияние на поведение системы.
По официальной документации Zeebe, рост очереди под нагрузкой может быть нормальным явлением, обусловленным особенностями распределения запросов в распределенной системе. Это указывает на то, что система может быть устойчива к временным всплескам, несмотря на внешние признаки перегрузки.
Прогрев. Во время нагрузочного тестирования мы выявили зависимость производительности системы от начального периода работы — очевидный признак влияния прогрева. При этом оба теста показали сопоставимые итоговые метрики производительности.

В документации Zeebe эффект зависимости системы от начального периода работы объясняется не только прогревом JVM, но и внутренними динамическими процессами в движке. По мере роста нагрузки система оптимизирует распределение запросов между брокерами и партициями, снижает конкуренцию за ресурсы и эффективнее использует вычислительные мощности. В результате брокеры достигают более высокой загрузки и стабильной пропускной способности.
При интерпретации результатов нагрузочного тестирования необходимо учитывать фазу прогрева как значимый фактор, влияющий на метрики производительности. Ее игнорирование может привести к занижению оценки реальных возможностей системы. Рекомендуется проводить длительные тесты с постепенным выходом на рабочий режим и учитывать стабилизированные метрики в качестве основы для анализа.
Использование диска. Посмотрим на метрики утилизации ресурсов диска.

После перехода на OpenStack 3.0 мы столкнулись с ограничениями по IOPS и пропускной способности на дисках уровня silver (IOPS — 1500, throughput — 25 МБ/с), что негативно сказывалось на производительности.
Для решения проблемы перешли на диски уровня gold (IOPS — 9000, throughput — 100 МБ/с) и тем самым почти вдвое увеличили производительность системы.

Это привело к значительному снижению backpressure, уменьшению времени жизни процессов и улучшению метрик RocksDB, включая задержки записи и частоту компрессий. Рост производительности подтвердил, что узким местом была именно дисковая подсистема.

В итоге мы сделали важный вывод: дисковая подсистема — критически важный компонент для стабильной работы Zeebe, особенно с учетом активной записи данных через RocksDB. Даже при достаточном объеме CPU и RAM слабые диски становятся узким местом, что приводит к деградации пропускной способности и росту задержек.
На практике диски справляются с нагрузкой, но при этом демонстрируют высокую утилизацию. Растут IOPS, пропускная способность и очередь операций ввода-вывода, что указывает на значительную нагрузку. Причем интенсивность использования диска напрямую зависит от мощности вычислительных ресурсов: чем выше CPU и RAM, тем активнее система генерирует данные и тем больше нагрузка на диск.
Заключение
Наш путь от первых тестов с каскадными сбоями до стабильных 100 Pi/s показал, что Zeebe — мощный инструмент для высоконагруженных систем, но требует глубокого понимания своей архитектуры и тщательной настройки инфраструктуры. Стереотипы о нестабильности движка часто связаны с использованием дефолтных конфигураций или устаревших версий, которые не раскрывают потенциал платформы.
Выводы, которые мы получили:
Партиционирование — основа масштабирования. Одна партиция не сможет утилизировать даже средний сервер. Производительность растет почти линейно с увеличением числа партиций, но упирается в количество ядер CPU и возможности дисковой подсистемы.
Диск — критический ресурс. RocksDB активно пишет данные, и медленные диски (низкие IOPS/throughput) становятся главным узким местом, вызывая backpressure и деградацию. Экономия на классе хранилища недопустима.
Воркеры требуют внимания. Внешние исполнители — не просто код бизнес-логики. Необходимо настраивать пулы потоков, max-jobs-active и строго обеспечивать идемпотентность, так как гарантия доставки — at least once.
Инфраструктура тестирования должна быть «чистой». Сторонние моки могут стать незаметным узким местом, искажающим результаты. Для нагрузочного тестирования оркестратора лучше минимизировать внешние зависимости.
Нужно учитывать период прогрева. Метрики стабилизации системы могут занимать часы. Краткосрочные тесты не показывают реальную производительность в продакшене.
Архитектурный сдвиг. Переход с Camunda 7 на 8 — это смена парадигмы. Она дает выигрыш в производительности, но требует пересмотра проектирования процессов.
Zeebe готов к нагрузкам уровня крупного банка, но цена этой производительности — операционная сложность. Успех зависит не только от кода процессов, но и от качества инфраструктуры и грамотной настройки кластера. Если есть готовность инвестировать время в тюнинг и мониторинг, Camunda 8 станет надежным фундаментом для масштабируемых бизнес-процессов.
Надеемся, наш опыт сэкономит кому-то время на поиске узких мест. Если остались вопросы по конфигурациям или метрикам — welcome в комментарии!
Эта статья — начало погружения в движок Zeebe. Мы подготовили серию статей. В двух следующих рассмотрим влияние истории на рантайм и нововведения версии движка, которая вызвала больше интереса и предоставила больше всего полезного — Camunda 8.6.
А в блоге Zeebe можно найти много полезной информации по оптимизации.