.NET MICROSERVICES MESSAGING

Veri Akademi8 dk okuma
Üç servis kutusu arasında akan mesaj zarflarını ve mor .NET aksanlı broker hub'ını gösteren mesajlaşma akışı

Sipariş servisi stok servisini doğrudan HTTP ile mi çağırmalı, yoksa bir mesaj bırakıp yoluna mı devam etmeli? .NET mikroservis mimarisinin en kritik kararlarından biri bu sorunun cevabında saklı. Senkron çağrılar yazımı kolaydır ama gizli bir bağımlılık ağı yaratır — biri yavaşladığında zincir kilitlenir. Asenkron mesajlaşma servisleri birbirinden ayırır; karşılığında mesaj sırası, idempotency, dead letter ve geri yayma gibi yeni türde sorunlar getirir.

Bu yazı .NET üzerinde mikroservisleri iletişime sokmak isteyen geliştiriciler için yazıldı. RabbitMQ ve Kafka tarafındaki temel farkı, MassTransit ile pratik bir yapıyı ve üretimde sık karşılaşılan tuzakları sıfırdan ele alacağız.

1. Mikroservis İletişiminde İki Yol

İki servis konuştuğunda iki temel desen vardır. Senkron iletişim bir HTTP veya gRPC çağrısıyla cevap bekler. Çağıran taraf cevap gelene kadar bloklanır. Asenkron mesajlaşma ise bir broker'a mesaj bırakır ve devam eder; mesajı bir tüketici sonra alır.

  • Senkron (REST, gRPC): Anlık cevap gerekiyorsa uygun. Sipariş özetini sorgulamak, kullanıcı profilini çekmek gibi okuma akışları.
  • Asenkron (queue, topic): Tetikleyici bir olay var ama cevap bekleyemiyorsan. "Sipariş oluştu" olayını duyan üç servis varsa hepsi paralel çalışır.
  • Senkron tehlikesi: Üç servis art arda çağrılırsa toplam gecikme katlanır; biri çökerse hepsi etkilenir. Mikroservislerin ana vaadi olan bağımsızlık zayıflar.
  • Asenkron tehlikesi: Mesajın gerçekten işlendiğinden, sırasının korunduğundan ve iki kere işlenmediğinden emin olmak ek altyapı ister.

Pratikte iyi bir mikroservis sistemi her iki yolu da kullanır. Komutlar genellikle senkron, olaylar asenkron işlenir. Kararı her bağlamda ayrıca vermek gerekir.

2. RabbitMQ ile Kafka — Farklı İki Felsefe

.NET dünyasında en sık karşılaşılan iki broker RabbitMQ ve Apache Kafka'dır. Aynı problemi farklı felsefelerle çözerler.

RabbitMQ "akıllı broker, sade tüketici" yaklaşımındadır. Mesajları kuyruklarda tutar, routing kurallarını broker'da çözer, mesaj tüketildiğinde siler. AMQP protokolü üzerinden çalışır. Klasik task queue, RPC, fan-out gibi senaryolarda hızlı kurulur. Tüketici sayısı arttıkça yatay ölçeklenmesi belirli bir noktadan sonra zorlaşır.

Kafka "aptal broker, akıllı tüketici" yaklaşımındadır. Mesajları bir log gibi diske yazar ve süresi dolana kadar saklar. Tüketici hangi offset'ten okuduğunu kendisi takip eder. Aynı topic'i farklı tüketici grupları farklı hızlarda okuyabilir. Olayı saatler sonra yeniden oynatabilirsin. Yüksek hacimli event stream, audit log, analytics pipeline için doğal seçim.

Sol tarafta dikey kuyruk metaforu, sağ tarafta partition'lara bölünmüş log silindiri ile RabbitMQ ve Kafka karşılaştırması

Seçim için pratik kılavuz: Mesaj işlendikten sonra unutulacaksa ve düşük gecikme önemliyse RabbitMQ. Olayı arşivlemek, geriye dönüp tekrar işlemek veya çok sayıda tüketici beslemek istiyorsan Kafka. Apache'nin Kafka resmi dokümantasyonu log tabanlı yaklaşımın iç işleyişini detaylıca anlatır.

3. MassTransit ile Pratik Uygulama

.NET tarafında broker'a doğrudan SDK ile bağlanmak yerine MassTransit kütüphanesi yaygın tercihtir. Hem RabbitMQ hem Azure Service Bus hem de Amazon SQS arkaplanını ortak bir API ile soyutlar. Tüketici tanımı, retry, scheduled message, saga gibi konuları çerçeve düzeyinde çözer.

Basit bir consumer şöyle görünür:

public record OrderCreated(Guid OrderId, decimal Total);

public class OrderCreatedConsumer : IConsumer<OrderCreated>
{
    private readonly IInventoryService _inventory;
    public OrderCreatedConsumer(IInventoryService inventory) => _inventory = inventory;

    public async Task Consume(ConsumeContext<OrderCreated> context)
    {
        await _inventory.ReserveAsync(context.Message.OrderId);
    }
}

Program tarafında MassTransit'i RabbitMQ ile bağlamak iki satır işidir:

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderCreatedConsumer>();
    x.UsingRabbitMq((ctx, cfg) =>
    {
        cfg.Host("rabbitmq://localhost", h => { h.Username("guest"); h.Password("guest"); });
        cfg.ConfigureEndpoints(ctx);
    });
});

Sipariş tarafı mesajı yayınlamak için IPublishEndpoint bağımlılığını alır ve Publish çağrısı yapar. Tüketici hangi kuyruktan dinleyeceğini biliyor, retry politikası, dead letter ve scheduled redelivery ayarları konfigürasyonda yapılır. .NET tarafında daha derin tarama için dotnet microservices eğitimi bu yapıların üretim senaryosunda nasıl kurulduğunu adım adım gösterir.

4. Event-Driven Pattern'ler

Asenkron mesajlaşmayı kurduktan sonra üzerine birkaç tekrarlı desen oturur. Bunları bilmek mimariyi gereksiz karmaşıklıktan korur.

Publish-Subscribe

Bir servis bir olay yayınlar; ilgilenen tüm servisler kendi tüketici endpoint'lerinde alır. "Sipariş oluştu" olayını stok, fatura ve bildirim servisleri eş zamanlı dinler. Yeni bir tüketici eklemek için sipariş servisinde hiçbir değişiklik gerekmez.

Outbox Pattern

Bir servis veritabanını günceller ve aynı işlemde bir olay yayınlamak ister. İki farklı sistem (DB + broker) olduğu için klasik transaction çalışmaz. Outbox deseninde olay, ana veriyle aynı transaction içinde bir outbox tablosuna yazılır. Ayrı bir worker bu tabloyu okuyup broker'a publish eder. Veritabanı yazımı başarısız olursa olay da yayınlanmaz. MassTransit bu deseni hazır destekler.

Saga (Süreç Yönetimi)

Birden fazla servisin koordine olması gereken uzun süreli akışlarda saga kullanılır. Sipariş → ödeme → stok → kargo akışında her adım bir mesajla tetiklenir; herhangi biri başarısız olursa kompansasyon mesajları geri akar. Saga state'i bir orchestrator'da veya choreography ile dağıtık şekilde tutulur.

5. Üretimde Karşılaşılan Tuzaklar

Mesajlaşma kurulduktan sonra üretimde en çok şu sorunlar baş gösterir:

  1. Idempotency eksikliği: Broker en az bir kez teslim garantisi verir, yani aynı mesaj iki kez işlenebilir. Tüketici aynı mesajı tekrar aldığında ikinci işlem yan etki yaratmamalı. Her olayın bir EventId'si olmalı, işlenmiş ID'ler tutulmalı.
  2. Mesaj sırası varsayımı: "Sipariş oluştu" mesajı "Sipariş iptal edildi" mesajından önce gelecek diye varsayma. Kafka'da partition garantisi vardır, RabbitMQ'da yalnızca tek tüketicili tek kuyrukta. Çok tüketicili senaryoda mantığı sıraya bağımlı kurma.
  3. Dead letter sessizliği: Bir mesaj N retry sonrası işlenemediğinde dead letter kuyruğuna düşer. DLQ izlenmiyorsa hatalar sessizce birikir. DLQ üzerinde alarm kurmak şart.
  4. Şişen tüketici: Tüketici yavaşladığında kuyruk birikir. Backlog'u izlemek ve consumer instance'ını otomatik ölçeklemek üretim için kritik.
  5. Distributed tracing eksikliği: Bir kullanıcı isteğinin beş servis arasında nasıl dolaştığını görmek istiyorsan correlation ID ve OpenTelemetry tracing baştan kurulmalı. Sonradan eklemek çok daha pahalıdır.

Mesaj broker'ı kurmak bir günlük iş; üretimde sağlıklı işletmek aylar süren bir disiplindir. .NET tarafında MassTransit veya NServiceBus gibi olgun kütüphaneler bu disiplinin önemli kısmını çerçeve düzeyinde sağlar — onları görmezden gelip her şeyi düz SDK ile yazmak çoğu ekip için sürdürülemez bir maliyettir.

Sıkça Sorulan Sorular

Mikroservislerde her zaman asenkron mesajlaşma mı kullanılmalı?

Hayır. Kullanıcının cevap beklediği okuma akışlarında senkron HTTP veya gRPC daha doğrudur. Asenkron mesajlaşma bir tetikleyici olay olduğunda, çağıranın cevap beklemediği durumlarda anlamlıdır. İyi sistemler komutları senkron, olayları asenkron işler.

RabbitMQ mı Kafka mı seçmeliyim?

Mesaj işlenip unutulacaksa ve düşük gecikme önemliyse RabbitMQ daha pratiktir. Olayı saklamak, geriye dönüp tekrar oynatmak veya çok sayıda bağımsız tüketiciye dağıtmak istiyorsan Kafka uygun. İkili kullanan ekipler de vardır — komut tarafı RabbitMQ, event stream tarafı Kafka.

MassTransit yerine doğrudan RabbitMQ.Client kullanmak mantıklı mı?

Tek bir servis veya çok özel düşük seviye kontrol gerekiyorsa olabilir. Üretim sistemi kuruyorsan retry, dead letter, scheduled message, saga, outbox gibi konuları çerçeve seviyesinde çözmek mantıklıdır. MassTransit bunları hazır verir; düz SDK ile her birini sıfırdan yazmak haftalar sürer.

Idempotent consumer'ı nasıl yazarım?

Her olayın benzersiz bir EventId'si olmalı. Tüketici işlemeden önce bu ID'yi bir tabloya ya da Redis cache'e yazıyor mu kontrol eder. Yazılıysa atlar; değilse işler ve sonra ID'yi kaydeder. İşleme + ID kayıt aynı transaction içinde olmalı ki tutarlılık bozulmasın.

Outbox pattern her servise gerekli mi?

Olay yayınlayan her servis için tavsiye edilir. Veritabanı yazımı ile broker publish arasında atomicity yoksa biri başarılı diğeri başarısız olduğunda sistem tutarsız hale gelir. Outbox bu yarış durumunu ortadan kaldırır. Sadece tüketen servislerde gerek yoktur.

Saga ile dağıtık transaction aynı şey mi?

Hayır. Klasik dağıtık transaction (two-phase commit) tüm katılımcıların aynı anda commit etmesini bekler ve mikroservislerde önerilmez. Saga ise lokal transaction'ların zinciridir; herhangi biri başarısız olursa daha önce yapılanları geri alan kompansasyon mesajları çalıştırılır. Sonuç tutarlılığı (eventual consistency) sağlar.

Mesaj kaybını nasıl önlerim?

Yayıncı tarafında publisher confirms veya idempotent producer açılmalı. Mesaj kalıcı (durable) işaretlenmeli ve broker disk fsync politikası bilinçli seçilmeli. Tüketici tarafında manuel ack kullanılmalı — mesajı işledikten SONRA ack atılmalı. Dead letter kuyruğu izlenmeli ki sessiz kayıplar yakalansın.