Привет! Меня зовут Игорь Шаталкин, я разработчик-эксперт в 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/articles/899628/
Добавить комментарий