Pull to refresh
0
Red Hat
Программные решения с открытым исходным кодом

Шаблоны модернизации приложения с помощью Apache Kafka, Debezium и Kubernetes, часть 1

Reading time11 min
Views4.1K

«Наши компьютеры создаются так же, как и наши города: долго, без планов и на руинах былого». Эллен Ульман (Ellen Ullman) написала это в 1998 году, но сегодня мы именно так и создаем современные приложения: долго, имея лишь краткосрочные планы и поверх унаследованного ПО. В этой статье мы рассмотрим несколько шаблонов и инструментов, которые, на наш взгляд, хорошо подходят для продуманной модернизации унаследованных приложений и создания современных управляемых событиями систем.

Модернизация приложений в контексте

Модернизация приложений – это когда вы берете унаследованное приложение и модернизируете его инфраструктуру (она же внутренняя архитектура), чтобы ускорить расширение его функциональности, повысить производительность и масштабируемость, реализовать новые сценарии использования и так далее. К счастью, все виды модернизации и миграции приложений уже давно и хорошо классифицированы, как показано на Рис. 1.

Рис. 1: Три вида модернизации и технологии, которые можно при этом использовать.
Рис. 1: Три вида модернизации и технологии, которые можно при этом использовать.

Есть несколько уровней модернизации, которые можно применять в зависимости от ваших потребностей и готовности к переменам:

  • Не трогать (retention). Это самый простой вариант, он вполне имеет смысл, если потребность в модернизации стоит не так остро.

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

  • Рехостинг. Обычно это означает взять приложение «как есть» и повторно развернуть его на новой инфраструктуре, например, в облаке или на платформе Kubernetes с помощью чего-то вроде KubeVirt. Это неплохой вариант, если приложение нельзя контейнеризировать, но вы хотите задействовать свои навыки работы с Kubernetes, лучшие практики и возможности новой инфраструктуры, чтобы управлять виртуальными машинами как контейнерами.

  • Реплатформинг. Это тот случай, когда только переезда на новую инфраструктуру недостаточно, и вы хотите немного изменить внешнюю часть приложения, не трогая его архитектуру. Например, изменить способ конфигурирования приложения, чтобы его можно было контейнеризировать, или перейти со старушки Java EE на современную среду исполнения с открытым кодом. Здесь можно использовать такой инструмент, как windup, который проанализирует ваше приложение и выдаст отчет о том, что надо сделать.

  • Рефакторинг. Сегодня модернизация приложений – это в основном перенос монолитных on-premises приложений в облачную микросервисную архитектуру, позволяющую ускорить выпуск релизов. В этом случае понадобится провести рефакторинг и перепроектировать архитектуру приложения. Именно эти вопросы мы и рассмотрим сегодня.

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

Основные вызовы при миграции монолитных унаследованных приложений

Во-первых, это необходимость релизить новые версии с требуемой частотой. Проблема номер два – масштабирование разработки, чтобы большее количество разработчиков и команд работали с общей базой кода, не наступая друг другу на пятки. Масштабирование приложения для надежной обработки растущей нагрузки – это проблема номер три. С другой стороны, выгоды от модернизации включают в себя сокращение сроков вывода на рынок, рост автономности команд при работе с базой кода, а также динамическое масштабирование для эффективного реагирования на изменения нагрузки. Каждое из этих преимуществ компенсирует усилия по модернизации приложения. На Рис. 2 показан пример инфраструктуры, обеспечивающей масштабирование унаследованного приложения при росте нагрузки.

Рис. 2: Рефакторинг унаследованного приложения в управляемые событиями микросервисы.
Рис. 2: Рефакторинг унаследованного приложения в управляемые событиями микросервисы.

Постановка конечных целей и контроль продвижения к ним

В нашем случае конечной целью является архитектурный стиль, который следует принципам микросервисов и использует технологии с открытым кодом, такие как Kubernetes, Apache Kafka и Debezium. На выходе модернизации у нас должны быть независимо развертываемые сервисы, смоделированные от бизнесовой предметной области. Каждый сервис должен владеть собственными данными, генерировать собственные события и так далее.

Планируя модернизацию, важно проработать и то, как мы будем измерять прогресс наших усилий. Для этого можно использовать такие показатели, как время реализации изменений (lead time for changes – время, за которое новый коммит попадает в продакшн), частота развертывания релизов, время восстановления, количество одновременно работающих пользователей и так далее.

Ниже мы рассмотрим три шаблона проектирования и три технологии с открытым исходным кодом (Kubernetes, Apache Kafka и Debezium), которые можно использовать для преобразования находящегося в эксплуатации монолитного приложения в современную систему на основе управляемых событиями сервисов. И начнем мы с шаблона который называется Strangler, что переводится на русский как «душитель».

Шаблон Strangler

Этот шаблон является наиболее популярной техникой миграции приложений. Его представил и популяризировал Мартин Фаулер (Martin Fowler), который впечатлился в Австралии фикусами-душителями. Семена этих растения попадают в крону дерева и дают побеги, которые затем растут вниз, чтобы укоренится в почве и постепенно задушить то дерево, которое и дало им приют. Причем здесь миграция приложений? При том, что наши сервисы тоже изначально строятся так, чтобы обвивать унаследованную систему. Какое-то время старая и новая системы будут сосуществовать, затем новая вырастет и полностью заменит старую. Основные компоненты шаблона Strangler при миграции унаследованного приложения показаны на Рис. 3.

Рис. 3: Шаблон Strangler при миграции унаследованных приложений.
Рис. 3: Шаблон Strangler при миграции унаследованных приложений.

Главное преимущество шаблона Strangler – возможность постепенно мигрировать с унаследованной системы на новую с низким уровнем риска. А теперь разберем этот шаблон по ключевым шагам.

Шаг 1. Идентификация функциональных границ

Самый первый вопрос – с чего начать миграцию. Здесь можно воспользоваться предметно-ориентированным проектированием, чтобы определить агрегаты и ограниченные контексты, каждый из которых представляет собой потенциальную единицу декомпозиции и потенциальную границу для микросервисов. Либо можно использовать технику event storming, созданную Антонио Брандолини (Antonio Brandolini) для получения общего понимание предметной модели. Еще один важный момент: как эти модели взаимодействуют с базой данных, и какая работа потребуется для декомпозиции базы данных. Ответив на эти вопросы, можно переходить к определению связей и зависимостей между ограниченными контекстами, чтобы прикинуть, насколько сложно будет их извлечь из старого приложения.

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

Шаг 2. Миграция функциональности

Чтобы Strangler-шаблон сработал, надо четко сопоставить входящие запросы с той функциональностью, которую мы хотим перенести. Также нужна возможность перенаправлять эти запросы на наш новый сервис (и обратно, если это необходимо). В зависимости от состояния унаследованного приложения, клиентских приложений и других факторов, оценка возможных вариантов такого перехвата может как простой, так и очень сложной:

  • Самый простой вариант – изменить клиентское приложение и перенаправлять входящие запросы на новый сервис.

  • Если унаследованное приложение работает по HTTP, то это очень хорошо. HTTP легко перенаправляется, и у нас есть широкий выбор всяких прозрачных прокси.

  • Однако на практике приложение скорее всего будет использовать не только REST API, но и SOAP, FTP, RPC или какие-то традиционные конечные точки обмена сообщениями. В этом случае может понадобиться написать свой собственный слой трансляции протоколов с помощью чего-то вроде Apache Camel.

Перехват – это, в принципе, довольно скользкий путь. Создавая свой собственный уровень трансляции протоколов, которым будут пользоваться сразу несколько сервисов, мы рискуем реализовать слишком много интеллекта на уровне общего для всех этих сервисов прокси. А это идет вразрез с принципом «умные микросервисы, тупые конвейеры». Так что лучше всего использовать шаблон Sidecar, как показано на Рис. 4.

Рис. 4: Шаблон Sidecar.
Рис. 4: Шаблон Sidecar.

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

Шаг 3. Миграция базы данных

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

Подход на уровне базы данных

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

Подход на уровне кода

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

Подход и на уровне базы данных, и на уровне кода

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

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

Триггеры

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

Запросы

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

Анализаторы логов

Анализаторы логов детектируют изменения путем сканирования файлов журнала транзакций базы данных. Эти файлы предназначены для резервного копирования и восстановления базы данных и надежно регистрируют все изменения, в том числе и по DELETE. Использование лог-анализаторов – это самый беспроблемный вариант, поскольку здесь не требуется модифицировать исходную базу данных и отсутствует оверхед в виде дополнительных запросов. Основным недостатком этого подхода заключается в том, что не существует какого-то единого стандарта на лог-файлы транзакций, и поэтому для их обработки понадобятся специализированные инструменты. Именно здесь и пригодится Debezium.

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

Debezium и отслеживание изменений в данных

Когда приложение пишет данные в базу, то изменения сначала фиксируются в логе, и лишь затем обновляются в таблицах БД. В MySQL лог-файл называется binlog, в PostgreSQL – write-ahead-log, в MongoDB – op. К счастью, у Debezium есть коннекторы для различных баз данных, поэтому он избавляет нас от необходимости разбираться с форматами логов. Debezium может читать логи и создавать общие абстрактные события в системе обмена сообщениями, вроде, Apache Kafka, которые содержат изменения данных. На Рис. 5 показаны коннекторы Debezium в качестве интерфейсов для различных баз данных.

Рис. 5: Коннекторы Debezium в микросервисной архитектуре.
Рис. 5: Коннекторы Debezium в микросервисной архитектуре.

Debezium –это самый широко используемый опенсорный инструмент CDC (change data capture), который благодаря множеству коннекторов и обширному функционалу отлично подходит для модернизации по методу Strangler.

Почему Debezium хорошо подходит для Strangler?

Одна из ключевых причин для миграции унаследованных монолитных приложений по шаблону Strangler – это уменьшение рисков и возможность всегда вернуться к исходному унаследованному приложению. Debezium хорошо вписывается в эту схему, поскольку он полностью прозрачен для унаследованного приложения и не требует никаких изменений в унаследованной модели данных. Пример использования Debezium в микросервисной архитектуре показан на Рис. 6.

Рис. 6: Использование Debezium в гибридном облаке.
Рис. 6: Использование Debezium в гибридном облаке.

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

Функционал Debezium для миграции унаследованных приложений

Вкратце рассмотрим некоторые из функциональных возможностей Debezium, которые пригодятся при миграции монолитного унаследованного приложения с использованием шаблона Strangler:

  • Снимки (snapshots). Debezium может сделать снимок текущего состояния исходной базы данных, который потом можно использовать для массового импорта данных. Как только снимок будет сделан, Debezium начнет стримить изменения для синхронизации целевой системы.

  • Фильтры. Debezium позволяет выбирать, из каких баз данных, таблиц и столбцов передавать изменения, что очень полезно, поскольку в рамках шаблона Strangler мы не перемещаем приложение целиком.

  • Функция Single message transformation (SMT) – может применяться как дополнительный уровень защиты данных от повреждений и защищать нашу новую модель данных от унаследованных именований, форматов данных, и даже позволяет нам отфильтровать устаревшие данные.

  • Использование Debezium вместе реестром схем, например, с Apicurio, для валидации схем, а также для принудительной проверки совместимости версий при изменении модели исходной базы данных. Иногда это помогает предотвратить негативное влияние изменений в исходной базе данных на новых потребителей сообщений.

  • Использование Debezium вместе с Apache Kafka –эти два инструмента отлично дополняют друг друга при миграции и модернизации приложений. Вот лишь некоторые плюсы их совместного применения: гарантированное упорядочивание изменений в базе данных, сжатие сообщений, возможность повторно перечитывать изменения столько раз, сколько потребуется, а также отслеживание смещений в логах.

Шаг 4. Релизы сервисов

Итак, после краткого обзора Debezium, возвращаемся к работе по шаблону Strangler. Предположим, что мы уже выполнили следующие вещи:

  • Идентифицировали функциональные границы.

  • Мигрировали функциональность.

  • Мигрировали БД.

  • Развернули сервисы в среде Kubernetes.

  • Провели миграцию данных с помощью Debezium и оставили его работать для синхронизации текущих изменений.

Трафик пока не маршрутизируется на наши новые сервисы, но мы готовы их зарелизить. В зависимости от возможностей нашего слоя маршрутизации, можно использовать такие методы, как dark launching, параллельное выполнение или канареечное развертывание, чтобы снизить или устранить риски при развертывании новых сервисов, как показано на Рис. 7.

Рис. 7: На новый сервис уходит только трафик на чтение.
Рис. 7: На новый сервис уходит только трафик на чтение.

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

Убедившись, что чтение работает без проблем, можно начать отправлять на новый сервис трафик на запись. Если на этом этапе нам по каким-то причинам все еще нужно, чтобы унаследованное приложение работало, то надо стримить изменения из новых служб в базу данных устаревшего приложения. Затем, когда потребность в унаследованном приложении исчезнет, надо будет остановить в нем любые действия по записи или изменению данных и прекратить репликацию данных из него. Описанная здесь фаза шаблона Strangler иллюстрируется Рис. 8.

Рис. 8: На новый сервис уходит трафик и на чтение, и на запись.
Рис. 8: На новый сервис уходит трафик и на чтение, и на запись.

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

Заключение

Итак, мы подробно разобрали использование шаблона Strangler для миграции монолитного унаследованного приложения, но еще не закончили модернизацию нашей микросервисной архитектуры. В следующей, заключительной части мы рассмотрим некоторые типовые проблемы, которые возникают на последующих этапах процесса модернизации, а также то, как Debezium, Apache Kafka и Kubernetes могут помочь в этом вопросе.

Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments2

Articles

Information

Website
www.redhat.com
Registered
Founded
Employees
5,001–10,000 employees
Location
США