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

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

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

Решение

Я твердо стою на позиции «начать с монолита». Это должен быть модульный монолит, чтобы нам было легко его разбить. Существует миф о монолите как о «блоке взаимосвязанного кода». Это далеко не так, большинство монолитов используют возможности современных языков программирования (например, пакеты, модули и т.д.), чтобы отделить различные части..

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

Как сделать объективный выбор в связи с этим? Когда наступает идеальное время для перехода на микросервисы?

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

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

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

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

  • Тестирование — вы не сможете провести такую работу без значительного количества тестов для конкретных модулей и большого набора интеграционных тестов. Рецензирование тестового кода скажет вам больше о вашей готовности, чем любые альтернативные способы. Можете ли вы протестировать модуль в изоляции?

Как только появится понимание этих особенностей, можно приступать к оценке преимуществ, которые вы можете получить при миграции с монолита на микросервисы.

С чего мы начнем?

Предполагая, что монолитный код уже обладает соответствующей модульной структурой и поддерживает SSO (Single Sign On. Технология единого входа), мы можем выбрать любой модуль. Как узнать, какой из них принесет наибольшую отдачу от вложенного времени и усилий?

В идеале мы хотим выбрать те части, которые принесут нам больше всего пользы и их легче всего будет мигрировать:

  • Посмотрите на баг-трекер/контроль версий — какой модуль наиболее подвержен сбоям?

  • Проверьте модульность — какой модуль самый маленький и наименее взаимозависимый? Можно ли четко разделить данные? — Лучше всего "начинать с наиболее низко висящих плодов" (т.е. с самого простого).

  • Составьте профиль приложения — какой модуль является наиболее дорогостоящим и может выиграть от масштабирования?

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

Как избежать крошечной монолитной архитектуры

Люди часто декламируют принципы микросервисов, но продолжают строить то, что не соответствует общим правилам. «Самовосстановление» — самый вопиющий пример.

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

Как система может функционировать, когда развертываемая служба не работает? Как мы можем протестировать что-то подобное?

Одной из самых больших проблем в такой архитектуре является масштаб развертывания. Мы оборачиваем отдельные сервисы в систему обнаружения, API-шлюзы и выключатели, чтобы активировать свойства восстановления. Часто API-шлюзы (и подобные сервисы) — это решения на базе SaaS, но даже если мы деплоим их сами, точное реплицирование нашего продакшна затруднительно.

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

Из-за такой сложной топологии правильное тестирование поведения при восстановлении практически невозможно в режиме локальной работы. Любой полученный результат будет неточным из-за сильно отличающейся логистики развертывания.

Но мы не можем обрушить продакшн микросервиса только для того, чтобы доказать свою точку зрения. Верно?

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

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

Здесь на помощь приходят логи и снапшоты. Мы можем добавить их в API бэкенда, а также в “отвалившийся” сервис, для проверки того, что результаты, которые мы получили, действительно являются результатами из кэша шлюза.

Rinse — Repeat англ. идиома «смыть и повторить»)

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

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

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

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

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

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

В заключение

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

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

Вот тут-то и приходят на помощь инструменты мониторинга разработчика (логи, счетчики, отчеты), чтобы убедиться, что даже при сбоях в продакшне, восстановление за пределами службы все еще работает. Это не тривиальная задача, поскольку слабая связанность — только первый шаг. Поведение во время различных типов сбоев можно проверить только в продакшне, и мы не хотим потерпеть неудачу из-за попытки отстоять свою позицию.


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

А уже завтра состоится занятие «Паттерны аутентификации и авторизации», успевайте записаться.

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


  1. Jeisooo
    13.09.2022 10:11

    Кроме того, чтобы корректно перевести текст и не потеряться в проф.терминах, переводчик должен еще уметь в синтаксис. Иначе читать это все становится невозможно. Особенно в таком объеме.

    Я вам советую не переводить дословно, а рерайтить слишком сложные куски.

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

    "Нам нужно изолировать функционал и убедится, что при масшабировании ничего не сломается"