Saga patroon in gedistribueerde transacties - Met voorbeelden in Go

Transacties in Microservices met het Saga patroon

Inhoud

De Saga patroon biedt een elegante oplossing door gedistribueerde transacties te verdelen in een reeks lokale transacties met compenserende acties.

In plaats van te vertrouwen op gedistribueerde vergrendelingen die bewerkingen over diensten kunnen blokkeren, maakt Saga uiteindelijk consistentie mogelijk via een reeks omkeerbare stappen, waardoor het ideaal is voor lange lopende zakelijke processen.

In microservices-architecturen is het behouden van gegevensconsistentie over diensten een van de meest uitdagende problemen. Traditionele ACID-transacties werken niet wanneer bewerkingen meerdere diensten met onafhankelijke databases betreffen, waardoor ontwikkelaars op zoek zijn naar alternatieve aanpakken om gegevensintegriteit te waarborgen.

Deze gids demonstreert de implementatie van het Saga patroon in Go met praktische voorbeelden die zowel de orchestratie- als de choreografiebenadering behandelen. Als je een snelle verwijzing nodig hebt voor Go-fundamenten, biedt de Go Cheat Sheet een nuttige overzicht.

bouwvakker met gedistribueerde transacties Deze mooie afbeelding is gegenereerd door AI model Flux 1 dev.

Het begrijpen van het Saga patroon

Het Saga patroon werd oorspronkelijk beschreven door Hector Garcia-Molina en Kenneth Salem in 1987. In de context van microservices is het een reeks lokale transacties waarbij elke transactie gegevens binnen één dienst bijwerkt. Als een stap mislukt, worden compenserende transacties uitgevoerd om de effecten van vorige stappen ongedaan te maken.

In tegenstelling tot traditionele gedistribueerde transacties die gebruikmaken van tweefasencommit (2PC), houdt Saga geen vergrendelingen over diensten, waardoor het geschikt is voor lange lopende zakelijke processen. Het compromis is uiteindelijke consistentie in plaats van sterke consistentie.

Belangrijke kenmerken

  • Geen gedistribueerde vergrendelingen: Elke dienst beheert zijn eigen lokale transactie
  • Compenserende acties: Elke bewerking heeft een overeenkomstige terugdraai-mechanisme
  • Uiteindelijke consistentie: Het systeem bereikt uiteindelijk een consistente toestand
  • Lange lopende: Geschikt voor processen die seconden, minuten of zelfs uren duren

Implementatiebenaderingen van het Saga patroon

Er zijn twee primaire benaderingen voor het implementeren van het Saga patroon: orchestratie en choreografie.

Orchestratiepatroon

Bij orchestratie beheert een centrale coördinator (orchestrator) de hele transactiestroom. De orchestrator is verantwoordelijk voor:

  • Het aanroepen van diensten in de juiste volgorde
  • Het afhandelen van fouten en het activeren van compensaties
  • Het behouden van de staat van de saga
  • Het coördineren van herproeven en time-outs

Voordelen:

  • Centrale controle en zichtbaarheid
  • Eenvoudiger te begrijpen en debuggen
  • Betere foutafhandeling en herstel
  • Eenvoudiger testen van de gehele stroom

Nadelen:

  • Enkel punt van falen (hoewel dit kan worden geminderd)
  • Extra dienst om te onderhouden
  • Kan een knelpunt worden voor complexe stromen

Voorbeeld in Go:

type OrderSagaOrchestrator struct {
    orderService    OrderService
    paymentService  PaymentService
    inventoryService InventoryService
    shippingService ShippingService
}

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    sagaID := generateSagaID()
    
    // Stap 1: Bestelling maken
    orderID, err := o.orderService.Create(order)
    if err != nil {
        return err
    }
    
    // Stap 2: Voorraad reserveren
    if err := o.inventoryService.Reserve(order.Items); err != nil {
        o.orderService.Cancel(orderID) // Compenseren
        return err
    }
    
    // Stap 3: Betaling verwerken
    paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
    if err != nil {
        o.inventoryService.Release(order.Items) // Compenseren
        o.orderService.Cancel(orderID)          // Compenseren
        return err
    }
    
    // Stap 4: Verzending maken
    if err := o.shippingService.CreateShipment(orderID); err != nil {
        o.paymentService.Refund(paymentID)      // Compenseren
        o.inventoryService.Release(order.Items) // Compenseren
        o.orderService.Cancel(orderID)          // Compenseren
        return err
    }
    
    return nil
}

Choreografiepatroon

Bij choreografie is er geen centrale coördinator. Elke dienst weet wat hij moet doen en communiceert via gebeurtenissen. Diensten luisteren naar gebeurtenissen en reageren daarop. Deze gebeurtenisgestuurde aanpak is vooral krachtig wanneer gecombineerd met message streaming platforms zoals AWS Kinesis, die schaalbare infrastructuur bieden voor gebeurtenisverdeling over microservices. Voor een uitgebreide gids over het implementeren van gebeurtenisgestuurde microservices met Kinesis, zie Het bouwen van gebeurtenisgestuurde microservices met AWS Kinesis.

Voordelen:

  • Decentraliseerd en schaalbaar
  • Geen enkel punt van falen
  • Diensten blijven los gekoppeld
  • Natuurlijke fit voor gebeurtenisgestuurde architecturen

Nadelen:

  • Moeilijker om de gehele stroom te begrijpen
  • Moeilijk om te debuggen en te traceren
  • Complexe foutafhandeling
  • Risico op cyclische afhankelijkheden

Voorbeeld met gebeurtenisgestuurde architectuur:

// 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) // Compensatie
}

// 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 {
    // Compensatie: betaling terugbetalen
    return s.client.Refund(event.PaymentID)
}

Compensatiestrategieën

Compensatie is het hart van het Saga patroon. Elke bewerking moet een overeenkomstige compensatie hebben die de effecten ervan kan ongedaan maken.

Typen compensatie

  1. Omkeerbare bewerkingen: Bewerkingen die direct ongedaan kunnen worden gemaakt

    • Voorbeeld: Voorraad vrijgeven, betalingen terugbetalen
  2. Compenserende acties: Verschillende bewerkingen die het omgekeerde effect bereiken

    • Voorbeeld: Een bestelling annuleren in plaats van te verwijderen
  3. Pessimistische compensatie: Vooraf toewijzen van middelen die vrijgegeven kunnen worden

    • Voorbeeld: Voorraad reserveren voordat betaling wordt verwerkt
  4. Optimistische compensatie: Bewerkingen uitvoeren en compenseren indien nodig

    • Voorbeeld: Betaling eerst verwerken, terugbetalen indien voorraad niet beschikbaar is

Idempotentie vereisten

Alle bewerkingen en compensaties moeten idempotent zijn. Dit zorgt ervoor dat het herproberen van een gefaalde bewerking geen dubbele effecten veroorzaakt.

func (s *PaymentService) Refund(paymentID string) error {
    // Controleer of al terugbetaald
    payment, err := s.getPayment(paymentID)
    if err != nil {
        return err
    }
    
    if payment.Status == "refunded" {
        return nil // Al terugbetaald, idempotent
    }
    
    // Verwerk terugbetaling
    return s.processRefund(paymentID)
}

Beste praktijken

1. Saga-staatbeheer

Houd de staat van elk Saga-exemplaar bij om de voortgang te volgen en herstel mogelijk te maken. Bij het opslaan van Saga-staat in een database is het kiezen van de juiste ORM cruciaal voor prestaties en onderhoudbaarheid. Voor implementaties op basis van PostgreSQL, overweeg dan de vergelijking in Vergelijking van Go ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc om de beste keuze te maken voor je Saga-staatopslagbehoeften:

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. Time-outbehandeling

Implementeer time-outs voor elke stap om te voorkomen dat Sagas oneindig lang blijven hangen:

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():
        // Time-out is opgetreden, compenseer
        if err := step.Compensate(); err != nil {
            return fmt.Errorf("compensatie is mislukt: %w", err)
        }
        return fmt.Errorf("stap %s is na %v time-out gegaan", step.Name(), o.timeout)
    }
}

3. Herproeflogica

Implementeer exponentiële backoff voor tijdelijke fouten:

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("bewerking is mislukt na %d herproeven", maxRetries)
}

4. Event sourcing voor Saga-staat

Gebruik event sourcing om een volledige audittrail te behouden. Bij het implementeren van eventopslag en herplaatsingsmechanismen kunnen Go-generieken helpen bij het maken van typesafe, herbruikbare eventverwerkingscode. Voor geavanceerde patronen met generieken in Go, zie Go-generieken: Gebruikscases en patronen.

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("maken van payload is mislukt: %w", err)
    }
    
    version, err := s.store.GetNextVersion(sagaID)
    if err != nil {
        return fmt.Errorf("maken van versie is mislukt: %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("maken van gebeurtenissen is mislukt: %w", err)
    }
    
    saga := NewSaga()
    for _, event := range events {
        if err := saga.Apply(event); err != nil {
            return nil, fmt.Errorf("toepassen van gebeurtenis is mislukt: %w", err)
        }
    }
    
    return saga, nil
}

5. Monitoring en observabiliteit

Implementeer uitgebreide logboekregistratie en tracing:

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 is gestart")
    
    // ... saga uitvoering
    
    return nil
}

Algemene patronen en anti-patronen

Patronen om te volgen

  • Saga-coördinatorpatroon: Gebruik een toegewezen dienst voor orchestratie
  • Outboxpatroon: Zorg voor betrouwbare gebeurtenisuitzending
  • Idempotente sleutels: Gebruik unieke sleutels voor alle bewerkingen
  • Saga-staatmachine: Model de saga als een staatmachine

Anti-patronen om te vermijden

  • Synchronische compensatie: Wacht niet op het voltooien van compensatie
  • Geneste Sagas: Vermijd dat Sagas andere Sagas aanroepen (gebruik sub-Sagas in plaats daarvan)
  • Gedeelde staat: Deel geen staat tussen Saga-stappen
  • Lange lopende stappen: Breng stappen die te lang duren op in kleinere stappen

Tools en frameworks

Verschillende frameworks kunnen helpen bij het implementeren van het Saga patroon:

  • Temporal: Workflow-orchestratieplatform met ingebouwde Saga-ondersteuning
  • Zeebe: Workflow-engine voor microservices-orchestratie
  • Eventuate Tram: Saga-framework voor Spring Boot
  • AWS Step Functions: Serverloze workflow-orchestratie
  • Apache Camel: Integratieframework met Saga-ondersteuning

Voor orchestrator-diensten die CLI-interfaces nodig hebben voor beheer en monitoring, biedt Het bouwen van CLI-toepassingen in Go met Cobra & Viper uitstekende patronen voor het maken van command-line-tools om te interageren met Saga-orchestrators.

Bij het implementeren van Saga-gebaseerde microservices in Kubernetes, kan het implementeren van een service mesh aanzienlijk de observabiliteit, beveiliging en verkeerbeheer verbeteren. Het implementeren van een service mesh met Istio en Linkerd bespreekt hoe service meshes gedistribueerde transactiepatronen aanvullen door cross-cutting zorgen zoals gedistribueerde tracing en circuit breaking te bieden.

Wanneer het Saga patroon te gebruiken is

Gebruik het Saga patroon wanneer:

  • ✅ Bewerkingen meerdere microservices betreffen
  • ✅ Lange lopende zakelijke processen
  • ✅ Uiteindelijke consistentie is aanvaardbaar
  • ✅ Je wilt vermijden dat gedistribueerde vergrendelingen worden gebruikt
  • ✅ Diensten onafhankelijke databases hebben

Vermijd wanneer:

  • ❌ Sterke consistentie vereist is
  • ❌ Bewerkingen eenvoudig en snel zijn
  • ❌ Alle diensten dezelfde database delen
  • ❌ Compensatielogica te complex is

Conclusie

Het Saga patroon is essentieel voor het beheren van gedistribueerde transacties in microservices-architecturen. Hoewel het complexiteit introduceert, biedt het een praktische oplossing voor het behouden van gegevensconsistentie over servicegrenzen. Kies voor orchestratie voor betere controle en zichtbaarheid, of voor choreografie voor schaalbaarheid en los gekoppeldheid. Zorg altijd dat bewerkingen idempotent zijn, implementeer correcte compensatielogica en behoud uitgebreide observabiliteit.

Het sleutel tot een succesvolle Saga-implementatie is het begrijpen van je consistentie-eisen, zorgvuldig ontwerpen van compensatielogica en het kiezen van de juiste aanpak voor je gebruikscase. Met een correcte implementatie maakt Saga het mogelijk om resiliënte, schaalbare microservices te bouwen die gegevensintegriteit behouden in gedistribueerde systemen.