В чём разница между хореографией и оркестрацией в 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).
Коротко о ключевых различиях:
- Управление: В хореографии управление децентрализовано (нет единой точки координации); в оркестрации управление централизовано в специальном сервисе/модуле (Saga Execution Coordinator).
- Стиль сообщений: Хореография основана на доменных событиях (events: «что произошло»), оркестрация — на командах (commands: «что нужно сделать») и ответах.
- Область применения: Хореография идеальна для коротких цепочек (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-системах применяются специализированные движки:
- Temporal.io / Cadence:
- Подход: Code-as-Configuration. Вся сага пишется как обычный детерминированный Java-код (
Workflow.sleep(), циклы,try-catch). - Под капотом: Event Sourcing. Движок перехватывает вызовы активностей, сохраняет историю истории в БД и при падении ноды воспроизводит состояние треда с точностью до строчки кода.
- Подход: Code-as-Configuration. Вся сага пишется как обычный детерминированный Java-код (
- Camunda 7 / 8 (Zeebe):
- Подход: BPMN 2.0. Бизнес-процесс рисуется графически в нотации BPMN, поддерживаются Compensation Boundary Events, тайм-ауты, ручные эскалации.
- Zeebe распределяет партиции событий через Raft, обеспечивая миллионный throughput.
- Axon Framework:
- Встроенный компонент
@Sagaс ассоциацией по ключам (associationProperty = "orderId").
- Встроенный компонент
4. Гибридный подход (Pragmatic Architecture)
В зрелых архитектурах используют гибрид:
- Оркестрация внутри Bounded Context: Внутри домена заказов сложный чекаут (резерв бонусов, бронь склада, проверка антифрода) оркестрируется внутренним SEC.
- Хореография между Bounded Contexts: По завершении саги чекаута публикуется публичное доменное событие
OrderConfirmedEvent, на которое независимо реагируют контексты аналитики, нотификаций и рекомендаций.
4 Tricky Questions
1. Что произойдет, если узел с оркестратором аварийно завершится (kill -9) во время выполнения саги?
Ответ: Если оркестратор спроектирован правильно, он сохраняет состояние перехода в Saga Log до отправки команды (или использует Event Sourcing). При рестарте упавшего инстанса или перехвате партиции другим узлом кластера:
- Загружается последнее зафиксированное состояние из Saga Log.
- Проверяется статус выполнения зависшей команды (по тайм-ауту или опросу статуса сервиса).
- Команда отправляется повторно. Благодаря идемпотентности сервиса-получателя повторная команда не приведёт к дублированию действий. Без персистентного Saga Log упавший оркестратор оставляет распределённую транзакцию в вечно «зависшем» несогласованном состоянии.
2. Почему в хореографии гораздо сложнее реализовать таймауты шагов (SLA) и эскалацию?
Ответ:
В оркестрации тайм-аут реализуется централизованно: оркестратор взводит таймер (например, в Temporal Workflow.await(Duration.ofMinutes(5)) или Dead Letter Scheduler) и, если ответ не получен, запускает компенсирующие транзакции.
В хореографии нет единого владельца времени:
- Либо каждый сервис должен реализовывать собственные сложные таймеры и публиковать события
StepTimeoutEvent. - Либо приходится выделять отдельный вспомогательный сервис мониторинга SLA, который фактически превращается в скрытый оркестратор.
3. Что такое «фантомная компенсация» (Phantom Compensation) и как от неё защититься?
Ответ:
Фантомная компенсация возникает, когда компенсирующее действие выполняется для транзакции, которая на самом деле никогда не выполнялась на целевом сервисе.
Пример: Оркестратор отправил команду ReserveInventory, но запрос застрял в сети. Оркестратор по тайм-ауту решает откатить сагу и шлёт ReleaseInventory. Сервис склада получает ReleaseInventory, увеличивает остаток на складе на +1, а затем с задержкой получает исходный ReserveInventory.
Защита:
- Использование уникального
SagaId/TransactionIdна всех шагах. - Проверка состояния: компенсирующая транзакция может выполниться успешно только в том случае, если прямая транзакция была реально применена (или запись помечается как
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).