İstek-cevap modeli, zincirdeki tek bir yavaş servis arkasındaki herkesi bekletene kadar iyi çalışır. Olay güdümlü mimari bu zinciri kırar: bir servis olanı bir mesaj kuyruğuna yayınlar, ilgilenen her tüketici kendi hızında tepki verir. Bu yazı kalıpları, ödünleşimleri ve bunların nasıl uygulanacağını anlatıyor.
Olay güdümlü mimari nedir
Bileşenlerin birbirini doğrudan çağırmak yerine olay üreterek ve tüketerek konuştuğu bir tasarımdır. Olay, sistemde olmuş bir şeydir: kullanıcı kaydoldu, sipariş verildi, ödeme onaylandı.
Senkron modelde A servisi B’yi çağırır ve cevabı bekler. Olay güdümlü modelde A olayı bir aracıya (broker) yazar ve işine devam eder; B, C ve D olayı ne zaman hazırsa o zaman işler. Kazanılanlar:
- Gevşek bağ: üretici, tüketicilerin kim olduğunu bilmez
- Ölçeklenebilirlik: yük artınca tüketici eklenir
- Dayanıklılık: bir tüketici çökse de üretici çalışmaya devam eder, mesajlar kuyrukta bekler
- Hızlı cevap: kullanıcıya arka plandaki işlerin bitmesini beklemeden dönülür
Mesaj kuyrukları
Kuyruk, üretici ile tüketici arasında mesajı kalıcı olarak saklayıp ileten aracıdır. Tüketici çökerse mesaj kaybolmaz, yeniden denenir.
Yaygın seçenekler:
- Apache Kafka: yüksek hacimli olay akışı, mesajları süreyle saklar, tüketiciler geçmişi yeniden okuyabilir
- RabbitMQ: esnek yönlendirme kalıpları olan klasik mesaj aracısı
- AWS SQS/SNS, Azure Service Bus, Google Pub/Sub: bulut sağlayıcılarının yönetilen servisleri
Kafka bir günlüktür: mesaj okununca silinmez, tüketici kaldığı yeri (offset) kendisi tutar. RabbitMQ bir kuyruktur: mesaj onaylanınca gider. Olayları sonradan yeniden oynatmanız gerekecekse (yeni bir servisin geçmişten durum kurması gibi) bu fark seçimi belirler.
Nerede işe yarar
Sipariş işleme
Müşteri siparişi tamamladığında OrderPlaced olayı yayınlanır ve birden fazla servis buna abone olur:
- Ödeme servisi tahsilatı yapar
- Stok servisi stoğu düşer
- Bildirim servisi onay e-postasını gönderir
- Analitik servisi satışı kaydeder
- Kargo servisi gönderimi hazırlar
Olaylar olmadan sipariş servisi bunları tek tek çağırmak ve her birinin hatasıyla uğraşmak zorunda kalırdı.
Finansal işlem izleme
Büyük bir işlem gerçekleştiğinde olay yayınlanır; dolandırıcılık tespiti, uyum kontrolleri ve bildirimler aynı anda tepki verir. Yeni bir kontrol eklemek, üreticiye dokunmadan yeni bir tüketici eklemektir.
Sensör verisi
Binlerce cihazın ürettiği ölçümler olay olarak yayınlanır, akış işleyiciler veriyi anlık analiz eder, eşik aşılınca alarm üretilir ve veri geçmiş analizi için saklanır.
En sık atlanan parça: outbox
Klasik hata şudur: servis veritabanına yazar, sonra olayı kuyruğa yayınlar. İkisi arasında süreç çökerse kayıt vardır ama olay yoktur; tersi sırada ise olay vardır ama kayıt yoktur. Veritabanı ile aracı arasında ortak bir transaction yoktur.
Outbox kalıbı bunu çözer: olay, iş verisiyle aynı transaction’da bir outbox tablosuna yazılır. Ayrı bir süreç bu tabloyu okuyup aracıya iletir ve iletileni işaretler.
BEGIN;
INSERT INTO orders (id, customer_id, total_minor) VALUES ($1, $2, $3);
INSERT INTO outbox (id, topic, payload, created_at)
VALUES (gen_random_uuid(), 'order.placed', $4::jsonb, now());
COMMIT;
Böylece “kayıt oluştu ama olay kayboldu” durumu ortadan kalkar. Bedeli, aynı olayın birden fazla iletilebilmesidir; bu yüzden tüketiciler idempotent olmalıdır.
Uygulama örneği (Node.js ve RabbitMQ)
Üretici, bağlantıyı her mesajda açıp kapatmak yerine uygulama boyunca bir kez açar ve onaylı (confirm) kanal kullanır:
const amqp = require('amqplib');
const connection = await amqp.connect(process.env.AMQP_URL);
const channel = await connection.createConfirmChannel();
await channel.assertExchange('orders', 'topic', { durable: true });
async function publish(routingKey, event) {
const body = Buffer.from(JSON.stringify(event));
// persistent: aracı yeniden başlasa da mesaj kalır
channel.publish('orders', routingKey, body, { persistent: true, messageId: event.id });
await channel.waitForConfirms(); // aracı mesajı aldığını onaylamadan dönme
}
Tüketici mesajı yalnız iş başarıyla bitince onaylar ve aynı mesajı ikinci kez işlemez:
await channel.assertQueue('inventory-service', {
durable: true,
deadLetterExchange: 'orders.dlx', // tekrar tekrar düşen mesajlar buraya
});
await channel.bindQueue('inventory-service', 'orders', 'order.placed');
channel.prefetch(10);
channel.consume('inventory-service', async (msg) => {
const event = JSON.parse(msg.content.toString());
try {
await db.tx(async (tx) => {
// İşlenmiş mesaj tablosu: aynı id ikinci kez gelirse hiçbir şey yapma
const fresh = await tx.oneOrNone(
'INSERT INTO processed_messages (id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING id',
[event.id],
);
if (fresh) await updateInventory(tx, event);
});
channel.ack(msg);
} catch (err) {
channel.nack(msg, false, false); // ölü mektup kuyruğuna gönder
}
});
Temel kalıplar
Yayınla-abone ol (publish-subscribe)
Aynı olay türüne birden fazla tüketici abone olur. Bir iş olayına birden fazla servisin tepki vermesi gerektiğinde doğal seçimdir.
Kuyruk üzerinden istek-cevap
Tüketici, işin sonucunu ayrı bir olay olarak geri yayınlar (OrderPaid, PaymentFailed). Senkron çağrı olmadan istek-cevap akışı kurulur; ilişkiyi bir korelasyon kimliği taşır.
Olay kaynaklama (event sourcing)
Uygulamanın durumu bir olaylar dizisi olarak saklanır; mevcut durum bu olayları baştan oynatarak kurulur. Tam bir denetim izi verir ama sorgulama ve şema evrimi daha fazla emek ister. Her sistem için gerekli değildir.
İyi uygulamalar
- Idempotent tüketici: aynı olay iki kez gelebilir; sonuç değişmemeli
- Olay sürümleme: olay şeması değiştiğinde eski ve yeni kod bir süre birlikte çalışır; alan eklemek serbest, alan silmek ya da anlamını değiştirmek yeni sürüm demektir
- Ölü mektup kuyruğu: belirli sayıda denemeden sonra düşen mesajlar ayrı kuyruğa alınır ve izlenir
- Dağıtık izleme: olaya bir iz kimliği (trace id) eklemek, isteği servisler arasında takip etmeyi mümkün kılar
- Açık olay sözleşmesi: hangi servisin hangi olayı ürettiği ve tükettiği yazılı olmalı
Zorluklar
- Nihai tutarlılık: veri servisler arasında bir süre farklı görünebilir; arayüz bunu kaldırabilmeli
- Hata ayıklama: sorunu birden fazla servis arasında izlemek zorlaşır
- Mesaj sırası: sıra yalnız aynı bölüm (partition) ya da kuyruk içinde garanti edilir; aynı kaydın olaylarını aynı anahtarla yayınlayın
- İşletme yükü: aracıyı işletmek, izlemek ve yedeklemek ayrı bir iştir
Sonuç
Mesaj kuyruklarıyla olay güdümlü mimari, servisleri birbirinden ayırarak daha dayanıklı ve ölçeklenebilir sistemler kurmayı sağlar. Tek bir olay türüyle başlayın, outbox ve idempotent tüketiciyi baştan kurun, deneyim biriktikçe kapsamı genişletin.
Mimarinizi olay güdümlü yapıya taşımayı düşünüyorsanız bize yazın.
ByteGurus
Yazar