Привет, Хабр! Меня зовут Владимир Швец, я ведущий разработчик центра Smart Process в МТС Digital. Расскажу о том, как мы собрали BPM-движок, который позволяет кастомизировать бизнес-процессы без перезагрузки стенда и перезапуска приложения.

Два программиста написали движок за две недели, поэтому такой BPM-механизм – быстрое и легкое решение, назвали его Scenario Engine. Мы применили движок для гибкого создания ряда процессов в рамках проекта интеграции с внешней системой. Ниже я разберу то, как работает движок, что у него под капотом, как мы его придумали и какие выводы сделали.

Цели у нас были такие: 

  • быстро создавать новые процессы;

  • менять бизнес-процессы без перезапуска приложения;

  • путем декомпозиции наших классов на небольшие методы и небольшие классы навести некоторый порядок в кодовой базе;

  • подготовиться к декомпозиции на микросервисы, так как наше решение испытывало ряд технологических проблем, связанных с ограничениями монолитной архитектуры;

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

Для решения таких задач подходит BPM. Что такое BPM? Это концепция процессного управления, рассматривающая бизнес-процессы как непрерывно адаптирующиеся к постоянным изменениям, моделируемые с использованием некоторой нотации и динамически перестраиваемые. 

Среди известных на рынке решений есть Activiti и Camunda. Второе решение, насколько я знаю, – форк от первого.

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

Какие технологии мы используем? 

Первое — это, естественно, язык программирования Java 8. Помимо него у нас используется Spring, в качестве ORM мы применяем jOOQ, база данных – PostgreSQL. Используются также коллекции Google Guava и планировщик задач Quartz.

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

Для примера изобразим процесс, состоящий из трех задач.

Задачи в рамках одного процесса выполняются последовательно. Также для использования всей вычислительной мощности устройств у нас есть параллельные процессы.

Теперь о том, как передаются параметры из задачи в задачу. Есть некоторая общая коллекция Params, которая является множеством параметров Param. Param – это совокупность ключа и некоторого значения. У нас есть начальная инициализация каждой задачи, в рамках которой задача получает переменные из некоторого контекста сценария. Сначала инициализируется контекст сценария, если ему это необходимо. 

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

Как это реализовано? 

У нас есть некоторый абстрактный класс сущности, назовем его Entity. Этот класс содержит общие для всех дочерних сущностей поля и методы. От него наследуются три разных класса, имеющие разные уникальные характеристики. Eсть сущность задачи Task — атомарное элементарное действие. Есть сущность сценария Scenario —  набор последовательно запускаемых задач. Eсть сущность процесса Process — совокупность параллельно отрабатывающих сценариев.

Какие свойства имеет любая сущность? Уникальный идентификатор (UUID), наименование name, контекст сontext, статус status. Статус может быть успешным или ошибочным. Статус сценария определяется статусом задач, которые в него входят. Если все задачи выполнены со статусом success, то у сценария тоже будет статус success. Пока задачи выполняются у сценария статус in progress. Если какая-то задача в ходе выполнения возвращает ошибку, то, соответственно, у сценария проставляется статус exception. 

Касательно статуса процесса – тут все не так однозначно, возможны два варианта. Есть процессы, которые состоят только из сценариев, которые должны отработать успешно, чтобы процесс можно было считать выполненным. В таком случае любой сценарий, завершившийся с ошибкой, проставляет процессу статус exception. Однако, бывают и такие процессы, которые можно считать завершившимися успешно, даже если какие-то входящие в него сценарии завершились с ошибкой. В таком случае процессу может быть проставлен статус success, даже несмотря на то, что один из сценариев не завершился. Данные случаи обрабатываются отдельно.

Помимо статуса есть время начала timeStart и время завершения timeFinish работы сущности. Также любая сущность обладает сообщением message. Если какая-то задача или сценарий хотят что-то сказать более общей сущности о себе, – они могут поместить это в категорию message. Как правило, message содержит более подробное сообщение об ошибке, если задача, сценарий или процесс завершились со статусом exception

Каждый таск имеет уникальные идентификаторы процесса и сценария, которым он принадлежит. У сценария же есть только уникальный идентификатор процесса. Помимо этого, у сценария есть список задач, из которых он состоит. Процесс же имеет множество сценариев, из которых он состоит. 

Как теперь все это конфигурировать? 

Задачи бывают функциональные и структурные. Из функциональных задач выделяем несколько типов: Read — смысл задачи заключается в том, чтобы считать некоторые данные в контекст задачи и потом вернуть это в контекст сценария. Второй тип задач — это Write, когда мы определенные параметры из контекста задачи, записываем в некоторую таблицу базы данных. В таком случае указывается таблица, а поля заполняемой таблицы заполняются согласно ключам параметров контекста. 

Есть задачи типа Call, они нужны для вызова внешних систем. Переменные берутся из контекста задачи и из этих переменных формируется запрос во внешнюю систему. Запрос отправляется, ответ получается синхронно, и парсится в контекст задачи, откуда параметры возвращаются в контекст сценария. А есть задачи типа Calc — это преобразование данных, когда нам нужно, например, просто переложить данные из параметров с одними ключами в параметры с другими ключи и произвести какие-либо вычисления. 

Структурные задачи управляют потоком выполнения, то есть сценарием. Первый тип здесь — это IfElse, задачи, которые позволяют запустить некоторый подсценарий, если какое-то значение параметра контекста равно какой-то определенной величине. Если значение равно другому числу – запускается другой подсценарий.

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

И есть задачи типа Terminator — если какой-то параметр равен определенному значению, то сценарий завершаются. Причем сценарий может завершится в таком случае как со статусом success, так и со статусом exception.

Как конфигурируется наша система? 

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

Какие таблицы определяют конфигурацию нашего движка? Прежде всего – таблица Process. Далее – таблица Scenario. И таблица задач Task. Все эти таблицы состоят из двух колонок: идентификатор и название.

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

Из каких сценариев состоит процесс? Есть специальная таблица Process_Scenario и она похожа на предыдущую. Разница в том, что в процессе нет порядкового номера сценария, потому что сценарии в рамках процесса запускаются параллельно, а не последовательно. Номер сценария процессу попросту не нужен.

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

Так выполняется первичная конфигурация BPM-движка. Если нужно в рамках сценария добавить некоторую задачу – мы просто делаем insert в таблицу Scenario_Task. Если в рамках процесса добавился параллельный запуск еще какого-то сценария – в таблицу Process_Scenario добавляем еще одну запись. Если в контекст какой-то задачи требуется добавить какой-то параметр – добавляем запись в таблицу Context

Для мониторинга того, что происходит с нашей системой, используется специальное логирование. Центр механизма логирования – таблица Unit. Данная таблица состоит из трёх колонок: собственный уникальный идентификатор UUID экземпляра сущности, идентификатор сущности Entity_ID и тип сущности Type (задача, сценарий, процесс). Эта таблица связана с таблицами Process, Scenario и Task. В таблицах Process, Scenario и Task общие абстрактные сущности (задачи, сценарии и процессы), а в таблице Unit записи о конкретных экземплярах процессов, сценариев или задач. 

И есть отдельная табличка с событиями Event. Она содержит следующие колонки: UUID процесса, UUID сценария, UUID задачи, статус, время начала, время завершения, сообщение и контекст. По этой таблице можно определить состояния каждой задачи, при этом у задачи будет Process_ID, Scenario_ID и Task_ID. Также можно выяснить состояние каждого сценария. У сценария будет, соответственно, Process_ID и Scenario_ID. В таблицу логируются и процессы. У процесса будет только Process_ID.

Какие результаты? 

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

  • в рамках декомпозиции наших больших методов и классов на маленькие методы мы улучшили структуру нашей бизнес-логики;

  • после того, как мы декомпозировали наш код на набор маленьких методов, их стало проще покрыть тестами, и процент кода, покрытого тестами, сильно повысился;

  • появилось понимание того, как дальше декомпозировать наш монолит на микросервисы;

  • решение мы смогли реализовать буквально за один спринт, еще примерно один спринт понадобился на доводку механизма до ума и исправление всех возникших в процессе разработки багов.

Что дальше?

Мы решили сделать сделать сервис-оркестратор, который считывал бы конфигурацию из базы данных и, в зависимости от этой конфигурации, запускал ту или иную задачу на выполнение. А задачи вывести в отдельные микросервисы, чтобы каждый микросервис имеет свою очередь на вход и на выход. Название очередей также конфигурировать через базу данных.

Спасибо за внимание! Если у вас есть вопросы о нашем BPM-механизме или вы делаете что-то аналогичное – с удовольствием пообщаюсь с вами в комментариях к этой статье!

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


  1. AndreySu
    13.07.2022 14:32
    +6

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

    Классика. Написать свой движок, стабилизировать его в проде, пофиксить проблемы не учтенные при проектировании проще чем изучить готовый продукт.

    В этот момент так же часто говорят - нам нужен только движок, безо всяких доп утилит и прочего. Проходит время и они потребуется так же.


  1. sshmakov
    13.07.2022 14:38
    +5

    Если нужно в рамках сценария добавить некоторую задачу – мы просто делаем insert в таблицу Scenario_Task

    То есть ни Camunda Modeler, ни Activiti Modeler, ни какие-то еще редакторы BPMN, плагины к IDEA, ни сам формат BPMN вы не поддерживаете?

    Стоило ли оно того, что бы потом для заведения задачи надо было SQL запрос выполнять?


    1. vlshvets Автор
      14.07.2022 13:08

      Начнём с аксиоматики. BPM - это управление бизнес-процессами, а BPMN - конкретная нотация отображения бизнес-процесса. Мы не ставили целью поддерживать нотацию BPMN. Мы хотели создать возможность менять бизнес процесс в рантайме, без привлечения разработчиков и без перезагрузки стенда. В рамках нашей постановки задачи запросы в SQL выглядят рабочим решением. Плюс поджимали сроки на реализацию. Мы понимаем, что в дальнейшем скорее всего потребуются и другие способы конфигурирования бизнес-процессов. Плюс есть вероятность, что потребуется и наглядная визуализация. На данный момент детально эти вопросы не рассматривались.


      1. sshmakov
        14.07.2022 16:19

        Я не спорю с тем, что вы, вероятно, имели причины сделать так, как сделали. Но из вашей статьи эти причины не очевидны. Тем более, что в заголовке упоминаете Camunda, а в комментариях пишете, что не было цели реализовать BPMN-движок. Тогда причем тут "импортозамещение Camunda"?


  1. Nikolay_Pervukhin
    13.07.2022 15:34
    +1

    Подскажите, пожалуйста, предполагается ли в будущем какая-то визуальная часть - фронт? Основным достоинством Camunda, Zeebe является возможность нарисовать процесс, согласовать его с бизнесом, а потом в сockpit увидеть визуально, где находится процесс, где ошибки, просматривать контекст и многое другое. Если основная задача была в декомпозиции, не смотрели ли на существующие аналоги - Spring Integration, Apache Camel ?


    1. vlshvets Автор
      14.07.2022 13:11

      Первая постановка задачи заключалась в создании механизма изменения бизнес-процессов без привлечения разработчиков и без перезагрузки стенда. В рамках данной постановки задачи визуальная часть не рассматривалась. Более того были весьма сжатые сроки на реализацию. Со временем есть вероятность, что фронт потребуется, как раз в части согласования бизнес процессов с бизнесом. Мы будем отталкиваться от требований бизнеса, но не обязательно, что будем стремиться к поддержанию именно BPMN нотации отображения бизнес-процессов.


  1. rinat_crone
    13.07.2022 16:38
    +5

    Как-то в статье не упоминается ни одного разумного аргумента для написания своего BPMN(-несовместимого, Карл!) движка. Не смогли осилить документацию по Camunda (ZeeBe), но решили что сможете написать свой движок?) Извините, МТС, но у вас джуны решения принимают, что ли?


    1. vlshvets Автор
      14.07.2022 13:17

      Мы не ставили задачу реализовать BPMN движок. Это разные понятия. BPM - это управление бизнес-процессами, BPMN - конкретная нотация отображения бизнес-процессов, как и многие другие, типо IDEF, Aris, UML и т.д. Наша первоначальная цель была предоставить возможность менять бизнес-процессы в рантайме без привлечения разработчиков и без перезагрузки стенда. Соответственно, в рамках этой задачи использовать такой тяжелый фреймворк как Camunda Platform является весьма избыточным решением. И о какой-либо совместимости с Camunda или Activity речи не шло. В данный момент движок конфигурируется через реляционную базу данных. Мы даём себе отчёт в том, что возможно в будущем потребуются и другие способы хранения конфигурации, начиная от NoSQL баз данных или каких-либо иных вариантах. Кроме того, мы задумывались и о визуализации процессов, но пока в приоритетном порядке рассматривали скорее семейство UML диаграмм. Решение было создано за весьма ограниченный промежуток времени. Поэтому мы выбирали наиболее простой путь.


  1. salkat
    13.07.2022 17:24
    +2

    Подскажите, плиз, неопытному, как в подобных системах происходит связь с реальностью? Вот нарисовали какие-то стрелочки, а дальше что? Какой-то сервер начинает слушать и отправлять https-запросы или какая-то интеграция с каким-то оборудованием или как это вообще происходит?


    1. booyakacrew
      13.07.2022 22:46
      +1

      Фактически да. "Какой-то сервер" должен контролировать созданный бизнес-процесс, согласно нарисованной вами схеме. В сторону фронтендов он выставит новый метод, означающий запуск нового бизнес-процесса. Через него он получит запрос на запуск процесса с определенным контекстом, далее может обогатить контекст дополнительной информацией из внешних систем по необходимости и выполнить этот процесс, в том числе и во внешних системах. Для обращений во внешние системы используется слой адаптеров, поддерживающий протоколы этих систем (зачастую тот, который предоставляет шина ESB) и оркестратор этого слоя, обеспечивающий абстракцию технических сценариев взаимодействия с внешними системами (роллбеки, повторы и тп) от бизнес-сценариев. (к примеру, bpmn для описания бизнес-уровня и bpel для оркестрации адаптеров+расширения типа bpel4people для "оркестрации" людей).

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


  1. Karchevskiy
    14.07.2022 10:07
    +1

    Camunda, как и другие bpmn движки для оркестрации очень медленная, из-за постоянного сохранения контекста в бд (как результат, еще и table bloat в подарок), в кишках у нее настоящий адъ, а бэклог с ошибками вообще не закрывается. Ее надо уметь готовить чтобы выжать хоть какой-то перформанс (в документации вообще ничего про это нет), не говоря о том, что она вообще никак не масштабируется из коробки (zeebe тоже шляпа). Camel и правда выглядит как то что надо в контексте оркестратора с динамическим добавлением процессов, но расширять и подтачивать его под свои задачи дело тоже не из простых. Так что дело полезное, удачи, ребята


    1. Kotskin
      14.07.2022 11:04
      +2

      А что за ад в кишках у camunda?


      1. Karchevskiy
        14.07.2022 11:57

        Вы пробовали сами закрывать ее проблемы или допиливать какой-нибудь свой функционал поверх? Попробуйте, например, смигрировать миллион процессов между двумя версиями. Или добавить например brave-контекст для трассировки сквозь процессы.


        1. MonkAlex
          14.07.2022 12:01

          Самописный движок для внутренних нужд будет лучше, по вашему?


          1. Karchevskiy
            14.07.2022 12:12

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


        1. Kotskin
          14.07.2022 12:20
          +1

          Пробовал, ада не встречал.


          1. Karchevskiy
            14.07.2022 12:33

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


            1. Kotskin
              14.07.2022 14:24

              А где прочитать предметную критику? Я ж ее и пытась из вас вытащить. Голдманс сакс гоняет 10 миллирадов инстансов в день, и у них все хорошо.


            1. Kotskin
              14.07.2022 14:36
              +2

              У нас в организации такое есть, универсальный совет такой:

              • Не делать одну здоровенную камунду, а делать н-камунд. То, что хочется переиспользовать - выносить в либы.

              • Отказаться от визуального программирования, если оно есть. Перейти на моделирование бизнес-процессов, а не флоу.

              • Не использовать контекст, использовать только бизнес-ки, который является сссылкой на обрабатываемую сущность в соседней системе. В прочем пару айдишников можно добавить, как раз для корреляций.

              • Не использовать встроенные корреляции по переменным, максимум по бизнес-ки. В иделае написать свою обвязку для корреляций, мы так сделали в итоге.

              • Затюнить Job Executor

              • Перейти на External Task в жирных или долгих подключениях.

              Миграции в общем виде автоматизировать нельзя, потому что в общем виде невозможно предугадать изменения бизнес-процессов.

              Вот то что точно можно сказать, что эти советы очень сложно найти в доке и они контринтуитивны, особенно с контекстом.

              Поэтому если у вас всегда FSM и не пахнет сетями Петри, то Camunda не нужна - у нее спект применения сильно уже, чем кажется на первый взгляд


              1. Karchevskiy
                14.07.2022 14:50

                Вы ловко уклонились от ответа. 10B инстансов в день - это вообще не тот параметр о котором я говорю. Речь идет именно об инстансах одного конкретного процесса. Естественно экзекьютор затюнен и корреляции идут по ключам. Экстернал таск - это так себе совет в контексте рпс, уж лучше асинхронный rest call в другой сервис. Камунда как оркестратор не справляется с корреляцией на тех числах, что я привел даже на самых жирных бд.


                1. Kotskin
                  14.07.2022 15:04

                  Э, так вы написали с 10м инстансами процессов, я подумал что про разные дифинишины. Но тут я уже хз, смотреть надо, я видел что такое нормально работает. Камунда это вообще не про рпс (если не брать с инмемори), а про инстансы в день. Мало организаций, где 10 миллионов заказов в час.

                  А перепутать легко, если начать логи в двх из кафки лить через камунду, то на второй день станет грустно .

                  На Jpoint в этом году, рассказывал как раз как не перепутать


              1. Karchevskiy
                14.07.2022 14:53
                +1

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


  1. amalinin
    14.07.2022 13:18
    +1

    Импортозамещение Camunda самописным BPM-механизмом

    Зачем импортозамещать опенсорс?


  1. Mar-inaa
    14.07.2022 13:19

    Не понятно зачем писали то? Если критерий "BPM-движок, который позволяет кастомизировать бизнес-процессы без перезагрузки стенда и перезапуска приложения ",то это позволяют и самые древние BPM движки, а уж Camunda и подавно.


    1. vlshvets Автор
      14.07.2022 13:21

      Camunda является избыточным решением в рамках нашей постановки задачи. Со старыми движками есть проблема в поддерживаемости, в завязке на старые технологии, в сложности допилить коробочное решение в части каких-то узкоспециализированных вопросов. Своё решение гораздо проще поддерживать и кастомизировать, используя самый современный стек.


      1. Karchevskiy
        14.07.2022 13:23
        +2

        У вас java 8


        1. vlshvets Автор
          14.07.2022 13:32

          Переход на Java 17 в работе.


      1. amalinin
        14.07.2022 17:50

        Если вам не нравится решение, которому несколько лет, то есть легковесный Zeebe (он же "Camunda platform 8"). Стильный, модный, молодежный, cloud native и т.д.


  1. msmurygin
    14.07.2022 15:26

    Вопросик: может я не внимательно прочитал, но все же спрошу.

    Существуют ли в вашем движке стандартные конекторы типа http,tcp, file и т. д. ? Откуда берутся данные ?

    Поддерживаются ли какие-либо типы авторизаций ?