Как мы делали свой движок Workflow

    Мы в компании DIRECTUM занимаемся разработкой ECM-системы DirectumRX. Основным элементом модуля Workflow для ECM-системы является движок. Он отвечает за изменение состояния экземпляра процесса (инстанса) по ходу жизненного цикла. Перед тем, как начать разрабатывать модуль Workflow, стоит решить: взять готовый движок или написать свой. Изначально мы пошли по первому варианту. Мы взяли движок Windows Workflow Foundation (WF), и в целом он нас устраивал. Но со временем мы поняли, что нам нужен свой движок. Как это случилось, и что из этого вышло, расскажу ниже.

    Старый движок


    Почему WF?


    В далеком 2013, когда пришла пора разработать модуль Workflow для DirectumRX, мы решили взять готовый движок. Смотрели из Windows Workflow Foundation (WF), ActiveFlow, K2.NET, WorkflowEngine.NET, cDevWorkflow, NetBpm. У некоторых не устраивала стоимость, некоторые были сырыми, некоторые, к тому моменту, долгое время не поддерживались.
    В итоге выбор пал на WF. Мы тогда активно использовали стек Microsoft (WCF, WPF) и решили, что еще одна W нам не помешает. Ещё одним плюсом был наш статус Microsoft Gold Application Development Partner, который давал возможность разрабатывать продукты с использованием технологий Microsoft. Ну и в целом возможности движка нас устраивали и покрывали почти все наши кейсы.

    Что не так с WF?


    Спустя 6 лет использования WF у нас накопился ряд проблем, а стоимость решения этих проблем была слишком высока. Мы начали задумываться над разработкой собственного движка. Расскажу о некоторых из них.

    Дорогая диагностика и исправление ошибок


    Годы шли, количество инсталляций продукта и нагрузка росли. Стали появляться баги, диагностика и исправление которых, отнимали очень много ресурсов. Этому способствовал комплекс причин: недостаток компетенций, ошибки проектирования при встраивании предыдущего движка, особенности WF.
    Нам хватило базовых компетенций, чтобы встроить WF DirectumRX, этого же уровня было достаточно, чтобы справляться с простыми багами. Для сложных случаев компетенций стало не хватать — разбор логов, анализ состояния инстанса, и так далее, давались с трудом.
    Можно было отправить человека на курсы по WF, но вряд ли там учат тому, как анализировать состояние инстанса и связывать его изменение с логами. И честно говоря, ни у кого не было особого желания повышать квалификацию по фактически мертвой технологии.
    Еще один выход – нанять человека с соответствующими компетенциями. Но найти такого в Ижевске не такая тривиальная задача, и не факт, что его уровня хватит для решения именно наших задач.
    По сути мы столкнулись с высоким порогом вхождения для поддержки WF. Так или иначе, я думаю, мы бы справились с этой проблемой, если бы не ряд других причин.
    Еще одной проблемой было то, что при построении схем процессов мы используем собственную нотацию. Она нагляднее и проще в разработке. Например, WF не позволяет реализовать полноценный граф, нельзя рисовать тупиковые блоки, существуют особенности рисования параллельных веток. Расплата за это — конвертация наших схем в схемы WF, которые не так просты и накладывают ряд ограничений. При дебаге приходилось анализировать состояние схемы WF, из-за этого терялась наглядность, приходилось сопоставлять блоки и грани между собой, чтобы понять, на каком шаге находится инстанс.
    image

    Представление схемы в DirectumRX
    image

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

    Postgres в качестве СУБД


    Вот уже который год в России идет тренд на импортозамещение, и все чаще и чаще одним из требований к платформе является поддержка СПО СУБД, либо СУБД отечественного производства. Чаще всего это Postgres. Из коробки WF поддерживает только MS SQL. Для работы с другими БД можно использовать сторонние провайдеры. Мы выбрали dotConnect от DevArt.
    Пока нагрузка была небольшая, все работало хорошо. Но стоило нам погонять систему под нагрузкой, как появились проблемы. WF мог внезапно остановиться и перестать обрабатывать инстансы (кончались prepared transactions), или все сообщения уходили в MSMQ Poisoned Queue и т.д. Со всеми этими проблемами мы справлялись, но тратили на это очень много времени. Не было гарантии, что не появится новая, на решение которой придется потратить еще столько же.

    Уход на .net core


    После того, как Microsoft анонсировала .Net Core, мы решили, что постепенно будем уходить на него, чтобы добиться кроссплатформенности для наших решений. Microsoft решили не брать на борт WF, что закрывало нам дорогу для перевода модуля Workflow на .Net Core в том виде, в котором он существовал. Мы в курсе, что существуют неофициальные порты WF на .Net Core, и среди них даже есть от разработчиков WF, но все они не на 100% совместимы. Еще одним фактором был отказ Microsoft от развития .Net. в пользу .Net Core.

    Новый движок


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

    Выбор


    Основными требованиями при выборе движка были:
    • работа на .Net Core;
    • масштабируемость
    • конвертация существующих экземпляров процессов, с возможностью продолжить выполнение после конвертации
    • разумная стоимость анализа имеющихся проблем
    • работа с разными СУБД

    Помимо этого, требовалось, чтобы активности (Activity) могли выполнять прикладной код на C#, была возможность отладки блоков и т.д.
    В рамках анализа существующих движков посмотрели:
    1. Core WF
    2. FlowWright
    3. K2 Workflow
    4. Workflow Core
    5. Zeebe
    6. Workflow Engine
    7. Durable Task Framework
    8. Camunda
    9. Orleans Activities

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

    Реализация/архитектура


    В предыдущей реализации модуль WF представлял из себя WCF-сервис, к которому были подключены библиотеки WF. Он умел создавать экземпляры процессов, стартовать процессы, выполнять блоки, в том числе бизнес-логику (код написанный разработчиками). Все это хостилось в приложении IIS.
    В новой реализации, следуя тренду микросервисной архитектуры, мы решили сразу разделить сервис на два: Workflow Process Service (WPS) и Workflow Block Service (WBS), которые могли бы хоститься отдельно. Еще одно звено в этой цепочке – это Сервис приложений, который выполняет системную и бизнес-логику DirectumRX, с ним работают клиенты.
    WPS «ходит» по схеме, WBS обрабатывает блоки и запускает бизнес-логику на каждом шаге. Команда на старт процесса приходит от Сервера приложений. Взаимодействие между сервисами осуществляется с помощью RabbitMQ. Ниже расскажу подробнее о каждом из них.


    WPS


    Workflow Process Service представляет из себя микросервис, который отвечает за запуск процессов и обход схемы процесса.
    Хранилище сервиса содержит схемы процессов с поддержкой версионности и сериализованное состояние экземпляров процессов. В качестве хранилища можно использовать MS SQL и Postgres.
    Сервис умеет обрабатывать сообщения, пришедшие от других сервисов посредством RabbitMQ. По сути, сообщения – это API сервиса. Типы сообщений, которые может принимать сервис:
    • StartProcess – создание нового экземпляра процесса и запуск обхода по нему;
    • CompleteBlock – завершение выполнения блока, после этого сообщения сервис двигает экземпляр процесса дальше по схеме;
    • Suspend/ResumeProcess – приостановить выполнение экземпляра процесса, например, из-за ошибки при обработке блока, и возобновить выполнение, после того, как ошибка будет исправлена;
    • Abort/RestartProcess – прекратить выполнение экземпляра процесса и запустить его сначала;
    • DeleteProcess – удаление экземпляра процесса.

    Схема состоит из блоков и связей между ними (граней). У каждой грани есть идентификатор, так называемый «Результат выполнения». Есть 5 типов блоков:
    • StartBlock;
    • Block;
    • OrBlock;
    • AndBlock;
    • FinishBlock.

    image

    Представление схемы на WPS
    Когда приходит сообщение на старт процесса, сервис создает экземпляр и начинает «ходить» по схеме. Класс, отвечающий за «хождение» по схеме, мы в шутку называем «Шагатель». Схема всегда начинается с блока StartBlock. Затем шагатель берет все выходящие грани и активирует их. Каждый блок, работает по принципу блока «И», т.е. все входящие грани должны быть активны, чтобы можно было активировать блок. Дальше алгоритм решает, какие блоки можно активировать и отправляет сообщение WBS на активацию этих блоков. WBS обрабатывает блок и возвращает результат выполнения WPS. В зависимости от результата выполнения, шагатель выбирает подходящие грани, выходящие из блока, для активации, и процесс продолжается.
    В процессе разработки натолкнулись на интересные ситуации, связанные с циклическими связями между блоками, которые добавили логики при решении, какой блок активировать/прекращать.
    Сервис является автономным, т.е. достаточно передать ему схемы в формате Json, написать свой обработчик блоков, и можно обмениваться сообщениями.

    WBS


    Workflow Block Service представляет собой сервис, который обрабатывает блоки схемы. Сервис знает про сущности бизнес-логики, такие, как задача, задание и т.д. Эти сущности можно добавить в среде разработки DirectumRX Development Studio (DDS). Например, у наших блоков есть событие на старт блока. Код обработчика этого события пишет разработчик в DDS, а WBS выполняет этот код. По сути, это наша реализация обработчика блоков, ее можно заменить своей.
    Сервис хранит состояние блоков. Помимо базовых свойств (Id, State), в блоке может содержаться другая информация, необходимая при выполнении/прекращении/приостановке блока.
    Блоки могут находиться в состоянии:
    • Completed – переходит в это состояние после успешного завершения работ по блоку;
    • Pending – находится в состоянии ожидания, когда выполняются какие-то работы в рамках блока, например, требуется какой-то ответ от пользователя;
    • Aborted – переходит в это состояние, когда процесс прекращают;
    • Suspended – переходит в это состояние, когда процесс останавливается при возникновении ошибки.

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

    Масштабируемость


    WPS и WBS можно развернуть в нескольких экземплярах. В один момент времени один экземпляр процесса может обрабатывать только один сервис WPS. То же самое касается обработки блоков – по одному экземпляру процесса в один момент времени может обрабатывать только один блок. В этом помогают блокировки, которые ставятся на процесс во время обработки. Если в очереди несколько сообщений на обработку процесса/блоков по одному процессу, то сообщение откладывается на некоторое время. В то же время каждый сервис может одновременно выполнять работы по нескольким экземплярам процессов.
    Может возникнуть ситуация, когда по одному процессу друг за другом приходит несколько сообщений для обработки блоков (параллельные ветки). Чтобы уменьшить количество ситуаций, когда приходится откладывать сообщения, WBS за раз берет в работу несколько сообщений и выполняет их друг за другом, минуя отправку в очередь на повторное выполнение из-за блокировки процесса.

    Конвертация


    После перехода на новый движок встал вопрос, а что делать с существующими экземплярами процессов? Предпочтительным был вариант их конвертации, чтобы они продолжили работать на новом движке. Плюсы очевидны: мы поддерживаем только один движок, исчезают проблемы поддержки старого движка (см. выше). Но тут были риски, что мы не сможем до конца разобраться, как достать нужные нам данные из сериализованных экземпляров процессов. Был и запасной вариант: дать существующим инстансам доработать на старом движке, а новые запускать на свежем. Минусы этого варианта вытекают из плюсов предыдущего, плюс нужны дополнительные ресурсы, чтобы крутить оба движка.
    Для конвертации нам нужно было взять старое состояние процесса в формате WF и сгенерировать состояния процессов и блоков. Мы написали утилиту, которая брала сериализованное состояние экземпляра процесса в БД, вытаскивала из него список активных блоков, результаты выполнения для граней, и виртуально выполняла процесс. В итоге мы получили состояние инстанса на момент конвертации.
    Сложности возникли с тем, как правильно десериализовать данные экземпляров процессов в WF. Состояние экземпляра процесса (инстанса) WF хранит в базе данных в виде xaml. Мы не смогли найти четкого описания структуры этого xaml, пришлось доходить до всего эмпирическим путем. Парсили данные вручную и вытаскивали нужную нам информацию. В рамках этой задачи мы прорабатывали еще один вариант – средствами WF десериализовать состояние инстанса и уже из объектов пытаться получить информацию. Но из-за того, что структура таких объектов была очень сложной, мы отказались от этой идеи и остановились на «ручном» парсинге xaml.
    В итоге конвертацию провели успешно, и все экземпляры процессов стали обрабатываться новым движком.

    Заключение


    Так что же дал нам свой движок Workflow? Собственно, нам удалось победить все проблемы, озвученные в начале статьи:
    • движок написан на .NET Core;
    • это self-host сервис, не зависящий от IIS;
    • в качестве тестовой эксплуатации мы активно используем новый движок в корпоративной системе и успели убедиться, что анализ багов занимает гораздо меньше времени;
    • провели нагрузочное тестирование на Postgres, по предварительным данным связка WPS+WBS без проблем справляется с нагрузкой от 5000 одновременно работающих пользователей;
    • ну и конечно, как и любая интересная работа, — это интересный опыт.

    Бонусом мы получили понятный и поддерживаемый код, который можем адаптировать под себя.
    Стоимость движка вышла сопоставимой с тем, что нам пришлось бы потратить на покупку/адаптацию стороннего продукта. На данный момент считаем, что решение разрабатывать свой движок оказалось оправданным.
    Еще нас ждет нагрузочное тестирование на 10000+ одновременно работающих пользователей. Возможно потребуются какие-то оптимизации, а может быть и так взлетит? ;-)
    Недавно мы выпустили DirectumRX 3.2, в состав которой вошёл новый Workflow. Посмотрим, как движок покажет себя у клиентов.
    Directum
    Цифровизация процессов и документов

    Комментарии 8

      0
      По аналогии с той же камундой — движок или часть его планирует стать опен-сорсом?
        0
        WBS точно нет, потому что он завязан на наши бизнес-сущности. А по WPS планы были, но пока он зависит от нашей сборки, которой нет в открытом доступе. По ней тоже есть планы. Ну и перед тем как выложить, хотелось кое-что порефакторить. Короче если выложим, то не скоро.
        0
        1. Как вы синхронизуеете контексты? или шарите данные между блоками?
        допустим отработал первый блок, второй должен продолжить работу с данными из первогь блока.

        2. как вы управляете конфигурацией блока? начальные variables/env?

        3. вы делаете валидацию конфигурафии? перед сохранением к примеру
          0
          1. Блок либо выполняется полностью, либо останавливает выполнение, если нужен ответ пользователя. И в том и в другом случае, состояние блока сохраняется в БД. Другое дело, что у нас нет API для доступа к этим данным из другого блока, пока вроде не было необходимости.
          2. Конфигурация блока задается в DirectumRX Development Studio. Это делает разработчик бизнес-логики, там же разрабатывается схема процесса. Как работать с этими данными знает только WBS. WPS ничего про это не знает, его задача двигать процесс по схеме.
          3. Если имеется ввиду валидация схемы процесса, то да. Срабатывает на этапе разработки при сохранении. Например, проверка на то, что из блока нет 2 и более исходящих ребер с одинаковыми результатами выполнения.
            0
            По п.1 дополню: один блок может обрабатываться только одним WBS, ставится блокировка, а результат работы блока — изменение данных/состояния бизнес-сущностей системы (экземпляры заданий, записей справочников и пр.), поэтому данные между блоками напрямую передавать особого смысла нет, всё берётся из и сохраняется в бизнес-сущности.

            И по п.2 дополню: есть параметры «по умолчанию» и есть события старта/окончания/перезапуска блока (это основные события, есть еще), для них пишутся обработчики в прикладном коде которые вызываются из WBS и позволяют эти параметры изменить.
            0
            Ух, ностальгия))
            Главная беда WF — низкое качество стора. Отсутствие документации, хранение данных в нечитаемом виде и прочее. Не думали просто стор переписать?
              +1
              Идея была, но это бы не решило всех проблем, которые накопились. Поэтому решили, что проще новый написать.
              0
              Не туда.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое