SAGA Pattern: Choreography vs Orchestration

Mikroservisler arasında birden çok servisi ilgilendiren işlemlerde veri tutarlılığını sağlamak için choreography ve orchestration tabanlı SAGA desenleri anlatılır.

SAGA Pattern: Choreography vs Orchestration

SAGA Pattern: Dağıtık İşlemlerde Tutarlılık Sağlamak

Bir mikroservis mimarisinde, bir işlem (ör. sipariş oluşturma) genellikle birden fazla servisi (Sipariş, Ödeme, Stok, Kargo) kapsar. Bu servislerin her birinin kendi veritabanı vardır. Tek bir veritabanında yaptığımız gibi ACID transaction (Atomik, Tutarlı, İzole, Dayanıklı) kullanamayız, çünkü farklı veritabanları arasında dağıtık transaction yapmak (iki fazlı commit - 2PC) performansı öldürür ve sistemleri bağımlı hale getirir.

SAGA Deseni, bu sorunu çözmek için ortaya çıkmıştır. Bir SAGA, her biri yerel bir transaction olan bir dizi adımdan (step) oluşur. Her adım, başarısız olursa, önceki adımları telafi eden (compensating) bir işlem tanımlar. SAGA, iki ana yaklaşımla uygulanır: Choreography (Koreografi) ve Orchestration (Orkestrasyon).


1. SAGA'nın Çalışma Mantığı: Telafi (Compensation)

SAGA'nın temel prensibi "önce yap, sonra telafi et" (forward recovery) veya "gerçekleşmeyen işlemi geri al" (backward recovery) şeklindedir.

Örnek Senaryo: Bir sipariş işlemi düşünelim:

  1. Sipariş Servisi: Siparişi "Beklemede" olarak kaydeder.

  2. Ödeme Servisi: Kredi kartından tutarı çeker.

  3. Stok Servisi: Ürünleri rezerve eder.

  4. Kargo Servisi: Kargo oluşturur.

Başarısızlık Senaryosu: Ödeme başarılı, stok rezervasyonu sırasında stok yetersiz çıktı. SAGA, bu durumda önceki adımları telafi etmelidir:

  • Telafi (Compensation): Ödeme servisine iade (refund) emri gönderilir. Sipariş "Başarısız" olarak işaretlenir.

Telafi edici işlem (compensating action), genellikle orijinal işlemin tam tersidir (ör. para iadesi, sipariş iptali).


2. Yaklaşım 1: Choreography (Koreografi) Tabanlı SAGA

Koreografi, merkezi bir yönetici olmadan, servislerin olaylar (events) aracılığıyla birbirleriyle haberleşmesidir. Her servis, kendi işini yapar, bir olay yayınlar, diğer servisler bu olayları dinler ve kendi adımlarını gerçekleştirir.

Akış Diyagramı:

text

Sipariş Servisi                Ödeme Servisi                Stok Servisi
     |                             |                             |
     |-- OrderCreated Event ------>|                             |
     |                             |-- PaymentProcessed Event --->|
     |                             |                             |-- StockReserved Event -->
     |<---- OrderConfirmed Event --|<----------------------------|

Uygulama (MassTransit veya Raw Event Bus ile):

csharp

// Sipariş Servisi: OrderCreated event'ini yayınlar
public class OrderService
{
    private readonly IEventBus _eventBus;

    public async Task CreateOrderAsync(CreateOrderCommand command)
    {
        // 1. Siparişi "Beklemede" olarak kaydet
        var order = new Order { Id = command.OrderId, Status = "Pending", ... };
        await _dbContext.Orders.AddAsync(order);
        await _dbContext.SaveChangesAsync();

        // 2. OrderCreated event'ini yayınla
        await _eventBus.PublishAsync(new OrderCreatedEvent(command.OrderId, command.Amount, command.Items));
    }
}

// Ödeme Servisi: OrderCreated event'ini dinler
public class PaymentHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event)
    {
        try
        {
            // 1. Ödeme işlemini yap
            await _paymentGateway.ChargeAsync(@event.Amount);
            
            // 2. PaymentProcessed event'ini yayınla
            await _eventBus.PublishAsync(new PaymentProcessedEvent(@event.OrderId));
        }
        catch (Exception ex)
        {
            // 3. Hata durumunda PaymentFailed event'ini yayınla
            await _eventBus.PublishAsync(new PaymentFailedEvent(@event.OrderId, ex.Message));
        }
    }
}

// Stok Servisi: PaymentProcessed event'ini dinler
public class StockHandler : IEventHandler<PaymentProcessedEvent>
{
    public async Task HandleAsync(PaymentProcessedEvent @event)
    {
        try
        {
            // 1. Stok rezervasyonu yap
            await _stockService.ReserveAsync(@event.OrderId);
            
            // 2. StockReserved event'ini yayınla
            await _eventBus.PublishAsync(new StockReservedEvent(@event.OrderId));
        }
        catch (Exception ex)
        {
            // Stok rezervasyonu başarısız -> telafi başlat!
            await _eventBus.PublishAsync(new StockReservationFailedEvent(@event.OrderId, ex.Message));
        }
    }
}

// İade (Compensation) Handler: PaymentFailed veya StockReservationFailed event'lerini dinler
public class CompensationHandler : IEventHandler<PaymentFailedEvent>, IEventHandler<StockReservationFailedEvent>
{
    public async Task HandleAsync(PaymentFailedEvent @event)
    {
        // Sipariş servisine iptal etmesi için sipariş iptal event'i gönder
        await _eventBus.PublishAsync(new OrderCancellationRequestedEvent(@event.OrderId, @event.Reason));
    }

    public async Task HandleAsync(StockReservationFailedEvent @event)
    {
        // Stok rezervasyonu başarısız oldu -> ödemeyi iade et
        await _paymentGateway.RefundAsync(@event.OrderId);
        // Siparişi iptal et
        await _eventBus.PublishAsync(new OrderCancellationRequestedEvent(@event.OrderId, @event.Reason));
    }
}

Artıları:

  • Gevşek Bağlantı (Loosely Coupled): Servisler birbirini tanımaz. Sadece event'leri dinler.

  • Esneklik: Yeni bir servis (ör. Analitik) eklemek kolaydır; sadece ilgili event'i dinler.

  • Hata Dayanıklılığı: Bir servis geçici olarak çalışmazsa, event'ler kuyrukta (broker) bekler.

Eksileri:

  • Karmaşıklık: Akışı takip etmek zordur (özellikle 10+ servis için). Olay zincirleri görselleştirilemez.

  • Döngüsel Bağımlılık Riskleri: Servisler arası döngüsel event'ler (A->B, B->A) oluşabilir.

  • Hata Yönetimi: Hata durumunda, telafi (compensation) zincirini doğru yönetmek zordur. Hangi adımın nerede başarısız olduğunu takip etmek zorlaşır.

Ne Zaman Kullanılır?

  • Servis sayısı azsa (2-5 servis).

  • Akış basit ve doğrusal (linear) ise.

  • Ekipler, servisler arası event yönetimine hakimse.


3. Yaklaşım 2: Orchestration (Orkestrasyon) Tabanlı SAGA

Orkestrasyonda, merkezi bir Orchestrator (Orkestratör) bulunur. Bu orchestrador, tüm adımları sırayla çağırır, başarısızlık durumunda telafi (compensation) adımlarını tetikler. Genellikle MassTransit, NServiceBus, Temporal, veya Azure Durable Functions gibi araçlar kullanılır.

Akış Diyagramı:

text

                +-------------------+
                |   Orchestrator    |
                +--------+----------+
                         |
                         v
        Sipariş <--- (1) ---> Sipariş Servisi
                         |
                         v
        Ödeme <--- (2) ---> Ödeme Servisi
                         |
                         v
        Stok <--- (3) ---> Stok Servisi
                         |
                         v
        Kargo <--- (4) ---> Kargo Servisi

Uygulama (MassTransit'in State Machine ile):

csharp

// MassTransit State Machine (Saga State Machine)
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
    public State Pending { get; set; }
    public State Completed { get; set; }
    public State Cancelled { get; set; }

    public Event<OrderCreatedEvent> OrderCreated { get; set; }
    public Event<PaymentProcessedEvent> PaymentProcessed { get; set; }
    public Event<PaymentFailedEvent> PaymentFailed { get; set; }
    public Event<StockReservedEvent> StockReserved { get; set; }
    public Event<StockReservationFailedEvent> StockReservationFailed { get; set; }

    public OrderSaga()
    {
        // Başlangıç durumu: Pending
        Initially(
            When(OrderCreated)
                .Then(context => { context.Instance.OrderId = context.Message.OrderId; })
                .Publish(context => new ProcessPaymentCommand(context.Message.OrderId, context.Message.Amount))
                .TransitionTo(Pending)
        );

        // Ödeme başarılı -> Stok rezervasyonu başlat
        During(Pending,
            When(PaymentProcessed)
                .Publish(context => new ReserveStockCommand(context.Message.OrderId))
                .TransitionTo(Pending)
        );

        // Stok rezervasyonu başarılı -> Sipariş tamamlandı
        During(Pending,
            When(StockReserved)
                .Publish(context => new OrderCompletedEvent(context.Message.OrderId))
                .Finalize() // Completed durumuna geç
        );

        // Hata Senaryoları: Ödeme Başarısız
        During(Pending,
            When(PaymentFailed)
                .Publish(context => new CancelOrderCommand(context.Message.OrderId, context.Message.Reason))
                .TransitionTo(Cancelled)
        );

        // Hata Senaryoları: Stok Rezervasyonu Başarısız -> Ödemeyi iade et
        During(Pending,
            When(StockReservationFailed)
                .Publish(context => new RefundPaymentCommand(context.Message.OrderId))
                .Publish(context => new CancelOrderCommand(context.Message.OrderId, context.Message.Reason))
                .TransitionTo(Cancelled)
        );
    }
}

Artıları:

  • Merkezi Kontrol: Tüm işlem akışı tek bir yerde (Orchestrator) tanımlıdır. İzleme ve loglama çok kolaydır.

  • Net Hata Yönetimi: Hangi adımda hata olduğu ve hangi telafinin yapılacağı açıkça bellidir.

  • Karmaşıklık Azalır: Servisler, orchestrator'dan gelen komutları (command) bekler; event'leri takip etmek zorunda kalmazlar.

  • Yeniden Deneme (Retry) ve Zaman Aşımı (Timeout) Desteği: Orchestrator, başarısız adımları yeniden deneyebilir veya zaman aşımına uğratabilir.

Eksileri:

  • Merkezi Tek Nokta (Single Point of Failure): Orchestrator çökerse, tüm işlemler durur. (MassTransit ve Durable Functions, state'i kalıcı hale getirerek bu riski azaltır.)

  • Bağlantı (Coupling): Servisler, orchestrator'un komutlarını bilmelidir. Servisler arası doğrudan bağımlılık yoktur, ancak orchestrator bir bağımlılık haline gelir.

  • Ek Yük: Orchestrator'ın yönetimi (state machine, persistence) ek bir maliyettir.

Ne Zaman Kullanılır?

  • Servis sayısı fazla ve akış karmaşıksa (5+ servis).

  • Hata yönetimi ve izlenebilirlik kritikse.

  • Orkestrasyon araçları (MassTransit, Azure Durable Functions) zaten kullanılıyorsa.


4. Choreography vs Orchestration: Karşılaştırma

Kriter Choreography Orchestration
Bağımlılık 🟢 Gevşek (Event based) 🟡 Orta (Orchestrator'a bağımlı)
Karmaşıklık Yönetimi 🔴 Yüksek (Akış dağınık) 🟢 Düşük (Merkezi kontrol)
Hata Yönetimi 🔴 Zor (Telafi zinciri karmaşık) 🟢 Kolay (Merkezi)
İzlenebilirlik (Traceability) 🔴 Zor (Event'ler dağınık) 🟢 Kolay (Merkezi log)
Esneklik (Yeni Servis Ekleme) 🟢 Kolay (Event dinler) 🟡 Orta (Orchestrator güncellenir)
Ölçeklenebilirlik 🟢 Yüksek (Dağıtık) 🟡 Orta (Orchestrator darboğaz olabilir)
Tek Nokta Arızası 🟢 Yok 🔴 Var (Orchestrator)
Uygun Senaryo Basit akış, az servis Karmaşık akış, çok servis

5. SAGA Uygularken Dikkat Edilmesi Gerekenler (Tuzaklar)

  1. Idempotency (Yinelenebilirlik): Aynı mesaj (event/command) birden fazla kez işlenebilir (ör. broker yeniden iletir). Servislerin idempotent olması şarttır. Yani aynı işlemi tekrarlayan çağrılar zararsız olmalıdır. (Örn. PaymentId ile ödeme kontrolü yapmak).

  2. Timeout ve Retry (Zaman Aşımı ve Yeniden Deneme): Bir servis yanıt vermezse ne olacak? Orchestrator veya choreography'de timeout ve retry mekanizmaları olmalıdır.

  3. Saga State Persistence (Durumun Kalıcılığı): Orchestrator (veya choreography'deki event'ler) durumunu (state) korumalıdır. MassTransit, Azure Durable Functions veya bir veritabanı (SQL) kullanarak durumu kalıcı hale getirin.

  4. Saga Context (Saga Bağlamı): Tüm adımlar arasında, işlemle ilgili verileri (örn. OrderId, PaymentId) taşıyacak bir bağlam (context) nesnesi tanımlayın.

  5. Monitoring ve Observability (İzlenebilirlik): SAGA işlemlerini izlemek çok zordur. Her adım için loglama ve distributed tracing (Jaeger, OpenTelemetry) entegrasyonu şarttır.

Sonuç:

SAGA, dağıtık işlemlerde veri tutarlılığını sağlamanın en etkili yoludur. Hangi yaklaşımı seçeceğiniz, servis sayınıza, ekibinize ve iş akışınızın karmaşıklığına bağlıdır.

  • Choreography (Koreografi): Servislerin birbirini 'konuşarak' anlaştığı, merkezi olmayan bir yapıdır. Daha gevşek bağlantılıdır ve küçük sistemler için idealdir.

  • Orchestration (Orkestrasyon): Bir orkestra şefi gibi merkezi bir yöneticinin tüm adımları kontrol ettiği yapıdır. Karmaşık, çok adımlı ve hata yönetiminin kritik olduğu sistemler için daha uygundur.

Unutmayın: İster choreography ister orchestration seçin, idempotency ve monitoring olmazsa olmazlardır. SAGA ile dağıtık sistemlerinizde ACID olmasa da, Eventual Consistency (Nihai Tutarlılık) sağlayarak iş akışlarınızı güvence altına alabilirsiniz.

Tüm yazılar