Паттерн Saga в микросервисной архитектуре

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Автор статьи: Артем Михайлов

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

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

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

Именно здесь нам на помощь приходит интересный паттерн — Saga. Суть его в том, чтобы разбить большую транзакцию на ряд более мелких, локальных транзакций внутри каждого микросервиса. Если что-то идет не так, мы можем применить компенсирующие действия для возврата системы в предыдущее согласованное состояние. Поэтому Saga становится мощным инструментом для управления транзакциями в микросервисной архитектуре.

Понимание паттерна Saga и его применение в микросервисах


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

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

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

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

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

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

Реализация паттерна Saga: шаг за шагом


1. Определение границ сервисов и определение саги:

Перед тем, как начать кодирование, нам нужно ясно определить границы наших микросервисов и определить сагу. Давайте представим, что у нас есть три микросервиса: «Orders» для управления заказами, «Payments» для обработки платежей и «Notifications» для отправки уведомлений. Наша сага будет охватывать процесс оформления заказа, который включает создание заказа, выполнение платежа и отправку уведомления о статусе заказа.

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

class OrderService:
    def create_order(self, order_data):
        # Здесь реализация создания заказа
        pass

    def cancel_order(self, order_id):
        # Здесь реализация отмены заказа и компенсирующих действий
        pass


Разделение саги на локальные транзакции:

Теперь, когда у нас есть наш OrderService, давайте разделим нашу сагу на локальные транзакции для каждого микросервиса. Для этого нам понадобится еще два класса: PaymentService и NotificationService, чтобы обрабатывать соответственно платежи и уведомления.

class PaymentService:
    def process_payment(self, order_id, payment_data):
        # Здесь реализация обработки платежа
        pass

    def rollback_payment(self, order_id):
        # Здесь реализация отмены платежа и компенсирующих действий
        pass

class NotificationService:
    def send_notification(self, order_id, notification_data):
        # Здесь реализация отправки уведомления
        pass

    def rollback_notification(self, order_id):
        # Здесь реализация отмены уведомления и компенсирующих действий
        pass


Обработка компенсирующих действий при ошибке:

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

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

class SagaCoordinator:
    def execute_saga(self, order_data, payment_data, notification_data):
        order_service = OrderService()
        payment_service = PaymentService()
        notification_service = NotificationService()

        try:
            # Шаг 1: Создание заказа
            order_id = order_service.create_order(order_data)

            # Шаг 2: Выполнение платежа
            payment_service.process_payment(order_id, payment_data)

            # Шаг 3: Отправка уведомления
            notification_service.send_notification(order_id, notification_data)

            # Все успешно выполнено, завершаем сагу
            print("Сага успешно завершена!")
        except Exception as e:
            # Обработка ошибки и выполнение компенсирующих действий
            print(f"Произошла ошибка: {e}")
            self.rollback_saga(order_id)

    def rollback_saga(self, order_id):
        # Вызываем компенсирующие действия для отмены всех предыдущих операций
        order_service = OrderService()
        payment_service = PaymentService()
        notification_service = NotificationService()

        try:
            # Шаг 1: Отмена платежа
            payment_service.rollback_payment(order_id)

            # Шаг 2: Отмена уведомления
            notification_service.rollback_notification(order_id)

            # Шаг 3: Отмена создания заказа
            order_service.cancel_order(order_id)

            print("Сага успешно отменена!")
        except Exception as e:
            # Обработка ошибки во время отката
            print(f"Ошибка при откате саги: {e}")


Теперь, когда у нас есть класс SagaCoordinator, который координирует выполнение саги и обработку ошибок, мы можем вызвать метод execute_saga() и передать ему данные для оформления заказа, обработки платежа и отправки уведомления.

if __name__ == "__main__":
    saga_coordinator = SagaCoordinator()
    order_data = {"customer_id": 123, "products": [1, 2, 3]}
    payment_data = {"amount": 100.0, "payment_method": "credit_card"}
    notification_data = {"message": "Ваш заказ успешно оформлен!"}
    saga_coordinator.execute_saga(order_data, payment_data, notification_data)


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

Сценарии и компенсирующие транзакции


Примеры типичных сценариев для паттерна Saga:

1. Оформление заказа с оплатой и доставкой:
Допустим, у нас есть микросервисы для управления заказами, платежами и доставкой. При оформлении заказа с клиента снимается оплата, и заказ передается на доставку. В этом сценарии, мы можем использовать паттерн Saga, чтобы обеспечить атомарность операций. Если оплата не прошла успешно, мы можем откатить заказ и вернуть средства клиенту. Если же доставка не удалась, мы можем отменить оплату и вернуть деньги клиенту.

2. Бронирование и отмена брони:
Представим, что у нас есть микросервисы для бронирования отелей и отмены бронирования. В этом сценарии, паттерн Saga позволяет нам обрабатывать бронирование и отмену отдельно. Если бронирование прошло успешно, но клиент решает отменить бронь, мы можем использовать компенсирующие действия для отката операции.

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

Как обрабатывать ошибки и откатывать изменения:

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

Для примера, реализуем сценарий оформления заказа с помощью Python и покажем, как обрабатывать ошибки и использовать компенсирующие действия.

class OrderService:
    def create_order(self, order_data):
        try:
            # Шаг 1: Создание заказа
            order_id = self._create_order_in_database(order_data)

            # Шаг 2: Выполнение платежа
            self._process_payment(order_id, order_data["payment_data"])

            # Шаг 3: Отправка уведомления
            self._send_notification(order_id, "Ваш заказ успешно оформлен!")

            print("Заказ успешно оформлен!")
            return order_id
        except Exception as e:
            print(f"Произошла ошибка при оформлении заказа: {e}")
            self._handle_saga_failure(order_id)

    def _create_order_in_database(self, order_data):
        # Здесь реализация создания заказа в базе данных
        pass

    def _process_payment(self, order_id, payment_data):
        # Здесь реализация обработки платежа
        pass

    def _send_notification(self, order_id, message):
        # Здесь реализация отправки уведомления
        pass

    def _handle_saga_failure(self, order_id):
        # Вызываем компенсирующие действия для отката предыдущих операций
        self._rollback_payment(order_id)
        self._rollback_order_creation(order_id)

    def _rollback_payment(self, order_id):
        # Здесь реализация отмены платежа
        pass

    def _rollback_order_creation(self, order_id):
        # Здесь реализация отмены создания заказа
        pass


В этом примере мы представили класс OrderService, который отвечает за оформление заказа и его выполнение в рамках саги. В методе create_order, мы вызываем шаги создания заказа, обработки платежа и отправки уведомления. Если в ходе выполнения возникнет ошибка, мы вызываем метод _handle_saga_failure, который аккуратно откатит все предыдущие изменения и вернет систему в предыдущее согласованное состояние.

Инструменты и подходы для реализации паттерна Saga


Координаторы транзакций:

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

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

Использование message brokers:

Использование message brokers (брокеров сообщений) является распространенным подходом для реализации асинхронной коммуникации между микросервисами. Брокеры сообщений позволяют отправлять и принимать сообщения между различными компонентами системы, что делает процесс координации транзакций более эффективным.

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

Фреймворки для паттерна Saga:

Существует несколько отличных фреймворков и библиотек, которые упрощают реализацию паттерна Saga и позволяют сосредоточиться на бизнес-логике приложения, минимизируя сложность управления транзакциями. Некоторые из них стоят особого внимания:

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

Orchestration-based Saga:
Этот подход базируется на использовании централизованного координатора (оркестратора), который управляет выполнением саги и отправляет запросы на выполнение шагов каждому микросервису. Подход «через оркестрацию» облегчает реализацию и отслеживание саги, так как всю логику координации можно вынести в отдельный сервис.

Axon Framework:
Это Java-фреймворк, который обеспечивает реализацию паттерна Saga через CQRS (Command Query Responsibility Segregation) и Event Sourcing. Axon Framework позволяет легко разделять команды и запросы в системе, что облегчает координацию транзакций и обработку событий.

Заключение


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

Сейчас количество компаний, переходящих на микросервисы с монолитов стремительно растет, таким компаниям требуются инженеры знающие паттерны работы с микросервисами и умеющие с ними работать. В связи с этим хочу пригласить вас на вебинар, где эксперты OTUS расскажут про, плюсы и минусы микросервисной архитектуры, а также про особенности перехода.
Источник: https://habr.com/ru/companies/otus/articles/751134/


Интересные статьи

Интересные статьи

Я часто взаимодействую с ботами в Telegram. Чаще как пользователь, но создать собственного бота или потрогать чужого я не боюсь. При разработке собственного решения чувствуется, что бот не похож на ...
Перевод обновлённого гайда Android по архитектуре приложений. Это — третья часть из пяти: рассказываем про события UI.
Расшифровка доклада Сергея Нестерова с конференции FrontendLive 2020.Привет! Меня зовут Сергей, уже больше двух лет я работаю в группе компаний Тинькофф. Моя команда зани...
В статье приводится описание того:- Как создать посетитель не привязанный к предметной области.- Как получать настоящий тип объекта и передавать его в шаблон функции имея на руках лишь ук...
В этой статье рассматривается создание достаточного простого автотеста. Статья будет полезна начинающим автоматизаторам. Материал изложен максимально доступно, однако, будет значител...