RabbitMQ ile Olay Tabanlı Mimari Rehberi

## Olay Tabanlı Mimarinin Sınırlarını Netleştirmek Olay tabanlı mimari (event-driven architecture), bir bileşenin durum değişikliğini "olay" olarak y...

RabbitMQ ile Olay Tabanlı Mimari Rehberi

Olay Tabanlı Mimarinin Sınırlarını Netleştirmek

Olay tabanlı mimari (event-driven architecture), bir bileşenin durum değişikliğini "olay" olarak yayınladığı, diğer bileşenlerin bu olaylara abone olarak tepki verdiği bir tasarım yaklaşımıdır. Bu mimaride üretici (producer) ile tüketici (consumer) birbirini doğrudan tanımaz; aracı bir mesaj altyapısı üzerinden dolaylı olarak haberleşir.

Bu dolaylılık iki temel kazanç sağlar: bileşenler arası bağımlılık azalır ve sistem yatay olarak ölçeklenebilir. Ancak aynı dolaylılık beraberinde zorluklar da getirir: hata ayıklama dağıtık hale gelir, olay sırası garanti edilmez ve "tam olarak bir kez" teslim gibi görünen garantiler çoğu zaman uygulama katmanında tamamlanmayı gerektirir.

RabbitMQ, AMQP protokolünü konuşan bir mesaj aracısıdır (message broker). Olayları kuyruklara yönlendirmek için exchange, binding ve queue kavramlarını kullanır. Bu rehber, orta seviye bir geliştiricinin RabbitMQ ile çalışırken karşılaşacağı temel kararları ve tuzakları ele alır.

Önkoşullar ve Kapsam

Bu rehberdeki kavramlar RabbitMQ'nun AMQP 0-9-1 modeline dayanır ve RabbitMQ 3.x ile 4.x sürümlerinde geçerlidir. Sürüme özgü davranış farkları ayrıca belirtilmediği sürece örnekler bu iki ana sürümde de aynı şekilde çalışır.

Kod örnekleri Node.js ve amqplib kütüphanesi üzerinden verilmiştir. amqplib API'si kararlıdır; ancak kütüphanenin belirli bir sürüm numarasını burada varsaymıyoruz. Örnekleri kendi ortamınızda çalıştırmadan üretime taşımayın.

Beklenen ön bilgi: temel HTTP/istemci-sunucu modeli, asenkron programlama ve kuyruk (queue) kavramı.

Exchange Türleri ve Yönlendirme Mantığı

RabbitMQ'da üretici mesajı doğrudan kuyruğa göndermez; bir exchange'e gönderir. Exchange, binding kurallarına göre mesajı bir veya birden fazla kuyruğa yönlendirir. Dört yaygın exchange türü vardır:

  • direct: Yönlendirme anahtarı (routing key) birebir eşleşirse mesaj kuyruğa gider.
  • fanout: Binding kuralını yok sayar; bağlı tüm kuyruklara kopya gönderir.
  • topic: Joker karakterli (* tek kelime, # sıfır veya daha fazla kelime) desen eşleşmesi yapar.
  • headers: Yönlendirme anahtarı yerine mesaj başlıklarına göre eşleştirir.

Seçim, olayın kaç tüketiciye ve hangi kriterle ulaşması gerektiğine bağlıdır. Tek bir işlem hattına olay gönderiyorsanız direct yeterlidir. Aynı olayı birden fazla bağımsız servise dağıtıyorsanız fanout veya topic daha uygundur.

Dayanıklı Kuyruk ve Mesaj Kurulumu

Varsayılan ayarlarla bir kuyruk veya mesaj, aracı yeniden başladığında kaybolabilir. Dayanıklılık için üç şeyin birlikte yapılması gerekir: kuyruğu durable tanımlamak, mesajı persistent göndermek ve yayıncı onaylarını (publisher confirms) kullanmak.

const amqp = require('amqplib');

async function kur() {
  const conn = await amqp.connect('amqp://localhost');
  const ch = await conn.createChannel();

  // Kuyruk dayanıklı: broker yeniden başlarsa kuyruk tanımı korunur
  await ch.assertQueue('siparis.olaylari', { durable: true });

  // Mesajı kalıcı olarak gönder
  const gonderildi = ch.sendToQueue(
    'siparis.olaylari',
    Buffer.from(JSON.stringify({ tip: 'siparis.olusturuldu', id: 42 })),
    { persistent: true }
  );

  // Yayıncı onayı: broker mesajı diske yazdığında true döner
  await ch.waitForConfirms();
  console.log('Mesaj onaylandı:', gonderildi);

  await ch.close();
  await conn.close();
}

kur().catch(console.error);

Beklenen davranış: waitForConfirms çözüldüğünde broker mesajı kabul etmiştir. Kuyruk durable ve mesaj persistent değilse, broker çökmesi durumunda mesaj kaybolabilir. Üçünün birlikte kullanılması gerekir; yalnızca persistent bayrağı tek başına yeterli değildir.

Tüketici Tarafı: Manuel Onay ve Yeniden Teslim

Tüketici tarafında en sık yapılan hata, mesajı işleme almadan otomatik onay (auto-ack) kullanmaktır. İşlem sırasında bir hata olursa mesaj kaybolur. Bunun yerine manuel onay tercih edilir: işlem başarıyla tamamlandığında ack, başarısız olduğunda nack gönderilir.

async function tuket() {
  const conn = await amqp.connect('amqp://localhost');
  const ch = await conn.createChannel();
  await ch.assertQueue('siparis.olaylari', { durable: true });

  // prefetch: aynı anda işlenecek mesaj sayısını sınırlar
  await ch.prefetch(1);

  ch.consume('siparis.olaylari', async (msg) => {
    if (!msg) return;
    try {
      const veri = JSON.parse(msg.content.toString());
      await siparisIsle(veri);
      ch.ack(msg); // başarılı: mesaj kuyruktan silinir
    } catch (err) {
      // requeue=false: mesaj hemen geri kuyruğa konmaz, dead-letter'a gidebilir
      ch.nack(msg, false, false);
    }
  }, { noAck: false }); // manuel onay
}

noAck: false manuel onayı etkinleştirir. prefetch(1) tüketicinin aynı anda yalnızca bir mesajı işlemesini sağlar; bu, yavaş işlemlerde yükü adil dağıtır. nack çağrısındaki üçüncü parametre requeue'dur. Sonsuz döngüye giren bir mesajı sürekli yeniden kuyruğa koymak yerine dead-letter exchange'e yönlendirmek daha güvenlidir.

Dead-Letter Exchange ile Hatalı Mesajları Ayırmak

İşlenemeyen mesajları ayrı bir kuyruğa yönlendirmek, hem hata ayıklamayı kolaylaştırır hem de ana kuyruğun tıkanmasını önler. Bunun için kuyruk tanımında x-dead-letter-exchange argümanı verilir.

await ch.assertExchange('dlx', 'direct', { durable: true });
await ch.assertQueue('siparis.dlq', { durable: true });
await ch.bindQueue('siparis.dlq', 'dlx', 'siparis.hata');

await ch.assertQueue('siparis.olaylari', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx',
    'x-dead-letter-routing-key': 'siparis.hata'
  }
});

Bir mesaj nack ile requeue=false olarak reddedildiğinde veya kuyruk mesaj sınırını aştığında, mesaj dlx exchange üzerinden siparis.dlq kuyruğuna düşer. DLQ'yu periyodik olarak izlemek, sessizce biriken hataları görünür kılar.

Yaygın Hatalar ve Güvenlik Etkileri

Sıra garantisi varsayımı. RabbitMQ tek bir kuyruk içinde mesaj sırasını korur, ancak birden fazla tüketici olduğunda işleme sırası garanti edilmez. Sıraya bağlı iş mantığı kurmayın; bunun yerine sürüm numarası veya zaman damgası gibi alanlarla çakışmaları uygulama katmanında çözün.

Idempotency eksikliği. Bir mesaj hata sonrası yeniden teslim edilebilir. Tüketici aynı mesajı iki kez işlerse yan etki iki kez oluşur. İşlemleri idempotent tasarlayın veya işlenen mesaj kimliklerini takip edin.

Kimlik doğrulama ve yetkilendirme. Varsayılan guest kullanıcısı yalnızca yerel bağlantılar için tasarlanmıştır; üretimde kullanılmamalıdır. Her servise ayrı kullanıcı ve yalnızca ihtiyaç duyduğu izinleri verin. AMQP bağlantısı TLS (amqps://) ile şifrelenmelidir; aksi halde kimlik bilgileri ve mesaj içerikleri ağda açık kalır.

Yönetim arayüzünü açık bırakmak. RabbitMQ yönetim eklentisi varsayılan olarak 15672 portunda çalışır. Bu arayüzü internete açık bırakmak, tüm kuyruk ve mesajların görüntülenmesine yol açar. Erişimi güvenilir ağla sınırlayın.

Mesaj boyutu ve içerik doğrulama. Büyük gövdeleri kuyruğa koymak bellek ve disk baskısı yaratır; büyük verileri harici depoda tutup mesaja yalnızca referans koyun. Ayrıca tüketici tarafında gelen mesajın şemasını doğrulayın; bozuk veya kötü niyetli içerik doğrudan işlenmemelidir.

Bağlantı sızıntısı. Her istek için yeni bağlantı açmak yerine uzun ömürlü bağlantı ve kanal (channel) kullanın. Kanallar iş parçacığı güvenli değildir; her iş parçacığı kendi kanalını kullanmalıdır.

Toparlarsak

Olay tabanlı mimaride güvenilirliği sağlayan şey broker'ın kendisi değil, tasarım kararlarıdır: dayanıklı kuyruk, kalıcı mesaj ve yayıncı onayı üçlüsü; tüketici tarafında manuel onay ve idempotent işleme; hatalı mesajlar için dead-letter kuyruğu; ve üretimde TLS ile kısıtlı kimlik bilgileri. Bu kararları baştan netleştirdiğinizde RabbitMQ, sistemin görünmez ama güvenilir bir omurgası haline gelir.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz