Outbox Pattern ile Eventual Consistency: Microservices'te Güvenilir Event İletişimi
Dağıtık sistemlerin (microservices) en büyük zorluklarından biri, bir servisin kendi veritabanını güncellerken aynı anda diğer servislere bir olay (event) bildirmesidir. Bu iki işlemin (veritabanı yazma + mesaj gönderme) aynı anda başarılı olmasını garanti etmek, klasik yaklaşımlarda neredeyse imkansızdır. İşte Outbox Pattern (Giden Kutusu Deseni), bu sorunu zarifçe çözen ve Event-Driven Architecture (EDA) ile Eventual Consistency'nin temel taşlarından biridir.
1. İkili Yazma (Dual-Write) Problemi
Bir servisin bir işlemi (örneğin sipariş oluşturma) gerçekleştirdiğini düşünelim:
-
Veritabanına kaydeder.
-
Diğer servislere haber vermek için bir mesaj gönderir (ör. RabbitMQ, Kafka).
Bu iki işlem arasında yaşanabilecek sorunlar şunlardır:
-
Veritabanı başarılı, mesaj başarısız: Mesaj gönderilemezse, diğer servisler bu işlemden haberdar olmaz. Sistem tutarsız hale gelir.
-
Mesaj başarılı, veritabanı başarısız: Veritabanı işlemi geri alınırsa, diğer servislere gerçekleşmemiş bir işlem için haber gider. Bu daha da kötü bir durumdur.
Bu durum, dağıtık sistemlerdeki "ikili yazma" (dual-write) problemi olarak bilinir ve izlenmesi ve düzeltilmesi çok zor olan sessiz tutarsızlıklara yol açar.
2. Outbox Pattern ile Çözüm: Aynı İşlem, Farklı Kader
Outbox Pattern, bu iki ayrı işlemi tek bir atomik işleme dönüştürür. Temel mantığı şudur:
Mesajı doğrudan mesaj kuyruğuna göndermek yerine, veritabanı işleminin bir parçası olarak bir "Outbox" (giden kutusu) tablosuna kaydedin.
Böylece:
-
Atomiklik: Veritabanı güncellemesi ve Outbox'a kayıt aynı transaction içinde gerçekleşir. İkisi de başarılı olur veya ikisi de başarısız olur.
-
Güvenilirlik: Mesaj, başarılı bir şekilde veritabanına kaydedilmiştir. Artık mesaj kuyruğu kapalı olsa veya bir hata olsa bile mesaj kaybolmaz.
sql
-- Aynı transaction içinde
BEGIN TRANSACTION;
-- 1. İş verilerini güncelle (Örn: Sipariş oluştur)
INSERT INTO Orders (...) VALUES (...);
-- 2. Gönderilecek mesajları Outbox tablosuna kaydet
INSERT INTO OutboxMessages (Id, Type, Content, OccurredOnUtc)
VALUES (NEWID(), 'OrderCreated', '{"OrderId": 123}', GETUTCDATE());
COMMIT;
3. Outbox Pattern Mimarisi
graph LR
A[İstemci] --> B(API / Servis);
B --> C[Veritabanı];
C --> D[Outbox Tablosu];
B -.->|Doğrudan göndermez| E[Message Broker];
subgraph Arka Plan İşlemi
F[Outbox İşlemcisi] --> D;
F -->|Mesajları gönder| E;
F -->|Başarılı olanları işaretle| D;
end
E --> G[Diğer Servisler];
-
Servis: İş mantığını çalıştırır ve aynı transaction içinde veritabanını ve Outbox tablosunu günceller.
-
Outbox Tablosu: Gönderilmeyi bekleyen mesajların saklandığı geçici bir alandır.
-
Outbox İşlemcisi (Relay): Arkada çalışan bir servis veya background job'dur.
-
Outbox tablosunu belirli aralıklarla kontrol eder.
-
İşlenmemiş mesajları alır ve mesaj kuyruğuna (broker) gönderir.
-
Mesaj başarıyla gönderildiğinde, Outbox'taki ilgili kaydı "işlendi" olarak işaretler.
-
4. .NET Core ile Outbox Pattern Uygulaması
Adım 1: Outbox Tablosunun Oluşturulması (Entity)
csharp
public class OutboxMessage
{
public Guid Id { get; set; } // Benzersiz mesaj ID'si
public string Type { get; set; } // Event tipi (örn. "OrderCreatedEvent")
public string Content { get; set; } // JSON formatında event verisi
public DateTime OccurredOnUtc { get; set; } // Event'in oluşma zamanı
public DateTime? ProcessedOnUtc { get; set; } // İşlendiği zaman (null ise işlenmemiştir)
public string? Error { get; set; } // Hata mesajı
public int RetryCount { get; set; } // Yeniden deneme sayısı
}
Bu yapı, .NET ekosisteminde sıkça kullanılan bir şablondur.
Adım 2: Servis Katmanında Kullanım (EF Core ile)
csharp
public class OrderService
{
private readonly AppDbContext _context;
private readonly ILogger<OrderService> _logger;
public OrderService(AppDbContext context, ILogger<OrderService> logger)
{
_context = context;
_logger = logger;
}
public async Task CreateOrderAsync(CreateOrderCommand command)
{
// 1. İş mantığını çalıştır ve yeni siparişi oluştur
var order = new Order { CustomerName = command.CustomerName };
_context.Orders.Add(order);
// 2. Outbox'a gönderilecek event'i kaydet
var orderCreatedEvent = new OrderCreatedEvent(order.Id, order.CustomerName);
var outboxMessage = new OutboxMessage
{
Id = Guid.NewGuid(),
Type = orderCreatedEvent.GetType().FullName!,
Content = JsonSerializer.Serialize(orderCreatedEvent),
OccurredOnUtc = DateTime.UtcNow
};
_context.OutboxMessages.Add(outboxMessage);
// 3. Değişiklikleri TEK BİR TRANSACTION olarak kaydet
await _context.SaveChangesAsync();
_logger.LogInformation("Sipariş oluşturuldu ve Outbox'a kaydedildi. OrderId: {OrderId}", order.Id);
}
}
Bu yaklaşım, olayların atomik olarak kalıcı hale getirilmesini sağlar.
Adım 3: Outbox İşlemcisi (BackgroundService ile)
csharp
public class ProcessOutboxMessagesJob : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ProcessOutboxMessagesJob> _logger;
public ProcessOutboxMessagesJob(IServiceScopeFactory scopeFactory, ILogger<ProcessOutboxMessagesJob> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessMessagesAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // 10 saniyede bir kontrol
}
}
private async Task ProcessMessagesAsync(CancellationToken stoppingToken)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var publisher = scope.ServiceProvider.GetRequiredService<IMessagePublisher>(); // RabbitMQ/Kafka
// İşlenmemiş mesajları getir (ProcessedOnUtc == null)
var messages = await context.OutboxMessages
.Where(m => m.ProcessedOnUtc == null)
.OrderBy(m => m.OccurredOnUtc)
.Take(50) // Toplu işlem için sınırlama
.ToListAsync(stoppingToken);
foreach (var message in messages)
{
try
{
// Mesajı mesaj kuyruğuna gönder
await publisher.PublishAsync(message.Type, message.Content, stoppingToken);
// Başarılıysa, ProcessedOnUtc alanını güncelle
message.ProcessedOnUtc = DateTime.UtcNow;
_logger.LogInformation("Outbox mesajı gönderildi. Id: {MessageId}", message.Id);
}
catch (Exception ex)
{
// Hata durumunda, yeniden deneme sayısını artır ve hatayı kaydet
message.RetryCount++;
message.Error = ex.Message;
_logger.LogError(ex, "Outbox mesajı gönderilemedi. Id: {MessageId}", message.Id);
// Hata oluşsa bile döngü devam eder ve diğer mesajlar işlenir.
}
}
// Tüm değişiklikleri (ProcessedOnUtc, RetryCount, Error) kaydet
await context.SaveChangesAsync(stoppingToken);
}
}
Bu yapı, güvenilir ve yeniden denenebilir bir teslimat mekanizması sağlar.
Adım 4: Servisin Başlatılması (Program.cs)
csharp
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<AppDbContext>(options => ...); builder.Services.AddScoped<OrderService>(); builder.Services.AddSingleton<IMessagePublisher, RabbitMqPublisher>(); // Message Broker builder.Services.AddHostedService<ProcessOutboxMessagesJob>(); // Outbox işlemcisini arka plana ekle
5. Önemli Hususlar ve Zorluklar
-
At-Least-Once (En Az Bir Kez) Teslimat: Outbox Pattern, mesajların en az bir kez gönderilmesini garanti eder. Ancak bu, mesajın birden fazla kez gönderilebileceği anlamına da gelir (örneğin, mesaj gönderildi ama
ProcessedOnUtcgüncellenemedi). Bu nedenle, mesajları tüketen servislerin Idempotent (Aynı işlemin tekrarı zararsız) olması çok önemlidir. -
Gecikme (Latency): Mesajlar doğrudan değil, periyodik bir işlemle gönderildiği için hafif bir gecikme oluşur.
-
Operasyonel Yük: Outbox tablosunun izlenmesi, yeniden deneme mekanizmaları ve olası tıkanıklıklar için yeni bir operasyonel yük getirir.
-
Sıralama (Ordering): Mesajların sırası, özellikle yatay ölçeklendirme yapıldığında karmaşık bir hal alabilir. Çoğu durumda, mesajların aggregate bazında sıralanması yeterlidir.
6. Hazır Kütüphane ve Araçlar
Tüm bu yapıyı sıfırdan yazmak yerine, .NET ekosisteminde bu işi sizin için yapan kütüphaneler mevcuttur:
-
MassTransit: Popüler bir dağıtık uygulama çerçevesidir. Entity Framework Core ile entegre Transactional Outbox desteği sunar.
-
CAP (DotNetCore.CAP): ThoughtWorks Technology Radar'da yer alan, Outbox desenini uygulayan bir .NET kütüphanesidir. RabbitMQ, Kafka, Azure Service Bus gibi birçok mesajlaşma platformunu destekler.
-
Brighter: Komut ve olay işleme için kullanılan bir başka kütüphanedir ve Outbox desteği sunar.
7. Sonuç
Outbox Pattern, mikroservis mimarilerinde Eventual Consistency (Nihai Tutarlılık) sağlamanın en güvenilir ve kanıtlanmış yollarından biridir. Veritabanı işlemleri ile mesajlaşmayı ayırarak, dağıtık sistemlerin en büyük kabusu olan veri tutarsızlığını ve mesaj kaybını ortadan kaldırır.
Ancak bu desenin getirdiği ek operasyonel yükü (gecikme, yönetim) ve tüketici servislerde Idempotency (aynı işlemin tekrarının zararsız olması) zorunluluğunu göz ardı etmemek gerekir. Eğer dağıtık bir sistem kuruyorsanız, Outbox Pattern, sağlam temeller üzerine inşa etmenizi sağlayacak olmazsa olmaz desenlerden biridir.