![](https://habrastorage.org/webt/ge/gi/co/gegiconfwag8g2kmxnd_ar4yakg.jpeg)
В этой статье я расскажу об опыте использования Apache ServiceMix (Camel) и RabbitMQ для построения такой интеграционной среды.
Стэк технологий
RabbitMQ
RabbitMQ – система обмена сообщениями на базе протокола AMQP. Подробно описывать саму систему не стану, на Хабре уже есть несколько статей, которые детально описывают функционал системы, расскажу о том, где и как RabbitMQ используется у нас.
![](https://habrastorage.org/webt/cy/gj/ou/cygjouggja5hrso04j1lhsvtw_s.jpeg)
Через RabbitMQ мы обмениваемся данными с информационными системами компании. Для каждой системы создается набор очередей. Например, нужно организовать поиск клиентов по ФИО и дате рождения в учетной системе. Создаем пару очередей. Одна входящая по отношению к учетной системе, в нее будем посылать запросы с указанием ФИО и даты рождения клиента. Учетная система слушает входящую очередь и обрабатывает поступающие запросы. Вторая — исходящая, в которую учетная система будет отправлять ответы со списком найденных клиентов, подходящих под условия запроса.
Почему это удобно:
- Сообщения, которые хранятся в очереди, персистентные. Т.е. если система какое-то время недоступна, то сообщения никуда не пропадут и будут обработаны после того, как система поднимется. Перезапуск самого RabbitMQ также не приводит к потере сообщений.
- Очередь служит буфером, который система может обрабатывать в комфортном режиме, избегая пиковых нагрузок. Понятно, что для запросов, выполнения которых ждет в интерфейсе пользователь, это не подойдет. В этом случае нужен незамедлительный ответ. А вот для разного рода асинхронных взаимодействий подходит очень хорошо.
- Асинхронный режим взаимодействия. При этом также поддерживаются и синхронные вызовы.
Научить систему взаимодействовать с RabbitMQ не сложно. У RabbitMQ есть готовые клиенты под различные платформы (Java, .Net, Python и пр.). Клиент простой и понятный. Код, который читает сообщения из очереди и / или отправляет сообщения в очередь, занимает несколько строк. Понятно, что не каждую систему можно подружить с RabbitMQ, например, в случае с легаси и коробочными системами сделать это будет затруднительно. В этом случае обмен строится с использованием технологий, которые поддерживают эти системы, где-то мы вызываем хранимые процедуры в БД, где-то используем web и rest сервисы, где-то что-то еще. Для того чтобы организовать такое взаимодействие и для многих других интеграционных задач, используется продукт Apache ServiceMix.
Apache ServiceMix
ServiceMix – Karaf контейнер с предустановленным набором бандлов, которые пригодятся для решения различных интеграционных задач.![](https://habrastorage.org/webt/5z/wh/_s/5zwh_sd8qqwp5qcmzqhmlro3kte.jpeg)
Немного более подробно о том, что входит в состав продукта:
- Собственно сам Karaf контейнер, в котором работают бандлы и который позволяет ими управлять: устанавливать / удалять / останавливать / запускать, просматривать логи, видеть зависимости компонентов и т.д.
- Широкий набор бандлов, которые выполняют классические интеграционные функции: валидация, различные трансформации (например, из JSON в XML, трансформации при помощи XSLT и т.д.), обогащение, роутинг, split and join, мониторинг, исполнение интеграционных процессов и т.д.
- Широкий набор различных адаптеров: файловые адаптеры, адаптеры к web и rest сервисам, JMS, RabbitMQ, Kafka и т.д. Полный список адаптеров можно посмотреть на сайте Camel.
Ограничиться использованием только предустановленных бандлов вряд ли удастся, но для начала установленного набора должно хватить. Т.к. мы работаем с Karaf контейнером, то можно устанавливать любые необходимые бандлы, сделать это можно либо устанавливая фичи (наборы бандлов), либо просто устанавливая отдельные бандлы. Само собой, можно писать собственные бандлы, либо заворачивать в бандлы сторонние java библиотеки.
Apache Camel
Ключевым компонентом ServiceMix является фреймворк Apache Camel, который позволяет строить интеграционные процессы, которые в терминологии Camel называются роутами.
Покажу на примере — что такое роут:
![](https://habrastorage.org/webt/tk/hn/ol/tkhnolyocsy70rx9smsulx_4m6a.jpeg)
Это пример простого роута, который преобразует поступающие разноформатные сообщения к общему выходному формату. Роут в зависимости от формата сообщения выполняет его маршрутизацию на соответствующую трансформацию, которая преобразует сообщение к общему формату, после чего отправляет результат на выход.
Camel поддерживает различные нотации для описания роутов, наиболее распространенные Java DSL и Spring XML. Мы используем Spring XML. Вот как бы выглядел роут на картинке в нотации Spring XML:
<route id="Normalizer">
<from uri="входной endpoint" />
<!-- Предположим что формат определяется на основании названия корневого тэга сообщения -->
<choice>
<when>
<xpath>/*[local-name()='Format_1']</xpath>
<to uri="xslt:classpath:xslt/Format_1_Transformation.xslt" />
</when>
<when>
<xpath>/*[local-name()='Format_2']</xpath>
<to uri="xslt:classpath:xslt/Format_2_Transformation.xslt" />
</when>
<when>
<xpath>/*[local-name()='Format_3']</xpath>
<to uri="xslt:classpath:xslt/Format_3_Transformation.xslt" />
</when>
</choice>
<to uri="выходной endpoint" />
</route>
Весьма приятным дополнением является то, что Camel прекрасно интегрирован со Spring. Можно использовать привычный Spring XML, в котором определяются бины, и в этом же Spring XML определять Camel роуты. При этом, из роутов можно вызывать бины, а из бинов можно обращаться к роутам. Вызов бинов из роутов реализован со свойственной Camel гибкостью, можно передавать в метод бина тело сообщения, можно передавать заголовки + тело, а можно передать только значение определенного тэга XML сообщения, указанного через XPATH выражение, а результат выполнения метода использовать в конструкции choice для последующего определения маршрута. И все это практически в одну строку.
Вот пример элегантности в стиле Camel:
<camel:camelContext>
<route id="Bean method invocation">
<from uri="входной endpoint" />
<when>
<simple>${bean:authManager?method=checkToken(${body})}</simple>
<to uri="продолжить обработку" />
</when>
</route>
</camel:camelContext>
<bean id="authManager" class="pachage.AuthManager" />
public class AuthManager {
public boolean checkToken(@Body Document xml, @XPath("/Root/Token/@Value") String token) {
return checkSessionToken(token);
}
}
Еще одно важное понятие в Camel – это endpoint (далее эндпоинт). Роуты могут читать сообщения из эндпоинтов, могут посылать сообщения в эндпоинты. Эндпоинтом может выступать, например, RabbitMQ очередь, файл в директории, опубликованный rest сервис и т.д. Если роут читает эндпоинт, то при поступлении сообщения в этот эндпоинт роут начинает его обработку. Это позволяет опубликовать роут, т.е. дать возможность обращаться к нему внешним системам. Если у вас есть роут, который выполняет какую-то задачу, например, проверяет корректность заполненных в анкете данных, то вы его можете опубликовать как web сервис, или дать возможность обращаться к нему через JMS, а можете сделать и то и другое, чтобы внешние системы могли пользоваться возможностями вашего роута, кто-то через вызовы web сервиса, а кто-то путем обмена сообщениями через JMS очереди.
Также эндпоинты используются для того, чтобы роуты могли взаимодействовать друг с другом. Один роут может послать сообщение в эндпоинт, который читает другой роут, таким образом передав сообщение ему в обработку. Это позволяет создать собственную палитру роутов, реализующих различные функции, востребованные в разных местах вашего приложения. Например, логирование сообщений. Вместо того чтобы каждый раз выполнять один и тот же набор действий по логированию, можно просто передавать обработку сообщений специально разработанному для этого роуту.
Интеграционная архитектура
В нашей компании используется некоторое количество информационных систем. Для того чтобы организовать интеграционную среду, через которую системы будут обмениваться данными, нужно для начала подключить наши системы к интеграционной платформе. Для этого для каждой системы на ServiceMix разрабатывается адаптер, который будет отвечать за обмен данными с системой и преобразование форматов данных.
![](https://habrastorage.org/webt/wb/2h/zs/wb2hzsq8fqs4m9zytjrgdretpiq.jpeg)
Для каждой системы выбирается одна технология обмена данными между системой и ServiceMix. Можно выбрать несколько, но это усложняет реализацию, как на стороне системы, так и на стороне ServiceMix. В общем случае использование нескольких разных технологий не оправдано, но технически вполне может быть реализовано. В основном для обмена с системами мы используем RabbitMQ (создаем наборы очередей, через которые ServiceMix обменивается сообщениями с интегрируемыми системами). Но встречаются и другие случаи, в которых нам очень помогает набор готовых адаптеров, который входит в состав ServiceMix. Например, в случае с нашей учетной системой мы используем хранимые процедуры в БД. Для вызова хранимых процедур используем компонент MyBatis, который позволяет маппировать сообщения, которые ходят по ServiceMix, на параметры хранимых процедур. Вот пример такого маппинга для процедуры установки логина пользователю по его ID:
<select id="setLogin" statementType="CALLABLE" parameterType="java.util.Map">
{call esb.SetLogin (
@UserId=#{UserId, mode=IN},
@LoginId=#{LoginId, mode=IN}
)}
</select>
Также адаптеры отвечают за преобразование форматов, в которых системы взаимодействуют с интеграционной платформой, к внутреннему формату. Кто-то предпочитает обмениваться в формате JSON, кто-то использует свой формат XML, но внутри интеграционной платформы компоненты обмениваются данными во внутреннем каноническом XML формате. Сейчас XML теряет популярность ввиду его тяжеловесности и появившихся альтернатив в виде JSON, Protobuf и т.д. Но, на мой взгляд, XML все же удобен для решения интеграционных задач. Есть много полезных технологий, таких как XSLT и XPATH, которые сильно упрощают жизнь, плюс он вполне человекочитаем.
Маршрутизация сообщений между компонентами (адаптерами) осуществляется на основании правил маршрутизации. Роуты внутри одного компонента интеграционной платформы взаимодействуют через внутренние эндпоинты Camel (direct, seda). Между собой компоненты обмениваются через очереди RabbitMQ. Это позволяет сделать компоненты интеграционной платформы независимыми. Падение одного компонента не влияет на работоспособность других. При временном повышении нагрузки сообщения будут накапливаться в очереди компонента, не оказывая влияния на весь остальной обмен.
![](https://habrastorage.org/webt/ek/oa/ah/ekoaahoa7xhkihjueuiolzoislw.jpeg)
Чем такая архитектура лучше, чем прямое взаимодействие между системами по принципу точка-точка:
- Каждой системе нужно поддерживать только один протокол и формат взаимодействия.
- Детали обмена с другими системами скрыты от систем за интеграционной платформой. Системы взаимодействуют только с интеграционной платформой и могут даже не знать про существование других систем, как минимум, не заботиться об особенностях взаимодействия с ними.
- Системы слабосвязаны, что дает дополнительную гибкость, например, если нужно заменить одну систему на новую.
- Инструменты мониторинга, тестирования, поддержки интеграции централизованы на стороне интеграционной платформы.
Отказоустойчивость
![](https://habrastorage.org/webt/n5/lm/ps/n5lmpsupchl3tlptud8jeggz6o8.jpeg)
Отказоустойчивость на уровне RabbitMQ достигается за счет создания кластера и синхронизации очередей. Каждое сообщение, которое попадает в очередь на одной из нод RabbitMQ, реплицируется на другие ноды согласно политике синхронизации (можно реплицировать на все ноды кластера, можно реплицировать на определенное количество нод, можно не реплицировать вообще). Мы используем конфигурацию из трех нод с репликацией сообщений на все ноды. Это позволяет сохранить полную работоспособность кластера в случае падения двух нод из трех. Но нужно понимать, что это и самый затратный вариант в плане времени обработки каждого сообщения и занимаемого места на диске. На клиенте к RabbitMQ прописываются все три ноды, при этом коннект открывается к одной из нод, но в случае ее падения клиент прозрачно переключится на другие ноды и продолжит работать.
Отказоустойчивость ServiceMix достигается за счет использования кластера из двух нод ServiceMix. При этом часть бандлов работают параллельно, если это допустимо. Например, адаптеры, которые читают одну RabbitMQ очередь, вполне могу делать это параллельно, одно сообщение всегда получит только кто-то один из них, но при этом сообщения будут равномерно распределяться между двумя нодами. В случае падения одной из нод всю нагрузку берет на себя вторая нода. Часть бандлов активна только на одной ноде, но при ее падении бандлы активируются на второй ноде. Например, в случае если адаптер читает общий сетевой каталог, то одновременное чтение одного и того же файла может привести к дублированию и коллизиям. Такой режим работы достигается за счет использования разделяемых блокировок на основе Hazelcast. Бандл, который первым захватил блокировку, переходит в активный режим и выполняет свою функцию, второй бандл висит в ожидании освобождения блокировки.
Hello world
В завершении хочу привести простой пример Hello world приложения на Camel, которое мы запустим на ServiceMix. Наше приложение будет просто один раз писать в лог ServiceMix фразу “Hello world” при запуске бандла (например, при деплое бандла или перезапуске ServiceMix).
- Качаем дистрибутив ServiceMix, распаковываем и запускаем servicemix.sh (bat).
- Создаем и настраиваем новый maven проект.
В src/main/resources нужно создать каталог META-INF, в котором создать подкаталог spring.
Т.к. нам нужно собрать бандл — правим pom.xml (добавляем инструкции packaging и build):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>HelloWorld</groupId> <artifactId>HelloWorld</artifactId> <version>1.0.0-SNAPSHOT</version> <name>HelloWorld</name> <description>HelloWorld</description> <packaging>bundle</packaging> <build> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>3.0.1</version> <extensions>true</extensions> <configuration> <instructions> <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> <Import-Package>*</Import-Package> <Export-Package>${project.groupId};version=${project.version}</Export-Package> </instructions> </configuration> </plugin> </plugins> </build> </project>
Каких-либо maven зависимостей добавлять не нужно.
- Настраиваем Camel контекст и роут.
В каталоге spring нужно создать файл camel-context.xml (загрузчик автоматически ищет файл с описанием Camel контекста в META-INF/spring и запускает роуты). В файл camel-context.xml поместим следующее содержимое (по тексту даны комментарии):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> <!-- Описание Camel контекста --> <camel:camelContext xmlns="http://camel.apache.org/schema/spring" id="HelloWorldContext"> <!-- Описание роута --> <route id="HelloWorldRoute"> <!-- Запуск обработки по таймеру один раз при старте роута --> <from uri="timer://startTimer?repeatCount=1" /> <!-- Записываем в тело сообщения текст Hello world --> <setBody> <constant>Hello world</constant> </setBody> <!-- Выводим тело сообщения в лог --> <log message="${body}"/> </route> </camel:camelContext> </beans>
Для того чтобы наш роут выполнил свою задачу (записал в лог текст “Hello world”), нужно передать ему на вход сообщение. В нашем случае эту задачу решает таймер <from uri=«timer://startTimer?repeatCount=1»/>, который, следуя инструкции repeatCount=1, один раз отправит на вход роута сообщение. Т.к. таймер посылает на вход роута пустое сообщение, нам нужно его чем-то заполнить – помещаем в тело сообщения текст “Hello world” <setBody>. В конце роута выводим содержимое тела сообщения в лог <log message="${body}">.
- Собираем наш проект: mvn package
- Деплоим собранный бандл.
Одним из способов задеплоить бандл в ServiceMix является копирование jar файла в каталог deploy. Копируем собранный jar в каталог ServiceMix/deploy.
Теперь смотрим лог ServiceMix: ServiceMix/data/log/servicemix.log. Помимо информации, что мы установили и запустили новый бандл, должна появиться надпись “Hello world”:
HelloWorldRoute | 43 — org.apache.camel.camel-core — 2.16.3 | Hello world
Попробуйте зайти в ssh консоль и просмотреть список Camel контекстов context-list и список роутов route-list. В выводе команд вы найдете наши HelloWorldContext и HelloWorldRoute.
Выводы
В заключение хочу сказать, что ServiceMix и Camel отличные продукты для построения интеграционных решений. Функциональные возможности поражают, практически для любой задачи можно найти элегантное и простое решение. Видно, что продукты очень хорошо продуманы, разработчики действительно потрудились на славу. В нашей компании ServiceMix используется уже 2 года, на текущий момент мы интегрировали порядка 16 наших информационных систем и разработали более 150 интеграционных сервисов. С некоторыми компонентами все же возникали проблемы, но всегда есть аналогичные компоненты, из которых можно выбрать или, в крайнем случае, разработать свой компонент. В целом, продукты работают стабильно и надежно, лично мое впечатление самое положительное. Несомненным плюсом является открытость исходного кода и отсутствие необходимости покупать лицензию. При этом продукты совершенно не уступают коммерческим аналогам. Также хочу отметить, что ServiceMix и Camel можно использовать не только для решения интеграционных задач. Например, Karaf прекрасно подходит под разворачивание в него web приложений. Эту возможность мы используем для того, чтобы предоставлять web интерфейсы администраторам для настройки интеграционной платформы. Camel прекрасно интегрируется с различными бизнесовыми компонентами. Например, мы используем Drools (движок бизнес правил) для отнесения поступающих сделок на соответствующий портфель по определенным правилам (обогащение сделки портфелем), которые настраиваются пользователями из миддл-офиса. Очень интересным продуктом является Activiti (BPM движок), который может работать в Karaf контейнере и интегрироваться с Camel. Все вместе это позволяет создавать очень интересные решения, совмещающие в себе интеграционные и бизнес функции.