.NET MICROSERVICES MESSAGING

Üç 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.

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.

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.

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.

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.

Ü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.