Разложение монолита
Эта статья является конспектом книги «От монолита к микросервисам». Материал статьи посвящен шаблонам разложения монолита.
В этой статье рассмотрим различные шаблоны миграции, а также советы для внедрения архитектуры, основанной на микрослужбах. Однако, чтобы внедрение работало как надо, необходимо обеспечить продолжение работы с существующим монолитным ПО и его использование клиентами.
Нужно помнить, что миграцию необходимо делать поступательно, то есть малыми шагами, что позволит обучаться и при необходимости менять свое мнение по ходу движения.
Вырезать, скопировать или заново реализовать?
Не всегда ясно, что делать с существующим кодом. Следует ли нам перенести код как есть или же пересоздать функциональность? Главное понять, что мы хотим именно скопировать код из монолита и, по крайней мере, сейчас не хотим удалять эту функциональность из самого монолита. Почему? Потому что, оставляя функциональность в монолите на некоторое время, вы получаете больше возможностей. Это даст нам точку отката или же возможность выполнять обе реализации параллельно. И дальше по ходу дела, как только вы будете довольны тем, что миграция прошла успешно, вы удалите эту функциональность из монолита.
Рефакторинг монолита
Самым большим препятствием для использования существующего кода из монолита в новых микрослужбах является то, что существующие кодовые базы традиционно не организованы вокруг понятий бизнес-домена. Более заметны технические категоризации. Когда вы пытаетесь перенести функциональность, относящуюся к бизнес-домену, это бывает трудно сделать: существующая кодовая база не соответствует этой категоризации, поэтому даже отыскать код, который вы пытаетесь перенести, бывает проблематично.
Модульный монолит?
После того как существующая кодовая база начала обретать смысл, стоит подумать о следующем очевидном шаге — взять только что выявленные стыки и начать извлекать их как отдельные модули, превращая монолит в модульный монолит. Это по-прежнему одна единица развертывания, но эта развернутая единица состоит из многочисленных статически связанных модулей. Наличие монолита, разложенного на модули, которые развиваются независимо, облегчает переход на архитектуру, основанной на микрослужбах.
Шаблон: приложение «Фикус-удавка»
Мартин Фаулер был первым, кто выявил этот шаблон, находясь под впечатлением особого вида фикуса, который поселяется на верхних ветвях деревьев. Существующее дерево первоначально становится опорной структурой для нового фикуса, и если он дойдет до заключительных стадий, то дерево умирает и трухлявеет, оставляя на своем месте лишь новый и уже самоподдерживающийся фикус.
В контексте софта параллель здесь состоит в том, чтобы наша новая система изначально поддерживалась существующей системой и обертывала ее. Идея состоит в том, что старое и новое могут сосуществовать, давая новой системе время вырасти и потенциально полностью заменить собой старую систему. Ключевая выгода от этого шаблона заключается в том, что он поддерживает нашу цель обеспечения поступательной миграции на новую систему.
Реализация шаблона «Фикус-удавка» основана на трех шагах (рис. 1). Для начала нужно определить части существующей системы, которые хотите мигрировать. Затем нужно реализовать эту функциональность в новой микрослужбе. Когда новая реализация будет готова, нужно иметь возможность перенаправить вызовы из монолита в новую микрослужбу.
Ключевой момент в этом подходе заключается не только в том, что мы мигрируем новую функциональность в новую систему поступательно, но и в том, что при необходимости мы можем очень легко откатить это изменение назад. Помните, что все мы совершаем ошибки, поэтому нам нужны методы, позволяющие не только совершать ошибки как можно дешевле, но и быстро их исправлять.
Шаблон выгоден, когда над существующим монолитом работают другие люди, так как он помогает уменьшить конкуренцию. Он также очень полезен, когда монолит является черно-ящичной системой, например, сторонний софт или служба SaaS.
Для того, чтобы шаблон работал как надо, необходимо, чтобы была возможность четко отображать входящий вызов интересующей функциональности, который хотите перенести. Например, на рис.2 в идеале хотелось бы вынести отправку «Уведомлений пользователю» в новую службу. Однако уведомления запускаются в результате многочисленных входящих вызовов монолита. Следовательно, мы не можем четко перенаправлять вызовы извне самой системы.
Также нужно будет учесть природу вызовов, осуществляемых внутрь существующей системы. Такой протокол, как HTTP, хорошо поддается перенаправлению. В сам HTTP встроены концепции прозрачного перенаправления, и прокси-селекторы используются для четкого понимания природы входящего запроса и его соответствующей переадресации. Другие типы протоколов, такие, как некоторые RPC, поддаются перенаправлению хуже. Чем больше работы нужно выполнить на прокси-слое, для того чтобы понять и потенциально трансформировать входящий вызов, тем менее жизнеспособным становится этот вариант. Несмотря на эти ограничения, приложение «Фикус-удавка» снова и снова доказывает, что это очень полезный метод миграции.
Пример: обратный прокси-селектор HTTP
На рис.3 изображен существующий монолит, который выставляет наружу HTTP-интерфейс. Цель: вставить обратный прокси-селектор HTTP между вышестоящими вызовами и нижестоящим монолитом.
Шаг 1 – если еще нет прокси-селектора, то необходимо его добавить. На этом первом шаге прокси-селектор просто позволит любым вызовам проходить насквозь без изменений.
Шаг 2 – мигрировать функциональность. После установки нашего прокси-селектора на свое место можно начать извлечение новой микрослужбы. Сам этот шаг можно разбить на несколько этапов. Прежде всего, привести базовую службу в рабочее состояние без реализации какой-либо функциональности. Служба должна будет принимать вызовы соответствующей функциональности, но на этом этапе можно просто возвращать код ошибки 501 Not Implemented. Даже на этом шаге можно развернул службу в производственной среде. Это позволит освоиться с процессом развертывания в производстве и протестировать службу прямо на месте.
Шаг 3 – перенаправить вызовы. Только после завершения переноса всей функциональности, нужно переконфигурировать прокси-селектор с целью перенаправления вызовов, как мы видим на рис. 4. Если по какой-либо причине это не получается, то вы можете выставить перенаправление в прежнее состояние — для большинства прокси-селекторов это очень быстрый и легкий процесс, дающий быстрый откат.
Пример: перехват сообщений
Предыдущий пример рассматривал перехват синхронных вызовов, но что если монолит управляется, получая сообщения через брокера сообщений? Фундаментальный шаблон тот же — нужен метод перехвата вызовов и перенаправления их в новую микрослужбу. Главное различие заключается в природе самого протокола.
На рис. 5 монолит получает многочисленные сообщения, подмножество которых необходимо перехватить.
Простой подход состоит в перехвате всех сообщений, предназначенных для нижестоящего монолита, и фильтрации сообщений в надлежащее место. Такой метод позволяет оставить монолит нетронутым, но мы помещаем на наш путь запросов еще одну очередь, что вносит добавочную задержку, и это еще одна вещь, которой нам нужно управлять. Другая озабоченность заключается в том, сколько «мозгов» мы помещаем в слой обмена сообщениями. Это может затруднить понимание системы и их изменение.
Альтернативный подход - изменить монолит и побудить его игнорировать отправляемые сообщения, которые должны быть получены новой службой, как мы видим на рис. 6. Здесь и новая служба, и монолит делят между собой одну и ту же очередь, и локально они используют процесс своего рода сопоставления с шаблоном для прослушивания сообщений, в которых они заинтересованы.
Такой подход к фильтрации снижает необходимость создания дополнительной очереди, но имеются и трудности. Когда вы хотите перенаправлять вызовы, это требует, чтобы два изменения были достаточно хорошо скоординированы. Вам нужно остановить чтение монолитом сообщений, предназначенных для новой службы, а затем побудить службу их забрать. Схожим образом, для отката перехвата сообщений требуется откат двух изменений.
Во время мигрирования функциональности старайтесь исключать любые изменения в переносимом поведении. Если возможно, откладывайте новые функции или устранение дефектов до завершения миграции. В противном случае вы уменьшите свою способность откатывать изменения назад в вашей системе.
Шаблон: «Ветвление по абстракции»
Чтобы шаблон «Фикус-удавка» работал как надо, необходима способность перехватывать вызовы по периметру нашего монолита. Однако, что произойдет, если функциональность, которую мы хотим извлечь, находится глубже внутри нашей существующей системы? Например, функциональность «Уведомления», как показано на рис. 2. В этом поможет шаблон «Ветвление по абстракции».
Он состоит из пяти шагов:
Создать абстракцию для заменяемой функциональности.
Изменить клиентов существующей функциональности так, чтобы они использовали новую абстракцию.
Создать новую реализацию абстракции с переработанной функциональностью. В нашем случае это новая реализация будет вызывать новую микрослужбу.
Переключиться на новую реализацию.
Очистить абстракцию и удалить старую реализацию.
Шаг 1 – создать абстракцию. Первая часть работы состоит в создании абстракции, представляющей взаимодействия между изменяемым кодом и элементами, вызывающими этот код.
Шаг 2 – использовать абстракцию. Теперь, когда наша абстракция создана, нам нужно изменить существующих клиентов функциональности "Уведомления", для того чтобы использовать эту новую точку абстракции, как мы видим на рис. 7.
Шаг 3 – создать новую реализацию. Когда новая абстракция находится на своем месте, можно начать работу над новой реализацией вызова службы. Внутри монолита реализация функциональности «Уведомления» будет представлять собой просто клиента, который вызывает внешнюю службу, как показано на рис. 8. Подавляющая часть функциональности будет находиться в самой службе. Ключевая вещь в этой точке состоит в том, что, хотя мы имеем две реализации абстракции в кодовой базе одновременно, только одна реализация в настоящее время активна в системе. До тех пор, пока мы не будем довольны тем, что наша новая реализация вызова службы готова, она практически находится в спячке.
Шаг 4 – переключить реализацию. Как только мы будем довольны тем, что новая реализация работает правильно, мы переключим нашу точку абстракции так, чтобы новая реализация стала активной, а старая функциональность больше не использовалась.
Шаг 5 – очистка. В этот момент наша старая функциональность «Уведомлений пользователя» больше не используется, поэтому очевидным шагом будет ее удалить. Наконец, когда старая реализация убрана, у нас есть возможность удалить саму точку абстракции. Однако существует возможность, что создание абстракции улучшило кодовую базу до такой степени, что вы предпочли бы оставить ее на месте. Если это нечто такое же простое, как интерфейс, то его сохранение будет минимально влиять на существующую кодовую базу.
Шаблон: «Параллельное выполнение»
И шаблон «Фикус-удавка», и шаблон «Ветвление по абстракции» позволяют старым и новым реализациям одной, и той же функциональности одновременно сосуществовать в производстве. Для снижения риска переключения на новую реализацию, основанную на службах, эти методы дают возможность быстро переключаться назад к предыдущей реализации.
Во время использования параллельного выполнения вместо вызова старой или новой реализации мы вызываем обе, что позволяет нам сравнивать результаты с целью обеспечения их эквивалентности. Несмотря на вызов обеих реализаций, в любой момент времени только одна из них считается источником истины. В типичной ситуации старая реализация считается источником истины, до тех пор, пока продолжающаяся верификация не покажет, что мы можем доверять новой реализации. Этот метод служит для верификации не только того, что наша новая реализация дает те же отклики, что и существующая, но и того, что она также работает в рамках приемлемых нефункциональных параметров (скорость отклика, количество тайм-аутов).
Далее будет описан пример на основе сравнения ценообразования кредитных деривативов. Автор книги участвовал в проекте по изменению платформы, используемой для расчетов типа финансового продукта, именуемого кредитными деривативами. Из-за больших рисков они приняли решение выполнять два набора вычислений бок о бок и проводить
ежедневные сравнения результатов. События ценообразования запускались с помощью событий, которые легко дублировались таким образом, чтобы обе системы выполняли расчеты, как видим на рис. 9. Вследствие таких сравнений были обнаружены проблемы и дефекты в существующей системе.
В конце концов, они пришли к использованию новой системы в качестве источника истины для расчетов, а некоторое время спустя вывели старую систему из эксплуатации.
Стоит отметить, что параллельное выполнение отличается от выпуска «канареечного» релиза. «Канареечный» релиз (canary
release) связан с направлением некоторого подмножества пользователей к новой функциональности, при этом подавляющая часть пользователей видит старую реализацию. Идея состоит в том, что если новая система имеет проблему, то только подмножество запросов подвержено влиянию этой проблемы. Еще один родственный метод называется «темным» запуском (dark launching). При «темном» запуске развертывается новая функциональность и тестируется, но новая функциональность для пользователей невидима. Поэтому параллельное выполнение является способом реализации «темного» запуска, поскольку новая функциональность практически невидима для пользователей до тех пор, пока вы не переключитесь на нее. «Темный» запуск, параллельные выполнения и выпуск «канареечных» релизов — все эти методы можно использовать для верификации того, что новая функциональность работает правильно, и для уменьшения влияния, если окажется, что это не так.
Реализация параллельного выполнения редко оказывается тривиальным делом и обычно подходит для тех случаев, когда считается, что изменяемая функциональность находится под высокой степенью риска.
Шаблон: «Сотрудник-декоратор»
Что произойдет, если вы захотите вызвать какое-то поведение, основываясь на том, что происходит внутри монолита, но вы неспособны изменить сам монолит? Шаблон «Сотрудник-декоратор» (decorating collaborator) окажет здесь большую помощь. Широко известный структурный шаблон «Декоратор» позволяет прикреплять новую функциональность к чему-либо без того, чтобы лежащая в основании вещь что-то об этом «знала». Мы собираемся использовать декоратор, чтобы сделать вид, что наш монолит делает вызовы нашей службы напрямую, даже если мы на самом деле не изменили лежащий в основании монолит. Вместо перехвата этих вызовов до того, как они достигнут монолита, мы даем вызову выполняться как обычно. Затем, основываясь на результате этого вызова, мы обращаемся к нашим внешним микрослужбам. Давайте рассмотрим эту идею на примере функционала «программы лояльности» для компании Music Corp (это выдуманная компания для иллюстрации концепций из другой книги автора Building Microservices)
Предположим, что хотим добавить для клиентов возможность зарабатывать баллы на основе размещаемых ими заказов, но текущая функциональность размещения заказов достаточно сложна, и мы предпочли бы не менять ее прямо сейчас. Поэтому функциональность размещения заказов останется в существующем монолите, но мы будем использовать прокси-селектор для перехвата этих вызовов и на основе результата решать, сколько баллов выставлять, как показано на рис. 10.
С шаблоном «Фикус-удавка» прокси-селектор был довольно упрощенным. Теперь же он должен делать свои собственные вызовы новой микрослужбы и туннелировать отклики назад клиенту. Нужно приглядывайте за сложностью, которая «сидит» в прокси-селекторе. Чем больше кода вы начинаете здесь добавлять, тем больше он становится микрослужбой сам по себе. Еще одна потенциальная трудность заключается в недостаточном объеме информации из входящего или исходящего запроса. Например, если мы хотим присудить баллы, основываясь на стоимости заказа, но стоимость заказа не ясна ни из запроса в «Размещении заказа», ни из отклика оттуда, то нам потребуется поиск дополнительной информации, возможно, обратный вызов в монолит для извлечения нужной информации. Поскольку этот вызов генерирует дополнительную нагрузку и, возможно, вводит циклическую зависимость, было бы лучше изменить монолит для обеспечения необходимой информации, после того как размещение заказа будет завершено.
Описанный шаблон лучше всего работает там, где требуемая информация извлекается из входящего запроса или отклика из монолита. Там, где для выполнения вызовов новой службы требуется больше информации, его реализация становится сложнее и запутаннее. Автор книги рекомендует, если запрос в монолит и отклик из него не содержит нужной вам информации, то хорошенько подумайте, прежде чем использовать этот шаблон.
Шаблон: «Захват изменений в данных»
В рамках шаблона «Захват изменений в данных» (change data capture), вместо того чтобы пытаться перехватывать вызовы в монолит, мы реагируем на изменения, вносимые в хранилище данных.
Пример – выпуск карточек лояльности. Мы хотим интегрировать немного функциональности с целью распечатки карточек лояльности для наших пользователей, когда они регистрируются. В настоящее время учетная запись лояльности создается при регистрации клиента. Когда регистрация возвращается из монолита, мы знаем только, что клиент был успешно зарегистрирован. Чтобы мы могли напечатать карточку, нам нужно больше сведений о клиенте, что затрудняет использования «сотрудника-декоратора».
Вместо этого мы решили использовать захват изменений в данных. Мы засекаем любые вставки в таблицу, и при наступлении вставки вызываем нашу новую службу «Печати карточек лояльности», как показано на рис. 11.
Для реализации захвата изменений в данных используются разнообразные методы, каждый из которых имеет разные компромиссы с точки зрения сложности, надежности и своевременности. Далее рассмотрим несколько вариантов.
Триггеры БД. Большинство реляционных БД позволяют инициировать настраиваемое под свои нужды поведение во время изменения данных. В примере выше наша служба вызывается всякий раз, когда выполняется вставка в нужную таблицу. Триггеры должны быть инсталлированы в саму базу данных, как и любая другая хранимая процедура. На первый взгляд, может показаться, что сделать это довольно просто. Нет необходимости иметь какой-либо другой софт. Однако, как и хранимые процедуры, бывает, что триггеры становятся проблемным местом. Чем их больше, тем труднее понять, как система на самом деле работает. Поэтому, если вы собираетесь их использовать, то делайте это очень аккуратно.
Опросники журналов транзакций. Внутри большинства БД, конечно же, всех транзакционных БД, существует журнал транзакций. Обычно это файл, в который заносится запись обо всех внесенных изменениях. Для захвата изменений в данных указанный журнал транзакций задействуется со стороны другой системы. Эти системы работают как отдельный процесс, и их взаимодействие с существующей БД осуществляется только через журнал транзакций. Рассмотренное решение во многих отношениях является самым изящным для реализации захвата изменений в данных. Сам журнал транзакций показывает изменения только в базовых данных, поэтому вас не беспокоит выяснение того, что изменилось. Инструментарий работает вне самой БД и запускает реплику журнала транзакций, поэтому у вас будет меньше проблем, связанных с конкуренцией.
Пакетное копирование изменений. Вероятно, наиболее упрощенный подход — написание программы, которая на регулярной основе сканирует затрагиваемую базу данных на предмет того, какие данные изменились, и копирует эти данные в место назначения, например, с помощью cron. Главная проблема — выяснить, какие данные фактически изменились с момента последнего запуска пакетного копирования. Некоторые БД позволяют просматривать метаданные таблиц, чтобы увидеть, когда части базы данных изменились, но этот подход далеко не универсален и будет давать вам временные метки изменений только на уровне таблицы, когда предпочтительней бы иметь информацию на уровне строк. Вы могли бы начать добавлять эти временные метки сами, но в результате объем работы значительно возрастет.
«Захват изменений в данных» — полезный шаблон общего назначения, в особенности, если необходимо реплицировать данные. В случае миграции на микрослужбы наиболее благоприятная зона находится там, где нужно реагировать на изменение в данных в вашем монолите, но вы неспособны перехватить это ни по периметру системы с помощью «удавки» или «декоратора», ни изменить лежащую в основании кодовую базу.
Вывод
Как мы увидели, широкий спектр методов позволяет выполнять поступательную декомпозицию существующих кодовых баз и помогает войти в мир микрослужб. По мнению автора книги, большинство людей в итоге используют сочетание подходов; редко бывает, что одна технология справляется с каждой ситуацией.