Привет! Меня зовут Евгений Сальников и я тимлид одной из команд направления Outbound, которое отвечает за сервисы доставки в Lamoda.

Эта статья написана по мотивам реальной задачи по обновлению нашей большой системы, а именно — переход с очень старой версии Apache Camel на актуальную. Я не расскажу чего-то особо нового, но если у вас уже есть Apache Camel и вам «только спросить», как с ним управляться — милости просим.

В чем проблема

Наша команда взаимодействует с большим количеством служб доставки. 

Для этого создан отдельный сервис: он принимает данные о создаваемых заказах в нашем формате, преобразует их в формат одной из служб доставки и отправляет запрос на ее endpoint. 

Звучит понятно и просто, но, у разных служб доставки разные правила игры: где-то API использует soap, где-то — json и rpc, а где-то — rest. А еще везде своя авторизация и свой набор полей. И нужно не просто создать отправку, но и запомнить ее номер и периодически получать по ней статус, преобразовывать в единый формат и отправлять для дальнейшей обработки уже другим системам. А потом приходит в голову мысль, что иногда API служб доставки бывают недоступны и надо бы защититься от такого сценария. Но подождите, ведь и некоторые наши внутренние службы могут не работать какое-то время! 

Так постепенно мы обрастаем очередями. Потом получается, что мы слишком часто шлем запросы на какой-то сервис и надо как-то уменьшить их частотность — то есть, сделать throttling. И тут мы подходим к проблеме, о которой и хочется поговорить — писать инструмент самому или использовать готовое решение.

Как мы подошли к решению

Случается, что история системы определяет путь ее развития. Например, изначально нужно что-то несложное и это можно быстро реализовать своими силами на привычных технологиях. Затем появляются новые требования и они также реализуются в текущей парадигме системы. 

В итоге система и ее сложность растут. Внезапно это уже не «cur, curl», а домен, слои, DTO, события, очереди, команды и дальше по нарастающей. А поддержка всего этого великолепия становится дорогой, а добавление нового — значительно сложнее.

Но нам повезло. Наши предки, стоящие у истоков создания системы взаимодействия со службами доставки, уже тогда понимали, что ее расширение будет продолжаться долгие годы — что, кстати, и происходит по сей день. Тогда было крайне важно найти инструмент, который позволит, прежде всего, заниматься реализацией бизнес-логики и переиспользовать необходимые блоки, а не решать инфраструктурные проблемы вроде написания бесчисленных обработчиков. 

Не могу сказать, что серебряная пуля найдена, но предки нашли Apache Camel. И это стало для меня интересным открытием. 

Что? Apache Camel?

Да, Apache Camel. Это интеграционный фреймворк на Java, который появился в далеком 2008 году. С тех пор появилось уже много версий Java и самого фреймворка. На момент написания этой статьи его актуальная версия — 3.14.2. Вот что пишут на официальном сайте:

«Camel — это интеграционный фреймворк с открытым исходным кодом, который позволяет вам быстро и легко интегрировать различные системы, потребляющие или производящие данные»

Я опущу примеры из официальной документации и ограничусь лишь тем, что из коробки «горбатая лошадь» умеет в базы данных и все виды очередей с retry и dlq. А еще она умеет принять json, обогатить его информацией из другого API, превратить в xml и отправить куда-то на soap-сервер. Как раз такой-то зоопарк нам и нужен! 

Хватит теории, давайте к практике. Как это включать?

Для начала стоит понять, как это запускать — здесь у нас тоже много работы. Дело в том, что для работы Apache Camel нужна Java, а для этого нужен контейнер для Java-приложения. А еще конфиги, очереди, веб-сервер и все, что вы привыкли видеть в большой системе — логи, интеграции с системами мониторинга и им подобные вещи.  

Наши предки выбрали Karaf, но обнаружили, что Servicemix уже имеет все, что нужно. Тут важно уточнить некоторые детали: Karaf — это среда выполнения только для Java-приложений, а Servicemix — это Karaf + очереди + управление приложениями и очередями. Однако, последняя версия Servicemix 7.0.1 была выпущена 22 мая 2017 года и, мне кажется, она немного устарела.

В нашем случае оставалось использовать громадный опыт предыдущих поколений. Так получилось, что нам предстояло перейти с Servicemix с Java 8 (ее поддержка тоже скоро закончится) на Spring Boot с Java 11. 

В этот момент мы осознали, что у нас в командах уже есть Kotlin, а это, можно сказать, та же Java (пожалуйста, не бейте меня за эти слова!) — значит, можно реализовать новый сервис именно на Kotlin. А раз у нас уже есть Spring Boot, и мы знаем, как его занести в k8s, то почему бы этого не сделать? 

Вот этим мы занялись. Так выглядел наш план работ:

  1. Текущая версия приложения работает на Servicemix и использует встроенный сервер ActiveMq, поэтому создадим еще один отдельный сервер.

  2. Учим текущую версию приложения работать с внутренним и внешним ActiveMq. На этом этапе важно понимать, что общая логика вроде преобразования ответа от курьерской службы в необходимый формат для наших внутренних систем все еще находится в текущей версии приложения. Изменения были лишь в том, что теперь эта логика может получать сообщения для обработки и из внешнего ActiveMq. Для этого необходимы минимальные доработки в виде добавления еще одного endpoint.

  3. Создаем модуль интеграции с новой службой, используя уже новые технологии старой Apache Camel и взаимодействуем с общей логикой через ActiveMq. Запускаем уже в k8s.

  4. Повторяем цикл, пока есть «старые» модули или новые интеграции.

Создаем новый модуль интеграции

Первые два пункта достаточно просты, поэтому сразу перейдем к разбору третьего. Используя Spring Initializr или всем известную IDE создаем новый проект и указываем, что будем использовать Kotlin. Добавляем в зависимости основной и другие необходимые модули, стартеры для ActiveMq и, например, Redis для хранения сессий и некоторых промежуточных данных.

Для начала нам понадобится создать роутер и класс, который конфигурирует правила для Apache Camel и является наследником org.apache.camel.builder.RouteBuilder. Конечно, важно не забыть добавить SpringBoot-аннотацию @Component. Получится что-то вроде этого:

В этом примере после старта приложения будет создан handler для очереди, указанной в константе QueueList.SOME_INTEGRATION_QUEUE_NAME. При получении сообщения из нее, он передаст его на вход bean GetPickups::class.java , а именно — в метод get. Результат его работы передается в следующий bean PickupsBuilder::class.java в метод build, далее — в Serializer. Затем переработанное сообщение перекладывается уже в следующую очередь.

Технически, всю логику можно сразу реализовать в роутере, но, на мой взгляд, так поступать не очень хорошо. Все же отдельный bean — это просто еще один компонент  вроде этого:

Код после правок без xml
Код после правок без xml

Звучит достаточно просто, но насколько это проще текущей версии с xml-конфигурированием и xslt-магией, переоценить сложно. В новой версии мы используем тот же фреймворк, но свежей версии и конфигурируем его кодом на Kotlin, поэтому мы можем использовать поддержку IDE и дебагер. 

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

Про K8s. Пожалуй, я опущу опус про облачные решения и замечу, что в компаниях широко распространена подготовка Java-приложения для работы в облаках. Для сборки я использую докер-образы  Maven и OpenJDK. Почему Maven, а не Gradle? Просто мы не стали его менять относительно текущего проекта, но в планах все же есть переход на Gradle DSL. 

По вкусу можно использовать и Spring Cloud, но текущая версия не умеет не только в модные штуки, но и просто в облака. При этом мы уже реализовали на нем «Service registration and discovery» и «Distributed/versioned configuration» в виде прототипов и, возможно, будем использовать в бою.

Тестирование

Для тестирования использованы интеграционные автотесты @CamelSpringBootTest и WireMock. Первое — это готовые тесты для Apache Camel. Для ознакомления с ними советую почитать статью о правилах тестирования

Oб WireMock уже сломали столько копий, что я не хочу поднимать еще одно.

Что получилось в итоге

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

Что было:

  • интеграционный фреймворк Apache Camel;

  • конфигурация в виде xml-скриптов;

  • все работает в устаревшем Apache Servicemix;

  • нет поддержки IDE;

  • нет возможности работать в k8s;

  • нет нормального отладчика;

  • нет интереса к системе;

  • монолит.

Что стало:

  • интеграционный фреймворк Apache Camel;

  • конфигурация в виде скриптов на Kotlin;

  • ServiceMix не нужен;

  • полная поддержка IDE;

  • работает в k8s;

  • встроенный из коробки дебагер в IDE;

  • один сервис на одного провайдера услуги;

  • есть интерес к системе.

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

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

Мы заранее проводили исследовательскую работу, готовили прототипы, проверяли совместимость. Это заняло много времени, но так родилась уверенность в задуманном. Порядка 20% времени ушло на задачи интеграции двух систем, но при этом они больше не будут повторяться. В теории это означает снижение time-to-market на 20%. Звучит слишком круто, но это требует дополнительных исследований. 


В чем же смысл этой статьи и что я хотел сказать? Apache Camel c 2008 стабильно выполняет свои задачи по сей день. На мой взгляд, если бы многие из разработчиков больше знали о данной технологии, мы сохранили бы лес серверов и тонну времени! 

Комментарии (0)