Шаблон Saga в распределённых транзакциях — с примерами на Go
Транзакции в микросервисах с использованием паттерна Saga
Шаблон Saga предлагает элегантное решение, разбивая распределенные транзакции на серию локальных транзакций с компенсирующими действиями.
Вместо того чтобы полагаться на распределенные блокировки, которые могут блокировать операции между сервисами, Saga обеспечивает eventual consistency (конечную согласованность) через последовательность обратимых шагов, что делает его идеальным решением для длительных бизнес-процессов.
В архитектурах микросервисов поддержание согласованности данных между сервисами является одной из самых сложных задач. Традиционные ACID-транзакции не работают, когда операции охватывают несколько сервисов с независимыми базами данных, оставляя разработчиков в поисках альтернативных подходов для обеспечения целостности данных.
Данное руководство демонстрирует реализацию шаблона Saga на языке Go с практическими примерами, охватывающими как подход оркестрации, так и хореографии. Если вам нужна быстрая справочная информация по основам Go, Шпаргалка по Go предоставляет полезный обзор.
Это красивое изображение сгенерировано AI-моделью Flux 1 dev.
Понимание шаблона Saga
Шаблон Saga был впервые описан Хектором Гарсия-Молиной и Кеннетом Сэйлем в 1987 году. В контексте микросервисов это последовательность локальных транзакций, где каждая транзакция обновляет данные внутри одного сервиса. Если любой шаг завершается ошибкой, выполняются компенсирующие транзакции для отмены эффектов предыдущих шагов.
В отличие от традиционных распределенных транзакций, использующих двухфазный коммит (2PC), Saga не удерживает блокировки между сервисами, что делает его подходящим для длительных бизнес-процессов. Компромисс заключается в обеспечении конечной согласованности вместо строгой согласованности.
Ключевые характеристики
- Отсутствие распределенных блокировок: Каждый сервис управляет своей собственной локальной транзакцией
- Компенсирующие действия: Каждая операция имеет соответствующий механизм отката
- Конечная согласованность: Система в конечном итоге достигает согласованного состояния
- Длительное выполнение: Подходит для процессов, занимающих секунды, минуты или даже часы
Подходы к реализации Saga
Существует два основных подхода к реализации шаблона Saga: оркестрация и хореография.
Шаблон оркестрации
При оркестрации центральный координатор (оркестратор) управляет всем потоком транзакций. Оркестратор отвечает за:
- Вызов сервисов в правильном порядке
- Обработку ошибок и запуск компенсирующих действий
- Поддержание состояния saga
- Координацию повторных попыток и таймаутов
Преимущества:
- Централизованный контроль и видимость
- Проще понять и отлаживать
- Лучшая обработка ошибок и восстановление
- Более простое тестирование общего потока
Недостатки:
- Единая точка отказа (хотя это можно смягчить)
- Дополнительный сервис для поддержки
- Может стать узким местом для сложных потоков
Пример на Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Шаг 1: Создать заказ
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Шаг 2: Зарезервировать инвентарь
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Компенсация
return err
}
// Шаг 3: Обработать платеж
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Компенсация
o.orderService.Cancel(orderID) // Компенсация
return err
}
// Шаг 4: Создать отгрузку
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Компенсация
o.inventoryService.Release(order.Items) // Компенсация
o.orderService.Cancel(orderID) // Компенсация
return err
}
return nil
}
Шаблон хореографии
При хореографии нет центрального координатора. Каждый сервис знает, что делать, и общается посредством событий. Сервисы подписываются на события и реагируют соответствующим образом. Этот событийно-ориентированный подход особенно эффективен в сочетании с платформами потоковой передачи сообщений, такими как AWS Kinesis, которые обеспечивают масштабируемую инфраструктуру для распределения событий между микросервисами. Для комплексного руководства по реализации событийно-ориентированных микросервисов с использованием Kinesis см. Построение событийно-ориентированных микросервисов с AWS Kinesis.
Преимущества:
- Децентрализация и масштабируемость
- Отсутствие единой точки отказа
- Сервисы остаются слабо связанными
- Естественная интеграция с событийно-ориентированными архитектурами
Недостатки:
- Сложнее понять общий поток
- Трудно отлаживать и отслеживать
- Сложная обработка ошибок
- Риск циклических зависимостей
Пример с событийно-ориентированной архитектурой:
// Сервис заказов
type OrderService struct {
eventBus EventBus
repo OrderRepository
}
func (s *OrderService) CreateOrder(order Order) (string, error) {
orderID, err := s.repo.Save(order)
if err != nil {
return "", err
}
s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
OrderID: orderID,
CustomerID: order.CustomerID,
Items: order.Items,
Total: order.Total,
})
return orderID, nil
}
// Примечание: s.repo.Save, за которым следует s.eventBus.Publish, является двойной записью.
// В продакшене замените это на паттерн транзакционного outbox, чтобы событие
// записывалось атомарно вместе со строкой заказа и публиковалось через реле.
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // Компенсация
}
// Сервис платежей
type PaymentService struct {
eventBus EventBus
client PaymentClient
}
func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
paymentID, err := s.client.Charge(event.CustomerID, event.Total)
if err != nil {
s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
OrderID: event.OrderID,
})
return
}
s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
OrderID: event.OrderID,
PaymentID: paymentID,
})
}
func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
// Компенсация: возврат платежа
return s.client.Refund(event.PaymentID)
}
Стратегии компенсации
Компенсация — это сердце шаблона Saga. Каждая операция должна иметь соответствующую компенсацию, способную отменить ее эффекты.
Типы компенсации
-
Обратимые операции: Операции, которые можно напрямую отменить
- Пример: Освобождение зарезервированного инвентаря, возврат платежей
-
Компенсирующие действия: Другие операции, которые достигают обратного эффекта
- Пример: Отмена заказа вместо его удаления
-
Пессимистичная компенсация: Предварительное выделение ресурсов, которые можно освободить
- Пример: Резервирование инвентаря перед списанием платежа
-
Оптимистичная компенсация: Выполнение операций и компенсация при необходимости
- Пример: Сначала списать платеж, вернуть, если инвентарь недоступен
Требования к идемпотентности
Все операции и компенсации должны быть идемпотентными. Это гарантирует, что повторная попытка неудачной операции не вызовет дублирующих эффектов. Не менее важно убедиться, что каждый участник saga надежно публикует свои события после локального коммита — паттерн транзакционного outbox является стандартным способом закрыть этот разрыв между записью в базу данных и публикацией в брокере.
func (s *PaymentService) Refund(paymentID string) error {
// Проверить, был ли уже выполнен возврат
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Уже возвращено, идемпотентно
}
// Обработать возврат
return s.processRefund(paymentID)
}
Лучшие практики
1. Управление состоянием Saga
Поддерживайте состояние каждого экземпляра saga для отслеживания прогресса и обеспечения восстановления. При сохранении состояния saga в базе данных выбор правильного ORM имеет решающее значение для производительности и поддерживаемости. Для реализаций на базе PostgreSQL рассмотрите сравнение в Сравнение Go ORM для PostgreSQL: GORM vs Ent vs Bun vs sqlc, чтобы выбрать лучший вариант для ваших потребностей в хранении состояния saga:
type SagaState struct {
ID string
Status SagaStatus
Steps []SagaStep
CurrentStep int
CreatedAt time.Time
UpdatedAt time.Time
}
type SagaStep struct {
Service string
Operation string
Status StepStatus
Compensated bool
Data map[string]interface{}
}
2. Обработка таймаутов
Реализуйте таймауты для каждого шага, чтобы предотвратить зависание sagas неопределенное время:
type SagaOrchestrator struct {
timeout time.Duration
}
func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- step.Execute()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
// Произошел таймаут, компенсировать
if err := step.Compensate(); err != nil {
return fmt.Errorf("compensation failed: %w", err)
}
return fmt.Errorf("step %s timed out after %v", step.Name(), o.timeout)
}
}
3. Логика повторных попыток
Реализуйте экспоненциальное отставание (backoff) для временных сбоев:
func retryWithBackoff(operation func() error, maxRetries int) error {
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err := operation()
if err == nil {
return nil
}
if !isTransientError(err) {
return err
}
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("operation failed after %d retries", maxRetries)
}
4. Event Sourcing для состояния Saga
Используйте event sourcing для ведения полного аудиторского следа. При реализации хранилищ событий и механизмов воспроизведения Go generics могут помочь создать типобезопасный, переиспользуемый код обработки событий. Для продвинутых паттернов с использованием generics в Go см. Go Generics: Случаи использования и паттерны.
type SagaEvent struct {
SagaID string
EventType string
Payload []byte
Timestamp time.Time
Version int64
}
type SagaEventStore struct {
store EventRepository
}
func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}
event := SagaEvent{
SagaID: sagaID,
EventType: eventType,
Payload: data,
Timestamp: time.Now(),
Version: version,
}
return s.store.Save(event)
}
func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
events, err := s.store.GetEvents(sagaID)
if err != nil {
return nil, fmt.Errorf("failed to get events: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("failed to apply event: %w", err)
}
}
return saga, nil
}
5. Мониторинг и наблюдаемость
Реализуйте всестороннее логирование и трассировку:
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
span := tracer.StartSpan("saga.create_order")
defer span.Finish()
span.SetTag("saga.id", sagaID)
span.SetTag("order.id", order.ID)
logger.WithFields(log.Fields{
"saga_id": sagaID,
"order_id": order.ID,
"step": "create_order",
}).Info("Saga started")
// ... выполнение saga
return nil
}
Общие паттерны и антипаттерны
Паттерны, которым следует следовать
- Паттерн координатора Saga: Используйте выделенный сервис для оркестрации
- Паттерн Outbox: Обеспечьте надежную публикацию событий
- Ключи идемпотентности: Используйте уникальные ключи для всех операций
- Машина состояний Saga: Моделируйте saga как машину состояний
Антипаттерны, которых следует избегать
- Синхронная компенсация: Не ждите завершения компенсации
- Вложенные Sagas: Избегайте sagas, вызывающих другие sagas (вместо этого используйте под-sagas)
- Общее состояние: Не разделяйте состояние между шагами saga
- Длительные шаги: Разбивайте шаги, которые занимают слишком много времени
Инструменты и фреймворки
Несколько фреймворков могут помочь в реализации шаблонов Saga:
- Temporal: Платформа оркестрации рабочих процессов с встроенной поддержкой Saga
- Zeebe: Двигатель рабочих процессов для оркестрации микросервисов
- Eventuate Tram: Фреймворк Saga для Spring Boot
- AWS Step Functions: Serverless-оркестрация рабочих процессов
- Apache Camel: Фреймворк интеграции с поддержкой Saga
Для сервисов-оркестраторов, которым нужны интерфейсы командной строки для управления и мониторинга, Построение CLI-приложений на Go с Cobra & Viper предоставляет отличные паттерны для создания инструментов командной строки для взаимодействия с оркестраторами saga.
При развертывании микросервисов на базе saga в Kubernetes реализация service mesh может значительно улучшить наблюдаемость, безопасность и управление трафиком. Реализация Service Mesh с Istio и Linkerd охватывает то, как service meshes дополняют паттерны распределенных транзакций, предоставляя сквозные возможности, такие как распределенная трассировка и circuit breaking.
Когда использовать шаблон Saga
Используйте шаблон Saga, когда:
- ✅ Операции охватывают несколько микросервисов
- ✅ Длительные бизнес-процессы
- ✅ Приемлемая конечная согласованность
- ✅ Вам нужно избегать распределенных блокировок
- ✅ Сервисы имеют независимые базы данных
Избегайте, когда:
- ❌ Требуется строгая согласованность
- ❌ Операции простые и быстрые
- ❌ Все сервисы используют одну и ту же базу данных
- ❌ Логика компенсации слишком сложна
Заключение
Шаблон Saga необходим для управления распределенными транзакциями в архитектурах микросервисов. Хотя он вносит сложность, он обеспечивает практическое решение для поддержания согласованности данных между границами сервисов. Выбирайте оркестрацию для лучшего контроля и видимости или хореографию для масштабируемости и слабой связанности. Всегда убедитесь, что операции идемпотентны, реализуйте правильную логику компенсации и поддерживайте всестороннюю наблюдаемость.
Ключом к успешной реализации Saga является понимание ваших требований к согласованности, тщательное проектирование логики компенсации и выбор правильного подхода для вашего случая использования. При правильной реализации Saga позволяет создавать устойчивые, масштабируемые микросервисы, которые поддерживают целостность данных в распределенных системах.
Полезные ссылки
- Microservices Patterns by Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentation
- Шпаргалка по Go
- Go Generics: Случаи использования и паттерны
- Сравнение Go ORM для PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Реализация CQRS на Go
- Построение CLI-приложений на Go с Cobra & Viper
- Реализация Service Mesh с Istio и Linkerd
- Построение событийно-ориентированных микросервисов с AWS Kinesis