Le pattern Saga dans les transactions distribuées - Avec des exemples en Go
Transactions dans les microservices avec le modèle Saga
Le motif Saga offre une solution élégante en découpant les transactions distribuées en une série de transactions locales avec des actions de compensation.
Plutôt que de s’appuyer sur des verrous distribués qui peuvent bloquer les opérations entre services, Saga permet d’atteindre une cohérence finale (eventual consistency) grâce à une séquence d’étapes réversibles, ce qui le rend idéal pour les processus métier de longue durée.
Dans les architectures microservices, le maintien de la cohérence des données entre les services est l’un des problèmes les plus difficiles. Les transactions ACID traditionnelles ne fonctionnent pas lorsque les opérations s’étendent sur plusieurs services avec des bases de données indépendantes, laissant les développeurs à la recherche d’approches alternatives pour garantir l’intégrité des données.
Ce guide démontre l’implémentation du motif Saga en Go avec des exemples pratiques couvrant les approches d’orchestration et de chorégraphie. Si vous avez besoin d’une référence rapide pour les fondamentaux de Go, la Fiche de référence Go fournit un aperçu utile.
Cette belle image est générée par le modèle IA Flux 1 dev.
Comprendre le motif Saga
Le motif Saga a été décrit à l’origine par Hector Garcia-Molina et Kenneth Salem en 1987. Dans le contexte des microservices, il s’agit d’une séquence de transactions locales où chaque transaction met à jour les données au sein d’un seul service. Si une étape échoue, des transactions de compensation sont exécutées pour annuler les effets des étapes précédentes.
Contrairement aux transactions distribuées traditionnelles qui utilisent le commit en deux phases (2PC), Saga ne maintient pas de verrous entre les services, ce qui le rend adapté aux processus métier de longue durée. Le compromis est une cohérence finale plutôt qu’une cohérence forte.
Caractéristiques principales
- Pas de verrous distribués : Chaque service gère sa propre transaction locale
- Actions de compensation : Chaque opération possède un mécanisme d’annulation correspondant
- Cohérence finale : Le système atteint éventuellement un état cohérent
- Longue durée : Adapté aux processus qui prennent des secondes, des minutes ou même des heures
Approches d’implémentation de Saga
Il existe deux approches principales pour implémenter le motif Saga : l’orchestration et la chorégraphie.
Motif d’Orchestration
Dans l’orchestration, un coordonateur central (orchestrateur) gère le flux de transaction entier. L’orchestrateur est responsable de :
- L’invoquer des services dans le bon ordre
- La gestion des échecs et le déclenchement des compensations
- Le maintien de l’état du saga
- La coordination des reprises et des délais d’attente (timeouts)
Avantages :
- Contrôle centralisé et visibilité
- Plus facile à comprendre et à déboguer
- Meilleure gestion des erreurs et récupération
- Test plus simple du flux global
Inconvénients :
- Point unique de défaillance (bien que cela puisse être atténué)
- Service supplémentaire à maintenir
- Peut devenir un goulot d’étranglement pour les flux complexes
Exemple en Go :
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Étape 1 : Créer la commande
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Étape 2 : Réserver le stock
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Compensation
return err
}
// Étape 3 : Traiter le paiement
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Compensation
o.orderService.Cancel(orderID) // Compensation
return err
}
// Étape 4 : Créer l'expédition
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Compensation
o.inventoryService.Release(order.Items) // Compensation
o.orderService.Cancel(orderID) // Compensation
return err
}
return nil
}
Motif de Chorégraphie
Dans la chorégraphie, il n’y a pas de coordonnateur central. Chaque service sait quoi faire et communique par le biais d’événements. Les services écoutent les événements et réagissent en conséquence. Cette approche orientée événements est particulièrement puissante lorsqu’elle est combinée avec des plateformes de streaming de messages comme AWS Kinesis, qui fournissent une infrastructure évoluable pour la distribution d’événements entre microservices. Pour un guide complet sur l’implémentation de microservices orientés événements avec Kinesis, consultez Construire des Microservices Orientés Événements avec AWS Kinesis.
Avantages :
- Décentralisé et évoluable
- Aucun point unique de défaillance
- Les services restent faiblement couplés
- Adapté naturellement aux architectures orientées événements
Inconvénients :
- Plus difficile à comprendre le flux global
- Difficile à déboguer et tracer
- Gestion des erreurs complexe
- Risque de dépendances cycliques
Exemple avec Architecture Orientée Événements :
// Service Commande
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
}
// Service Paiement
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 : rembourser le paiement
return s.client.Refund(event.PaymentID)
}
Stratégies de Compensation
La compensation est le cœur du motif Saga. Chaque opération doit avoir une compensation correspondante capable d’inverser ses effets.
Types de Compensation
-
Opérations Réversibles : Opérations qui peuvent être directement annulées
- Exemple : Libérer le stock réservé, rembourser les paiements
-
Actions de Compensation : Opérations différentes qui atteignent l’effet inverse
- Exemple : Annuler une commande au lieu de la supprimer
-
Compensation Pessimiste : Pré-allouer des ressources qui peuvent être libérées
- Exemple : Réserver le stock avant de charger le paiement
-
Compensation Optimiste : Exécuter des opérations et compenser si nécessaire
- Exemple : Charger le paiement en premier, rembourser si le stock est indisponible
Exigences d’Idempotence
Toutes les opérations et compensations doivent être idempotentes. Cela garantit que la reprise d’une opération échouée ne provoque pas d’effets dupliqués.
func (s *PaymentService) Refund(paymentID string) error {
// Vérifier si déjà remboursé
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Déjà remboursé, idempotent
}
// Traiter le remboursement
return s.processRefund(paymentID)
}
Bonnes Pratiques
1. Gestion de l’État du Saga
Maintenir l’état de chaque instance de saga pour suivre la progression et permettre la récupération. Lors de la persistance de l’état du saga dans une base de données, le choix du bon ORM est crucial pour les performances et la maintenabilité. Pour les implémentations basées sur PostgreSQL, consultez la comparaison dans Comparaison des ORM Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc pour sélectionner le meilleur outil pour vos besoins de stockage d’état de 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. Gestion des Délais d’Attente (Timeouts)
Implémenter des délais d’attente pour chaque étape afin d’empêcher les sagas de rester bloqués indéfiniment :
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():
// Délai dépassé, compenser
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. Logique de Reprise
Implémenter une attente exponentielle (backoff) pour les pannes transitoires :
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 pour l’État du Saga
Utiliser l’event sourcing pour maintenir une piste d’audit complète. Lors de l’implémentation des magasins d’événements et des mécanismes de relecture, les génériques Go peuvent aider à créer un code de gestion d’événements sûr et réutilisable. Pour des modèles avancés utilisant les génériques en Go, consultez Génériques Go : Cas d’Utilisation et Modèles.
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. Surveillance et Observabilité
Implémenter une journalisation et un traçage complets :
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")
// ... exécution du saga
return nil
}
Modèles Courants et Anti-Modèles
Modèles à Suivre
- Modèle de Coordonnateur Saga : Utiliser un service dédié pour l’orchestration
- Modèle Outbox : Assurer une publication d’événements fiable
- Clés d’Idempotence : Utiliser des clés uniques pour toutes les opérations
- Machine à États Saga : Modéliser le saga comme une machine à états
Anti-Modèles à Éviter
- Compensation Synchronisée : Ne pas attendre la fin de la compensation
- Sagas Imbriqués : Éviter que les sagas appellent d’autres sagas (utilisez des sous-sagas à la place)
- État Partagé : Ne pas partager l’état entre les étapes du saga
- Étapes de Longue Durée : Découper les étapes qui prennent trop de temps
Outils et Frameworks
Plusieurs frameworks peuvent aider à implémenter les motifs Saga :
- Temporal : Plateforme d’orchestration de workflows avec support Saga intégré
- Zeebe : Moteur de workflow pour l’orchestration de microservices
- Eventuate Tram : Framework Saga pour Spring Boot
- AWS Step Functions : Orchestration de workflows serverless
- Apache Camel : Framework d’intégration avec support Saga
Pour les services orchestrateurs nécessitant des interfaces CLI pour la gestion et la surveillance, Construire des Applications CLI en Go avec Cobra & Viper fournit d’excellents modèles pour créer des outils en ligne de commande pour interagir avec les orchestrateurs de saga.
Lors du déploiement de microservices basés sur Saga dans Kubernetes, l’implémentation d’un maillage de services (service mesh) peut améliorer considérablement l’observabilité, la sécurité et la gestion du trafic. Implémentation de Service Mesh avec Istio et Linkerd explique comment les service meshes complètent les motifs de transactions distribuées en fournissant des préoccupations transversales telles que le traçage distribué et le circuit breaking.
Quand Utiliser le Motif Saga
Utilisez le motif Saga lorsque :
- ✅ Les opérations s’étendent sur plusieurs microservices
- ✅ Processus métier de longue durée
- ✅ La cohérence finale est acceptable
- ✅ Vous devez éviter les verrous distribués
- ✅ Les services ont des bases de données indépendantes
À éviter lorsque :
- ❌ Une cohérence forte est requise
- ❌ Les opérations sont simples et rapides
- ❌ Tous les services partagent la même base de données
- ❌ La logique de compensation est trop complexe
Conclusion
Le motif Saga est essentiel pour gérer les transactions distribuées dans les architectures microservices. Bien qu’il introduise de la complexité, il offre une solution pratique pour maintenir la cohérence des données à travers les limites des services. Choisissez l’orchestration pour un meilleur contrôle et une meilleure visibilité, ou la chorégraphie pour l’évolutivité et le couplage lâche. Assurez-vous toujours que les opérations sont idempotentes, implémentez une logique de compensation appropriée et maintenez une observabilité complète.
La clé d’une implémentation Saga réussie est de comprendre vos exigences de cohérence, de concevoir soigneusement la logique de compensation et de choisir la bonne approche pour votre cas d’utilisation. Avec une implémentation appropriée, Saga vous permet de construire des microservices résilients et évolutifs qui maintiennent l’intégrité des données à travers les systèmes distribués.
Liens Utiles
- Microservices Patterns par Chris Richardson
- Saga Pattern - Martin Fowler
- Framework Saga Eventuate Tram
- Moteur de Workflow Temporal
- Documentation AWS Step Functions
- Fiche de référence Go
- Génériques Go : Cas d’Utilisation et Modèles
- Comparaison des ORM Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc
- Implémentation de CQRS en Go
- Construire des Applications CLI en Go avec Cobra & Viper
- Implémentation de Service Mesh avec Istio et Linkerd
- Construire des Microservices Orientés Événements avec AWS Kinesis