Микросервисная архитектура предполагает декомпозицию системы на относительно независимые фрагменты с собственными источниками данных, которые могут переиспользоваться в различных процессах и обмениваться данными. Но в таком решении есть и оборотная сторона, связанная с необходимостью включения логики оркестрации или непосредственно в код сервисов (что затрудняет возможность гибкого изменения процесса), либо использовать внешний оркестратор, который будет обеспечивать запуск микросервисов с входными параметрами, получение и передачу результата, а также управление сценарием при возникновении ошибок или определенных ситуаций при выполнении процесса.
Второй вариант может быть реализован в виде исполняемого кода, либо с использованием специальных движков для исполнения сценария бизнес-процесса, который может включать в себя вызов внешних сервисов. Стандартом в области описания бизнес-процессов является визуальная нотация BPMN 2.0 и наибольший интерес представляет соединение графической диаграммы и исполняемых сценариев, которое также называется Executable BPMN 2.0 и среды для его исполнения, среди которых можно назвать jBPM, Flowable, Camunda BPM и Activiti (она интересна еще и тем, что на ней реализуется управление процессами в Open Source системе управления документами Alfresco). В этой статье мы рассмотрим основы BPMN и создадим простой процесс для управления системой полива в зависимости от измеренной влажности (все компоненты системы реализованы как микросервисы).
Прежде чем мы перейдем к рассмотрению Activiti и ее настройке, нужно сказать несколько слов о BPMN. Эта нотация появилась как результат упрощения и обобщения для описания реальных бизнес-процессов другой известной нотации UML Activity Diagram (в более новой версии UML AD используются сходные графические элементы, как в BPMN). BPMN определяет соглашения по представлению потока управления и потока данных (разные варианты линий со стрелками), описанию выполняемых задач (они могут быть реализованы как пользовательские User Task, например при описании документооборота, так и выполняться через сценарий Script Task), разделения и склейки потока управления (выбор одного из вариантов, параллельное выполнение), передачи и приема сигналов и сообщений, а также описания стартового события сценария (может быть сообщением или, например, таймером). Важным аспектом описания является возможность определения альтернативных завершений задачи (по таймауту, при возникновении исключения, при прерывании и т.д.), благодаря этому можно описывать не только основной (успешный) сценарий, но и корректно обрабатывать ошибки и неожиданные ситуации.
Все элементы имеют обобщенное графическое отображение (например, задачи изображаются как прямоугольник, шлюзы управления — ромбом, события — окружностью) и уточняющую нотацию (чаще всего пиктограмма внутри объекта, например для идентификации сценарной или пользовательской задачи).
BPMN файл представляет из себя XML-документ, который содержит описание элементов процесса (события начала и завершения, коннекторы для передачи сигналов, задачи и соединители между элементами для определения потока выполнения). Также в элементах могут указываться артефакты (документы, аннотации) и к любому из элементов может быть добавлено описание (например, для пояснения потока выполнения после ветвления), а также может содержаться дополнительная часть описания расположения элементов на диаграмме).
<definitions targetNamespace="http://activiti.org/bpmn20"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI">
<process id="helloworld">
<documentation>Simple "Hello World" process</documentation>
<startEvent id="startevent1" name="Start"></startEvent>
<scriptTask id="task1" name="Hello World" scriptFormat="groovy">
<script>
execution.setVariable("message", "Hello, World!")
</script>
</scriptTask>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow1" name="" sourceRef="startevent1" targetRef="task1"></sequenceFlow>
<sequenceFlow id="flow2" name="" sourceRef="task1" targetRef="endevent1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_helloWorldProcess">
<bpmndi:BPMNPlane bpmnElement="helloWorldProcess" id="BPMNPlane_helloWorldProcess">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="55" width="55" x="273" y="10"></omgdc:Bounds>
</bpmndi:BPMNShape>
<!-- здесь другие элементы -->
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
Здесь определено начальное событие (startevent1
), задача для пользователя (task1
) и конечное событие (endevent1
), а также направление потока выполнения (после startevent1
переходим к task1
, после task1
к endevent1
). Визуально диаграмма может выглядеть следующим образом:
Для проектирования процесса можно использовать любой инструмент для разработки BPMN-диаграмм, например встроенный визуальный редактор процессов Activiti Explorer. Разработанный процесс может запускаться как внутри Activiti, так и в любом коде через импорт движка Activiti как зависимости:
implementation 'org.activiti:activiti-engine:7.1.0.M6'
и в дальнейшем в коде создается движок и выполняется запуск процесса:
val processEngine = ProcessEngineConfiguration.
createStandaloneInMemProcessEngineConfiguration().
setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_FALSE).
setJdbcUrl("jdbc:h2:mem:my-own-db;DB_CLOSE_DELAY=1000").
setAsyncExecutorActivate(false).
buildProcessEngine();
Дальше из processEngine можно получить доступ к сервисам:
processEngine.getRuntimeService()
— управление выполнением сценарияprocessEngine.getRepositoryService()
— отвечает за загрузку и хранение ресурсов (включая описание BPMN-процессов)processEngine.getIdentityService()
— отвечает за управление учетными записямиprocessEngine.getHistoryService()
— возможность посмотреть историю запущенного workflowprocessEngine.getTaskService()
— возможность программно создавать задачи и смотреть за текущим состоянием
Для запуска процесса из созданного processEngine необходимо обратиться к репозиторию и затем передать на выполнение в RuntimeService:
val repositoryService = processEngine.getRepositoryService();
repositoryService.createDeployment()
.addClasspathResource("hello.bpmn20.xml")
.deploy();
val variables = mutableMapOf<String, Object>();
variables["name"] = "World";
val runtimeService = processEngine.getRuntimeService();
val processInstance = runtimeService.startProcessInstanceByKey("helloworld", variables);
Для передачи значений между элементами (узлами) процесса используется контекст переменных, которые могут быть инициализированы при старте процесса, модифицироваться во время выполнения процесса (через execution.SetVariable
) и использоваться при принятии решений в узлах ветвления (ExclusiveGateway
), а также быть получены после завершения выполнения процесса. Также существует контекст задачи (объект Task
) из которого можно получать сохраненные локальные результаты, например, может быть добавлен Listener по завершению ScriptTask (внутри определения задачи):
<activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener">
<activiti:field name="script">
<activiti:string>
execution.setVariable('message', task.getVariable('message'));
</activiti:string>
</activiti:field>
</activiti:taskListener>
Теперь посмотрим внимательнее на scriptTask. В атрибуте scriptFormat указывается язык сценария (groovy, JavaScript, Python), при этом для поддержки Groovy и Python нужно добавить соответствующие зависимости (org.codehaus.groovy:groovy-all:3.0.8
или org.python:jython:2.7.2
). Для запуска JavaScript используются возможности Nashorn API, поэтому для выполнения сетевых запросов можно использовать классы Java:
function read(inputStream){
var inReader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
var inputLine;
var response = new java.lang.StringBuffer();
while ((inputLine = inReader.readLine()) != null) {
response.append(inputLine);
}
inReader.close();
return response.toString();
}
var con = new java.net.URL("URL").openConnection();
con.requestMethod = "GET";
response = read(con.inputStream);
Теперь можно выполнить соответствующие вызовы для обращения к микросервисам из ServiceTask
и сохранить промежуточные результаты в переменные задачи (или процесса) и использовать их в проверке ExclusiveGateway
. Предположим, что наши микросервисы опубликованы на адресах http://gateway/metrics
(возвращает json с текущим значением влажности) и http://gateway/automation
(принимает параметром sprinkler значение 0 или 1 при необходимости включить или выключить поливальную машину). Тогда процесс будет состоять из startEvent
, getMetrics
, gateway
, turnOnAutomation
, turnOffAutomation
, endEvent
. Для getMetrics
и управления автоматизацией код создается аналогично упомянутому выше сценарию, единственный новый элемент — exclusiveGateway
:
<exclusiveGateway id="gateway" name="Exclusive Gateway" />
<sequenceFlow id="flow2" sourceRef="gateway" targetRef="turnOnAutomatic">
<conditionExpression xsi:type="tFormalExpression">${humidity < 30}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow3" sourceRef="gateway" targetRef="turnOffAutomatic">
<conditionExpression xsi:type="tFormalExpression">${humidity >= 30}</conditionExpression>
</sequenceFlow>
Таким образом, использованием BPMN-движка помогает выполнить координацию вызовов микросервисов при возникновении необходимых условий и одновременно с этим сохранить все преимущества визуального проектирования и документирования процесса
Полный исходный текст с описанием BPMN-процесса и код для запуска процесса (и эмуляции API микросервисов) доступен на Github.
Приглашаем всех желающих на открытое занятие «Шардирование в микросервисной архитектуре», на котором рассмотрим виды шардинга, проанализируем стратегии шардирования. Также рассмотрим консистентное шардирование, поиск, вычисления, хранение и как правильно делить данные. Регистрация на занятие.