В данной статье я расскажу вам о том, каким образом можно построить граф маршрутов для приложений с Apache Camel, отслеживать состояния этих маршрутов и собирать для них метрики.
Мы используем Apache Camel в spring приложениях и в Apache ServiceMix. И если маршруты в отдельном сервисе — это штука понятная и легко обозримая, то в рамках шины данных, где таких маршрутов много, не все так просто.


Что такое 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&amp;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)


  1. SnowBearRu
    10.01.2019 09:59

    А в сторону hawt.io не смотрели? По крайней мере некая статистика и роуты в нем визуально оценить можно. Может даже допилить под себя…


    1. Avvero Автор
      10.01.2019 10:42

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


  1. Throwable
    10.01.2019 11:59

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


    1. Avvero Автор
      10.01.2019 12:11

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


    1. sshikov
      10.01.2019 17:28

      В нашем это именно потоковые данные (биржа, например). Сделки, котировки и разные другие документы.


  1. sshikov
    10.01.2019 15:10

    Эк вы ловко объединили два jms:topic в один, из разных контекстов. Я догадываюсь, что у вас наверное возможно такое — но в общем случае это могут быть совсем разные топики на разных jms серверах. И простое объединение узлов графа на базе их названий в camel некорректно (а другого способа не существует, потому что у нас например соединение проходит через qpid, и там внутри тоже существует обработка — которую в подобный граф не включить.


    1. Avvero Автор
      10.01.2019 17:08

      Эк вы ловко объединили два jms:topic в один, из разных контекстов

      Почему из разных? Общий jms брокер. И в рамках одного брокера всякий jms:topic:foo ссылается на один и тот же топик foo.
      И простое объединение узлов графа на базе их названий в camel некорректно

      Да, некорректно. И я так не делаю. Информации, чтобы отличать похожие узлы, достаточно и я их отличаю. Но я прячу детали от пользователя и использую, например, цвета, чтобы визуально показать, что это разные сервисы или контексты.


      1. sshikov
        10.01.2019 17:27

        >Почему из разных
        Потому что по тексту это выглядит как объединение двух разных routes из разных контекстов (ну, во всяком случае я вас так понял). А в разных контекстах jms:topic:foo вовсе не обязаны быть двумя одинаковыми брокерами.

        Ну то есть, я не говорю что так нельзя, но скажем на нашей шине такое бы не прокатило, потому что jms: (или в нашем случае amqp:) это всего-лишь имя в некотором camel registry, и два разных jms вполне могут ссылаться как на один брокер, так и на разные, и понять это можно только если залезть в определение самого jms, и даже там это будет не очевидно.


        1. Avvero Автор
          10.01.2019 17:38

          Интересно, а как Вы отличаете jms брокеров тогда? Через connectionFactory?


          1. sshikov
            10.01.2019 17:47

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