Event-Driven Mimari Nedir?
Event-driven mimari, bileşenlerin birbirini doğrudan çağırmak yerine olay (event) yayınlayarak haberleştiği bir yaklaşımdır. Bir servis bir işi tamamladığında veya bir durum değiştiğinde olay yayınlar; ilgili servisler bu olayı dinleyip kendi işlerini yürütür. Böylece gönderici, alıcının kim olduğunu ve ne yaptığını bilmek zorunda kalmaz.
Bu gevşek bağlılığın pratikteki karşılığı şudur: yeni bir tüketici eklemek için mevcut yayıncı kodunu değiştirmeniz gerekmez. Buna karşılık sistemin akışını tek bir yerden okumak zorlaşır; bir olayın kimler tarafından tüketildiğini ayrıca takip etmeniz gerekir.
RabbitMQ'nun Rolü
RabbitMQ, mesajları kuyruklar üzerinden taşıyan bir mesaj aracısıdır (message broker). Yayıncı (producer) mesajı bir exchange'e gönderir; exchange, bağlama (binding) kurallarına göre mesajı bir veya birden fazla kuyruğa yönlendirir; tüketici (consumer) kuyruktan mesajı alır.
Temel kavramlar:
- Exchange: Mesajın ilk geldiği nokta. Türü yönlendirme davranışını belirler.
- Queue: Mesajların tüketilmeyi beklediği tampon.
- Binding: Exchange ile kuyruk arasındaki yönlendirme kuralı.
- Routing key: Yayıncının mesaja eklediği, yönlendirmede kullanılan etiket.
Yaygın exchange türleri ve davranışları:
- direct: Routing key birebir eşleşirse yönlendirir.
- fanout: Bağlı tüm kuyruklara koşulsuz dağıtır.
- topic: Routing key'i joker karakterlerle (
*tek kelime,#sıfır veya daha fazla kelime) eşleştirir. - headers: Routing key yerine mesaj başlıklarına göre yönlendirir.
Önkoşullar ve Sürüm Kapsamı
- Çalışan bir RabbitMQ sunucusu (yerel Docker konteyneri yeterlidir).
- AMQP 0-9-1 protokolünü konuşan bir istemci kütüphanesi. Örneklerde Node.js için
amqplibkullanılacaktır. - Bu rehberdeki kod örnekleri, kütüphanenin kararlı API'sine dayanan temel yayınlama/tüketme akışını gösterir. Belirli bir sürüm numarasına bağlı davranış iddiası içermez; kullandığınız kütüphanenin sürüm notlarını kendiniz doğrulayın.
Adım Adım: Basit Bir Yayıncı ve Tüketici
1. Bağlantı ve kanal açma
const amqp = require('amqplib');
async function baglan() {
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
return { conn, ch };
}
Kanal (channel), bir bağlantı üzerinden yürütülen sanal oturumdur. Çok sayıda kanal açmak yerine, iş parçacığı başına bir kanal kullanmak yaygın bir yaklaşımdır.
2. Kuyruk tanımlama ve mesaj gönderme
async function gonder(ch, kuyruk, icerik) {
await ch.assertQueue(kuyruk, { durable: true });
ch.sendToQueue(kuyruk, Buffer.from(JSON.stringify(icerik)), {
persistent: true,
});
}
Beklenen davranış: assertQueue kuyruk yoksa oluşturur, varsa mevcut tanımın uyumlu olduğunu doğrular. durable: true kuyruğun sunucu yeniden başladığında kaybolmamasını sağlar; persistent: true ise mesajın diske yazılmasını ister. İkisi birlikte kullanılmazsa kalıcılık beklentisi karşılanmaz.
3. Tüketici tanımlama
async function dinle(ch, kuyruk) {
await ch.assertQueue(kuyruk, { durable: true });
ch.prefetch(1);
ch.consume(kuyruk, (msg) => {
if (msg === null) return;
try {
const veri = JSON.parse(msg.content.toString());
// işleme
ch.ack(msg);
} catch (err) {
ch.nack(msg, false, false); // başarısız: yeniden kuyruğa koyma
}
});
}
Beklenen davranış: prefetch(1) tüketiciye aynı anda tek mesaj verilmesini söyler; böylece bir mesaj işlenmeden yenisi gönderilmez. ack mesajın başarıyla işlendiğini bildirir ve kuyruktan düşürür. nack ile requeue=false verildiğinde mesaj kuyruğa geri konmaz.
Yaygın Hatalar
- Otomatik ack kullanmak: Tüketici
noAck: trueile mesajı alıp işleme sırasında çökerse mesaj kaybolur. Kritik işlerde elleacktercih edin. - Idempotency'yi atlamak: Ağ hatası veya yeniden teslim yüzünden aynı mesaj birden fazla işlenebilir. Tüketiciyi aynı mesajı iki kez işlemenin zarar vermeyeceği şekilde yazın (örneğin işlem kimliği ile tekrar kontrolü).
- Sonsuz yeniden kuyruğa alma:
nack(..., requeue=true)döngüsü, sürekli başarısız olan bir mesajı kuyrukta döndürür. Kalıcı hatalarda mesajı ölü mektup kuyruğuna (dead-letter queue) yönlendirin. - Kuyruk tanımını değiştirmek:
assertQueueçağrısındadurableveya argümanları değiştirmek, mevcut kuyrukla çakışırsa hata verir. Şema değişikliklerini yeni kuyruk adı veya kontrollü geçiş ile yönetin. - Bağlantıyı paylaşmak ama kanalı paylaşmamak: Aynı kanalı eşzamanlı çok sayıda işlem için kullanmak, hataların birbirine karışmasına yol açar.
Güvenlik Etkileri
- Kimlik doğrulama ve yetkilendirme: RabbitMQ varsayılan olarak yerel kullanıcıyla çalışır. Üretimde ayrı kullanıcılar tanımlayın ve yalnızca gereken exchange/kuyruk izinlerini verin.
guestkullanıcısını uzak erişime açık bırakmayın. - Şifreli bağlantı: Kimlik bilgileri ve mesaj içeriği ağ üzerinden açık gitmesin; TLS kullanın (
amqps://). - Mesaj içeriği: Kuyruğa hassas veri (parola, token, kişisel veri) koymadan önce gerekli olup olmadığını değerlendirin. Kuyruk ve yönetim arayüzü erişimlerini ayrı ayrı sınırlayın.
- Yönetim arayüzü: Varsayılan port üzerinden açık bırakılan yönetim paneli, sistemin tamamına erişim anlamına gelebilir; ağ erişimini kısıtlayın.
Tasarım Notları
- Olay mı, komut mu?: "SiparişOluşturuldu" bir olaydır; "FaturayıKes" bir komuttur. Olaylar geçmişte olanı anlatır, komutlar bir şeyin yapılmasını ister. İkisini aynı kanalda karıştırmak akışı okumayı zorlaştırır.
- Şema evrimi: Mesaj içeriğine sürüm alanı eklemek, tüketicilerin eski ve yeni biçimleri birlikte ele almasını kolaylaştırır.
- Gözlemlenebilirlik: Kuyruk uzunluğu, işlenmeyen mesaj sayısı ve tüketici gecikmesi gibi metrikleri izleyin. Sorunlar genellikle tüketici tarafında birikme olarak görünür.
Sonuç
Event-driven mimari, servisleri gevşek bağlar ve yeni tüketicilerin eklenmesini kolaylaştırır; karşılığında akışın takibini ve hata yönetimini daha dikkatli kurmayı gerektirir. RabbitMQ bu modelde exchange, kuyruk ve binding üçlüsüyle yönlendirmeyi yönetir. Sağlam bir kurulum için elle ack, idempotent tüketici, ölü mektup kuyruğu, TLS ve daraltılmış izinler temel yapı taşlarıdır.