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

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

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

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

Именно здесь нам на помощь приходит интересный паттерн — 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 расскажут про, плюсы и минусы микросервисной архитектуры, а также про особенности перехода.

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


  1. vigilante
    28.07.2023 16:59
    +2

    Есть ещё Temporal — тоже очень удобен для реализации saga


  1. softaria
    28.07.2023 16:59
    +2

    А если упало компенсирующее действие?


    1. vigilante
      28.07.2023 16:59
      +4

      Там тоже saga и так до бесконечности :)


    1. flx0
      28.07.2023 16:59
      +2

      Автор почему-то забыл упомянуть про главное свойство саги, без которого это все не имеет смысла. Все компенсирующие действия должны быть идемпотентными (т.е. f(f(x))=f(x)). Поэтому если мы валимся на откате, мы этого просто не замечаем и повторяем откат.
      Более-менее нормальное описание этого дела есть тут


      1. softaria
        28.07.2023 16:59
        +1

        Спасибо. В таком варианте это имеет смысл. Странно, что автор проигнорировал ключевую вещь.

        Хотя тут сохраняется еще один вопрос. Если все упало вообще наглухо, то как после рестарта системы понять нужны ли еще какие-то компенсирующие действия?


        1. softaria
          28.07.2023 16:59

          Отвечу сам себе - вдруг кому-то будет полезно.

          По ссылке, предоставленной @flx0 написано, что уровень оркестрации должен иметь свой стейт. Тогда все встает на свои места. Только вот выглядит это теперь как вариация стандартного координатора транзакций. Которой , видимо, и является.


        1. flx0
          28.07.2023 16:59
          +1

          Если есть записанная сага, значит нужны. Даже если они все уже были произведены, но финализации не произошло, благодаря идемпотентости их можно повторить еще раз и ничего плохого не случится. После успешного отката сага удаляется (атомарно ставится флажок "удалено"). Соответственно, если саги нет, последняя транзакция была либо успешно завершена, либо откатилась, и система находится в консистентном состоянии.


          1. softaria
            28.07.2023 16:59
            +1

            Ага, тоже уже прочитал. И вот это наличие стейта у оркестратора - вторая ключевая особенность паттерна, не описанная автором.


  1. HemulGM
    28.07.2023 16:59
    +5

    Какой паттерн? Здесь нет никакого паттерна. Это обычный try/except. Более того, он у вас работает не отказоустойчиво. Что будет, если при откате у вас второй откат одного из действий приведёт к ошибке? Правильно, остальные даже не попытаются откатиться, а первый успешно откатился. У вас будет сильно нарушена целостность системы.

    Это типичный шаблон try/except, со всеми вытекающими проблемами и это далеко не новый "паттерн".


    1. vigilante
      28.07.2023 16:59

      Это паттерн микросервисной архитектуры https://microservices.io/patterns/data/saga.html Если try catch ловит исключения, то здесь речь скорее о бизнес-логике, когда, например, заказ невозможно оплатить из-за того, что продавец его не подтвердил, и это не исключение, а логичное поведение.


      1. HemulGM
        28.07.2023 16:59

        Т.е. без "паттерна" это никому не понятно и никто не делает откаты транзакций, если они произведены разными подсистемами?

        И я не уверен, что при не удачной генерации оповещения производится откат всего процесса.


        1. hVostt
          28.07.2023 16:59

          Не откат, а компенсация. Есть большая разница между откатом транзакции в БД и компенсацией в Саге. Это не одно и то же.


          1. HemulGM
            28.07.2023 16:59

            А где я сказал, что это одно и то же?


            1. hVostt
              28.07.2023 16:59

              Это следует из ваших утверждений. Сага это не про откат транзакций и даже не про откат процесса. Предусматривается определённый план действий и его реализация на случай возникновения программных или логических ошибок.


  1. NightShad0w
    28.07.2023 16:59
    +2

    Фарш невозможно провернуть назад. А по делу, не совсем понятно, что скрывается за откатом локальной транзакции. Ушедшее оповещение откатывается новым сообщением, об отмене платежа? А неудача в отправке сообщения, откатывает платеж платежом отрицательных значений?


    1. HemulGM
      28.07.2023 16:59
      +3

      Успешно совершена оплата, отдан сигнал на подготовку к отправке товара, но уведомление система не смогла сгенерировать и мы отменяем всё))) Я походу понял, почему у нас Почта в России не очень работает) Там Saga используется)