TL;DR
Implementeremo un pattern Saga resiliente utilizzando gRPC per gestire transazioni distribuite tra microservizi. Copriremo le basi, ti mostreremo come configurarlo e includeremo anche alcuni esempi di codice interessanti. Alla fine, sarai in grado di orchestrare transazioni distribuite come un direttore d'orchestra che guida una sinfonia di microservizi.
La Saga della Saga: Una Breve Introduzione
Prima di addentrarci nei dettagli, facciamo un rapido riepilogo di cosa tratta il pattern Saga:
- Una saga è una sequenza di transazioni locali
- Ogni transazione aggiorna i dati all'interno di un singolo servizio
- Se un passaggio fallisce, vengono eseguite transazioni di compensazione per annullare le modifiche precedenti
Pensalo come un pulsante di annullamento sofisticato per il tuo sistema distribuito. Ora, vediamo come possiamo implementarlo usando gRPC.
Perché gRPC per le Sagas?
Potresti chiederti, "Perché gRPC? Non posso semplicemente usare REST?" Beh, potresti, ma gRPC offre alcuni vantaggi significativi:
- Serializzazione binaria efficiente (Protocol Buffers)
- Tipizzazione forte
- Streaming bidirezionale
- Supporto integrato per autenticazione, bilanciamento del carico e altro
Inoltre, è incredibilmente veloce. Chi non ama la velocità?
Preparare il Palcoscenico
Iniziamo definendo il nostro servizio in Protocol Buffers. Creeremo un semplice servizio OrderSaga:
syntax = "proto3";
package ordersaga;
service OrderSaga {
rpc StartSaga(SagaRequest) returns (SagaResponse) {}
rpc CompensateSaga(CompensationRequest) returns (CompensationResponse) {}
}
message SagaRequest {
string order_id = 1;
double amount = 2;
}
message SagaResponse {
bool success = 1;
string message = 2;
}
message CompensationRequest {
string order_id = 1;
}
message CompensationResponse {
bool success = 1;
string message = 2;
}
Questo imposta il nostro servizio di base con due RPC: uno per avviare la saga e un altro per la compensazione se qualcosa va storto.
Implementazione del Coordinatore della Saga
Ora, creiamo un Coordinatore della Saga che orchestrerà la nostra transazione distribuita. Useremo Go per questo esempio, ma sentiti libero di usare il linguaggio che preferisci.
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/your/proto"
)
type server struct {
pb.UnimplementedOrderSagaServer
}
func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
// Implementa la logica della saga qui
log.Printf("Inizio saga per ordine: %s", req.OrderId)
// Chiama altri microservizi per eseguire la transazione distribuita
if err := createOrder(req.OrderId); err != nil {
return &pb.SagaResponse{Success: false, Message: "Creazione ordine fallita"}, nil
}
if err := processPayment(req.OrderId, req.Amount); err != nil {
// Compensa per la creazione dell'ordine
cancelOrder(req.OrderId)
return &pb.SagaResponse{Success: false, Message: "Elaborazione pagamento fallita"}, nil
}
if err := updateInventory(req.OrderId); err != nil {
// Compensa per la creazione dell'ordine e il pagamento
cancelOrder(req.OrderId)
refundPayment(req.OrderId, req.Amount)
return &pb.SagaResponse{Success: false, Message: "Aggiornamento inventario fallito"}, nil
}
return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
}
func (s *server) CompensateSaga(ctx context.Context, req *pb.CompensationRequest) (*pb.CompensationResponse, error) {
// Implementa la logica di compensazione qui
log.Printf("Compensazione saga per ordine: %s", req.OrderId)
// Chiama i metodi di compensazione per ogni passaggio
cancelOrder(req.OrderId)
refundPayment(req.OrderId, 0) // Potresti voler memorizzare l'importo da qualche parte
restoreInventory(req.OrderId)
return &pb.CompensationResponse{Success: true, Message: "Compensazione completata"}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Ascolto fallito: %v", err)
}
s := grpc.NewServer()
pb.RegisterOrderSagaServer(s, &server{})
log.Println("Server in ascolto su :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("Servizio fallito: %v", err)
}
}
// Implementa queste funzioni per interagire con altri microservizi
func createOrder(orderId string) error { /* ... */ }
func processPayment(orderId string, amount float64) error { /* ... */ }
func updateInventory(orderId string) error { /* ... */ }
func cancelOrder(orderId string) error { /* ... */ }
func refundPayment(orderId string, amount float64) error { /* ... */ }
func restoreInventory(orderId string) error { /* ... */ }
Questa implementazione mostra la struttura di base del nostro Coordinatore della Saga. Gestisce la logica principale della transazione distribuita e fornisce meccanismi di compensazione se un passaggio fallisce.
Gestione dei Fallimenti e dei Tentativi
In un sistema distribuito, i fallimenti non sono solo possibili – sono inevitabili. Aggiungiamo un po' di resilienza alla nostra implementazione della Saga:
func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
maxRetries := 3
var err error
for i := 0; i < maxRetries; i++ {
err = s.executeSaga(ctx, req)
if err == nil {
return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
}
log.Printf("Tentativo %d fallito: %v. Riprovo...", i+1, err)
}
// Se abbiamo esaurito tutti i tentativi, compensa e restituisci errore
s.CompensateSaga(ctx, &pb.CompensationRequest{OrderId: req.OrderId})
return &pb.SagaResponse{Success: false, Message: "Saga fallita dopo diversi tentativi"}, err
}
func (s *server) executeSaga(ctx context.Context, req *pb.SagaRequest) error {
// Implementa qui la logica effettiva della saga
// ...
}
Questo meccanismo di ripetizione offre alla nostra Saga alcune possibilità di successo prima di arrendersi e avviare la compensazione.
Monitoraggio e Logging
Quando si gestiscono transazioni distribuite, la visibilità è fondamentale. Aggiungiamo un po' di logging e metriche al nostro Coordinatore della Saga:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
sagaSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
Name: "saga_success_total",
Help: "Il numero totale di saghe riuscite",
})
sagaFailureCounter = promauto.NewCounter(prometheus.CounterOpts{
Name: "saga_failure_total",
Help: "Il numero totale di saghe fallite",
})
)
func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
log.Printf("Inizio saga per ordine: %s", req.OrderId)
defer func(start time.Time) {
log.Printf("Saga per ordine %s completata in %v", req.OrderId, time.Since(start))
}(time.Now())
// ... (logica della saga)
if err != nil {
sagaFailureCounter.Inc()
log.Printf("Saga fallita per ordine %s: %v", req.OrderId, err)
return &pb.SagaResponse{Success: false, Message: "Saga fallita"}, err
}
sagaSuccessCounter.Inc()
return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
}
Queste metriche possono essere facilmente integrate con sistemi di monitoraggio come Prometheus per darti informazioni in tempo reale sulle prestazioni della tua Saga.
Testare la Tua Saga
Testare le transazioni distribuite può essere complicato, ma è cruciale. Ecco un semplice esempio di come potresti testare il tuo Coordinatore della Saga:
func TestStartSaga(t *testing.T) {
// Configura un server mock
s := &server{}
// Crea una richiesta di test
req := &pb.SagaRequest{
OrderId: "test-order-123",
Amount: 100.50,
}
// Chiama il metodo StartSaga
resp, err := s.StartSaga(context.Background(), req)
// Verifica i risultati
if err != nil {
t.Errorf("StartSaga ha restituito un errore: %v", err)
}
if !resp.Success {
t.Errorf("StartSaga fallita: %s", resp.Message)
}
}
Ricorda di testare anche gli scenari di fallimento e la logica di compensazione!
Conclusione
Ecco fatto! Abbiamo implementato un pattern Saga resiliente utilizzando gRPC per gestire transazioni distribuite. Ricapitoliamo ciò che abbiamo imparato:
- Il pattern Saga aiuta a gestire transazioni distribuite tra microservizi
- gRPC fornisce un modo efficiente e fortemente tipizzato per implementare le Sagas
- Una corretta gestione degli errori e dei tentativi è cruciale per la resilienza
- Il monitoraggio e il logging offrono visibilità sulle tue transazioni distribuite
- Il testing è impegnativo ma essenziale per Sagas affidabili
Ricorda, le transazioni distribuite sono complesse. Questa implementazione è un punto di partenza, e probabilmente dovrai adattarla al tuo caso d'uso specifico. Ma con queste conoscenze, sei ben avviato a domare il mostro delle transazioni distribuite.
Spunti di Riflessione
Prima di andare, ecco alcune domande su cui riflettere:
- Come gestiresti le Sagas a lungo termine che potrebbero superare i limiti di timeout di gRPC?
- Quali strategie potresti adottare per rendere il tuo Coordinatore della Saga stesso tollerante ai guasti?
- Come potresti integrare questo pattern Saga con architetture esistenti basate su eventi?
Buona programmazione, e che le tue transazioni siano sempre coerenti!