Привет! Меня зовут Игорь Шаталкин, я разработчик-эксперт в CUSTIS. В этой статье продолжим обсуждение монолитов и микросервисов. Я подробно рассмотрю важные моменты работы с микросервисной архитектурой и поделюсь как своим опытом, так и опытом компании CUSTIS: с чем нам приходилось сталкиваться в проектах и какими способами мы решали возникшие проблемы.
Проектирование архитектуры
Ключевой вызов — это оптимальное разделение приложения на модули. Слишком мелкая декомпозиция приводит к:
необходимости изменения нескольких модулей при доработках;
дополнительным затратам на API;
сложностям с согласованным развёртыванием.
При проектировании следует проводить границы так, чтобы большинство доработок делалось в рамках одного модуля.
Одно из решений — это подход Monolith First, в который входят:
начальная разработка монолитного приложения;
последующее отделение модулей на основе естественно сложившихся границ.
На всякий случай добавлю, что поручать проектирование микросервисов стоит лишь архитектору с соответствующими знаниями и опытом.
Обеспечение целостности данных
В отличие от монолита, работающего с единой БД (базой данных), микросервисы работают с несколькими базами. Это создаёт проблемы при координации транзакций. Например, сервис, отвечающий за баланс клиента, списал средства, а сервис, отвечающий за бронирование заказа, упал, и система должна уметь штатно обрабатывать такую ситуацию (вернуть деньги обратно на баланс). Для поддержания целостности данных микросервисов можно использовать:
Паттерн Outbox
Сообщение уходит только тогда, когда вызывающая система успешно зафиксировала транзакцию.
Принимающая сторона должна реализовать идемпотентную обработку, поскольку очереди, как правило, не гарантируют, что сообщение будет доставлено один и только один раз.
Паттерн Saga
Каждый из сервисов последовательно выполняет необходимые действия и фиксирует свою транзакцию.
Если какой-то из сервисов упал, запускаются компенсирующие транзакции, которые откатывают каждую из частей в первоначальное состояние.
Отображение данных на UI
Что касается UI, то основные сложности возникают при необходимости отображения данных из нескольких микросервисов на одном интерфейсе, особенно при реализации фильтрации и сортировки. Например, на интерфейсе заказов нужно отобразить информацию о складах, а также сделать фильтрацию и сортировку заказов по ним. Поскольку эта информация лежит в разных БД, применить фильтры и сортировку на уровне БД не получится.
Как решить проблему:
Для небольших объёмов данных:
Дополнительные API между системами. Так можно вначале получать отсортированные и отфильтрованные склады, а затем уже выполнять поиск заказов с учётом полученных ранее данных.
Для больших объёмов данных:
Использование View для эффективных сортировок и фильтраций на уровне БД при помощи Join-операций. Если микросервису А требуются дополнительные данные из B для UI, то А делает Join на View, подготовленную в B. В нашем случае все микросервисы физически лежали в одной БД, но в разных схемах, поэтому фильтрация и сортировка были перенесены на уровень БД.
Оптимизация создания новых микросервисов
При использовании микросервисной архитектуры важно оптимизировать процесс создания новых сервисов. На основе нашего опыта мы выработали два эффективных подхода.
Клонирование существующего микросервиса — копируем код работающего сервиса и очищаем его от доменной логики. Этот метод эффективен при небольшом количестве сервисов.
Использование шаблонов — создаём базовую заготовку и автоматизируем процесс генерации нового сервиса скриптами. Требует больших первоначальных усилий, но окупается при частом создании новых сервисов.
Управление общим кодом и зависимостями
Один из ключевых вызовов в микросервисной архитектуре — эффективное управление общим кодом. Для решения этой задачи мы разработали концепцию ядра — набора общих библиотек, используемых всеми микросервисами.
В ядро выносится код, необходимый для:
запуска приложения;
базовых операций;
общей инфраструктурной логики.
Важно отметить, что процесс добавления кода в ядро должен быть тщательно продуман. Мы практикуем два подхода.
Прямая разработка в ядре — когда точно понимаем требования.
Плюс: изменения вносятся напрямую в целевую библиотеку.
Минус: эти изменения нельзя сразу же протестировать в прикладном коде.Органический перенос проверенного кода из микросервисов — переносим код в базовую библиотеку в момент, когда он понадобился ещё в одном микросервисе.
Плюс: переносится уже работающий и отлаженный код.
Минус: требуется повторное тестирование сервиса, в котором изначально находился этот код.
Синхронизация зависимостей и общего кода
Управление зависимостями в микросервисной архитектуре требует особого внимания, поскольку обновление ядра приводит к необходимости его выпуска и пересадки на новую версию всех модулей.
Что поможет решить проблему:
скрипты автоматического обновления ядра;
периодическая проверка кода, который может дублироваться, и вынос общего кода в ядро.
К проблеме синхронизации общих кусков кода также близка проблема синхронизации зависимостей. Что делать, если А и B зависят от некоего пакета Х и вышла его новая версия?
Решение:
Пересадка обоих модулей на новую версию пакета Х одновременно. Это может быть дороже в моменте, но позволяет фокусно тратить ресурсы на пересадку. В конечном счёте дешевле пересадить все модули сразу (и поправить в них критические изменения), чем заниматься ими по очереди.
Мы стараемся распространять зависимости через ядро (набор общих библиотек), чтобы достаточно было сделать пересадку ядра на новую версию пакета, а уже после пересадить все микросервисы на новую версию ядра.
Локальная разработка и отладка
Организация эффективной локальной разработки — критически важный аспект. Иногда возникают ситуации, когда для работы сервиса А нужен B, а для него — ещё С и т. д. Эти зависимости не всегда видны в начале отладки. В зависимости от задачи мы использует один из подходов:
специально выделенный тестовый стенд, на котором развёрнуты сервисы B, С и все другие, необходимые для работы A;
docker-compose, который позволяет поднимать все нужные сервисы на машине разработчика.
Иногда мы комбинируем оба этих подхода, т.е. часть сервисов поднимается через docker-compose, а часть работает на выделенном стенде.
Управление релизами
Особого внимания заслуживает процесс выпуска версий. На практике бывают ситуации, когда изменения в двух или более сервисах должны попадать на бой синхронно (хотя в теории микросервисы не должны влиять на работу друг друга). При необходимости синхронного обновления нескольких сервисов мы следуем чёткому алгоритму:
если возможно, то останавливаем все сервисы на время обновления;
если нет, то используем промежуточные версии с поддержкой обратной совместимости АПИ.
При выпуске версий может случиться проблема с миграцией данных, когда миграции одного микросервиса зависят от другого.
Решение:
Выпускать промежуточные версии. Например, если часть миграций сервиса А зависит от сервиса В, то можно выпустить версии А.1 и А.2, а между ними версию сервиса B.
Отслеживание действий пользователя в системе
Когда система состоит из множества распределённых микросервисов, становится сложнее отслеживать действия пользователя и точно понимать, что происходило в системе в целом. Это затрудняет диагностику и поиск проблем.
Решение:
Внедрить уникальный идентификатор запроса, который будет передаваться через все взаимодействия между сервисами.
Использовать этот идентификатордля мониторинга логов и анализа, какие сервисы участвовали в обработке запроса, что помогает быстро обнаружить проблемы.
Обработка ошибок в распределённой системе
Ошибки в одном сервисе могут повлиять на работу других сервисов. Важно не только правильно отреагировать на ошибки, но и грамотно сообщить об этом пользователю. В некоторых случаях нужно предоставить подробности ошибки, а в других — только общую информацию.
Решение:
Внедрить возможность просмотра стека вызовов между сервисами, чтобы точно понять, какой сервис первым вызвал сбой и какие модули пострадали от этого.
В зависимости от контекста ошибки решать, нужно ли предоставлять пользователю подробности ошибки или ограничиться общей информацией.
Ресурсоёмкость
Микросервисная архитектура требует больших ресурсов по сравнению с монолитами, поскольку каждый сервис работает в отдельном контейнере и генерирует свои технические логи. Это может привести к увеличению потребления памяти и CPU, что требует дополнительного внимания.
Решение:
Заложить в проект достаточный бюджет на ресурсы, учитывая потребности каждого микросервиса.
Внимательно следить за использованием ресурсов, например, уменьшив объём логирования, чтобы сократить нагрузку на систему.
Регулярно анализировать производительность и потребление ресурсов, чтобы оптимизировать систему по мере её роста.
Такой структурированный подход к реализации микросервисной архитектуры позволяет организовать эффективный процесс разработки, минимизирует риски и нивелирует сложности, возникающие при работе с микросервисами. Чтобы работа с микросервисами была комфортной, я рекомендую:
привлекать опытных специалистов для проектирования;
учитывать механизмы обеспечения целостности данных;
продумывать стратегию выпуска версий и отображения данных на UI;
создавать механизмы трассировки исключений и отслеживания функций, вызванных в результате действий пользователя;
определить механизм отладки микросервисов и распространения общего кода.
В следующей статье я планирую разобрать, как перейти от монолита к микросервисам.
Источник: https://habr.com/ru/companies/custis/articles/899628/