Привет! Меня зовут Евгений Сальников и я тимлид одной из команд направления 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, то почему бы этого не сделать?
Вот этим мы занялись. Так выглядел наш план работ:
Текущая версия приложения работает на Servicemix и использует встроенный сервер ActiveMq, поэтому создадим еще один отдельный сервер.
Учим текущую версию приложения работать с внутренним и внешним ActiveMq. На этом этапе важно понимать, что общая логика вроде преобразования ответа от курьерской службы в необходимый формат для наших внутренних систем все еще находится в текущей версии приложения. Изменения были лишь в том, что теперь эта логика может получать сообщения для обработки и из внешнего ActiveMq. Для этого необходимы минимальные доработки в виде добавления еще одного endpoint.
Создаем модуль интеграции с новой службой, используя уже новые технологии старой Apache Camel и взаимодействуем с общей логикой через ActiveMq. Запускаем уже в k8s.
Повторяем цикл, пока есть «старые» модули или новые интеграции.
Создаем новый модуль интеграции
Первые два пункта достаточно просты, поэтому сразу перейдем к разбору третьего. Используя 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-конфигурированием и 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 стабильно выполняет свои задачи по сей день. На мой взгляд, если бы многие из разработчиков больше знали о данной технологии, мы сохранили бы лес серверов и тонну времени!