🏗️ Раздел 17 · Вопрос #2

В чём разница между хореографией и оркестрацией в Saga

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


🟢 Junior Level

В распределённых системах паттерн Saga заменяет классические ACID-транзакции цепочкой локальных транзакций с компенсирующими действиями при сбоях. Координация этих шагов реализуется двумя принципиально разными способами: хореографией (Choreography) и оркестрацией (Orchestration).

Хореография (Event-Driven):
[Order Service] ──OrderCreated──► [Payment Service] ──PaymentCompleted──► [Delivery Service]
     ▲                                   │
     └────────PaymentFailed (Rollback)───┘

Оркестрация (Command-Driven):
                   ┌───────────────────────────┐
                   │    Saga Orchestrator      │
                   │ (State Machine + Saga Log)│
                   └──────┬───────┬───────┬────┘
           1. CreateOrder │       │ 2. Charge │ 3. Dispatch
                          ▼       ▼           ▼
                   [Order Service] [Payment] [Delivery]

Основная метафора:

  • Хореография похожа на танец на балу: нет ведущего, каждый танцор выучил свою партию и реагирует на движения и жесты партнёров. Сервисы обмениваются событиями (OrderCreated, PaymentAuthorized) через брокер сообщений.
  • Оркестрация похожа на симфонический оркестр с дирижёром: музыканты не смотрят друг на друга, они смотрят на дирижёра (оркестратор), который явно указывает, кому, когда и какую команду исполнить (ReserveInventoryCommand, ChargeCardCommand).

Коротко о ключевых различиях:

  1. Управление: В хореографии управление децентрализовано (нет единой точки координации); в оркестрации управление централизовано в специальном сервисе/модуле (Saga Execution Coordinator).
  2. Стиль сообщений: Хореография основана на доменных событиях (events: «что произошло»), оркестрация — на командах (commands: «что нужно сделать») и ответах.
  3. Область применения: Хореография идеальна для коротких цепочек (2–3 сервиса). Оркестрация незаменима для сложных бизнес-процессов (4+ сервисов, развилки, тайм-ауты).

🟡 Middle Level

1. Архитектура хореографии (Choreography)

Сервисы реагируют на события соседей, выполняют локальную транзакцию в своей БД и публикуют собственное событие через Transactional Outbox.

// Сервис оплаты реагирует на событие создания заказа
@Component
public class PaymentEventHandler {

    private final PaymentService paymentService;
    private final EventPublisher eventPublisher;

    @KafkaListener(topics = "order-events", groupId = "payment-group")
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            paymentService.processPayment(event.orderId(), event.amount());
            // Успех: публикуем событие для следующего шага (Delivery)
            eventPublisher.publish(new PaymentSucceededEvent(event.orderId()));
        } catch (InsufficientFundsException e) {
            // Ошибка: публикуем компенсирующее событие
            eventPublisher.publish(new PaymentFailedEvent(event.orderId(), e.getMessage()));
        }
    }
}

Особенности отката: Если на шаге 3 (Delivery) происходит сбой, сервис доставки публикует DeliveryFailedEvent. Сервис оплаты обязан слушать это событие и выполнить возврат денег (refund), а сервис заказов — отменить заказ (CANCELLED).

2. Архитектура оркестрации (Orchestration)

Координатор (Saga Execution Coordinator, SEC) хранит текущее состояние процесса (FSM — Finite State Machine) в постоянном хранилище (Saga Log) и отправляет адресные команды сервисам-участникам.

@Component
public class OrderSagaOrchestrator {

    private final SagaRepository sagaRepo;
    private final CommandGateway commandGateway;

    @Transactional
    public void onEvent(SagaReplyEvent reply) {
        OrderSagaData saga = sagaRepo.findById(reply.sagaId())
            .orElseThrow();

        switch (saga.getState()) {
            case PAYMENT_PENDING -> {
                if (reply.isSuccess()) {
                    saga.transitionTo(SagaState.DELIVERY_PENDING);
                    commandGateway.send(new ScheduleDeliveryCommand(saga.getOrderId()));
                } else {
                    saga.transitionTo(SagaState.COMPENSATING_ORDER);
                    commandGateway.send(new CancelOrderCommand(saga.getOrderId()));
                }
            }
            case DELIVERY_PENDING -> {
                if (reply.isSuccess()) {
                    saga.transitionTo(SagaState.COMPLETED);
                } else {
                    saga.transitionTo(SagaState.REFUNDING_PAYMENT);
                    commandGateway.send(new RefundPaymentCommand(saga.getOrderId()));
                }
            }
            case REFUNDING_PAYMENT -> {
                saga.transitionTo(SagaState.COMPENSATING_ORDER);
                commandGateway.send(new CancelOrderCommand(saga.getOrderId()));
            }
            case COMPENSATING_ORDER -> saga.transitionTo(SagaState.REJECTED);
        }
        sagaRepo.save(saga);
    }
}

3. Детальное техническое сравнение

Критерий Хореография (Choreography) Оркестрация (Orchestration)
Стиль коммуникации Event-Driven (Pub/Sub: «Заказ создан») Command-Driven (Point-to-Point: «Спиши деньги»)
Координация Распределённая, заложена в логику каждого участника Централизованная в Saga Execution Coordinator (SEC)
Связность (Coupling) Слабая по вызовам, но высокая семантическая связность по событиям Центральная связность: оркестратор знает обо всех шагах процесса
Наблюдаемость (Observability) Низкая: чтобы понять статус процесса, нужно собирать логи со всех сервисов или использовать Distributed Tracing Высокая: состояние любого заказа видно в одной таблице/панели оркестратора
Циклические зависимости Высокий риск создания «заколдованного круга» событий ($A \to B \to C \to A$) Исключены: граф переходов состояний строго направленный (DAG)
Точка отказа (SPOF) Отсутствует единый управляющий сервис Оркестратор является критическим компонентом (требует кластеризации и репликации БД)
Сложность изменения При добавлении шага приходится перенастраивать подписки нескольких сервисов Достаточно изменить граф состояний в одном оркестраторе

🔴 Senior Level

1. Архитектурные риски и ловушки

А. Хореография: «Spaghetti Architecture» и скрытый распределённый монолит

При росте системы (10+ сервисов) хореография приводит к ситуации, когда ни один инженер не способен предсказать полный путь обработки запроса без построения графа зависимостей через Jaeger/Zipkin. Изменение схемы одного события (OrderCreatedEvent) ломает 5 независимых сервисов-подписчиков. Возникают циклы: ServiceA порождает событие, которое через 3 сервиса возвращается в ServiceA как стимул для новой транзакции.

Б. Оркестрация: «Smart Orchestrator, Anemic Microservices»

Самая частая ошибка при оркестрации — перенос бизнес-логики сервисов в оркестратор. Микросервисы деградируют до примитивных CRUD-исполнителей, а оркестратор превращается в раздутый «божественный объект» (God Object), требующий деплоя при каждом малейшем изменении правил валидации или расчёта скидок. Правило: Оркестратор должен управлять только последовательностью шагов (Workflow & State), но не принимать доменных решений о том, как резервировать товар.

2. Гонки сообщений и несинхронный приём (Out-of-Order Delivery)

В хореографии при параллельной обработке очередей возможна рассинхронизация:

  • Сервис доставки может получить OrderCancelledEvent (компенсация) раньше, чем до него дошло исходное OrderCreatedEvent (например, из-за сетевой задержки или ребалансировки партиции Kafka).
  • Решение: State-based идемпотентность и буферизация. Если приходит отмена для несуществующего заказа, сервис фиксирует запись: Order { id: 123, state: CANCELLED_PREMATURELY }. Когда позже придёт исходный CreateOrder, сервис увидит статус и просто проигнорирует создание.
Хронология гонки (Race Condition):
1. [Broker] ────OrderCancelled──► [Delivery] (заказ создан со статусом CANCELLED_PREMATURELY)
2. [Broker] ────CreateDelivery──► [Delivery] (обнаружен флаг отмены -> действие пропускается)

3. Промышленные движки оркестрации

Писать самодельный оркестратор на switch-case допустимо только для простых задач. В Highload- и Enterprise-системах применяются специализированные движки:

  1. Temporal.io / Cadence:
    • Подход: Code-as-Configuration. Вся сага пишется как обычный детерминированный Java-код (Workflow.sleep(), циклы, try-catch).
    • Под капотом: Event Sourcing. Движок перехватывает вызовы активностей, сохраняет историю истории в БД и при падении ноды воспроизводит состояние треда с точностью до строчки кода.
  2. Camunda 7 / 8 (Zeebe):
    • Подход: BPMN 2.0. Бизнес-процесс рисуется графически в нотации BPMN, поддерживаются Compensation Boundary Events, тайм-ауты, ручные эскалации.
    • Zeebe распределяет партиции событий через Raft, обеспечивая миллионный throughput.
  3. Axon Framework:
    • Встроенный компонент @Saga с ассоциацией по ключам (associationProperty = "orderId").

4. Гибридный подход (Pragmatic Architecture)

В зрелых архитектурах используют гибрид:

  • Оркестрация внутри Bounded Context: Внутри домена заказов сложный чекаут (резерв бонусов, бронь склада, проверка антифрода) оркестрируется внутренним SEC.
  • Хореография между Bounded Contexts: По завершении саги чекаута публикуется публичное доменное событие OrderConfirmedEvent, на которое независимо реагируют контексты аналитики, нотификаций и рекомендаций.

4 Tricky Questions

1. Что произойдет, если узел с оркестратором аварийно завершится (kill -9) во время выполнения саги?

Ответ: Если оркестратор спроектирован правильно, он сохраняет состояние перехода в Saga Log до отправки команды (или использует Event Sourcing). При рестарте упавшего инстанса или перехвате партиции другим узлом кластера:

  1. Загружается последнее зафиксированное состояние из Saga Log.
  2. Проверяется статус выполнения зависшей команды (по тайм-ауту или опросу статуса сервиса).
  3. Команда отправляется повторно. Благодаря идемпотентности сервиса-получателя повторная команда не приведёт к дублированию действий. Без персистентного Saga Log упавший оркестратор оставляет распределённую транзакцию в вечно «зависшем» несогласованном состоянии.

2. Почему в хореографии гораздо сложнее реализовать таймауты шагов (SLA) и эскалацию?

Ответ: В оркестрации тайм-аут реализуется централизованно: оркестратор взводит таймер (например, в Temporal Workflow.await(Duration.ofMinutes(5)) или Dead Letter Scheduler) и, если ответ не получен, запускает компенсирующие транзакции. В хореографии нет единого владельца времени:

  • Либо каждый сервис должен реализовывать собственные сложные таймеры и публиковать события StepTimeoutEvent.
  • Либо приходится выделять отдельный вспомогательный сервис мониторинга SLA, который фактически превращается в скрытый оркестратор.

3. Что такое «фантомная компенсация» (Phantom Compensation) и как от неё защититься?

Ответ: Фантомная компенсация возникает, когда компенсирующее действие выполняется для транзакции, которая на самом деле никогда не выполнялась на целевом сервисе. Пример: Оркестратор отправил команду ReserveInventory, но запрос застрял в сети. Оркестратор по тайм-ауту решает откатить сагу и шлёт ReleaseInventory. Сервис склада получает ReleaseInventory, увеличивает остаток на складе на +1, а затем с задержкой получает исходный ReserveInventory. Защита:

  1. Использование уникального SagaId / TransactionId на всех шагах.
  2. Проверка состояния: компенсирующая транзакция может выполниться успешно только в том случае, если прямая транзакция была реально применена (или запись помечается как CANCELLED до выполнения исходного запроса, блокируя его исполнение).

4. Как меняется сетевой latency при выборе между хореографией и оркестрацией при цепочке из 4 шагов?

Ответ:

  • В хореографии сообщения идут по цепочке: $A \to B \to C \to D$. Время выполнения равно: \(T_{total} = T_{A} + RTT(A \to B) + T_{B} + RTT(B \to C) + T_{C} + RTT(C \to D) + T_{D}\)
  • В оркестрации каждый шаг требует обмена с координатором: $O \to A \to O \to B \to O \to C \to O \to D \to O$. Сетевых пересылок (RTT) становится в два раза больше: \(RTT_{total} = 2 \times N \times RTT\) Однако оркестрация позволяет выполнять независимые шаги параллельно (например, одновременно запросить проверку антифрода и резервирование товаров), что в хореографии требует создания сложных узлов слияния событий (Fan-Out / Fan-In).

🎯 Шпаргалка для интервью

Главное:

  • Хореография: децентрализованная, event-driven, каждый сервис слушает события соседей и решает, что делать. Нет единой точки отказа, но при усложнении превращается в запутанный «клубок спагетти» событий.
  • Оркестрация: централизованная, command-driven, специальный координатор (SEC) явно говорит каждому сервису, что делать, опираясь на стейт-машину и Saga Log. Легко мониторить и тестировать, но оркестратор требует масштабирования и защиты от сбоев.

Эмпирическое правило выбора:

  • До 3 сервисов с линейным флоу $\to$ Хореография.
  • 4+ сервисов, ветвления, параллельные шаги, строгие тайм-ауты, необходимость визуального мониторинга бизнес-процесса $\to$ Оркестрация (Temporal, Camunda, Zeebe).
  • Разные домены/Bounded Contexts $\to$ Оркестрация внутри, хореография снаружи.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Оркестратор — это сервис с try-catch вокруг REST-вызовов» (Синхронный REST-оркестратор без Saga Log упадет при перезагрузке и потеряет транзакцию).
  • ❌ «В хореографии сервисы полностью изолированы и не знают друг о друге» (Сервисы жестко связаны семантикой чужих событий и их версиями).
  • ❌ «Оркестрация всегда лучше хореографии» (Оркестрация добавляет накладные расходы на сетевые hop’ы и риск создания God Object).

Связанные темы: