Фреймворк Camel: сравнение компонентов HTTP и AHC

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

Рассматривать будем простой сервис на Camel, который получает запросы от jetty-компонента, обрабатывает их, например, выводит в лог, вызывает другой сервис через http, и обрабатывает ответ от него, например, пишет в лог. Использовать будем Camel версии 3.4.0 и Java 8.

Для тестирования использовались скрипты JMeter для вызова нашего сервиса в соответствии с задуманной частотой и интервалами, а так же небольшой Http-сервис, играющий роль внешнего по отношению к нашему сервису, и выполняющий задержку 5 секунд. Все взаимодействие происходит по локальной петле (127.0.0.1), так что сетевые задержки не учтены, но для сравнительного анализа они и не нужны.

HTTP-компонент

В данном разделе будет рассматриваться стандартный HTTP-компонент для взаимодействия по HTTP. Код простого сервиса:

from("jetty:http://localhost:8080/test")
     .log("receive request body ${body}")
     .removeHeaders("CamelHttp*")
     .to("http://{{another.url}}")
     .log("finish process body ${body}");

Примечание: удаление заголовков, начинающихся на "CamelHttp" необходимо потому, что они выставляются в Jetty-компоненте и могут повлиять на работу Http-компонента.

Для проверки работы данного сервиса запустим скрипт JMeter, который отправляет 25 одновременных запросов.

Samples

Min

Max

Error %

25

5012

7013

20.000%

В результате видим, что 20% или 5 из 25 запросов обработались с ошибкой(Read timed out). Связано это с тем, что у http-компонента по умолчанию установлено ограничение в 20 соединений к одному хосту. Изменяется это ограничение параметром connectionsPerRoute

from("jetty:http://localhost:8080/test")
     .log("receive request body ${body}")
     .removeHeaders("CamelHttp*")
     .to("http://{{another.url}}?connectionsPerRoute=200")
     .log("finish process body ${body}");

После этого исправления все 25 сообщений обрабатываются без ошибок. Но есть еще одно ограничение – это ограничение пула потоков jetty-компонента, по умолчанию 200. Для проверки этого ограничения запустим следующие 4 сценария JMeter:

  1. 200 одновременных запросов

  2. 300 одновременных запросов

  3. 300 запросов равномерно распределенных в течении 5 секунд, с повтором 5 раз

  4. 200 запросов равномерно распределенных в течении 5 секунд, с повтором 5 раз

После запуска 1 сценария в JVM произошел рост потоков до 214 штук, и далее количество потоков не менялось для всех сценариев.

Результаты выполнения тестовых сценариев:

 

Процент ошибок

200 запросов единовременно

0%

300 запросов единовременно

34.667%

300 запросов с повтором 5 раз

71.733%

200 запросов с повтором 5 раз

0%

Первый и четвертый сценарии демонстрируют нормальную работу с допустимой нагрузкой

Второй сценарий с 300 одновременными запросами демонстрирует резкое превышение возможностей настроенного сервиса, 200 запросов обрабатываются потоками jetty-компонента, а остальные 100 дожидаются в пуле задач jetty, и в результате обрабатываются не 5 секунд, а 10. Соответственно 34% ошибок – это примерно эти 100 запросов.

Третий сценарий демонстрирует продолжительную работу сервиса по нагрузкой, превышающей его возможности – 300 запросов равномерно распределяются в 5 секундный интервал, и каждый из них повторяется 5 раз, т.е. каждую секунду в сервис поступает 60 запросов, а так как сервис не может обрабатывать более 200 запросов в один момент времени лишние запросы хранятся в пуле задач и для клиента обрабатываются дольше положенных 5 секунд, в результате клиенты отваливаются по таймауту.

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

Для того чтобы увеличить количество одновременно обрабатываемых запросов, можно увеличить пул потоков jetty-компонента, однако следует помнить что каждый поток в JVM по умолчанию потребляет 1 МБ ОЗУ для хранения стэка, и бесконечно плодить потоки в современном мире Docker-контейнеров и микросервисов невозможно, лимиты по памяти не позволят это сделать. Лучше рассмотрим другой подход в следующем разделе.

AHC-компонент

AHC-компонент - это еще один компонент фреймворка Camel для взаимодействия по HTTP. Основан он на библиотеке AsyncHttpClient, позволяющей реализовывать асинхронное (реактивное) взаимодействие. За счет этого компонента попытаемся добиться реактивной работы сервиса – в обычном синхронном режиме с http-компонентом наши потоки просто стояли и ждали, пока внешний сервис нам ответ, т.е. в пустую тратили 5 секунд времени. В асинхронном же компоненте они сразу после отправки запроса освобождаются и готовы принимать новые запросы, а ответы на эти запросы при их получении обрабатываются другим пулом потоков. Изменения в нашем сервисе будут совсем небольшие:

from("jetty:http://localhost:8080/test")
     .log("receive request body ${body}")
     .removeHeaders("CamelHttp*")
     .to("ahc:http://{{another.url}}")
     .log("finish process body ${body}");

Сценарий, в котором 300 запросов запускаются единовременно выполнился без ошибок. Что уже плюс, так как синхронный http-компонент не мог его вообще осилить. Рассмотрим состояние потоков JVM:

Потоков, если сравнить с предыдущим вариантом тоже сравнительно меньше. Рассмотри результаты других сценариев:

 

Процент ошибок

300 запросов с повтором 5 раз

0%

800 запросов с повтором 5 раз

0%

1200 запросов с повтором 5 раз

1.533%

1600 запросов с повтором 5 раз

15.02%

За счет того, что запросы идут не одновременно, общее количество потоков меньше чем в первом сценарии.

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

Возможные проблемы с AHC-компонентом

Если в сервисе используется динамическое создание AHC-эндпоинтов, то это может случайно выстрелить вам ногу. Рассмотрим пример:

from("jetty:http://localhost:8080/test")
     .log("receive request body ${body}")
     .removeHeaders("CamelHttp*") 
     .setHeader("rand", ()->new Random().nextInt(10000) )
     .toD("ahc:http://{{another.url}}?rand=${headers.rand}")
     .log("finish process body ${body}");

После запуска сценария с единовременным стартом 300 запросов состояние потоков в JVM:

Как видим, поток слишком много. Дело в том, что по умолчанию AHC-компонент для каждого эндпоинта создает свою инстанцию объекта AsyncHttpClient, у каждой из которых свой пул соединений и потоков, в результате для каждого запроса создается по 2 потока – один поток ввода/вывода, другой поток-таймер для контроля таймаутов и поддержания соединений в состоянии KeepAlive. Чтобы этого избежать необходимо настроить инстанцию AsyncHttpClient на уровне компонента, которая будет передаваться в эндпоинт при его создании.

AhcComponent ahc = getContext().getComponent("ahc", AhcComponent.class);
ahc.setClient(new DefaultAsyncHttpClient());

После этого создание множества инстанций AsyncHttpClient’a прекратятся.

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 11

    0
    Одно мелкое замечание — указывайте версию Camel, с которой работаете. Их много разных, Camel очень древний уже проект, и у кого-то вполне может быть более старая, чем у вас. И в любом случае, пост могут найти поиском и прочитать через несколько лет после публикации.

    P.S. Версия Java кстати тоже не помешает. Много потоков, много создаваемых объектов, влияние GC вполне может быть сильным. Не говоря уже про то, что если вы что-то меряете — то потребление ресурсов, таких как память и процессор, тоже интересно было бы поглядеть. Поэтому — как можно более полное описание тестового стенда :)
      0
      Добавил описание версий Camel и Java, а тестового стенда как такового и нет, все делалось на обычном ПК
        0
        Ну, параметры ПК же тоже интересны — ну хотя бы, сколько ядер, сколько памяти?
      +1
      Camel — нечто нечитаемое и непостижимое.

      from(«jetty:http://localhost:8080/test») //допустим, создали endpoint принимающий запросы

      .log(«receive request body ${body}») // ${body} — магическая константа.

      .removeHeaders(«CamelHttp*») //ну конечно же каждый разработчик от рождения знает: «удаление заголовков, начинающихся на „CamelHttp“ необходимо потому, что они выставляются в Jetty-компоненте и могут повлиять на работу Http-компонента.»

      .to(«http://{{another.url}}?connectionsPerRoute=200») //почему это connectionsPerRoute=200 находится в урле? а если мне нужно передать в запросе параметр зарезервированный в camel?

      .log(«finish process body ${body}»);//${body} — магическая константа.А куда делся запрос который пришел на «jetty:http://localhost:8080/test»

      и что в конце концов получит вызвавший endpoint?
        –1
        Теперь так много где стало, увы. Надо просто верить в «магию»
          0
          В camel кстати нет практически никакой магии — там все весьма прозрачно (да, я код не только читал, но и собирал патчи к некоторым компонентам). И не просто прозрачно — а практически совсем не менялось с самого момента его рождения.

          Ну, так, версия 3 конечно внесла некоторые изменения, но миграция с 2 на 3 у нас вызвала может изменение пары строк в проекте. Это очень мало, надо сказать, для перехода через десяток минорных версий и одну мажорную.
          +1

          Справедливости ради, тут всё же речь о замерах, как я понял, а не об основах Camel.
          Вообще Camel, по моему, классный фреймворк. По возможностям похож на Spring Integration, но мне верблюд как-то ближе.
          Я вообще уже некоторое время подумываю написать про него статью, что и как работает. Только не уверен, интересно ли кому.

            0
            Уже была. И очень неплохая.
              0

              Какая?

                0
                Блин, да вот попытался найти, но она куда-то в дебри хабра пропала. Борисов кажется автор, и там не совсем camel, а скорее ServiceMix + Camel, и в целом она про ESB.

                Если найду — отпишусь непременно. Если кто возьмется писать новую — я с радостью поучаствую в любом формате.
            0
            В начале статьи указано, что основы Camel рассматриваться не будут. Для этого уже есть хорошие статьи на просторах интернета, правда англоязычные, но все же. Более углубленно про него можно прочитать в книге Camel in Action основателя фреймворка Claus Ibsen. Либо изучать документацию на официальном сайте.
            Вкратце по вопросам:
            ${body} не константа, а наоборот выражение на встроенном Simple-языке, автоматически подставляет тело сообщения.
            .to(«http://{{another.url}}?connectionsPerRoute=200») Если нужно передать в запросе параметр зарезервированный в camel, то можно просто задать заголовок с нужным именем — он автоматически будет передан в сервис как HTTP-заголовок.

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

          Only users with full accounts can post comments. Log in, please.