Kafka ile RabbitMQ’yu karşılaştırırken genelde bir özellik tablosuyla başlanır: throughput, replay, routing, ordering, retry. Tablo faydalıdır ama tek başına seçim yapmayı pek kolaylaştırmaz, çünkü maddeler birbirinden bağımsız görünür.
Bu farkları başka bir yerden okumayı daha kullanışlı buluyorum:
Bir mesaj broker’a girdikten sonra ne oluyor?
RabbitMQ mesajın teslim durumunu takip eder ve ack geldikten sonra mesajı kuyruktan çıkarır. Kafka ise mesajı bir log’da tutar; nerede kalındığı bilgisini consumer kendi offset’iyle taşır.
İki sistem arasındaki önemli farkların çoğu bu noktadan başlıyor: mesajın broker’daki yaşam döngüsü, consumer’ın konumunu kimin tuttuğu ve mesaj işlendikten sonra broker’ın ne yaptığı. Replay, consumer group, retry ve routing konularına da buradan geçebiliriz.
Önce: broker’a neden ihtiyaç duyuyoruz?
Message broker, iki uygulama arasındaki iletişimi asenkron hale getiren aracı katmandır. Üretici (producer) mesajı broker’a bırakır, tüketici (consumer) uygun olduğunda alır. İki taraf birbirini tanımak ve aynı anda ayakta olmak zorunda değildir.
Sipariş servisinin e-posta, fatura, stok ve kargo servislerini doğrudan HTTP ile çağırdığı bir sistem düşünelim:
@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(Order.from(request));
emailService.sendConfirmation(order); // yavaşsa sipariş yavaşlar
invoiceService.create(order); // hata verirse sipariş geri döner
stockService.reserve(order);
shippingService.schedule(order);
return order;
}
@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(Order.from(request));
// Sipariş servisi kimin dinlediğini bilmiyor
broker.publish(new OrderCreated(order.getId(), order.getTotal()));
return order;
}
Broker kullanmanın temel faydaları birkaç başlıkta toplanabilir:
Decoupling
Sipariş servisi
OrderCreatedolayını yayınlar ve gerisini bilmez. Yarın altıncı bir servis eklendiğinde sipariş kodu değişmez.Tamponlama
Trafik arttığında mesajlar kuyrukta birikebilir; consumer’lar yetişebildikleri hızda işler.
Dayanıklılık
Fatura servisi bir süre ayakta olmasa da mesajlar broker’da bekler; servis geri geldiğinde kaldığı yerden devam eder.
Yatay ölçekleme
Aynı kuyruğu tüketen consumer sayısını artırarak işleme kapasitesini büyütebilirsiniz.
Fan-out
Tek bir olayı birden çok bağımsız servise ulaştırmak — üretici tarafında bir değişiklik yapmadan.
Her yere broker koymak da doğru değil
Anında cevap gereken akışlarda uygun değil. “Bu ürünün fiyatı kaç?” sorusunun cevabı bir kuyruktan gelmez; orada HTTP veya gRPC daha doğrudur.
Küçük ve düşük trafikli bir uygulamada maliyeti faydasını geçebilir. Broker kendi operasyonel yükünü getirir: cluster, izleme, alarm, sürüm yükseltme.
Basit background job için önce veritabanını değerlendirin. Tek tabloya yazılan bir job kuyruğu belirli bir ölçeğe kadar iş görür.
Broker’a ihtiyacınız olduğuna karar verdiyseniz asıl karar başlıyor: hangisi?
İki model: postane ve arşiv
RabbitMQ — postane
Bir postane gibi düşünülebilir: mesajı adresine göre doğru kuyruğa yönlendirir ve consumer’ın teslim durumunu takip eder.
Mesaj acklendiğinde kuyruktan çıkar.
Kafka — arşiv
Append-only bir arşiv gibi düşünülebilir: mesajlar log’a sırayla yazılır ve retention süresi boyunca orada kalır.
Consumer nerede kaldığını kendi offset’iyle takip eder.
Aynı şeyin akış hali:
graph LR
P1["Producer"] --> EX{{"Exchange"}}
EX -->|"routing key"| Q1[("Queue")]
Q1 -->|"push"| C1["Consumer"]
C1 -.->|"ack → mesaj kuyruktan çıkar"| Q1RabbitMQ’da akış tek yönlü ve sonlu: broker mesajı bir consumer’a iter, ack geldiğinde kuyruktan çıkarır. Geri dönen tek şey teslim bilgisidir, mesajın kendisi değil.
graph LR
P2["Producer"] --> T["Topic partition<br/>append-only log"]
T -->|"offset 512"| C2["Consumer A"]
T -->|"offset 40"| C3["Consumer B"]
T -->|"offset 0'dan itibaren"| C4["Consumer C<br/>yeni katıldı"]Kafka’da log ortada durur ve her consumer kendi konumundan okur. Consumer B geriden geliyor, Consumer C yeni bağlandı ve geçmişi baştan okuyor. Hiçbiri diğerini etkilemiyor, log’a dokunulmuyor.
Bundan sonraki bölümlerde bu iki modelin nelere yol açtığına bakacağız.
Mesaj log’da kalınca: replay
Mesaj okunduktan sonra da yerinde duruyorsa geri dönüp tekrar okunabilir. Kafka’nın replay yeteneği buradan geliyor.
Bunun nasıl çalıştığını görmek için Kafka’nın dört kavramı yeterli.
Topic ve partition
Topic, mesajların yazıldığı mantıksal kategoridir: orders, user-signups, payment-events.
Bir topic partition denen parçalara bölünür. Her partition, yalnızca sonuna ekleme yapılan sıralı bir log dosyasıdır:
orders topic'i, 3 partition
partition-0: [0][1][2][3][4][5] ← yeni mesajlar buraya
partition-1: [0][1][2][3]
partition-2: [0][1][2][3][4][5][6][7]
Partition sadece bir depolama detayı değil, aynı zamanda Kafka’nın paralellik birimidir. Bunun sonuçlarına birazdan geleceğiz.
Bir mesajın hangi partition’a gideceğini partitioner belirler. Varsayılan davranışta:
- Mesajın key‘i varsa: key’in hash’ine göre bir partition seçilir; aynı key aynı partition’a gider.
- Key yoksa: mesajlar partition’lara dengeli dağıtılır.
“Key yoksa dengeli dağıtılır” uzun vadede doğru, ama mesaj mesaj değil: Kafka 2.4’ten beri varsayılan davranış sticky partitioning. Producer, bir batch dolana ya da linger.ms süresi bitene kadar aynı partition’a yapışır, sonra bir sonrakine geçer — batch’ler büyür, gönderim ucuzlar, dağılım da zaman içinde dengelenir. Sonraki sürümlerde bu davranış broker gecikmesini de hesaba katacak şekilde iyileştirildi.
Offset
Partition içindeki her mesajın sıra numarasıdır. Consumer “4523’e kadar okudum” bilgisini offset olarak commit eder.
Replay de bundan ibaret: offset geri alınır, aynı mesajlar tekrar akar.
mevcut durum: offset = 4523
offset geri alınır: offset = 4100
sonuç: 423 mesaj yeniden işlenir
E-posta servisinde bir hata yüzünden son iki saatte yanlış içerik gittiyse, kodu düzeltip offset’i geri almak yeterli olur. RabbitMQ’da o mesajlar acklendiği anda kuyruktan çıkmıştı.
Consumer group
Aynı group.id‘ye sahip consumer’lar bir grup oluşturur ve iki kuralı vardır:
Grup içinde: iş bölüşülür
Bir partition, grup içinde yalnızca bir consumer’a atanır. Böylece aynı mesaj grup içinde iki kez işlenmez.
Gruplar arasında: iş kopyalanır
Farklı gruplar aynı topic’i birbirinden bağımsız okur; her grubun kendi offset’i vardır. Fan-out böyle yapılır.
graph LR
T["orders topic<br/>3 partition"]
T --> G1["Grup: fatura-servisi<br/>offset 4523"]
T --> G2["Grup: analitik<br/>offset 4523"]
T --> G3["Grup: arama-indeksi<br/>offset 120 — geriden geliyor"]Bunun pratik bir sonucu var: yarın arama indeksini sıfırdan kurmanız gerekirse yeni bir consumer group açıp offset’i başa alırsınız. Topic’teki geçmiş yeni servisi besler ve mevcut servislerin bir şey yapması gerekmez.
Retention
Mesajlar okunduktan sonra silinmediğine göre bir yerde durmaları gerekir. Retention politikası bunu yönetir: süre bazlı (örneğin 7 gün), boyut bazlı ya da log compaction ile — her key’in yalnızca son değeri saklanır.
Log compaction, bir topic’i “olay akışı” olmaktan çıkarıp “durum tablosu"na yaklaştırır. user-profiles topic’inde her kullanıcı id’si bir key ise, compaction sonrası her kullanıcının en güncel hali kalır. Yeni bir servis topic’i baştan okuyup kendi yerel kopyasını kurabilir.
Broker teslimi takip edince: mesajı reddedebilmek
Şimdi ters yöne bakalım. RabbitMQ’da mesaj bir consumer’a teslim edildiğinde broker bunu bilir ve mesajı “uçuşta” sayar. Bu bilgi, Kafka’da doğrudan karşılığı olmayan bir şeyi mümkün kılıyor: mesajı reddetmek.
Ack, nack, requeue
Consumer mesajı işleyince ack gönderir ve mesaj kuyruktan çıkar. İşleyemezse nack ile ya tekrar kuyruğa koydurur (requeue) ya da düşürtür.
@RabbitListener(queues = "email-jobs")
public void handle(EmailJob job, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
emailSender.send(job);
channel.basicAck(tag, false); // işlendi
} catch (TransientFailure e) {
channel.basicNack(tag, false, true); // geçici hata, kuyruğa geri koy
} catch (PermanentFailure e) {
channel.basicNack(tag, false, false); // kalıcı hata, DLX'e yönlendir
}
}
Kafka’da bu üç satırın doğrudan karşılığı yok. Orada mesaj bazında bir reddetme mekanizması değil, offset vardır:
- Başarısız mesajın offset’ini commit ederseniz o mesaj atlanmış olur.
- Commit etmezseniz o partition’daki sıra ilerlemez ve arkadaki mesajlar bekler.
İkincisine head-of-line blocking deniyor ve task queue senaryolarında en sık karşılaşılan zorluklardan biri.
Dead Letter Exchange
Reddedilen, TTL’i dolan veya kuyruk limitini aşan mesajlar bir DLX’e yönlendirilir. Hatalı mesajları karantinaya almanın ve gecikmeli retry kurmanın yerleşik yolu budur:
graph LR
Q["email-jobs"] -->|"nack (requeue=false)"| DLX{{"email-dlx"}}
DLX --> DLQ[("email-dlq<br/>karantina")]
DLQ -.->|"incele, düzelt,<br/>elle geri gönder"| QRabbitMQ bunu yerleşik olarak sunarken Kafka’da genellikle retry topic’leri ve ayrı bir DLQ akışı tasarlamanız gerekir:
email-jobs → (hata) → email-retry-5m → (hata) → email-retry-30m → (hata) → email-dlq
Yaygın ve işleyen bir pattern, ama yazılması ve işletilmesi gereken ek bir katman.
Prefetch
Broker’ın bir consumer’a ack beklemeden aynı anda kaç mesaj göndereceğini belirler:
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: manual
Süresi öngörülemeyen işlerde prefetch değerini düşük tutmak işe yarar. Yüksek bir değerde worker’a peşin verilen işler, o worker yavaş bir işe takıldığında onunla birlikte bekler; düşük değerde her worker müsait oldukça sıradaki işi alır.
RabbitMQ’nun mesaj bazında kontrol sunan başka özellikleri de var:
- TTL — mesaja veya kuyruğa yaşam süresi verme
- Priority queue — öncelikli mesajları öne alma
- Delayed messages — plugin ile, mesajı belirli süre sonra teslim etme
- Quorum queues — Raft tabanlı replikasyon; modern kurulumlarda klasik mirrored queue yerine tercih edilir
Bunların Kafka’da yerleşik karşılığı yok; benzer davranışlar uygulama tarafında kurulur.
Peki RabbitMQ Streams? O da replay yapmıyor mu?
Yeni RabbitMQ sürümlerinde Streams adında, append-only bir log tipi var: mesaj okunduğunda silinmiyor ve offset ile geri okunabiliyor.
Yani “replay yalnızca Kafka’da var” demek doğru olmaz. Ama iki şeyi ayırmakta fayda var:
- Özellik olarak replay RabbitMQ tarafında da mevcut.
- Ekosistem olarak fark sürüyor: Kafka Connect, Kafka Streams, ksqlDB, Debezium, şema kayıt defteri ve olgun izleme araçları Kafka tarafında.
Elinizde zaten RabbitMQ varsa ve ihtiyacınız sınırlı bir replay ise Streams’e bakmaya değer. Bir event backbone kuruyorsanız ekosistem farkı belirleyici olabilir.
Yönlendirmeyi kim yapıyor?
Mesaj ack sonrası kuyruktan çıkıyorsa, doğru kuyruğa gitmesi broker’ın sorumluluğundadır. Mesaj log’da duruyorsa, kimin neyi okuyacağı okuyan tarafın kararıdır.
Bu yüzden iki sistemin routing modeli birbirine hiç benzemiyor.
RabbitMQ: exchange’ler
Producer mesajı doğrudan kuyruğa değil, bir exchange’e gönderir. Exchange, binding kurallarına ve mesajın routing key‘ine bakarak hangi kuyruklara kopyalayacağına karar verir.
Producer → Exchange → (Binding + Routing Key) → Queue → Consumer
Dört exchange tipi var:
| Tip | Nasıl karar verir | Örnek |
|---|---|---|
| Direct | Routing key tam eşleşirse | payment.failed → yalnızca bu key ile bind edilmiş kuyruk |
| Fanout | Key’e bakmaz, bağlı tüm kuyruklara kopyalar | Broadcast |
| Topic | Pattern eşleşmesi | order.* → order.created, order.cancelled |
| Headers | Routing key yerine mesaj header’larına bakar | {"format": "pdf", "priority": "high"} |
Topic exchange’te * tek kelime, # sıfır veya daha çok kelime anlamına gelir. order.# deseni order.eu.created gibi çok seviyeli key’leri de yakalar.
RabbitMQ burada daha esnek bir routing modeli sunuyor: mesajın hangi kuyruklara gideceği broker konfigürasyonuyla belirlenir, üretici veya tüketici kodunu değiştirmeden.
Kafka: topic, partition, key
Kafka’da RabbitMQ’daki exchange/binding modelinin karşılığı yok. Producer bir topic’e yazar, partition seçimi key ve partitioner üzerinden yapılır. Consumer hangi topic’i okuyacağını kendisi bilir.
Filtreleme gerekiyorsa iki yol var: ya konuları ayrı topic’lere bölersiniz, ya da consumer tarafında ilgilenmediğiniz mesajları atlarsınız. İkisi de exchange’lerin verdiği esnekliğin yerini tam olarak tutmaz.
Paralellik neyle sınırlı?
Partition, Kafka’nın paralellik birimi olduğuna göre aynı consumer group’ta partition sayısından fazla aktif consumer olamaz.
orders topic'i, 4 partition, tek consumer group
4 consumer → her biri 1 partition alır ✅
6 consumer → 4'ü çalışır, 2'si boş bekler ⚠️
RabbitMQ’da partition sayısına bağlı böyle bir consumer-group sınırı yok; bir kuyruğa istediğiniz kadar consumer bağlayabilirsiniz. Elbette burada da throughput broker, ağ ve consumer kaynaklarıyla sınırlıdır — ama sınır yapısal bir tavan değil, kapasite meselesidir.
Kafka: consumer group paralelliği ≤ partition sayısı
RabbitMQ: bir kuyruğun consumer sayısı partition sayısıyla sınırlı değil
Partition sayısı geri alınması kolay bir karar değil
Varsayılan partitioning davranışında partition sayısını değiştirmek, key’in partition’a eşlenmesini de değiştirir. Yani aynı key geçiş sonrası farklı bir partition’a düşebilir ve o key için sıralama garantisi geçiş anında kırılır.
Partition sayısını azaltmak ise desteklenmez.
Bu yüzden partition sayısı yalnızca kapasite kararı değil, key bazlı sıralama açısından da düşünülmesi gereken bir karardır.
Sıcak key, tek partition'a yığılmaya yol açar
Key seçimi hem sıralamayı hem yük dağılımını belirler. tenantId‘yi key yaparsanız ve müşterilerinizden biri trafiğin büyük kısmını üretiyorsa, o trafik tek bir partition’a — dolayısıyla tek bir consumer’a — düşer. Diğer partition’lar boş dururken o partition birikmeye başlar.
Ölçüt şu: key’in kardinalitesi partition sayısından belirgin şekilde büyük ve dağılımı görece dengeli olmalı.
Sıralama
Kafka’nın sıralama garantisi topic genelinde değil, partition içindedir. Bu ayrım önemli, çünkü genellikle “tüm olaylar sırayla işlensin” değil, “aynı entity’nin olayları sırayla işlensin” ihtiyacına cevap verir.
Key mekanizması bu ihtiyacı karşılar:
// key = orderId → bu siparişin tüm olayları aynı partition'da, sırayla
kafkaTemplate.send("order-events", order.getId(), new OrderCreated(...));
kafkaTemplate.send("order-events", order.getId(), new OrderPaid(...));
kafkaTemplate.send("order-events", order.getId(), new OrderShipped(...));
graph LR
A["orderId = 1042"] --> P1["partition-2<br/>created → paid → shipped"]
B["orderId = 1043"] --> P2["partition-0<br/>created → paid"]
C["orderId = 1044"] --> P1Topic genelinde global sıra gerekiyorsa tek partition kullanmak gerekir; bu da paralelliği ortadan kaldırır ve nadiren tercih edilir.
RabbitMQ tarafında ise tek kuyruk ve tek consumer varsa FIFO sıra korunur. Ölçek için birden çok consumer bağlandığında mesajlar farklı worker’lara dağılır ve işlenme sırası garanti edilmez. Requeue de sırayı etkiler: nack ile geri konan mesaj kuyruğun sonuna değil, mümkün olduğunca eski konumuna geri konur — yani neredeyse anında tekrar teslim edilir.
Kafka’daki key→partition modeline benzer bir davranış istiyorsanız Consistent Hash Exchange plugin’i ile mesajları key’e göre ayrı kuyruklara dağıtabilirsiniz; ancak bu, Kafka’daki kadar yerleşik bir mekanizma değil.
Entity bazlı sıralama gereksiniminiz varsa — event sourcing, durum makineleri, sipariş yaşam döngüsü — Kafka’nın modeli bu ihtiyaca daha doğrudan cevap veriyor.
İki sistemde de aynı olan konu: delivery garantileri
Buraya kadar farklara baktık. Delivery garantileri ise iki sistemde de aynı üç seviyede tanımlanır:
| Garanti | Anlamı | Riski |
|---|---|---|
| At-most-once | En fazla bir kez teslim edilir | Mesaj kaybolabilir |
| At-least-once | En az bir kez teslim edilir | Mesaj tekrarlanabilir |
| Exactly-once | Tam bir kez işlenir | En zor ve en maliyetli |
Kafka’da producer tarafında acks=all ve min.insync.replicas ile mesajın yeterli sayıda replica’ya yazılması beklenir; enable.idempotence=true producer retry’larının duplicate üretmesini engeller — bu ayar Kafka 3.0’dan beri zaten varsayılan olarak açık, yani çoğu kurulumda açmanız gereken değil, kapatmamanız gereken bir şey. Consumer tarafında garantiyi belirleyen şey offset’i ne zaman commit ettiğinizdir:
// Önce işle, sonra commit et
process(record);
consumer.commitSync();
// İşlemeden sonra, commit'ten önce çökerse
// → aynı mesaj tekrar gelir
// Önce commit et, sonra işle
consumer.commitSync();
process(record);
// Commit'ten sonra, işlemeden önce çökerse
// → mesaj işlenmemiş olur
RabbitMQ’da producer tarafında publisher confirms broker’ın mesajı aldığını doğrular; mesajın persistent ve kuyruğun durable olması diske yazılmasını sağlar. Consumer tarafında manual ack at-least-once, auto-ack ise at-most-once anlamına gelir.
Kafka’nın transactions özelliği exactly-once sunar, ancak kapsamı esas olarak “Kafka’dan oku → işle → Kafka’ya yaz” akışlarıdır. Dış bir sisteme yazarken — veritabanı, HTTP API, e-posta sağlayıcısı — bu garanti geçerli değildir. RabbitMQ zaten exactly-once iddiasında bulunmaz.
Her iki broker’da da pratik yaklaşım genellikle at-least-once teslimat ve idempotent consumer kullanmaktır. Bu, çoğu zaman mesajın taşıdığı bir id’yi kaydedip tekrar kontrolü yapmaktan ibarettir:
if (!processedEvents.markIfAbsent(event.id())) {
return; // zaten işlenmiş
}
process(event);
Consumer mesajı ne zaman alıyor?
Buraya kadar mesajın broker’da ne olduğuna baktık. Peki consumer tarafına ne zaman geçiyor? Boşta CPU kaldığında mı, bellek müsait olduğunda mı?
Cevap: ikisi de değil. Broker’ın sizin CPU’nuzdan ve belleğinizden haberi yok. İki sistem de akışı bambaşka bir şeyle sınırlıyor — ve sık sorulan “bir anda çok sayıda event gelirse uygulama çöker mi?” sorusunun cevabı tam olarak burada.
Kafka: consumer ister, broker verir
Kafka pull modeliyle çalışır. Broker kimseye bir şey itmez; consumer düzenli olarak poll() çağırır ve o çağrının döndürdüğü kadar kayıt işler.
while (running) {
ConsumerRecords<String, Report> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, Report> record : records) {
process(record); // burası ne kadar sürerse, döngü o kadar yavaş döner
}
consumer.commitSync();
}
Bu döngünün hızını belirleyen tek şey process()‘in hızı. Yavaşsa döngü yavaş döner, poll() daha seyrek çağrılır ve broker’dan daha az kayıt gelir. Yani backpressure Kafka’da modelin kendisinden geliyor: istemediğiniz sürece size bir şey gelmez.
Bir poll() çağrısının ne kadar veri getireceğini birkaç ayar belirliyor:
| Ayar | Varsayılan | Ne yapar |
|---|---|---|
max.poll.records | 500 | Tek poll() çağrısının döndüreceği maksimum kayıt sayısı |
max.partition.fetch.bytes | 1 MiB | Partition başına tek seferde çekilecek en fazla veri |
fetch.min.bytes | 1 | Broker en az bu kadar veri birikene kadar bekleyebilir |
fetch.max.wait.ms | 500 | fetch.min.bytes dolmazsa en fazla bu kadar bekler |
Pratikte ayarlayacağınız tek şey genelde max.poll.records. Bir iş 30 saniye sürüyorsa 500 kayıt almanın anlamı yok:
spring:
kafka:
consumer:
max-poll-records: 1 # bir poll = bir iş
Kafka istemcisi arka planda bir miktar ön çekim (prefetch) yapar; ağ gidiş-dönüşünü beklememek için birkaç batch’i bellekte tutar. Ama uygulama kodunuzun gördüğü kayıt sayısını max.poll.records sınırlar. Yani “aynı anda elimde kaç iş var” sorusunun cevabı ön çekim değil, bu ayardır.
RabbitMQ: broker iter, prefetch frenler
RabbitMQ push modeliyle çalışır. basic.consume ile abone olduğunuz anda broker mesajları size akıtmaya başlar — gelir gelmez, siz istemeden.
Buradaki tehlike de gerçek. Freni koymazsanız broker kuyruktaki her şeyi bağlantı hızının elverdiği kadar hızlı gönderir, istemci kütüphanesi de hepsini belleğe alır. Klasik OutOfMemoryError sebebi budur.
Fren, daha önce değindiğimiz prefetch ayarı:
prefetch = 10
broker → consumer'a en fazla 10 ADET UNACKED mesaj gönderir
consumer 1 tanesini ack'ler → broker 1 tane daha gönderir
Yani prefetch, kredi bazlı kayan bir penceredir: elinizdeki işlenmemiş mesaj sayısı tavana dayandığında broker susar.
prefetch = 0 sınırsız demektir
Ham AMQP istemcisinde basicQos çağırmazsanız prefetch sınırsızdır — broker kuyruktaki milyonlarca mesajı size göndermeye çalışır.
Spring AMQP bu tuzağı varsayılan olarak kapatır (prefetch: 250), ama değerin sizin iş sürenize göre doğru olduğunu yine de kontrol etmek gerekir.
Doğru değeri seçmek de basit bir kurala bağlı:
kısa ve birbirine yakın süreli işler → yüksek prefetch (100–250), gidiş-dönüş maliyeti amorti olur
uzun ve değişken süreli işler → prefetch: 1, her worker müsait oldukça sıradakini alır
İkisini yan yana koyunca
| Kafka | RabbitMQ | |
|---|---|---|
| Model | Pull — consumer ister | Push — broker gönderir |
| Akışı ne sınırlar | max.poll.records ve döngünüzün hızı | prefetch (unacked mesaj tavanı) |
| “Sınırsız” ayarı var mı | Yok — poll() çağırmazsanız hiçbir şey gelmez | Var — prefetch=0, klasik OOM sebebi |
| Consumer yavaşlarsa | Lag büyür; broker etkilenmez | Broker teslimi durdurur; kuyruk şişerse flow control publisher’a yansır |
| Backpressure nereden gelir | Modelin kendisinden | Bir ayardan |
Bölümün başındaki sorunun cevabı da bu son satırda:
Kafka’da uygulamayı boğmak için özel çaba göstermeniz gerekir. RabbitMQ’da boğmamak için
prefetch‘i ayarlamış olmanız gerekir.
Uzun süren işler: 30 dakikalık rapor
Şimdi somut soruya gelelim: bir rapor 30 dakika sürüyor ve iş bitince mesajı ackliyoruz. Bu güvenli mi?
RabbitMQ’da büyük ölçüde evet, ama bir sınırı var. Kafka’da hayır — ve varsayılan ayarlarla sonuç sessiz bir hata değil, çok daha gürültülü bir şey: aynı rapor tekrar tekrar üretilir.
Kafka’da ne oluyor?
İki ayrı zaman aşımı var ve sık sık birbirine karıştırılıyor:
| Ayar | Varsayılan | Neyi ölçer |
|---|---|---|
session.timeout.ms | 45 sn | Heartbeat’ler kesildi mi — yani süreç hayatta mı |
max.poll.interval.ms | 5 dakika | İki poll() çağrısı arasında ne kadar geçti — yani işleme ne kadar sürüyor |
Heartbeat’leri ayrı bir arka plan thread’i attığı için 30 dakika süren bir iş session.timeout.ms‘i tetiklemez; süreç canlı görünmeye devam eder. Sizi vuran ikincisi:
00:00 poll() → 1 kayıt: "aylık mutabakat raporu"
00:00 rapor üretimi başlar
05:00 max.poll.interval.ms aşıldı
→ broker consumer'ı ölü sayar, gruptan atar
→ rebalance: partition başka bir worker'a geçer
→ yeni worker son commit'lenmiş offset'ten başlar
→ AYNI RAPORU baştan üretmeye başlar
30:00 ilk worker raporu bitirir, commitSync() çağırır
→ CommitFailedException: artık o partition'ın sahibi değil
→ 30 dakikalık iş çöpe gitti, offset ilerlemedi
35:00 ikinci worker da 5. dakikada atılmıştı... döngü sürer
Yani evet: varsayılan ayarlarla Kafka aynı raporu sonsuza kadar üretir ve hiçbir zaman commit edemez. Üstelik her denemede rapor gerçekten üretildiği için sistem meşgul görünür ve sorun geç fark edilir.
RabbitMQ’da ne oluyor?
RabbitMQ’da max.poll.interval.ms benzeri bir kavram yok; bir mesajı istediğiniz kadar unacked tutabilirsiniz. Bağlantı heartbeat’i (varsayılan 60 sn) ayrı bir I/O thread’i tarafından atıldığı için uzun işlemeden etkilenmez.
Ama bir sınır var ve tam da bu senaryoya denk geliyor:
consumer_timeout — varsayılanı 30 dakika
RabbitMQ 3.8.15’ten beri broker tarafında bir teslimat zaman aşımı var: bir mesaj varsayılan olarak 30 dakika içinde acklenmezse kanal kapatılır ve mesaj requeue edilir.
Yani 30 dakikalık rapor örneği, RabbitMQ’nun varsayılan sınırının tam üzerinde duruyor. Uzun işler çalıştıracaksanız rabbitmq.conf içinde yükseltmeniz gerekir:
# 2 saat
consumer_timeout = 7200000
Sınır aşıldığında kapatılan şey mesaj değil kanal olduğu için, yalnızca süresi dolan mesaj değil, o kanalda tutulan bütün unacked mesajlar requeue edilir. prefetch yüksekse bu, tek bir yavaş iş yüzünden onlarca mesajın yeniden teslim edilmesi demektir.
Karşılaştırma:
| Kafka | RabbitMQ | |
|---|---|---|
| Bir işi ne kadar tutabilirim | max.poll.interval.ms — varsayılan 5 dakika | consumer_timeout — varsayılan 30 dakika |
| Ayar nerede | Consumer tarafında | Broker tarafında (tüm kuyrukları etkiler) |
| Aşılırsa | Gruptan atılma + rebalance + commit hatası | Kanal kapanır, mesaj requeue olur |
| Etki alanı | Tüm partition — arkadaki işler de durur | Kanal kapandığı için o kanaldaki tüm unacked mesajlar |
Peki nasıl kurgulanmalı?
İlk akla gelen çözüm — “mesajı alır almaz ackleyip arkada async işlesek?” — sezgisel olarak doğru yerde duruyor ama düz haliyle iki şeyi birden bozuyor. Dört yaklaşımı yan yana koyalım:
| Yaklaşım | Garanti | Backpressure | Ne zaman |
|---|---|---|---|
| 1. Zaman aşımını yükselt | at-least-once | Korunur | Süre öngörülebilir ve üst sınırı biliniyorsa |
2. pause() + async + boş poll() | at-least-once | Korunur | Süre değişken, hızlı çökme tespiti isteniyorsa |
3. Hemen ackle, arkada işle | at-most-once | Yok | Neredeyse hiç — aşağıya bakın |
| 4. İş tablosu (claim + reaper) | at-least-once, kendi veritabanınızda | Korunur | Saatler süren, durum ve iptal gereken işler |
1. Zaman aşımını yükseltmek
En basit ve çoğu zaman yeterli olan çözüm:
spring:
kafka:
consumer:
max-poll-records: 1 # bir poll = bir rapor
properties:
max.poll.interval.ms: 2700000 # 45 dakika
Bedeli şu: gerçekten çöken bir worker’ın partition’ı artık 45 dakika boyunca sahipsiz kalır. Yani hızlı hata tespitini uzun iş süresine feda etmiş olursunuz. İş süresinin bilinen bir üst sınırı varsa gayet makul bir takas.
2. pause() + async + boş poll()
Kafka’nın bu iş için tasarlanmış mekanizması. İşi arka plana atarsınız ama poll() çağırmayı sürdürürsünüz — sadece önce partition’ı duraklatarak, ki yeni kayıt gelmesin:
ConsumerRecords<String, Report> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, Report> record : records) {
consumer.pause(consumer.assignment()); // yeni kayıt isteme
Future<?> job = executor.submit(() -> generate(record.value()));
while (!job.isDone()) {
consumer.poll(Duration.ofSeconds(1)); // boş döner ama sayaç sıfırlanır
}
consumer.commitSync(); // iş gerçekten bitti
consumer.resume(consumer.assignment());
}
Duraklatılmış bir consumer’da poll() kayıt döndürmez, ama hem heartbeat’i hem max.poll.interval.ms sayacını canlı tutar. Böylece iş 30 dakika sürse de consumer gruptan atılmaz, üstelik gerçekten çökerse saniyeler içinde fark edilir.
Spring Kafka kullanıyorsanız aynı davranışa container’ın pause() / resume() metotlarıyla ulaşabilirsiniz; ama süre öngörülebilirse 1. yaklaşım daha az koda mal olur.
3. Hemen ackleyip arkada işlemek — neden tuzak?
Bu, iki ayrı problemi aynı anda yaratıyor:
At-most-once’a düşersiniz
Offset commit edildi, mesaj gitti. Worker 12. dakikada çökerse o rapor sessizce kaybolur — kimse istemediği hâlde beklemeye devam eder.
Backpressure’ı kaparsınız
poll()artık hiçbir şey beklemediği için kayıt akmaya devam eder ve hepsi executor’ın kuyruğunda birikir. Sınırsız birThreadPoolExecutorkuyruğu, bir önceki bölümdekiprefetch=0senaryosunun aynısına —OutOfMemoryError‘a — çıkar.Bunu engellemek için kuyruğu sınırlayıp doluyken
poll()‘ü durdurmanız gerekir — ki o noktada 2. yaklaşımı, daha kötü bir biçimde yeniden yazmış olursunuz.
Yani: raporu kaybetmek kabul edilebilir değilse bu yolu seçmeyin.
4. İş tablosu: mesajı tetikleyiciye indirgemek
Saatler süren, ilerleme gösteren, iptal edilebilen işler için doğru cevap genelde şu: işin dayanıklılığını broker’dan alıp kendi veritabanınıza koymak. Mesaj artık işin kendisi değil, sadece bir tetikleyici.
graph TD
M["report-jobs mesajı<br/>sadece bir job_id taşır"] --> C["Consumer"]
C --> CL["report_jobs satırını<br/>RUNNING'e çek<br/>(idempotent claim + lease)"]
CL --> OK["offset'i HEMEN commit et"]
OK --> W["işi arka planda yürüt,<br/>ilerlemeyi satıra yaz"]
W --> D["DONE"]
W --> F["FAILED + attempt++"]
RP["Reaper<br/>lease'i dolmuş RUNNING satırları"] -.->|"tekrar kuyruğa koy"| MBuradaki kilit nokta üçüncü kutu: offset hemen commit ediliyor ve bu 3. yaklaşımdaki gibi görünüyor — ama at-most-once değil. Çünkü dayanıklılık artık offset’te değil, veritabanı satırında. Broker’ın işi “tetikleyiciyi bir kez teslim et"e indi; işin gerçekten yapıldığının kaydını siz tutuyorsunuz.
Karşılığında elinize şunlar geçiyor:
- İşin durumunu ve ilerlemesini kullanıcıya gösterebilirsiniz
- Deneme sayısı, hata mesajı ve süre gibi bilgiler sorgulanabilir bir yerde durur
- Takılı kalan işler
leasesüresi dolduğunda reaper tarafından toplanır - İptal etmek, satırı
CANCELLED‘a çekmek kadar basit
Bu tasarımın ayrıntıları — idempotent claim, lease, stuck-job recovery, retry ve retention — Asenkron e-posta gönderim sistemi yazısında stack’ten bağımsız olarak anlatılıyor. Oradaki e-posta kuyruğu ile buradaki rapor kuyruğu aynı desenin iki örneği.
Not: bu yaklaşıma geçtiğinizde broker seçimi de büyük ölçüde önemsizleşir — iş yönetimi artık broker’ın değil, sizin tablonuzun işi.
Özet: uzun işler için kural
Kafka’da uzun işi tutmak bir ayar gerektirir —
max.poll.interval.msvarsayılanı 5 dakika ve aşıldığında sonuç sessiz bir kayıp değil, sonsuz bir tekrar döngüsüdür. RabbitMQ’da tutmak daha doğaldır amaconsumer_timeout‘un 30 dakikalık varsayılanını bilmek gerekir. Birkaç dakikayı geçen işlerde ikisinde de doğru cevap aynı: mesajı bir tetikleyiciye indirip işin durumunu kendi veritabanınızda tutmak.
Asıl ayrım: mesaj bir “iş” mi, bir “olgu” mu?
Buraya kadar iki modelin farklarına baktık. Şimdi asıl soruya gelelim: elinizde somut bir ihtiyaç varken hangisini seçeceksiniz?
Özellik listesi bu soruyu tek başına cevaplamıyor, çünkü listedeki maddelerin çoğunu ikisi de yapabiliyor. Daha ayırt edici bir soru şu:
Bu mesaj ne anlatıyor: yapılması gereken bir iş mi, yoksa olmuş bitmiş bir olgu mu?
"Bu raporu üret" → iş — yapılmalı, yapılınca değeri biter
"Sipariş 1042 oluşturuldu" → olgu — kim okursa okusun doğru, silinmesi bilgi kaybı
Bu ayrım geri kalan her şeyi belirliyor:
| İş (task) | Olgu (event) | |
|---|---|---|
| Kim ilgilenir | Tam olarak bir worker | Bilinmeyen sayıda okuyucu |
| İş bitince mesaj | Değersizdir, silinir | Hâlâ doğrudur, saklanır |
| İkinci kez işlemek | Zarar — ikinci fatura, ikinci e-posta | Fayda — projeksiyonu yeniden kurar |
| Başarısız olursa | Tekrar denenmeli; sonunda birinin yapması lazım | Olay yine de olmuştur, sorun tüketicidedir |
| Sıra | Genelde önemsiz | Entity bazında çoğu zaman önemli |
| Doğal veri yapısı | Kuyruk | Log |
Sağ sütun Kafka’nın, sol sütun RabbitMQ’nun tarif ettiği dünya. En çok dikkat çeken satır üçüncüsü:
Kafka’nın en güçlü özelliği — mesajın durması ve yeniden okunabilmesi — task queue tarafında bir kazanç değil, yönetilmesi gereken bir risktir. Yanlışlıkla geri alınan bir offset event akışında “projeksiyonu yeniden kur” demektir; e-posta kuyruğunda 40.000 kişiye ikinci kez e-posta demektir.
Bu soyut kaldığı sürece ikna edici olmuyor. O yüzden aynı işi iki broker’da da kuralım ve nerede zorlandığına bakalım.
Rapor üretimini Kafka ile kursak ne olurdu?
Somut bir senaryo:
İş : müşteri panelinden istenen raporlar
Hacim : günde ~12.000 istek
Süre : 2 saniye (günlük özet) – 8 dakika (aylık mutabakat)
Ay sonu : üç gün boyunca normalin ~20 katı istek
Gereksinim : başarısız rapor tekrar denenmeli, kurumsal müşteri öne alınmalı
Kafka kurulumu gayet makul görünüyor: report-jobs topic’i, 12 partition, report-workers consumer group’unda 12 worker. Ve çalışır — bu senaryoda Kafka’nın “yapamayacağı” bir şey yok. Sorunlar ilk gün değil, üçüncü ayda çıkıyor.
1. Uzun bir iş, arkasındaki bütün işleri bekletir
partition-7‘ye 8 dakikalık bir aylık mutabakat raporu düştü. O partition’ı grup içinde yalnızca bir worker işler ve mesajları sırayla alır. Arkadaki 40 tane iki saniyelik rapor, diğer 11 worker boşta beklerken 8 dakika sıra bekler.
graph LR
subgraph K["Kafka — atama yazma anında yapılır"]
KP7["partition-7<br/>8 dk · 2 sn · 2 sn · 2 sn"] --> KW7["worker-7<br/>meşgul, sıra ilerlemiyor"]
KP3["partition-3<br/>boş"] --> KW3["worker-3<br/>boşta"]
endgraph LR
subgraph R["RabbitMQ — atama teslim anında yapılır"]
RQ[("report-jobs<br/>8 dk · 2 sn · 2 sn · 2 sn")] --> RW1["worker-1<br/>8 dk'lık işi aldı"]
RQ --> RW2["worker-2<br/>sıradakini aldı"]
RQ --> RW3["worker-3<br/>sıradakini aldı"]
endFark tek bir cümlede toplanabiliyor ve task queue tartışmasının büyük kısmını da bu cümle açıklıyor:
Kafka: mesaj → partition → o partition'ın worker'ı (atama yazma anında)
RabbitMQ: mesaj → kuyruk → o an boşta olan worker (atama teslim anında)
İşlerin süresi birbirine yakınsa bu fark hissedilmez. Süreler değişkense — ki rapor, PDF ve resim işleme senaryolarında tipik olarak değişkendir — doğrudan kuyruk gecikmesine yansır.
2. Başarısız bir rapor
Bir rapor, S3’e yüklenirken timeout aldı. Kafka’da elinizde iki seçenek var, ikisi de rahatsız edici:
- Offset’i commit etmezseniz mesaj tekrar gelir, ama o partition ilerlemez. Hata kalıcıysa (bozuk parametre, silinmiş müşteri) partition sonsuza kadar aynı mesajda takılır ve arkasındaki her şey durur. Buna poison message deniyor.
- Commit edip mesajı bir retry topic’ine yazarsanız iş çözülür, ama Kafka’da “bunu beş dakika sonra teslim et” diye bir şey yok. Retry topic’inin consumer’ı mesajın timestamp’ine bakıp bekler — yani
pause()/resume()ile kendi partition’ını bilerek bekletir.
Yazmanız gereken katman şuna benziyor:
report-jobs → report-retry-5m → report-retry-30m → report-retry-2h → report-dlq
+ her seviye için ayrı bir consumer
+ timestamp'e bakıp bekleten pause/resume mantığı
+ orijinal topic, partition, offset ve hata bilgisini header'da taşıma
+ DLQ'dan seçerek geri gönderme aracı
+ izlenecek dört ek consumer lag metriği
RabbitMQ’da aynı davranışın karşılığı zaten kutudan çıkıyor:
channel.basicNack(tag, false, false); // → DLX → gecikmeli kuyruk → tekrar dene
Bunu Kafka aleyhine fazla büyütmemek gerekir: Spring Kafka’da @RetryableTopic bu topic’leri, consumer’ları ve DLQ akışını sizin yerinize kurar. Yine de altta duran yapı aynı kalır — n tane ek topic, n tane ek consumer ve izlenecek n tane ek lag metriği. Fark “mümkün mü” değil, “kaç hareketli parça” farkı.
3. Ay sonu: yirmi kat yük
Kuyruk birikti, worker sayısını artırmak istiyorsunuz.
Kafka
12 partition, tek consumer group
40 worker deploy edilir → 12'si çalışır, 28'i boşta bekler
Partition'ı 12 → 48 çıkarmak:
- key varsa aynı key farklı partition'a düşer, sıra garantisi geçişte kırılır
- sonradan azaltmak desteklenmez
- yani bu, ay sonunda alelacele verilecek bir karar değil
RabbitMQ
replicas: 12 → 40
bitti
Kafka’da partition sayısı bir kapasite tavanıdır ve önceden, kalıcı olarak seçilir. Task queue’larda yük genelde tam olarak öngörülemediği için bu tavan can sıkıcı bir yerde durur: fazla partition açarsanız gereksiz overhead, az açarsanız ölçekleme tavanı.
4. Kurumsal müşteriyi öne almak
Kafka’da log sıralıdır; “bu mesajı öne al” diye bir kavram yok. Yapabileceğiniz şey ayrı bir report-jobs-priority topic’i açıp worker’ın iki topic’i ağırlıklı okumasını sağlamak — yani öncelik mantığını, iki topic arasındaki adil payı ve açlık (starvation) korumasını siz yazarsınız.
RabbitMQ’da bu bir kuyruk argümanı:
// x-max-priority ile açılmış bir kuyruğa
rabbitTemplate.convertAndSend("report-jobs", job, m -> {
m.getMessageProperties().setPriority(customer.isEnterprise() ? 9 : 1);
return m;
});
Öncelikli kuyruk desteği klasik kuyruklarda uzun süredir mevcut; quorum queue kullanıyorsanız kendi RabbitMQ sürümünüzde önceliğin nasıl desteklendiğini doğrulamakta fayda var — model klasik kuyruktakinden daha sadedir.
5. “Bu raporu gece 03:00’te üret”
Kafka’da gecikmeli teslim yerleşik değil. Mesajı erken teslim alıp uygulama tarafında bekletmek de partition’ı bloklar; pratikte iş bir scheduler’a veya veritabanına taşınır.
RabbitMQ’da iki yerleşik yol var: mesaja TTL verip DLX üzerinden hedef kuyruğa düşürmek ya da delayed message plugin’ini kullanmak.
Peki Kafka’yı seçmek ne kazandırırdı?
Listenin tek taraflı görünmemesi için tersini de yazalım — Kafka’nın bu senaryoda gerçekten getirdikleri:
Hacim tavanı
Günde 12.000 iş Kafka için hiçbir şey. Aynı akış günde 12 milyona çıksaydı Kafka’nın diske dayalı tamponu ve sıralı I/O’su belirleyici olurdu.
Zaten kurulu olması
Kafka event backbone olarak çalışıyorsa, birkaç job türü için ikinci bir broker’ı işletmek, izlemek ve yükseltmek gerçek bir maliyettir. Retry-topic pattern’i bu maliyetin altında kalabilir.
İsteklerin geçmişi
Rapor istekleri log’da kaldığı için “hangi rapor ne sıklıkla isteniyor, hangi müşteri neyi çekiyor” sorusu topic’i baştan okuyarak cevaplanır. RabbitMQ’da bunun için ayrıca bir yere yazmanız gerekir.
Aynı isteği birden çok taraf tüketecekse
Rapor isteği hem işlenecek, hem kullanım kotasından düşülecek, hem audit log’a yazılacaksa: tek topic, üç consumer group.
Özet: “neden RabbitMQ” sorusunun cevabı
Yukarıdaki kazançlara bakın: hepsi gerçek, ama hiçbiri rapor üretme senaryosunun günlük ihtiyacı değil. Hacim düşük, tüketici tek, geçmişin analitik değeri varsa da bunu zaten veritabanından alıyorsunuz. Kaybettikleriniz ise — retry, öncelik, gecikme, esnek worker ölçeği, uzun işin arkasını bekletmemesi — tam olarak bu senaryonun her gün karşılaştığı şeyler.
Yani cevap şu:
RabbitMQ’yu seçme sebebi Kafka’nın bu işi yapamaması değil. Bu senaryoda Kafka’nın güçlü olduğu şeylere ihtiyacınız yok, zayıf olduğu şeyler ise tam da günlük ihtiyacınız.
Aynı cümleyi tersine çevirdiğinizde Kafka’yı seçme sebebini de elde ediyorsunuz — bir sonraki bölüm bunun için.
Ters yön: event akışını RabbitMQ ile kursak ne olurdu?
Bu sefer senaryo şu: OrderCreated olayı yayınlanıyor ve beş servis bunu tüketiyor — fatura, stok, e-posta, analitik, sadakat puanı.
RabbitMQ ile kurulumu zor değil: bir fanout exchange, ona bağlı beş durable kuyruk. Aylarca da sorunsuz çalışır. Zorluk, sistem yaşlandıkça çıkıyor.
1. Altıncı tüketici geliyor
Arama indeksi eklendi. Kuyruğunu bind ettiği andan sonraki mesajları görür; iki yıllık sipariş geçmişi orada yok.
Yapmanız gereken: veritabanından geçmişi tarayan ayrı bir backfill scripti. Ve bu script kendi problemlerini getiriyor — backfill ile canlı akışın çakıştığı yerde aynı sipariş iki kez işlenebilir ya da tam geçiş anındakiler kaçabilir; üstelik bu, üretimdeki asıl kod yolundan farklı, daha az test edilmiş ikinci bir yol.
Kafka’da aynı iş üç satırlık bir konfigürasyon:
spring:
kafka:
consumer:
group-id: search-indexer # yeni grup → kendi offset'i
auto-offset-reset: earliest # log'un başından oku
2. Tüketici altı saat bozuk çalıştı
Fatura servisi hatalı bir KDV hesabıyla deploy edildi ve altı saat boyunca mesajları işleyip ackledi. Mesajlar kuyruktan çıktı, geri dönüşü yok. Düzeltmek yine veritabanı taramasına kalıyor.
Kafka’da bu, consumer group’un offset’ini altı saat geri almaktan ibaret — mesajlar hâlâ log’da.
3. Geride kalan bir tüketici bütün broker’ı yavaşlatabilir
Analitik servisi üç saat düştü ve kuyruğunda milyonlarca mesaj birikti. RabbitMQ mesajları öncelikle bellekte tutmaya çalışır; bellek eşiği aşıldığında broker flow control uygular ve bu yalnızca o kuyruğu değil, publisher’ları yavaşlatır. Yani analitik servisinin sorunu sipariş servisine geri yansır.
Kafka’da geride kalan bir consumer için özel bir durum yok: mesajlar zaten diskte, o sadece daha eski bir offset’ten okur. Page cache dışına düşen okumalar disk I/O yaratır ama producer’lara yansımaz.
RabbitMQ: birikme broker'ın problemidir → yayılabilir
Kafka: birikme tüketicinin problemidir → izole kalır
İki model arasındaki en az konuşulan, ama operasyonda en çok hissedilen farklardan biri bu.
Lazy queue ve quorum queue davranışları bu tabloyu yumuşatır: mesajlar daha erken diske alınır ve bellek baskısı azalır. Ancak sürekli diske yazma da bedelsiz değildir ve flow control’ün publisher’a yansıyabilmesi modelin bir parçası olmaya devam eder.
4. Her tüketici için bir kopya
Fanout exchange mesajı beş kuyruğa kopyalar. Beş milyon olay, beş kuyrukta beş milyon kopya demek. Kafka’da ise bir log ve beş offset var; tüketici eklemek depolama maliyetini artırmaz.
5. Entity bazlı sıra
Sipariş 1042’nin created, paid, shipped olayları tek kuyruk ve tek consumer’da sırayla işlenir. Ölçek için ikinci consumer’ı bağladığınız anda bu üç mesaj üç farklı worker’a düşebilir ve shipped, paid‘den önce işlenebilir.
Kafka’da key = orderId demek yeterli: aynı siparişin olayları aynı partition’a, dolayısıyla aynı consumer’a, sırayla gider.
6. Etrafındaki araçlar
Debezium ile CDC, Kafka Connect ile hedeflere yazma, Kafka Streams veya Flink ile akış üzerinde işlem, şema kayıt defteri ile şema evrimi — bu ekosistem Kafka’nın etrafında kurulu. Event backbone kuruyorsanız bir süre sonra bunlardan en az birine ihtiyaç duyuyorsunuz.
Özet: “neden Kafka” sorusunun cevabı
Yukarıdaki maddelerin hiçbiri “RabbitMQ bunu yapamaz” demiyor; her birinin bir çözümü var. Ama dikkat edin, hepsinin çözümü sizin yazacağınız ve işleteceğiniz bir şey: backfill scripti, düzeltme taraması, ayrı bir analitik deposu, kopya kuyrukların maliyeti, consistent hash plugin’i.
Kafka’da bunlar modelin kendisinden çıkıyor — çünkü mesaj zaten duruyor ve konumu zaten tüketicide.
Kafka’yı seçme sebebi throughput değil. Mesajın işlendikten sonra da değerli olması — yarın yeni bir tüketicinin, düzeltilmiş bir kodun ya da yeni bir sorunun aynı geçmişe ihtiyaç duyması.
“İkisinde de var” itirazı: aynı özellik, iki farklı implementasyon
Bu tartışmanın en sık takıldığı yer burası. “RabbitMQ’da da fanout var”, “dead letter queue ikisinde de var”, “retry ikisinde de yapılıyor” — üçü de doğru. Özellik listesi seviyesinde bakınca fark kalmıyor gibi görünüyor.
Fark listede değil, mekanizmanın kime ait olduğunda. RabbitMQ’da teslim durumunu broker tutar; dolayısıyla retry sayacı, gecikme, dead-lettering ve mesajı reddetme broker özelliğidir, konfigüre edersiniz. Kafka’da konumu consumer tutar; dolayısıyla aynı şeyler uygulama kodu ve ek topic’e dönüşür.
Bu farkın üç somut yerde nasıl göründüğüne bakalım.
Fan-out: kopyalanan şey mesaj mı, konum mu?
RabbitMQ’nun fanout exchange’i de, Kafka’nın consumer group’ları da aynı sonucu üretir: bir olay beş servise ulaşır. Ama üretme biçimleri farklı:
RabbitMQ fanout: broker mesajı 5 kuyruğa KOPYALAR → 5 mesaj
Kafka groups: tek log, 5 grup kendi OFFSET'ini tutar → 1 mesaj, 5 sayı
Aşağıdaki satırların hepsi bu tek farktan çıkıyor:
| RabbitMQ fanout | Kafka consumer group | |
|---|---|---|
| Topolojiyi kim belirler | Broker — exchange ve binding’ler | Consumer — kendi subscribe eder |
| Yeni tüketici ne görür | Yalnızca bind ettiği andan sonrasını | auto-offset-reset=earliest ile retention boyunca her şeyi |
| Depolama maliyeti | Tüketici sayısı kadar kopya | Sabit; tüketici eklemek maliyeti artırmaz |
| Yavaş tüketici | Kendi kuyruğu şişer; bellek eşiği aşılırsa flow control publisher’a yansır | Lag’i büyür; diskten okur, producer etkilenmez |
| Unutulmuş tüketici | Kimse tüketmeyen kuyruk sonsuza kadar büyür — klasik bir üretim kazası | Offset eskir, retention deposu sınırlar |
| Geri okuma | Mümkün değil, mesaj ack ile gitti | Offset’i geri al |
| Filtreleme | Broker tarafında — routing key, header, pattern | Consumer tarafında ya da ayrı topic |
Yani “RabbitMQ’da da fanout var” cümlesi doğru ama eksik: fanout bugünden itibaren dağıtır. Kafka’nın consumer group’u ise dağıtmaz — herkesin aynı geçmişe kendi hızında bakmasına izin verir.
Pratik ayırt edici soru şu: “Yarın bu olaya yeni bir tüketici eklersem, geçmişe ihtiyacı olur mu?”
Hayırsa fanout yeterlidir ve tartışmayı orada kapatabilirsiniz. Evetse RabbitMQ tarafında cevap “backfill scripti yazarsınız” olur; Kafka tarafında cevap bir satır konfigürasyondur.
Retry ve backoff: kim bekletiyor?
“Retry ikisinde de var” da doğru. Ama retry’ın asıl sorusu şu: bekleme süresi boyunca ne bloklanıyor?
RabbitMQ’da
En basit yol nack(requeue=true) — ve buradaki tuzağı bilmekte fayda var: requeue edilen mesaj kuyruğun sonuna değil, mümkün olduğunca kendi eski konumuna geri konur. Yani hemen tekrar teslim edilir. Kalıcı bir hatada bu, saniyede binlerce denemeye çıkan sıcak bir döngüdür.
Gerçek backoff için gecikmeli bir kuyruk merdiveni kurulur:
work-queue ──nack(requeue=false)──▶ work-dlx ──▶ retry-5s (TTL=5s, consumer yok)
│ TTL dolar → dead-letter
▼
work-exchange ──▶ work-queue
retry-5s, retry-30s, retry-5m diye üç kuyruk açar ve x-death başlığındaki deneme sayısına göre hangisine göndereceğinize karar verirsiniz. Deneme sayacını broker tutuyor — siz taşımıyorsunuz:
List<Map<String, ?>> death = (List<Map<String, ?>>)
message.getMessageProperties().getHeaders().get("x-death");
long attempts = death == null ? 0 : (Long) death.get(0).get("count");
Alternatif olarak rabbitmq_delayed_message_exchange plugin’i mesaj başına serbest gecikme verir ve merdiveni tamamen ortadan kaldırır.
Mesaj başına TTL'de baş-blokajı
Gecikmeyi mesaj başına TTL ile kurmak cazip görünür ama kuyrukta mesajlar yalnızca baştan süre aşımına uğrar. 30 saniyelik TTL’i olan bir mesajın arkasındaki 5 saniyelik mesaj, öndeki mesaj kuyruğun başından çıkana kadar dead-letter edilmez.
Bu yüzden gecikme merdiveni kuyruk başına sabit TTL ile kurulur: retry-5s, retry-30s, retry-5m.
Kafka’da
İki seçenek var ve seçim doğrudan bir ödünleşme:
// Spring Kafka — partition'ı duraklatıp aynı yerde tekrar dener
new DefaultErrorHandler(recoverer, new ExponentialBackOff(1_000L, 2.0));
- Sıra korunur — mesaj hâlâ kendi partition’ında
- Ama o partition ilerlemez; arkadaki her şey bekler
- Uzun backoff
max.poll.interval.ms‘i zorlar; aşılırsa consumer gruptan atılır ve rebalance başlar
@RetryableTopic(attempts = "4",
backoff = @Backoff(delay = 1_000, multiplier = 2.0))
@KafkaListener(topics = "report-jobs")
public void handle(ReportJob job) { ... }
Arka planda report-jobs-retry-0, -retry-1, -retry-2 ve report-jobs-dlt topic’lerini açar.
- Partition ilerler, uzun backoff mümkün
- Ama mesaj artık başka bir topic’te: aynı key için sıra garantisi kırıldı
Ödünleşmeyi tabloya dökersek:
| Sıra korunur | Partition ilerler | Uzun backoff | |
|---|---|---|---|
| Kafka — blocking retry | ✅ | ❌ | ❌ |
| Kafka — retry topic’leri | ❌ | ✅ | ✅ |
RabbitMQ — nack(requeue=true) | — | ⚠️ sıcak döngü | ❌ |
| RabbitMQ — TTL + DLX merdiveni | — | ✅ | ✅ |
RabbitMQ satırlarında “sıra” sütununun boş olması tesadüf değil: orada zaten çok consumer’lı bir kuyrukta sıra garantisi yok, dolayısıyla retry bir şey bozmuyor. Kafka’da ise retry, kaybedecek bir garantiyi var olduğu için ödünleşmeye dönüşüyor.
Asıl fark bu: RabbitMQ’da retry, mesajın kendi problemidir. Kafka’da retry, ya partition’ın ilerlemesini ya da sıralama garantisini feda etmenizi gerektirir.
DLQ: broker özelliği mi, sizin yazdığınız topic mi?
“Dead letter queue ikisinde de var” cümlesi burada en çok yanıltan cümle. RabbitMQ’da DLX bir broker mekanizması; Kafka’da DLT sadece başka bir topic — oraya yazan da sizin consumer’ınız.
| RabbitMQ (DLX) | Kafka (DLT) | |
|---|---|---|
| Broker özelliği mi | Evet — kuyruk argümanı | Hayır — sıradan bir topic |
| Neyle tetiklenir | nack(requeue=false), mesaj/kuyruk TTL’i, kuyruk uzunluk limiti, quorum queue’da x-delivery-limit | Yalnızca consumer kodunun oraya yazmasıyla |
| Consumer ayaktayken değilse | TTL ve uzunluk limiti yine çalışır, mesaj yine DLX’e düşer | Hiçbir şey olmaz; kimse yazmaz |
| Deneme sayısını kim tutar | Broker — x-death.count | Siz — header’da taşırsınız |
| Ne kaydedilir | Sebep, kaynak exchange, routing key, kuyruk, sayı, zaman | Ne koyarsanız; Spring Kafka orijinal topic/partition/offset ve exception bilgisini ekler |
| Zehirli mesajın etki alanı | Yalnızca o mesaj | Blocking retry’da tüm partition |
| Geri gönderme | Management UI veya shovel ile kuyruklar arası taşıma | Header’ları temizleyip orijinal topic’e yeniden yazan bir araç yazarsınız |
Bu tablodaki en pratik satır dördüncüsü. Kafka’da bir mesajın kaç kez denendiğini bilmek istiyorsanız bu bilgiyi header’a siz koyar, retry topic’leri arasında siz taşır ve DLT’ye siz yazarsınız. Zincirin herhangi bir halkasında bunu unutan bir kod, sonsuz retry döngüsü üretir.
İki ince nokta:
RabbitMQ’da klasik dead-lettering at-most-once’tır: mesaj DLX’e giderken kaybolabilir. Quorum queue’larda
dead-letter-strategyayarıyla at-least-once’a çekilebilir; karşılığında ek yük gelir.Kafka’da DLT’ye yazan taraf consumer olduğu için, DLT’ye yazma işleminin kendisi de başarısız olabilir. Bu durumda ne yapacağınıza — atla, blokla, yerel diske düş — karar vermeniz gerekir.
Bonus: consumer çökerse ne kadarı tekrar işlenir?
Retry tartışmasının az konuşulan tarafı bu ve implementasyon farkı burada çok net:
RabbitMQ: consumer düşer → kanal kapanır
→ yalnızca o consumer'ın unacked mesajları geri konur
→ tanecik: mesaj
Kafka: consumer düşer → rebalance
→ yeni sahip son commit'lenmiş offset'ten başlar
→ tanecik: son commit'ten bu yana gelen her şey
enable.auto.commit=true ve 5 saniyelik commit aralığıyla çalışan bir Kafka consumer’ının çökmesi, beş saniyelik trafiğin tamamının yeniden işlenmesi demektir. RabbitMQ’da aynı olay, o an elde tutulan prefetch kadar mesajın geri konmasıdır.
İkisi de at-least-once; ama “en az bir kez"in pratikte kaç mesaj ettiği farklı.
Toparlarsak
Üç başlıkta da aynı cümle çıkıyor karşımıza:
RabbitMQ teslim durumunu takip ettiği için retry, gecikme, dead-lettering ve reddetme birer broker ayarıdır. Kafka konumu takip ettiği için aynı davranışlar birer uygulama kararı, ek topic ve ek header’dır.
“İkisinde de var” doğru. Ama birinde konfigürasyon, ötekinde kod — ve işletilecek olan da o kod.
Karar için altı soru
İki senaryoyu da gördükten sonra seçimi altı soruya indirgeyebiliriz. Cevaplar genelde aynı tarafa yığılır; yığılmıyorsa muhtemelen tek bir broker’la çözülecek bir problem değildir.
| Soru | Kafka’ya işaret eder | RabbitMQ’ya işaret eder |
|---|---|---|
| 1. Mesaj ne anlatıyor? | “Şu oldu” — bir olgu | “Şunu yap” — bir iş |
| 2. Kaç taraf tüketecek? | Bilinmeyen sayıda, bağımsız | Tam olarak biri |
| 3. Yarın yeni bir tüketici geçmişe ihtiyaç duyar mı? | Evet | Hayır, dünkü iş dünde kaldı |
| 4. Aynı mesajı ikinci kez işlemek? | Faydalı — durumu yeniden kurar | Zararlı — çift gönderim, çift ücret |
| 5. İşlerin süresi nasıl? | Birbirine yakın, kısa | Değişken — saniyeler ile dakikalar arası |
| 6. Mesaj başına kontrol gerekiyor mu? | Hayır | Evet — öncelik, gecikme, reddetme, retry |
Aynı akışın karar ağacı hali:
graph TD
A["Bu mesaj ne anlatıyor?"] -->|"'Şu oldu' — olgu"| C{"Geçmişe ihtiyaç var mı?<br/>replay, yeni tüketici,<br/>projeksiyon yeniden kurma"}
A -->|"'Şunu yap' — iş"| B{"Kaç taraf işleyecek?"}
B -->|"Birden çok bağımsız taraf"| C
B -->|"Tek worker"| D{"Mesaj başına kontrol?<br/>öncelik, gecikme,<br/>reddetme, değişken süre"}
D -->|"Evet"| R["RabbitMQ"]
D -->|"Hayır, ama hacim çok yüksek"| K["Kafka"]
C -->|"Evet"| K
C -->|"Hayır"| RYedinci bir soru daha var ve pratikte ilk altısından daha belirleyici olabiliyor: hangisi zaten kurulu ve ekip hangisini işletmeyi biliyor? İkinci bir broker; cluster, izleme, alarm, yedekleme ve sürüm yükseltme demek. Marjinal bir kazanç için bu yükü almak çoğu zaman doğru karar değildir — bu da meşru bir mühendislik gerekçesidir.
Bu hesabı değiştiren iki güncel gelişme var:
KRaft. Kafka artık ZooKeeper’sız çalışıyor; metadata’yı kendi Raft tabanlı quorum’unda tutuyor. KRaft 3.3’ten beri üretime hazır, 4.0’dan itibaren de tek seçenek. Bu, “Kafka işletmek iki ayrı dağıtık sistem işletmek demek” argümanını ortadan kaldırdı. Geri kalan yük — partition ve replication planlama, consumer lag izleme — yerinde duruyor.
Yönetilen servisler. MSK, Confluent Cloud, Redpanda Cloud ya da RabbitMQ tarafında CloudAMQP gibi seçenekler operasyonel yükün büyük bölümünü devralıyor. O noktada soru “hangisini işletebiliriz"den “hangisinin faturasını ve bağımlılığını kabul ederiz"e kayıyor.
Broker seçiminden önce cevaplanması gereken bir soru daha var: mesajın kuyruğa girdiğini garanti edebiliyor musunuz? Veritabanına yazdıktan sonra broker’a publish edene kadar süreç çökerse, sipariş kaydedilmiş ama e-posta hiç gönderilmemiş olur.
Bu problem broker seçiminden bağımsızdır ve çözümü outbox pattern’idir. Asenkron e-posta gönderim sistemi yazısında bu tasarımı stack’ten bağımsız olarak ele almıştım.
Genel karar
Altı soruyu somut ihtiyaç başlıklarına indirgersek:
Kafka daha uygun olabilir
- Event streaming / event sourcing yapıyorsanız; olaylar sistemin kalıcı kaydıysa
- Çok yüksek throughput gerekiyorsa — log toplama, clickstream, telemetri, IoT
- Aynı veriyi birden çok bağımsız tüketici okuyacaksa
- Entity bazlı sıralama gerekiyorsa
- Stream processing yapacaksanız — Kafka Streams, Flink, ksqlDB
- Yeni servisleri geçmiş veriyle besleyerek devreye almak istiyorsanız
- CDC kuruyorsanız — Debezium ile veritabanı değişikliklerini yayınlamak
RabbitMQ daha uygun olabilir
- Klasik task queue / background job ihtiyacınız varsa
- Karmaşık routing gerekiyorsa — içeriğe veya etikete göre dağıtım
- Mesaj başına iş takibi istiyorsanız — ack, nack, retry, DLX
- Request/reply (RPC) deseni kuracaksanız
- Öncelik, TTL, gecikmeli mesaj gibi kontroller gerekiyorsa
- Mesaj işlendikten sonra saklanmasına gerek yoksa
- Daha basit operasyon ve düşük kaynak tüketimi istiyorsanız
- AMQP, MQTT, STOMP gibi farklı protokoller gerekiyorsa
Nereden başlamalı sorusuna da şöyle bakılabilir: klasik bir task queue ihtiyacınız varsa RabbitMQ daha doğal bir başlangıç noktasıdır. Event streaming, replay veya yüksek hacimli akış işleme ihtiyacı varsa Kafka’yı değerlendirmek daha anlamlı olur.
İkisi rakip olmaktan çok tamamlayıcı; büyük sistemlerde ikisi bir arada kullanılır — event backbone için Kafka, iş kuyrukları için RabbitMQ.
Hangisini seçerseniz seçin, izlenecek metrik farklı: Kafka’da consumer lag (grubun kaç mesaj geride olduğu), RabbitMQ’da kuyruk derinliği ve unacked mesaj sayısı. İkisi de “consumer’lar yetişemiyor” durumunun ilk göstergesidir.
Hızlı karşılaştırma
| Özellik | Kafka | RabbitMQ |
|---|---|---|
| Model | Dağıtık log / event streaming | Message queue |
| Mesaj saklama | Retention süresince log’da kalır | Ack sonrası kuyruktan çıkar |
| Replay | Offset ile | Streams dışında yok |
| Routing | Topic, partition, key | Dört exchange tipi |
| Sıralama | Partition içinde garantili | Tek consumer’da FIFO |
| Delivery | At-most / at-least-once, sınırlı exactly-once | At-most / at-least-once |
| Retry & hata | Retry topic + DLQ tasarlanır | Yerleşik — nack, requeue, DLX, TTL |
| Push / pull | Pull | Push, prefetch ile kontrollü |
| Backpressure | Modelin kendisinde — poll() çağırmazsanız gelmez | prefetch ayarında — 0 sınırsız demek |
| Uzun süren iş | max.poll.interval.ms (varsayılan 5 dk) yükseltilmeli | consumer_timeout (varsayılan 30 dk) yükseltilmeli |
| Grup paralelliği | Partition sayısıyla sınırlı | Partition sayısıyla sınırlı değil |
| Throughput | Çok yüksek | Yüksek |
| Gecikme | Düşük, batch ayarına bağlı | Çok düşük |
| Öncelikli mesaj | Uygulama tarafında | Yerleşik |
| Gecikmeli mesaj | Uygulama tarafında | Plugin ile |
| RPC deseni | Uygun değil | Doğal destek |
| Operasyonel yük | Daha yüksek — KRaft ile azaldı ama duruyor | Daha düşük |
| Ekosistem | Connect, Streams, ksqlDB, Debezium | Geniş dil desteği, yönetim UI’ı, plugin’ler |
Toparlarsak
Kafka ve RabbitMQ aynı probleme farklı modellerle yaklaşıyor.
Kafka’da mesajlar log’da kalıyor ve consumer’lar kendi konumlarını takip ediyor. Bu model replay, bağımsız consumer grupları ve event streaming gibi ihtiyaçlarda güçlü.
RabbitMQ’da ise broker mesajın teslim durumunu takip ediyor. Ack, requeue, routing ve DLX gibi özellikler task queue senaryolarında daha doğal bir model sunuyor.
Seçim yaparken “hangisi daha hızlı?” sorusu pek yol göstermiyor; iki tarafın da somut senaryolarını kurunca ortaya çıkan soru şu:
Bu mesaj işlendikten sonra hâlâ değerli mi?
Değerliyse — yarın yeni bir tüketici, düzeltilmiş bir kod ya da yeni bir soru aynı geçmişe ihtiyaç duyacaksa — mesaj bir olgudur ve log’da durması gerekir: Kafka.
Değilse — iş yapıldığında bitiyorsa, önemli olan onun bir kez ve sonunda mutlaka yapılması, gerekirse önceliklendirilmesi, ertelenmesi ve reddedilebilmesiyse — mesaj bir iştir ve kuyrukta durması gerekir: RabbitMQ.
İki sistemi aynı mimaride birlikte kullanmak da gayet normaldir; zaten çoğu sistemde her iki tür mesaj da var.

Yorumlar