Mikroservis Mimarisi & Dağıtık Veri Yönetimi 9 dk okuma 3 okunma

Microservices'te Veri Tutarlılığı: Sagas ve Outbox

Mikroservislerde her servisin kendi veritabanına sahip olması ACID garantisini imkansız kılar. Saga Pattern, dağıtık işlemleri telafi edici adımlarla yönetir. Outbox Pattern ise veritabanı işlemi ile event yayınını atomik hale getirir. Bu iki desen birlikte "effectively-once" semantiği sağlar.

Microservices'te Veri Tutarlılığı: Sagas ve Outbox

Microservices'te Veri Tutarlılığı: Sagas ve Outbox

Mikroservis mimarisinin en büyük zorluklarından biri, dağıtık veri tutarlılığıdır. Her servisin kendi veritabanına sahip olduğu bir dünyada, tek bir iş mantığı işlemi (ör. sipariş oluşturma) birden fazla servisi ve birden fazla veritabanını kapsar. Geleneksel iki fazlı commit (2PC) protokolü, performans ve ölçeklenebilirlik açısından mikroservisler için uygun değildir. İşte bu noktada iki tamamlayıcı desen devreye girer: Saga Pattern ve Outbox Pattern. Bu yazıda, bu iki deseni, nasıl birlikte çalıştıklarını ve .NET'te nasıl uygulandıklarını detaylıca ele alacağız.


1. Neden Dağıtık İşlemler Zor?

Klasik monolitik uygulamada, bir işlem (transaction) tek bir veritabanında çalışır ve ACID garantisi ile korunur:

text

BEGIN TRANSACTION;
  INSERT INTO Orders ...;
  UPDATE Inventory SET Quantity = Quantity - 1 ...;
  INSERT INTO Payments ...;
COMMIT;

Herhangi bir adım başarısız olursa, tüm işlem geri alınır (rollback). Veri tutarlılığı garanti edilir.

Mikroservislerde ise:

text

Order Service → Sipariş veritabanı
Inventory Service → Stok veritabanı
Payment Service → Ödeme veritabanı

Her servis kendi veritabanına sahiptir. İki fazlı commit (2PC) kullanmak teorik olarak mümkündür ancak:

  • Performans cezası: Tüm katılımcılar hazır olana kadar kilitlenir.

  • Ölçeklenebilirlik sınırı: Katılımcı sayısı arttıkça başarısızlık olasılığı artar.

  • Koordinatör tek nokta arızası: Koordinatör çökerse sistem kilitlenir.

  • Modern veritabanları 2PC'yi desteklemez: MongoDB, Cassandra gibi sistemler 2PC'yi desteklemez.

Çözüm: Distributed transaction yerine eventual consistency (nihai tutarlılık) ve Saga Pattern.


2. Saga Pattern Nedir?

Saga, bir dağıtık işlemi, her biri yerel bir transaction olan bir dizi adıma böler. Her adım, başarısız olursa telafi edici bir işlem (compensating action) tanımlar. Saga, ya tüm adımlar başarıyla tamamlanır (forward recovery) ya da önceki adımlar telafi edilir (backward recovery).

Örnek Saga: Sipariş İşlemi

text

1. Order Service: Sipariş oluştur (PENDING)
   ↓
2. Payment Service: Ödemeyi çek
   ↓
3. Inventory Service: Stok rezerve et
   ↓
4. Shipment Service: Kargo oluştur
   ↓
5. Order Service: Siparişi CONFIRMED yap

Başarısızlık Senaryosu: 3. adımda stok yetersiz çıkarsa:

text

Telafi (Compensation):
← 3. Inventory: (zaten başarısız, telafi gerekmez)
← 2. Payment: Ödemeyi iade et (refund)
← 1. Order: Siparişi CANCELLED yap

Kritik Kural: Her adımın bir telafi edici işlemi olmalıdır. Telafi, orijinal işlemin tam tersidir (para çekildiyse iade, stok düşüldüyse geri ekle).


3. Saga Uygulama Yaklaşımları

A. Choreography (Koreografi) - Merkezi Yönetim Yok

Her servis kendi işini yapar, bir event yayınlar, diğer servisler bu event'i dinler ve kendi adımlarını gerçekleştirir.

Akış:

text

Order Service: OrderCreated event yayınlar
    ↓
Payment Service: OrderCreated dinler → ödeme yapar → PaymentProcessed event yayınlar
    ↓
Inventory Service: PaymentProcessed dinler → stok rezerve eder → StockReserved event yayınlar
    ↓
Shipment Service: StockReserved dinler → kargo oluşturur → ShipmentCreated event yayınlar
    ↓
Order Service: ShipmentCreated dinler → siparişi CONFIRMED yapar

Başarısızlık Durumu:

text

Inventory Service: StockReservationFailed event yayınlar
    ↓
Payment Service: StockReservationFailed dinler → ödemeyi iade eder
    ↓
Order Service: StockReservationFailed dinler → siparişi CANCELLED yapar

Avantajlar:

  • Gevşek bağlantı (loosely coupled): Servisler birbirini tanımaz.

  • Basit, küçük saga'lar için ideal.

Dezavantajlar:

  • Akışı takip etmek zordur (10+ servis için).

  • Döngüsel bağımlılık riski.

  • Merkezi bir görünüm yoktur.

B. Orchestration (Orkestrasyon) - Merkezi Yönetici

Bir Orchestrator (Saga Orchestrator) servisi, tüm adımları sırayla çağırır ve başarısızlık durumunda telafi adımlarını tetikler.

Akış:

text

Saga Orchestrator:
1. OrderService.CreateOrder() çağır
2. PaymentService.ProcessPayment() çağır
3. InventoryService.ReserveStock() çağır
4. ShipmentService.CreateShipment() çağır
5. OrderService.ConfirmOrder() çağır

Başarısızlık Durumu (3. adım):

text

Saga Orchestrator:
← PaymentService.RefundPayment() çağır (telafi)
← OrderService.CancelOrder() çağır (telafi)

Avantajlar:

  • Merkezi kontrol: Tüm akış tek bir yerde tanımlı.

  • İzleme ve loglama kolay.

  • Karmaşık saga'lar için ideal.

Dezavantajlar:

  • Orchestrator tek nokta arızası olabilir (MassTransit state machine ile kalıcı hale getirilir).

  • Servisler orchestrator'ı tanımak zorunda.


4. Outbox Pattern Nedir?

Saga'ların en kritik sorunu şudur: Bir servis, hem veritabanını güncelleyip hem de event yayınlamak zorundadır. Ancak bu iki işlem atomik değildir:

  • Veritabanı başarılı, event başarısız: Diğer servisler durumdan haberdar olmaz.

  • Event başarılı, veritabanı başarısız: Gerçekleşmemiş bir işlem için event yayınlanır.

Outbox Pattern, bu sorunu çözer: Event'i doğrudan mesaj kuyruğuna göndermek yerine, veritabanı işleminin bir parçası olarak bir Outbox tablosuna kaydedin. Aynı transaction içinde hem iş verisi hem de event yazılır.

Outbox Tablosu:

sql

CREATE TABLE OutboxMessages (
    Id UNIQUEIDENTIFIER PRIMARY KEY,
    Type NVARCHAR(255) NOT NULL,          -- Event tipi (OrderCreated)
    Content NVARCHAR(MAX) NOT NULL,        -- Event verisi (JSON)
    OccurredOnUtc DATETIME2 NOT NULL,      -- Event zamanı
    ProcessedOnUtc DATETIME2 NULL,         -- İşlendiği zaman
    Error NVARCHAR(MAX) NULL,              -- Hata mesajı
    RetryCount INT NOT NULL DEFAULT 0      -- Yeniden deneme sayısı
);

Çalışma Prensibi:

  1. İşlemi Yap + Outbox'a Kaydet (Aynı Transaction):

csharp

using var transaction = await _context.Database.BeginTransactionAsync();

// 1. İş verisini kaydet
var order = new Order { Id = orderId, Status = "Pending" };
_context.Orders.Add(order);

// 2. Outbox'a event'i kaydet (aynı transaction)
var outboxMessage = new OutboxMessage
{
    Id = Guid.NewGuid(),
    Type = "OrderCreated",
    Content = JsonSerializer.Serialize(new OrderCreatedEvent(orderId)),
    OccurredOnUtc = DateTime.UtcNow
};
_context.OutboxMessages.Add(outboxMessage);

// 3. Tek transaction'da kaydet
await _context.SaveChangesAsync();
await transaction.CommitAsync();
  1. Arka Plan İşlemi (Outbox Processor): Outbox tablosundaki işlenmemiş mesajları okur, mesaj kuyruğuna gönderir ve işlendi olarak işaretler.

csharp

public class OutboxProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var publisher = scope.ServiceProvider.GetRequiredService<IMessagePublisher>();

            var messages = await context.OutboxMessages
                .Where(m => m.ProcessedOnUtc == null)
                .OrderBy(m => m.OccurredOnUtc)
                .Take(50)
                .ToListAsync(stoppingToken);

            foreach (var message in messages)
            {
                try
                {
                    await publisher.PublishAsync(message.Type, message.Content, stoppingToken);
                    message.ProcessedOnUtc = DateTime.UtcNow;
                }
                catch (Exception ex)
                {
                    message.RetryCount++;
                    message.Error = ex.Message;
                }
            }

            await context.SaveChangesAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
}

Outbox Pattern'ın Avantajları:

  • Atomiklik: İş verisi ve event aynı transaction'da kaydedilir.

  • Güvenilirlik: Mesaj kuyruğu kapalı olsa bile event kaybolmaz.

  • Idempotency: Aynı event birden fazla kez gönderilse bile, consumer idempotent ise zararsızdır.


5. Sagas ve Outbox Birlikte Nasıl Çalışır?

Saga'lar ve Outbox Pattern, birbirini tamamlayan iki desendir:

Desen Görevi
Saga Dağıtık işlemi adımlara böler ve telafi mantığını yönetir.
Outbox Her adımda veritabanı + event yayınını atomik hale getirir.

Birlikte Akış:

text

Order Service:
1. Sipariş oluştur (PENDING) + Outbox'a OrderCreated yaz (tek transaction)
   ↓
Outbox Processor → OrderCreated event'ini Kafka/RabbitMQ'ya gönder
   ↓
Payment Service:
2. OrderCreated dinle → Ödeme yap + Outbox'a PaymentProcessed yaz (tek transaction)
   ↓
Outbox Processor → PaymentProcessed event'ini yayınla
   ↓
Inventory Service:
3. PaymentProcessed dinle → Stok rezerve et + Outbox'a StockReserved yaz
   ↓
... ve böyle devam eder

Bu yapıda her adım atomiktir; hem iş verisi hem de event ya birlikte kaydedilir ya da birlikte geri alınır.


6. MassTransit ile Saga ve Outbox Uygulaması

MassTransit, .NET ekosisteminde hem Saga (State Machine) hem de Outbox Pattern'ı native olarak destekler.

A. MassTransit State Machine ile Saga

csharp

public class OrderState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }
    public Guid OrderId { get; set; }
    public bool PaymentProcessed { get; set; }
    public bool StockReserved { get; set; }
}

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public State Submitted { get; set; }
    public State PaymentProcessing { get; set; }
    public State StockProcessing { get; set; }
    public State Completed { get; set; }
    public State Cancelled { get; set; }

    public Event<OrderSubmittedEvent> OrderSubmitted { get; set; }
    public Event<PaymentProcessedEvent> PaymentProcessed { get; set; }
    public Event<StockReservedEvent> StockReserved { get; set; }
    public Event<StockReservationFailedEvent> StockReservationFailed { get; set; }

    public OrderStateMachine()
    {
        Initially(
            When(OrderSubmitted)
                .Then(context => context.Instance.OrderId = context.Message.OrderId)
                .Publish(context => new ProcessPaymentCommand(context.Instance.OrderId))
                .TransitionTo(Submitted)
        );

        During(Submitted,
            When(PaymentProcessed)
                .Then(context => context.Instance.PaymentProcessed = true)
                .Publish(context => new ReserveStockCommand(context.Instance.OrderId))
                .TransitionTo(PaymentProcessing)
        );

        During(PaymentProcessing,
            When(StockReserved)
                .Then(context => context.Instance.StockReserved = true)
                .Publish(context => new OrderCompletedEvent(context.Instance.OrderId))
                .Finalize()
        );

        // Telafi: Stok rezervasyonu başarısız → ödemeyi iade et
        During(PaymentProcessing,
            When(StockReservationFailed)
                .Publish(context => new RefundPaymentCommand(context.Instance.OrderId))
                .Publish(context => new CancelOrderCommand(context.Instance.OrderId))
                .TransitionTo(Cancelled)
        );
    }
}

B. MassTransit Outbox Konfigürasyonu (Entity Framework Core)

csharp

// Program.cs
builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderConsumer>();
    x.AddSagaStateMachine<OrderStateMachine, OrderState>()
        .EntityFrameworkRepository(r =>
        {
            r.ExistingDbContext<AppDbContext>();
            r.UsePostgres();
        });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("localhost", "/", h => { ... });

        // Outbox Pattern'ı etkinleştir
        cfg.UseEntityFrameworkOutbox<AppDbContext>(context);

        cfg.ConfigureEndpoints(context);
    });
});

// DbContext konfigürasyonu
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.AddInboxStateEntity();      // Inbox tablosu
    modelBuilder.AddOutboxMessageEntity();   // Outbox mesaj tablosu
    modelBuilder.AddOutboxStateEntity();     // Outbox durum tablosu
}

MassTransit, Outbox mesajlarını otomatik olarak yönetir; iş verisi ile event aynı transaction'da kaydedilir.


7. Inbox Pattern: Consumer Tarafında Idempotency

Outbox Pattern, gönderen tarafında atomikliği sağlar. Alan tarafında (consumer) ise Inbox Pattern kullanılır: Her mesajın ID'si bir Inbox tablosunda saklanır ve aynı mesaj tekrar geldiğinde işlenmez.

sql

CREATE TABLE InboxMessages (
    MessageId UNIQUEIDENTIFIER PRIMARY KEY,
    ConsumerType NVARCHAR(255) NOT NULL,
    ProcessedOnUtc DATETIME2 NOT NULL
);

MassTransit, AddInboxStateEntity() ile Inbox Pattern'ı da destekler. Böylece:

  • Outbox: Gönderen tarafında atomiklik.

  • Inbox: Alan tarafında idempotency.

İkisi birlikte "etkili olarak tam olarak bir kez" (effectively-once) semantiği sağlar.


8. Saga ve Outbox En İyi Pratikleri

Pratik Açıklama
Her Adım İçin Telafi Tanımlayın Saga'daki her adımın bir compensating action'ı olmalı.
Idempotency Sağlayın Hem Outbox hem Inbox pattern ile mesaj tekrarlarına karşı dayanıklı olun.
Saga State'ini Kalıcı Hale Getirin Orchestration saga'ları için EF Core, MongoDB veya NHibernate repository kullanın.
Outbox Processor'ı İzleyin İşlenmemiş mesaj sayısını ve retry oranlarını izleyin.
Dead Letter Queue Kullanın Sürekli başarısız olan Outbox mesajlarını DLQ'ya yönlendirin.
Timeout Belirleyin Saga adımlarında timeout kullanın; sonsuza kadar beklemeyin.
Saga Tasarımını Basit Tutun Çok fazla adım içeren saga'ları daha küçük saga'lara bölün.
Monitoring ve Tracing Distributed tracing ile saga akışını izleyin.
Test Edin Saga'ların başarı ve başarısızlık senaryolarını entegrasyon testleri ile doğrulayın.

9. Choreography vs. Orchestration: Ne Zaman Hangisi?

Kriter Choreography Orchestration
Servis Sayısı Az (2-5) Çok (5+)
Akış Karmaşıklığı Basit, doğrusal Karmaşık, dallanan
Merkezi Kontrol Yok Var
İzlenebilirlik Zor Kolay
Bağlantı Gevşek Orchestrator'a bağımlı
Hata Yönetimi Dağınık Merkezi
Kullanım Alanı Basit event zincirleri Karmaşık iş akışları

Öneri: Küçük ve basit saga'lar için choreography, karmaşık ve uzun süren saga'lar için orchestration tercih edin. MassTransit State Machine, orchestration için ideal bir araçtır.


10. Gerçek Dünya Senaryosu: E-Ticaret Sipariş Süreci

Senaryo: Bir e-ticaret uygulamasında, sipariş oluşturma 4 servisi kapsıyor: Order, Payment, Inventory ve Shipment.

Saga Akışı:

text

1. Order Service: Sipariş oluştur (PENDING) + Outbox'a OrderCreated yaz
2. Payment Service: OrderCreated dinle → Ödeme yap + Outbox'a PaymentProcessed yaz
3. Inventory Service: PaymentProcessed dinle → Stok rezerve et + Outbox'a StockReserved yaz
4. Shipment Service: StockReserved dinle → Kargo oluştur + Outbox'a ShipmentCreated yaz
5. Order Service: ShipmentCreated dinle → Siparişi CONFIRMED yap

Başarısızlık Senaryosu: 3. adımda stok yetersiz.

text

3. Inventory Service: StockReservationFailed yayınla
   ↓
2. Payment Service: StockReservationFailed dinle → RefundPayment + Outbox'a PaymentRefunded yaz
   ↓
1. Order Service: StockReservationFailed dinle → Siparişi CANCELLED yap

Sonuç:

  • Order Service: Sipariş CANCELLED.

  • Payment Service: Ödeme iade edildi.

  • Inventory Service: Stok değişmedi.

  • Shipment Service: Hiç tetiklenmedi.

Tüm servisler eventual consistency içinde tutarlı hale gelir.


Sonuç

Mikroservis mimarisinde veri tutarlılığı, ACID transaction'larla değil, eventual consistency ve telafi edici işlemlerle sağlanır. Saga Pattern, dağıtık işlemleri adımlara böler ve başarısızlık durumunda telafi mantığını yönetir. Outbox Pattern, her adımda veritabanı işlemi ile event yayınını atomik hale getirir. Inbox Pattern ise consumer tarafında idempotency sağlar. MassTransit, .NET ekosisteminde hem State Machine ile Saga'ları hem de Outbox/Inbox pattern'larını native olarak destekler. Bu iki desen birlikte kullanıldığında, mikroservisleriniz "etkili olarak tam olarak bir kez" (effectively-once) semantiği ile güvenilir bir şekilde çalışır. Unutmayın: Dağıtık sistemlerde tutarlılık, bir "an" değil, bir "yolculuk"tur; önemli olan, her adımın güvenilir ve telafi edilebilir olmasıdır.

Tüm yazılar Soru ya da yorumun mu var? Yaz