Добро пожаловать во второй пост серии о Flowable Async Executor. В первой части мы рассмотрели базовые понятия: что такое асинхронные задания и таймеры, и почему они полезны при построении BPMN- и CMMN-моделей. В последнем разделе мы также показали общую схему новой архитектуры Async Executor.

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

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

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

Компоненты верхнего уровня

Как мы уже выяснили в первой части этой серии, с точки зрения архитектуры Async Executor состоит из следующих элементов:

  • Выборка (Acquisition): Async Executor должен забирать новые асинхронные задания или таймеры (когда наступает их срок исполнения).

  • Выполнение (Execution): После выборки задания, естественно, должны быть выполнены.

  • Сохранение (Persistence): Все задания должны сохраняться и удаляться только после полного завершения работы. Если на момент создания задания или таймера неактивен ни один экземпляр Async Executor, задание всё равно должно быть сохранено и обработано, когда экземпляр станет доступен.

  • Обработка ошибок (Error Handling): Задания могут завершиться с ошибкой при выполнении, например, при вызове внешнего сервиса, но также возможны ошибки и на этапе выборки. Задания должны автоматически повторяться при сбоях. Если задание продолжает завершаться с ошибкой, оно автоматически переводится в статус «deadletter» (аналогично концепции в очередях сообщений).

Все эти элементы должны корректно работать и в многонодовой конфигурации. Задания, которые были выбраны на одном экземпляре, не должны подхватываться другими. Если задание было выбрано на одном экземпляре, но сервер по какой-то причине вышел из строя, это задание должно быть автоматически подхвачено другим экземпляром Flowable Async Executor.

На следующей схеме показаны основные подкомпоненты, участвующие в реализации этих требований. Конечно, здесь не отражены детали низкоуровневой реализации, такие как хранение данных, передача данных или трансформации. Но, поскольку исходный код Flowable открыт, вы всегда можете изучить детали самостоятельно. Здесь нет никаких секретов!

Давайте рассмотрим каждый из этих подкомпонентов слева направо. Для каждого из них мы также опишем, как его можно настроить. Мы будем делать это независимо от среды и опишем настройки концептуально, так как конкретный способ задания параметров и даже имена свойств зависят от вашей среды (Spring Boot, standalone, Flowable Enterprise и т.д.).

Global Acquire Lock

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

Есть три основные причины, почему эта реализация превосходит старую. Мы подробно разберём их в третьей и четвёртой частях, но уже сейчас кратко опишем их здесь.

Во-первых, когда только один экземпляр Async Executor одновременно выбирает задания, эти задания можно обрабатывать пакетно (вместо по одному с проверкой оптимистической блокировки, как было раньше), потому что нам просто не нужно беспокоиться о конкурентном доступе. Все предыдущие бенчмарки показали, что ограничение сетевого трафика — всегда лучший способ повысить производительность. Пакетные обновления вместо отдельных операций значительно помогают в плане производительности Flowable.

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

В-третьих, наши эксперименты показали, что одной из главных проблем при масштабной обработке заданий была конкуренция за таблицы базы данных. Базы данных могут выдерживать большую конкуренцию — это одна из их основных задач — но только до определённого предела. Если этот предел превышен, время отклика базы резко ухудшается, вплоть до возникновения исключений типа «deadlock» в некоторых СУБД (даже если это не настоящие дедлоки, а механизм снижения нагрузки на таблицы). Global Acquire Lock резко снижает конкуренцию за таблицы, что в целом приводит к лучшей производительности.

Настраивать Global Acquire Lock обычно не требуется. Он включается с помощью параметра “global-acquire-lock-enabled=true” (в зависимости от вашей среды).

Обычно единственное, что может потребовать настройки — это время ожидания, через которое Async Executor повторно попытается получить глобальную блокировку, если предыдущая попытка не удалась. По умолчанию это 1 секунда. Если уменьшить это значение, пропускная способность немного увеличится, но может возрасти нагрузка на базу данных. Если увеличить — возможны большие задержки в обработке заданий, что может быть приемлемо для вашей нагрузки и снизит давление на базу.

Обратите внимание: Global Acquire Lock можно отключить. В этом случае Async Executor будет использовать старую реализацию.

Выборка асинхронных заданий и таймеров

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

Основная задача логики, отвечающей за выборку заданий, — обеспечить, чтобы потоки выполнения (см. ниже) никогда не оставались без работы. Для каждого типа заданий — обычно это асинхронные задания и таймеры, но могут быть и другие типы, например, Async History — существует отдельный поток, который за один раз выбирает пачку заданий.

Здесь важную роль играют транзакции базы данных. Сначала выборка происходит в одной транзакции, затем, в следующей транзакции, происходит «взятие во владение» (taking ownership). Когда экземпляр «забирает» задание, он записывает свой ID в соответствующую строку задания в таблице базы данных. Кстати, именно этот этап теперь можно выполнять пакетно (bulk), благодаря Global Acquire Lock. Если задание уже принадлежит какому-то экземпляру Async Executor, другой экземпляр не сможет его выбрать. На владение заданием также установлен тайм-аут.

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

max-async-jobs-due-per-acquisition=512

max-timer-jobs-per-acquisition=512

Благодаря Global Acquire Lock эти значения могут быть большими (сотни заданий за раз) без проблем.

Время, в течение которого задание считается принадлежащим экземпляру, прежде чем оно будет считаться «потерянным» и сможет быть подхвачено другим Async Executor (например, если сервер вышел из строя), настраивается так:

timer-lock-time-in-millis=3600000

async-job-lock-time-in-millis=3600000

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

default-async-job-acquire-wait-time-in-millis=10000

default-timer-job-acquire-wait-time-in-millis=10000

Ещё одна ситуация, когда потоки выборки ждут — если внутренняя очередь заполнена (см. следующий раздел). Это позволяет потокам «догнать» выполнение, прежде чем в очередь попадут новые задания. Параметр:

default-queue-size-full-wait-time=5000

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

move-timer-executor-pool-size=4

Внутренняя очередь

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

Единственный параметр настройки — это её размер:

queue-capacity=2048

Установка достаточно большого значения критична, чтобы избежать ситуации, когда потоки выполнения будут простаивать без работы. Имейте в виду, что изменение размера очереди может потребовать также корректировки времени владения заданиями. Если задания слишком долго находятся в очереди, они могут быть подхвачены другими экземплярами, так как считаются «зависшими». В нормально работающей системе это не должно быть проблемой.

Если вы используете Flowable Control, размер очереди можно проверить визуально:

Когда очередь заполнена, задания отклоняются. Это означает, что владение ими снимается, и другой экземпляр теперь может их подобрать. В Flowable Control это также можно увидеть на следующей диаграмме:

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

Пул потоков выполнения

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

Если у вас много заданий, обычно стоит добавить больше потоков именно здесь. Обратите внимание, что в Flowable пул потоков настроен так, что количество потоков автоматически уменьшается, если они не нужны. Количество потоков настраивается следующими параметрами:

thread-pool.core-pool-size=32

thread-pool.max-pool-size=32

Обычно рекомендуется указывать одинаковые значения (Flowable сам уменьшит число потоков при необходимости, не беспокойтесь). Добавлять потоки в современных операционных системах и на современном железе не проблема. Почти всегда логика ограничена скоростью ввода-вывода, и процессору хватает времени переключаться между процессами/потоками. Например, выполнение большого числа асинхронных HTTP-запросов не вызывает затруднений, так как основное время уходит на ожидание I/O, и это отличный сценарий для большого пула потоков.

Настройки по умолчанию

В рамках улучшенной реализации Flowable Async Executor мы также позаботились о том, чтобы значения по умолчанию были достаточно хорошими. Это значит, что с этими настройками вы получите приемлемую производительность по заданиям. Однако если вы хотите масштабироваться, первым шагом будет увеличение числа потоков, а затем, как правило, потребуется увеличить размер очереди. Далее — настройка размера выборки (acquire size) в зависимости от типа ваших заданий. В Flowable Control есть встроенные графики, которые показывают все эти параметры в реальном времени. Если хотите узнать больше, попробуйте Flowable trial.

Заключение

В этом посте мы рассмотрели различные подкомпоненты, из которых состоит Flowable Async Executor. Мы также начали освещать улучшения, появившиеся с новым подходом Global Acquire Lock, и его положительные побочные эффекты.

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

Об авторе:

Joram Barrez Principal Software Architect

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

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

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