Modello Saga nei Trasferimenti di dati Distribuiti - Esempi in Go
Transazioni nei Microservizi con il pattern Saga
Il pattern Saga
fornisce una soluzione elegante suddividendo le transazioni distribuite in una serie di transazioni locali con azioni di compensazione.
Invece di affidarsi a blocchi distribuiti che possono bloccare le operazioni tra i servizi, il pattern Saga consente una coerenza eventuale attraverso una sequenza di passaggi reversibili, rendendolo ideale per processi aziendali di lunga durata.
Nelle architetture a microservizi, mantenere la coerenza dei dati tra i servizi è uno dei problemi più complessi. Le transazioni tradizionali ACID non funzionano quando le operazioni si estendono a diversi servizi con database indipendenti, lasciando gli sviluppatori alla ricerca di approcci alternativi per garantire l’integrità dei dati.
Questo documento dimostra l’implementazione del pattern Saga in Go con esempi pratici che coprono entrambi gli approcci di orchestrazione e di coreografia. Se hai bisogno di un riferimento rapido per i fondamenti di Go, il Go Cheat Sheet fornisce un utile riepilogo.
Questa immagine gradevole è generata dal modello AI Flux 1 dev.
Comprendere il Pattern Saga
Il pattern Saga è stato originariamente descritto da Hector Garcia-Molina e Kenneth Salem nel 1987. Nel contesto dei microservizi, è una sequenza di transazioni locali in cui ogni transazione aggiorna i dati all’interno di un singolo servizio. Se qualsiasi passo fallisce, vengono eseguite transazioni di compensazione per annullare gli effetti dei passaggi precedenti.
A differenza delle tradizionali transazioni distribuite che utilizzano il commit a due fasi (2PC), il pattern Saga non mantiene blocchi tra i servizi, rendendolo adatto ai processi aziendali di lunga durata. Il compromesso è la coerenza eventuale invece di quella forte.
Caratteristiche Principali
- Nessun Blocco Distribuito: Ogni servizio gestisce la propria transazione locale
- Azioni di Compensazione: Ogni operazione ha un meccanismo di rollback corrispondente
- Coerenza Eventuale: Il sistema raggiunge eventualmente uno stato coerente
- Lunga Durata: Adatto a processi che durano secondi, minuti o anche ore
Approcci all’Implementazione del Pattern Saga
Esistono due approcci principali per implementare il pattern Saga: l’orchestrazione e la coreografia.
Pattern di Orchestrazione
Nell’orchestrazione, un coordinatore centrale (orchestratore) gestisce l’intero flusso delle transazioni. L’orchestratore è responsabile di:
- Invocare i servizi nell’ordine corretto
- Gestire gli errori e attivare le compensazioni
- Mantenere lo stato del saga
- Coordinare i retry e i timeout
Vantaggi:
- Controllo e visibilità centralizzati
- Più facile da comprendere e debuggare
- Migliore gestione degli errori e del recupero
- Test più semplice del flusso complessivo
Svantaggi:
- Punto unico di fallimento (sebbene possa essere mitigato)
- Servizio aggiuntivo da mantenere
- Può diventare un collo di bottiglia per flussi complessi
Esempio in Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Step 1: Create order
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Step 2: Reserve inventory
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Compensate
return err
}
// Step 3: Process payment
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Compensate
o.orderService.Cancel(orderID) // Compensate
return err
}
// Step 4: Create shipment
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Compensate
o.inventoryService.Release(order.Items) // Compensate
o.orderService.Cancel(orderID) // Compensate
return err
}
return nil
}
Pattern di Coreografia
Nella coreografia, non esiste un coordinatore centrale. Ogni servizio sa cosa deve fare e comunica attraverso eventi. I servizi ascoltano gli eventi e reagiscono di conseguenza. Questo approccio basato sugli eventi è particolarmente potente quando combinato con piattaforme di streaming di messaggi come AWS Kinesis, che forniscono un’infrastruttura scalabile per la distribuzione degli eventi tra i microservizi. Per una guida completa sull’implementazione di microservizi basati sugli eventi con Kinesis, vedi Costruire Microservizi basati sugli Eventi con AWS Kinesis.
Vantaggi:
- Decentralizzato e scalabile
- Nessun punto unico di fallimento
- I servizi rimangono debolmente accoppiati
- Adatto naturale per architetture basate sugli eventi
Svantaggi:
- Più difficile comprendere il flusso complessivo
- Difficile debug e tracciamento
- Gestione complessa degli errori
- Rischio di dipendenze cicliche
Esempio con Architettura basata sugli Eventi:
// Order Service
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
}
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // Compensation
}
// Payment Service
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 {
// Compensation: refund payment
return s.client.Refund(event.PaymentID)
}
Strategie di Compensazione
La compensazione è il cuore del pattern Saga. Ogni operazione deve avere una corrispondente compensazione che possa annullarne gli effetti.
Tipi di Compensazione
-
Operazioni Reversibili: Operazioni che possono essere annullate direttamente
- Esempio: Rilasciare l’inventario riservato, rimborsare i pagamenti
-
Azioni di Compensazione: Operazioni diverse che raggiungono l’effetto opposto
- Esempio: Annullare un ordine invece di eliminarlo
-
Compensazione Pessimista: Pre-allocazione di risorse che possono essere rilasciate
- Esempio: Riservare l’inventario prima di addebitare il pagamento
-
Compensazione Ottimista: Eseguire le operazioni e compensare se necessario
- Esempio: Addebitare il pagamento prima, rimborsare se l’inventario non è disponibile
Requisiti di Idempotenza
Tutte le operazioni e le compensazioni devono essere idempotenti. Questo garantisce che il riprovare un’operazione fallita non causi effetti duplicati.
func (s *PaymentService) Refund(paymentID string) error {
// Check if already refunded
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Already refunded, idempotent
}
// Process refund
return s.processRefund(paymentID)
}
Linee Guida per l’Implementazione
1. Gestione dello Stato del Saga
Mantieni lo stato di ogni istanza del saga per tracciare i progressi e abilitare il recupero. Quando si persiste lo stato del saga in un database, scegliere l’ORM giusto è cruciale per le prestazioni e la manutenibilità. Per le implementazioni basate su PostgreSQL, considera il confronto in Confronto tra ORMs Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc per selezionare l’opzione migliore per le tue esigenze di archiviazione dello stato del 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. Gestione dei Timeout
Implementa i timeout per ogni passo per prevenire che i sagas si blocchino indefinitamente:
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():
// Timeout occurred, compensate
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. Logica di Retry
Implementa un backoff esponenziale per gli errori transitori:
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 per lo Stato del Saga
Utilizza l’event sourcing per mantenere un registro completo. Quando si implementano archivi di eventi e meccanismi di replay, i generics in Go possono aiutare a creare codice di gestione degli eventi tipo-sicuro e riutilizzabile. Per modelli avanzati con generics in Go, vedi Generics in Go: Use Cases and Patterns.
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. Monitoraggio e Osservabilità
Implementa un logging e un tracciamento completi:
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 execution
return nil
}
Pattern Comuni e Anti-Pattern
Pattern da Seguire
- Pattern del Coordinatore Saga: Utilizza un servizio dedicato per l’orchestrazione
- Pattern dell’Outbox: Assicura una pubblicazione affidabile degli eventi
- Chiavi di Idempotenza: Utilizza chiavi uniche per tutte le operazioni
- Macchina a Stati Saga: Modella il saga come una macchina a stati
Anti-Pattern da Evitare
- Compensazione Sincrona: Non attendere che la compensazione venga completata
- Saghe Annidate: Evita che le saghe chiamino altre saghe (usa le sottosaghe invece)
- Stato Condiviso: Non condividere lo stato tra i passaggi del saga
- Passaggi di Lunga Durata: Suddividi i passaggi che durano troppo a lungo
Strumenti e Framework
Vários framework possono aiutare a implementare il pattern Saga:
- Temporal: Piattaforma di orchestrazione dei workflow con supporto integrato per il pattern Saga
- Zeebe: Motore di workflow per l’orchestrazione dei microservizi
- Eventuate Tram: Framework Saga per Spring Boot
- AWS Step Functions: Orchestratore di workflow serverless
- Apache Camel: Framework di integrazione con supporto per il pattern Saga
Per i servizi orchestratori che necessitano di interfacce CLI per la gestione e il monitoraggio, Costruire Applicazioni CLI in Go con Cobra & Viper fornisce eccellenti pattern per creare strumenti CLI per interagire con gli orchestratori Saga.
Quando si distribuiscono microservizi basati su Saga in Kubernetes, l’implementazione di una rete di servizi può migliorare significativamente l’osservabilità, la sicurezza e la gestione del traffico. Implementare una Rete di Servizi con Istio e Linkerd copre come le reti di servizi complementano i pattern di transazioni distribuite fornendo preoccupazioni trasversali come il tracciamento distribuito e il circuit breaker.
Quando Utilizzare il Pattern Saga
Utilizza il pattern Saga quando:
- ✅ Le operazioni si estendono a diversi microservizi
- ✅ Processi aziendali di lunga durata
- ✅ La coerenza eventuale è accettabile
- ✅ Devi evitare i blocchi distribuiti
- ✅ I servizi hanno database indipendenti
Evita quando:
- ❌ È richiesta una coerenza forte
- ❌ Le operazioni sono semplici e veloci
- ❌ Tutti i servizi condividono lo stesso database
- ❌ La logica di compensazione è troppo complessa
Conclusione
Il pattern Saga è essenziale per gestire le transazioni distribuite nell’architettura a microservizi. Sebbene introduca complessità, fornisce una soluzione pratica per mantenere la coerenza dei dati tra i confini dei servizi. Scegli l’orchestrazione per un controllo e una visibilità migliori, o la coreografia per scalabilità e accoppiamento debole. Assicurati sempre che le operazioni siano idempotenti, implementa una logica di compensazione corretta e mantieni un’osservabilità completa.
La chiave per un’implementazione riuscita del pattern Saga è comprendere i requisiti di coerenza, progettare con cura la logica di compensazione e scegliere l’approccio giusto per il tuo caso d’uso. Con un’implementazione corretta, il pattern Saga ti permette di costruire microservizi resilienti e scalabili che mantengono l’integrità dei dati nei sistemi distribuiti.
Link Utili
- Microservices Patterns by Chris Richardson
- Saga Pattern - Martin Fowler
- Eventuate Tram Saga Framework
- Temporal Workflow Engine
- AWS Step Functions Documentation
- Go Cheat Sheet
- Go Generics: Use Cases and Patterns
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Building CLI Applications in Go with Cobra & Viper
- Implementing Service Mesh with Istio and Linkerd
- Building Event-Driven Microservices with AWS Kinesis