Привет! Меня зовут Иван, я ведущий инженер компании Nexign. В этой статье я хотел бы немного рассказать о решении для поддержки бизнес-процессов оператора и управления доходами и поделиться опытом разработки одного из его компонентов с использованием микросервисов. Эта информация будет полезна как для инженеров, которые собираются применять микросервисную архитектуру в разработке приложений, так и для владельцев и менеджеров продуктов, которые должны иметь представление о ее основах для оценки связанных с проектом рисков.
Что такое решения для поддержки бизнес-процессов оператора
В первую очередь определим, чем являются решения для поддержки бизнес-процессов оператора и управления доходами. Это продукты для работы с финансовыми объектами заказчика, включающие в себя автоматизацию процессов тарификации и выставление клиентам счетов за услуги на основе данных о платежах, корректировках, агрегированной информации о событиях потребления и сопутствующих начислениях. Решения для управления доходами также включают в себя автоматизацию работы с дебиторской задолженностью. Мы архитектурно выделили отдельный функциональный блок, посвященный этой задаче, и стали строить его с использованием микросервисов.
Наш функциональный блок предназначен для автоматизации принятия решений о формировании управляющих воздействий на внешние системы на основе анализа финансовых данных и ключевых характеристик клиентов, полученных из различных источников. Для достижения этой цели используются бизнес-правила (меры). Они объединяются в планы работ, которые реализуют последовательность применения мер при выполнении условий, задаваемых различными параметрами.
Функциональный блок для автоматизации работы с дебиторской задолженностью включает в себя следующие функции:
конфигурация и хранение мер и планов работ;
принятие решений об использовании той или иной меры с последующим формированием управляющих воздействий для внешних систем;
предоставление информации сторонним системам о ходе работы с задолженностью.
Как было сказано выше, мы приняли решение строить наш блок по микросервисной архитектуре. Основными факторами выбора данного подхода cтали гибкость развития, управления и масштабирования. Тем не менее, любая разработка ассоциируется с рядом сложностей, и я расскажу, каких стратегий мы придерживаемся для решения некоторых из них.
Нарезка микросервисов
Первой сложностью, связанной с микросервисной архитектурой, является нарезка микросервисов.
Микросервис – это автономный, независимо развертываемый программный компонент, который реализует определенные полезные функции. Его API может включать в себя команды, запросы или события. Правильная декомпозиция на микросервисы и определение способов их взаимодействия спасет от тонн сожженных человеко-дней, связанных с "перекомпоновками" сервисов при дальнейшей разработке решения.
При нарезке микросервисов стоит помнить об одном из главных принципов подхода – слабой связанности сервисов. Микросервисы должны быть независимы друг от друга. В идеале они не должны иметь общих структур и хранилищ данных, а также могут обладать различной внутренней архитектурой, а иногда и отдельным стеком технологий. Другими словами, каждый сервис должен иметь свой API и собственную базу данных или отдельную схему, если у него есть потребность в персисте данных, и должен хорошо справляться с ограниченным и связанным набором задач. Нарезка микросервисов также зависит от латентностей сети, обеспечения согласованности данных между сервисами при записи, так как некоторые операции вынуждены обновлять данные в нескольких сервисах, и получения согласованного представления данных, потому что информация может поступать из различных баз.
При разработке нашего функционального блока мы выбрали следующий алгоритм нарезки микросервисов:
определение функциональных требований: создание обобщенной доменной модели и определение системных операций;
распределение на сервисы по бизнес-функциям (это не всегда возможно из-за пересечений);
определение API сервисов.
Исходя из назначения нашего функционального блока, мы рассматриваем вариант нарезки продукта на следующие сервисы:
сервисы по конфигурированию и хранению профилей;
сервисы по конфигурированию и хранению мер;
сервисы по конфигурированию и хранению планов работ;
сервис по работе с внешними событиями;
несколько сервисов, служащих для исполнения планов работ и управления НСИ.
Также проиллюстрирую нарезку микросервисов с помощью картинки:
Выбор способов взаимодействия сервисов
Второй сложностью, связанной с микросервисной архитектурой, является корректный выбор способов взаимодействия сервисов. Сразу отмечу, что серебряной пули здесь нет: каждый выбирает подход, основанный на индивидуальных потребностях и ограничениях.
Взаимодействие сервисов может быть синхронным и асинхронным, и каждый из этих способов имеет свои плюсы и минусы.
Синхронное взаимодействие основано преимущественно на технологиях RPC или REST. REST является самым распространенным вариантом, но общение в форме "запрос-ответ" может ограничивать работу сервисов. В свою очередь, RPC (если говорить о самой распространенной реализации gRPC) немного сложнее, но использование HTTP/2 и ProtocolBuffers увеличивает его эффективность. Одним из главных недостатков синхронных вариантов взаимодействия является необходимость указания местоположения всех сервисов. При обращении к другому сервису каждый экземпляр сервиса должен знать о конкретном "местоположении" соседа, но этот вопрос легко решается с использованием serviceDiscovery. Синхронные варианты взаимодействия также ассоциируются с трудностями при возникновении отказов или таймаутов.
В свою очередь, асинхронные способы взаимодействия можно строить на базе синхронных протоколов с использованием идентификаторов запросов и колбэков, но в большинстве случаев они основаны на применении брокеров сообщений. Хотелось бы отметить, что корректное функционирование асинхронной связи требует определения типов сообщений: что вы будете гонять через брокер – сущности, команды или события? Типы сообщений также влияют на выбор шаблонов взаимодействия, таких как однонаправленные уведомления, издатель/подписчик и прочие. Асинхронный способ на базе брокера обеспечивает более гибкое и надежное общение между сервисами и увеличивает доступность системы, но он сложнее в реализации. Брокер сообщений также может стать узким местом, поскольку он содержит всю систему взаимодействия между сервисами.
При разработке нашего функционального блока мы выбрали комбинированный вариант обмена, стараясь применять асинхронный способ взаимодействия там, где без него не обойтись. Мы строим синхронное вхаимодействие на базе RPC, а асинхронное – на основе Apache Kafka.
Обеспечение транзакционности
Еще одной сложностью, связанной с микросервисами, является обеспечение транзакционности. В микросервисной архитектуре трудно поддерживать транзакции типа ACID, так как некоторые данные могут обновляться на нескольких сервисах, а согласованность данных является одним из ключевых моментов разработки.
Вкратце проиллюстрируем ситуацию. Представим, что у нас есть бизнес-транзакция "Some Action", которая должна провести более мелкие транзакции в других сервисах. При этом все действие "Some Action" имеет смысл только при условии, что все дочерние транзакции выполнятся успешно. Если хоть одна из них завершилась с ошибкой, то нужно откатить все предыдущие выполненные дочерние транзакции, так как иначе мы получим неконсистентное состояние.
Эта задача имеет несколько способов решения, включая распределенные транзакции и саги (наборы локальных транзакций), также известные как повествования. Мы остановились на варианте с сагами и оркестратором. Этот способ показался нам подходящим ввиду своей простоты и относительно недорогой стоимости разработки.
Суть решения довольно проста. Бизнес-транзакция разбивается на последовательность более мелких транзакций, в которых происходят обращения к сервисам, отвечающим за локальные транзакции. После этого составляется некий сценарий выполнения этой транзакции (сага), который включает в себя обработку ошибок (отказов сервисов). Задача по координации исполнения саги ложится на оркестратор.
Исходя из требований для разных ошибок (отказов) задается свой способ или последовательность способов обработки:
переповтор обращения;
запуск компенсирующих транзакций;
завершение транзакции с ошибкой.
Мне бы хотелось кратко остановиться на компенсирующих транзакциях. По сути, компенсирующая транзакция – это откат каждой уже сделанной локальной транзакции. Недостатком этого способа обработки является необходимость реализации дополнительных компенсирующих методов, но они могут также отсутствовать по естественным причинам.
Ниже приведена иллюстрация, которая хорошо отражает суть на примере бронирования путевки.
Финальным шагом решения этой задачи остается выбор компонента для исполнения саги (оркестратора). Здесь можно самим изобрести велосипед и реализовать что-то свое, а можно рассмотреть готовые решения, такие как Camunda, Apache AirFlow и другие. Важно помнить, что оркестратор/координатор должен быть надежным, масштабируемым и способным продолжить сагу с места "падения".
При разработке нашего функционального блока мы пробовали разные готовые инструменты, включая и Camunda, на которой мы пишем bpmn-сценарии в качестве саг, и Temporal, на котором сага непосредственно кодится. В конечном итоге мы остановились на Temporal, так как подумали, что это production-ready решение, которое довольно легко встраивается в проект. По большому счету вся интеграция Temporal сводится к разметке кода аннотациями и написанию обвязки по его взаимодействию с сервером. Хочу отметить, что основным плюсом данного решения является простота развертывания благодаря dockerCompose для быстрого старта на локальной машине или в тестовом окружении и Helm-чартам для деплоя в kubernetes. При деплое в k8s можно также сразу получить мониторинг Temporal из коробки с отображением метрик в Grafana.
Заключение
В данной статье я рассмотрел только "вершину айсберга" разработки приложений на основе микросервисной архитектуры. Хочу отметить, что сложностей, которые вносит использование микросервисов на всех этапах жизненного цикла ПО от продумывания концепции решения до его эксплуатации, довольно много. Тем не менее, с ними можно бороться, и я надеюсь, что данная статья поможет вам в этом.
Если у вас есть вопросы или замечания, делитесь ими в комментариях.
Также хочется снова подчеркнуть важность осознанного применения этого подхода. Стоит помнить, что многие задачи, часто решаемые с использованием микросервисов, легко выполняются без них или не имеют к ним отношения.
Даже самый яркий и дорогой изумруд не заменит молотка, если нужно забить гвоздь©.