В данной статье я расскажу вам о том, каким образом можно построить граф маршрутов для приложений с Apache Camel, отслеживать состояния этих маршрутов и собирать для них метрики.
Мы используем Apache Camel в spring приложениях и в Apache ServiceMix. И если маршруты в отдельном сервисе — это штука понятная и легко обозримая, то в рамках шины данных, где таких маршрутов много, не все так просто.
Apache Camel — открытый фреймворк для интеграции приложений за счет использования простого dsl и богатого набора готовых компонентов доступа к данным.
Apache ServiceMix — открытое решение на базе Apache ActiveMQ, Camel, CXF, Karaf, позволяющее построить платформу для интеграционных решений. Apache ServiceMix можно использовать в роли корпоративной сервисной шины. Camel в этом случае позволит упростить создание маршрутов в шине с помощью dsl в виде xml, java, scala и т.д. Например, если нам нужно перекидывать сообщения из одной очереди в другую (давайте не будем думать о том, зачем это нам нужно), мы можем описать маршрут в xml файле (пример ниже), закинуть его в нужную директорию сервиса и он будет развернут.
<?xml version="1.0" encoding="UTF-8"?>
<blueprint
xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.osgi.org/xmlns/blueprint/v1.0.0
http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
<camelContext xmlns="http://camel.apache.org/schema/blueprint">
<route>
<from uri="file:camel/input"/>
<log message="Moving ${file:name} to the output directory"/>
<to uri="file:camel/output"/>
</route>
</camelContext>
</blueprint>
Описанный маршрут перекладывает файлы из одной директории в другую.
За годы использования шина накопила больше сотни разной сложности маршрутов, а мы пришли к пониманию, что становится все труднее и труднее помнить все эти связи. Рисование схем маршрутов руками или описание их в виде таблиц перестало казаться удобным и легко поддерживаемым решением. Но стало казаться, что автоматическое построение графа маршрута всех спасет.
Для построения графа понадобятся вершины и ребра. И из них-то мы и слепим нечто прекрасное!
Элементы маршрута
Точка входа (она одна) для маршрута описывается оператором from
с указанием endpoint'a. Т.е. для
<from uri="file:camel/input"/>
endpoint'ом будет file:camel/input
. Он говорит нам о том, что в начале маршрута файлы будут забираться из директории camel/input
.
Точки выхода из маршрута (их много, именно поэтому я указал множественное число) определяются по разному — в зависимости от шаблона обмена сообщениями также с указанием endpoint'a. В приведенном выше примере такая точка описывается через to
. Т.е. для
<to uri="file:camel/output"/>
endpoint'ом будет file:camel/output
. Он говорит нам о том, что в конце маршрута будет происходить сохранение файлов в директорию camel/output
.
Endpoint'ы — это нужные нам "вершины". Ребра же будут определять сами маршруты.
Получение описания маршрутов
Servicemix предоставляет возможность получить доступ к различной информации средствами JMX и мы воспользуемся jolokia для доступа к этой информации через http.
В качестве примера возьмем такое описание маршрутов
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd">
<camelContext xmlns="http://camel.apache.org/schema/spring">
<route id="the-clock-is-ticking">
<from uri="timer://foo?fixedRate=true&period=1000"/>
<to uri="jms:topic:timer?connectionFactory=demo"/>
</route>
<route id="service-a">
<from uri="jms:topic:timer?connectionFactory=demo"/>
<to uri="jms:queue:service-a?connectionFactory=demo"/>
</route>
<route id="service-a-log">
<from uri="jms:queue:service-a?connectionFactory=demo"/>
<to uri="log:service-a"/>
</route>
</camelContext>
</beans>
Список маршрутов
Метод http://host:8181/jolokia/read/org.apache.camel:type=routes,*
возвращает список маршрутов с деталями.
Пример возвращаемых данных для маршрута service-a
:
"org.apache.camel:context=a.xml,name=\"service-a\",type=routes": {
"StatisticsEnabled": true,
"EndpointUri": "jms:\/\/topic:timer?connectionFactory=demo",
"CamelManagementName": "a.xml",
"ExchangesCompleted": 173,
"LastProcessingTime": 2,
"ExchangesFailed": 0,
"Description": null,
"FirstExchangeCompletedExchangeId": "ID-...",
"StartTimestamp": "2018-12-17T07:01:12Z",
"FirstExchangeCompletedTimestamp": "2018-12-17T07:01:13Z",
"LastExchangeFailureTimestamp": null,
"MaxProcessingTime": 35,
"LastExchangeCompletedTimestamp": "2018-12-17T07:04:05Z",
"Load15": "",
"DeltaProcessingTime": -8,
"OldestInflightDuration": null,
"ExternalRedeliveries": 0,
"ExchangesTotal": 173,
"ResetTimestamp": "2018-12-17T07:01:12Z",
"ExchangesInflight": 0,
"MeanProcessingTime": 4,
"LastExchangeFailureExchangeId": null,
"FirstExchangeFailureExchangeId": null,
"Uptime": "2 minutes",
"CamelId": "camel-3",
"TotalProcessingTime": 827,
"FirstExchangeFailureTimestamp": null,
"RouteId": "service-a",
"RoutePolicyList": "",
"FailuresHandled": 0,
"MessageHistory": true,
"Load05": "",
"OldestInflightExchangeId": null,
"State": "Started",
"InflightExchanges": 0,
"Redeliveries": 0,
"MinProcessingTime": 0,
"LastExchangeCompletedExchangeId": "ID-...",
"Tracing": false,
"Load01": ""
}
Деталей много и среди них особый интерес для построения графа представляют RouteId
, Context
, EndpointUri
, State
, Uptime
.
Важно упомянуть, что метод возвращает и метрики по маршруту: ExchangesTotal
, ExchangesCompleted
, ExchangesFailed
, ExchangesInflight
и т.д.
Приведенный выше метод покрывает 90% наших потребностей в данных, но вот чего он не возвращает, так это информацию о точках выхода из маршрута. Для получения этой информации нужно воспользоваться методом получения деталей маршрута и методом получения схемы. Одного из методов не достаточно, так как в некоторых случаях методы возвращают не все данные, необходимые для формирования списка точек выхода.
Детали маршрута
Детали маршрута получаем из метода
http://host:8181/jolokia/exec/org.apache.camel:context=a.xml,type=routes,name="service-a"/createRouteStaticEndpointJson(boolean)/true
Пример возвращаемых данных:
{
"request": {
"mbean": "org.apache.camel:context=a.xml,name=\"service-a\",type=routes",
"arguments": ["true"],
"type": "exec",
"operation": "createRouteStaticEndpointJson(boolean)"
},
"value": "{\"routes\": { \"service-a\": { \"inputs\": [ { \"uri\": \"jms:\/\/topic:timer?connectionFactory=demo\" } ], \"outputs\": [ { \"uri\": \"jms:\/\/queue:service-a?connectionFactory=demo\" } ] }}\n}\n",
"timestamp": 1545040570,
"status": 200
}
Схема маршрута
Схему маршрута получаем из метода
http://host:8181/jolokia/exec/org.apache.camel:context=a.xml,type=routes,name="service-a"/dumpRouteAsXml(boolean)/true
.
Метод возвращает схему маршрута в xml виде только в том случае, если она была в нем оформлена. К примеру, если маршрут описан с помощью org.apache.camel.builder.RouteBuilder
(используется при описании маршрутов в spring приложении), то метод ничего не вернет.
Пример возвращаемых данных:
{
"request": {
"mbean": "org.apache.camel:context=a.xml,name=\"service-a\",type=routes",
"arguments": ["true"],
"type": "exec",
"operation": "dumpRouteAsXml(boolean)"
},
"value": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<route customId=\"true\" id=\"service-a\" xmlns=\"http:\/\/camel.apache.org\/schema\/spring\">\n <from uri=\"jms:topic:timer?connectionFactory=demo\"\/>\n <to uri=\"jms:queue:service-a?connectionFactory=demo\" id=\"to5\"\/>\n<\/route>\n",
"timestamp": 1545040727,
"status": 200
}
Рисуем граф
Объединив всю полученную информацию можно смело рисовать граф, я воспользовался vis.js
и получил такой результат
Точки — точки входа и выхода, ребра — маршруты, а серые цифры на маршрутах — метрика ExchangesTotal
.
Построение графа для нескольких сервисов
Описанный подход к построению графа подходит и для случая, когда camel используется не только в шине данных, но и в приложениях. Например, описав в приложении маршрут таким образом:
@Component
public class EventRoutes extends RouteBuilder {
@Override
public void configure() throws Exception {
from("jms:topic:timer")
.inOnly("bean:service?method=handle");
}
}
Можно объединить все данные по маршрутам из servicemix
и приложения и нарисовать общий граф
Обратите внимание, что на схеме появился новый маршрут из jms:topic:timer
в bean:service
.
Заключение
Реализовав описанный подход к построению графа маршрутов, мы смогли получить общую картину как для шины, так и для интегрированных сервисов. Собственно граф нашей шины выглядит так
Proof of concept приложения можно посмотреть тут — github
Комментарии (10)
Throwable
10.01.2019 11:59Всегда интересовала целевая задача, где бы появлялось много сложных маршрутов и был необходим роутинг. Возможно это процессинг и анализ большого числа потоковых данных.
Avvero Автор
10.01.2019 12:11В нашем случае такая архитектура, когда общение между сервисами реализовано в асинхронном режиме в виде событий.
sshikov
10.01.2019 17:28В нашем это именно потоковые данные (биржа, например). Сделки, котировки и разные другие документы.
sshikov
10.01.2019 15:10Эк вы ловко объединили два jms:topic в один, из разных контекстов. Я догадываюсь, что у вас наверное возможно такое — но в общем случае это могут быть совсем разные топики на разных jms серверах. И простое объединение узлов графа на базе их названий в camel некорректно (а другого способа не существует, потому что у нас например соединение проходит через qpid, и там внутри тоже существует обработка — которую в подобный граф не включить.
Avvero Автор
10.01.2019 17:08Эк вы ловко объединили два jms:topic в один, из разных контекстов
Почему из разных? Общий jms брокер. И в рамках одного брокера всякий jms:topic:foo ссылается на один и тот же топик foo.
И простое объединение узлов графа на базе их названий в camel некорректно
Да, некорректно. И я так не делаю. Информации, чтобы отличать похожие узлы, достаточно и я их отличаю. Но я прячу детали от пользователя и использую, например, цвета, чтобы визуально показать, что это разные сервисы или контексты.sshikov
10.01.2019 17:27>Почему из разных
Потому что по тексту это выглядит как объединение двух разных routes из разных контекстов (ну, во всяком случае я вас так понял). А в разных контекстах jms:topic:foo вовсе не обязаны быть двумя одинаковыми брокерами.
Ну то есть, я не говорю что так нельзя, но скажем на нашей шине такое бы не прокатило, потому что jms: (или в нашем случае amqp:) это всего-лишь имя в некотором camel registry, и два разных jms вполне могут ссылаться как на один брокер, так и на разные, и понять это можно только если залезть в определение самого jms, и даже там это будет не очевидно.Avvero Автор
10.01.2019 17:38Интересно, а как Вы отличаете jms брокеров тогда? Через connectionFactory?
sshikov
10.01.2019 17:47Конечно. Ну то есть, я хочу сказать, что только посмотрев туда в свойства, вы точно можете сказать, один это брокер, или два разных. Впрочем, у нас такая задача не стояла, как у вас, и никто не пытался построить граф всех маршрутов, в первую очередь потому, что большая часть обработки была в qpid, и в других местах вне camel, поэтому даже если бы мы такой граф построили, от него было бы не очень много пользы. Поэтому нас вполне устроила hawt.io.
SnowBearRu
А в сторону hawt.io не смотрели? По крайней мере некая статистика и роуты в нем визуально оценить можно. Может даже допилить под себя…
Avvero Автор
Конечно, хорошая штука. Но на сколько я помню, через нее удобно работать с отдельными маршрутами. Оценить же весь граф маршрутов в целом сложно.