Bir video yüklendiğinde ne olmalı?
Kullanıcı bir video yükledi. Arka planda üç iş yapılmalı:
- küçük resim (thumbnail) üretmek
- farklı çözünürlüklere dönüştürmek (transcode)
- altyazı çıkarmak
En basit yaklaşım, hepsini yükleme isteğinin içinde sırayla yapmaktır:
request → [thumbnail 30ms] → [transcode 2sn] → [altyazı 150ms] → response
Bu tasarımın üç sorunu var:
- Kullanıcı 2+ saniye bekliyor. Oysa onu ilgilendiren tek şey videonun alınmış olması.
- Bir bölüm koparsa zincir kopuyor. Altyazı servisi çökmüşse yükleme başarısız olur. Video yüklemeyi engeller.
- Yük dalgalanması kaldırılamıyor. Saniyede 500 yükleme gelirken transcode 5 tane yapabiliyorsa sistem çöker.
Çözüm araya bir mesaj kuyruğu koymak. Ama asıl mesele bu kuyruğu nasıl koyduğumuz. Bu yazıda RabbitMQ üzerinde şunları kuracağız:
- tek olayın üç bağımsız hatta dağıtılması
- her hattın kendi retry bütçesi ve kendi dead letter queue’su
- quorum queue ile küme genelinde dayanıklılık
- yük altında worker’ların kendiliğinden artıp azalması
- ve tüm bunları Grafana’da izlemek
Mimari
POST /media/upload
│
▼ TEK yayın, routing key = "media.all"
┌───────────────────────────┐
│ media.topic (topic) │
└─────────────┬─────────────┘
binding: "media.*" │
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
media.thumbnail media.transcode media.subtitle
~30 ms ~2 saniye ~150 ms + %10 HATA
│ │ │
└───── retry bitti → mesajı reddet (requeue yok) ────────┤
▼
┌───────────────────────────┐
│ media.dlx (direct) │
└─────────────┬─────────────┘
┌────────────────────┼────────────────────┐
▼ ▼ ▼
media.thumbnail.dlq media.transcode.dlq media.subtitle.dlq
1. Her kanalın kendi kuyruğu var
Tek bir ortak kuyruk olsaydı eğer 2 saniyelik transcode işleri 30 milisaniyelik thumbnail işlerini tıkardı.
Ayrı kuyruklarla:
- altyazı servisi 30 saniye cevap vermese bile thumbnail ve transcode akmaya devam eder
- yalnızca transcode yavaşsa yalnızca onun worker’ları artırılır
media.subtitle.dlqderinliğine bakıp doğrudan “altyazı hattı bozuk” denebilir
2. Producer, kanalların varlığından habersiz
Uygulama tek bir yayın yapar: media.all. Üç kuyruk da media.* deseniyle bağlı olduğu için broker mesajı üçe kopyalar.
Yarın dördüncü bir kanal eklemek istediğinizde yeni bir kuyruk ve yeni bir binding tanımlarsınız herhangi bir şekilde koda dokunmanıza gerek kalmaz.
3. Ana exchange topic, dead letter exchange direct
Ana exchange de üç kuyruk birden etkileniyor. media.* yani media.subtitle, transcode, thumbnail bu topic yaklaşımıyla eşleşiyor. Yarın media.test diye bir kuyruk eklenirse de eşleşme sağlanacaktır. Dead letter exchange de ise direct kullanıyoruz çünkü desen eşleştirme istemiyoruz. Hangi hata hangi DLQ’ya gidecek, kesin olmalı. subtitle → yalnızca media.subtitle.dlq gitmeli.
Peki neden Kubernetes?
Kurulum adımlarına girmeyeceğim ama neden orada çalıştığımızı söylemek gerekiyor — üç sebep var:
- Quorum queue tek düğümde anlamsızdır. Dayanıklılığı Raft konsensüsünden gelir: mesaj ancak düğümlerin çoğunluğu diske yazdığında onaylanır. Bunu görebilmek için gerçek bir kümeye ihtiyaç var — biz 3 replikalı bir RabbitMQ kümesi çalıştırıyoruz.
- Ölçekleme ve çökme senaryolarını gerçekten test edebilmek için. Pod’u öldürüp mesajların ne olduğuna bakmak, laptop’ta
Ctrl+Cyapmaktan farklıdır. - Gözlemlenebilirlik hazır geliyor. Prometheus, Grafana ve yük üretici (k6) aynı küme içinde çalışıyor; RabbitMQ’nun kendi metrikleri ile uygulamanın metriklerini yan yana koyabiliyoruz.
RabbitMQ kümesini RabbitMQ Cluster Operator yönetiyor: tek bir YAML tanımı yazıyoruz, operator gerisini (StatefulSet, servisler, parolalar, sertifikalar) kendisi kuruyor.
Kısaca kubernetes ortamını da aktarayım ancak çok detayına girmeyeceğim.
Yerel makinede kind ile kurulmuş 1 control-plane + 3 worker düğümlü bir küme.
mediaflow-worker ← rabbitmq-server-2
mediaflow-worker2 ← rabbitmq-server-1
mediaflow-worker3 ← rabbitmq-server-0 + mediaflow (uygulama)
RabbitMQ'nun üç replikası üç ayrı düğüme dağılmış durumda — quorum'un anlamı bu: bir düğüm giderse kalan ikisi çoğunluğu oluşturur ve kuyruklar çalışmaya devam eder.

Grafana’nın hazır gelen “Kubernetes / Compute Resources / Namespace (Pods)” panosu.
Şimdi kod tarafını inceleyelim.
Uygulama Node.js ve Express ile yazıldı, RabbitMQ istemcisi olarak amqplib kullanıyor. Bağlantı kurma, kanal açma gibi standart kısımları atlayıp öğretici olan kısımları göstereceğim.
Adım 1 — Topolojiyi kodla tanımlamak
Exchange’leri, kuyrukları ve binding’leri RabbitMQ panelinden elle oluşturmak yerine uygulama açılışında kodla tanımlıyoruz.
// Ana exchange: topic — tek yayın, çok kural
await ch.assertExchange('media.topic', 'topic', { durable: true });
// Dead letter exchange: direct — hangi hata hangi DLQ'ya, kesin
await ch.assertExchange('media.dlx', 'direct', { durable: true });
for (const channel of ['thumbnail', 'transcode', 'subtitle']) {
// Ana kuyruk
await ch.assertQueue(`media.${channel}`, {
durable: true,
arguments: {
'x-queue-type': 'quorum', // küme genelinde replike
'x-delivery-limit': CHANNELS[channel].deliveryLimit,
'x-dead-letter-exchange': 'media.dlx', // işlenemezse buraya
'x-dead-letter-routing-key': channel,
'x-dead-letter-strategy': 'at-least-once', // ↓ aşağıdaki nota bak
'x-overflow': 'reject-publish',
},
});
// Bu kanalın kendi DLQ'su
await ch.assertQueue(`media.${channel}.dlq`, {
durable: true,
arguments: { 'x-queue-type': 'quorum', 'x-delivery-limit': 100 },
});
await ch.bindQueue(`media.${channel}`, 'media.topic', 'media.*'); // desen
await ch.bindQueue(`media.${channel}.dlq`, 'media.dlx', channel); // tam eşleşme
}
x-queue-type: quorum — Kuyruk, Raft algoritmasıyla küme genelinde replike edilir. Bir düğüm ölse bile mesaj yaşar.
x-dead-letter-exchange + routing-key — Mesaj artık işlenemiyorsa nereye gideceğini söyler. DLQ’ya doğrudan bağlanmıyoruz. Mesaj önce dead letter exchange’e yeniden yönlendiriliyor, oradaki binding onu doğru DLQ’ya ulaştırıyor.
x-dead-letter-strategy: at-least-once + x-overflow: reject-publish — Bu ikisi çoğu projede eksiktir ve DLQ’yu delik bırakır. Quorum queue’da dead-lettering varsayılan olarak at-most-once’tır: kuyruktan DLX’e aktarım sırasında mesaj kaybolabilir. at-least-once bunu düzeltir, ama RabbitMQ onu yalnızca overflow davranışı reject-publish ise kabul eder. İkisi birlikte gitmek zorunda.
$ kubectl exec rabbitmq-server-0 -c rabbitmq -- \
rabbitmqctl list_queues name type arguments
media.thumbnail quorum [{"x-queue-type","quorum"},{"x-delivery-limit",10},
{"x-dead-letter-exchange","media.dlx"},
{"x-dead-letter-routing-key","thumbnail"},
{"x-dead-letter-strategy","at-least-once"},
{"x-overflow","reject-publish"}]
media.transcode quorum [... {"x-delivery-limit",5} ...]
media.subtitle quorum [... {"x-delivery-limit",3} ...]
media.thumbnail.dlq quorum [{"x-queue-type","quorum"},{"x-delivery-limit",100}]
media.transcode.dlq quorum [{"x-queue-type","quorum"},{"x-delivery-limit",100}]
media.subtitle.dlq quorum [{"x-queue-type","quorum"},{"x-delivery-limit",100}]
Altı kuyruk da quorum, her birinin kendi teslim limiti ve kendi DLQ yönlendirmesi var.
Yönetim panelinde binding’ler şöyle görünüyor:

Type topic, ve üç kuyruk da aynı media.* routing key deseniyle bağlı. “Tek yayın, üç kopya” davranışının kaynağı bu tablo.
app.post('/media/upload', async (req, res) => {
const event = parseUpload(req.body);
await publish('media.topic', 'media.all', event); // TEK yayın
res.status(202).json({ eventId: event.eventId });
});
Burada thumbnail, transcode, subtitle kelimeleri hiç geçmiyor. Uygulama sadece “bu medya yüklendi” diyor.
Broker tarafında olan şu:
"media.all"
│
▼
media.topic
│ üç binding de "media.*" desenine uyuyor
┌─────────────────┼─────────────────┐
▼ ▼ ▼
media.thumbnail media.transcode media.subtitle
1 kopya 1 kopya 1 kopya
Bir mesaj gönderdik, RabbitMQ üç kuyruğa birer kopya koydu.
Üç kanal aynı olayı alıyor ama tamamen farklı davranıyor. Tüm ayarlar tek bir yerde:
export const CHANNELS = {
thumbnail: { latencyMs: 30, failRate: 0, workers: [4, 16], prefetch: 20, retry: 3 },
transcode: { latencyMs: 2000, failRate: 0, workers: [2, 8], prefetch: 5, retry: 3 },
subtitle: { latencyMs: 150, failRate: 0.1, workers: [2, 4], prefetch: 10, retry: 2 },
};
Sağlayıcılar simüle ediliyor. biri yavaş, biri kararsız:
async function callProvider(channel, event) {
const spec = CHANNELS[channel];
await sleep(spec.latencyMs); // dış servis gecikmesi
// ~%10 asset DETERMİNİSTİK olarak patlar
if (hash(`${channel}:${event.assetId}`) % 100 < spec.failRate * 100) {
throw new ProviderError(`${channel} sağlayıcısı 503 döndü`);
}
}
Concurrency ve prefetch — karıştırılan iki kavram
await ch.prefetch(spec.prefetch); // consumer BAŞINA sınır
for (let i = 0; i < workerCount; i++) {
await ch.consume(queue, handler); // her consume() bir "worker"
}
| concurrency | Aynı kuyruk üzerinde kaç consumer paralel çalışabilir? |
| prefetch | Her consumer’ın üzerinde aynı anda kaç ack’lenmemiş mesaj bulunabilir? |
Uçuştaki mesaj sayısı = consumers × prefetch
thumbnail tepede 16 × 20 = 320 mesajı aynı anda elinde tutabilir. Uygulama o an çökerse bu 320 mesajın hepsi yeniden teslim edilir.
Bunu bilmeden prefetch büyütmek cazip görünür: “daha çok mesaj çekeyim, daha hızlı olsun.” Gerçekte yaptığınız şey kuyruğu broker’dan kendi belleğinize taşımaktır
Güvenilirlik: dört ayrı mekanizma
Bu dördü sık karıştırılır. Her biri başka bir problemi çözer:
Üretici RabbitMQ Tüketici
│ │ │
├── yayınla ───────────► │ │
│ ◄──── ACK / NACK ───── │ ① PUBLISHER CONFIRM │
│ ◄──── iade ─────────── │ ② PUBLISHER RETURN │
│ │ │
│ ├──── teslim ──────────────► │
│ │ ├── hata → ③ RETRY
│ │ ◄──── reddet (requeue yok)─┤
│ │ │
│ └──► ④ DLQ │
| # | Mekanizma | Cevapladığı soru | Olmazsa |
|---|---|---|---|
| ① | Publisher confirm | Broker mesajı kabul etti mi? | “Gönderdim” sanırsınız, mesaj hiç ulaşmamıştır |
| ② | Publisher return | Mesaj bir kuyruğa yönlendirilebildi mi? | Yanlış routing key sessizce çöpe gider |
| ③ | Consumer retry | Geçici hata düzelir mi? | İlk ağ hıçkırığında mesaj DLQ’ya düşer |
| ④ | DLQ | Artık işlenemeyen mesaj nereye? | Mesaj kaybolur ya da sonsuz döngüye girer |
yayın tarafı
const ch = await conn.createConfirmChannel();
ch.on('return', (msg) => {
// Exchange'e ulaştı ama hiçbir kuyruğa yönlendirilemedi
log.warn({ routingKey: msg.fields.routingKey }, 'unroutable mesaj');
});
ch.publish('media.topic', 'media.all', payload,
{ persistent: true, mandatory: true },
(err) => err ? nacked++ : confirmed++);
En sık yapılan hata burada: geri dönen (returned) bir mesaj yine de onaylanmıştır (confirmed). Broker onu kabul etti, sadece gidecek kuyruk bulamadı ve iade etti. return bir nack değildir. Bu yüzden iki ayrı sayaç tutuyoruz.
mandatory: true olmasaydı yanlış yazılmış bir routing key ile gönderilen mesaj sessizce yok olurdu ve siz “yayın başarılı” logunu görüp her şeyin yolunda olduğunu sanardınız.
Retry
async function withRetry(fn, { attempts, initialMs, multiplier }) {
let delay = initialMs;
for (let i = 1; i <= attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts) throw err;
await sleep(delay);
delay *= multiplier; // 500ms → 1sn → 2sn
}
}
}
Bu retry broker’a görünmez. Mesaj bir kez teslim edilir, biz tüketici fonksiyonunun içinde bekleyip tekrar deneriz.
ack ve nack — sistemin garantisini belirleyen iki satır
try {
await withRetry(() => callProvider(channel, event), spec.retry);
ch.ack(msg); // İŞ BİTTİ, ŞİMDİ onayla
} catch {
ch.nack(msg, false, false); // retry bitti → DLQ'ya
}
ack işten sonra atılır. Bu at-least-once teslimattır: işlem sırasında çökersek broker onay görmez ve mesajı yeniden teslim eder. Mesaj kaybolmaz ama iki kez işlenebilir. ack‘i işin başına alsaydık çökme durumunda mesaj kaybolurdu.
nack‘in son parametresi requeue, ve false. Yani mesaj kuyruğa geri konmuyor, dead letter exchange’e yönlendiriliyor. true deseydik ne olacağını birazdan göreceğiz
Şimdi ise çalıştırıp izlemeye geçelim.
Senaryo 1 — Bir yükleme, üç kanal
curl -X POST localhost:8080/media/upload -H 'Content-Type: application/json' \
-d '{"assetId":"demo-001","ownerId":"u-1","durationSec":120}'
Uygulama loglarında üç satır çıkıyor:
$ kubectl logs deploy/mediaflow --tail=30 | grep işlendi
thumbnail assetId=demo-001
subtitle assetId=demo-001
transcode assetId=demo-001 ← ~2 saniye sonra
Bir yayın yaptık, üç kez işlendi. Üretici hiçbir kanalın adını bilmiyordu.
Senaryo 2 — Yavaş sağlayıcı kuyruğu şişirir
2000 mesaj basalım:
curl -X POST localhost:8080/media/burst -H 'Content-Type: application/json' -d '{"count":2000}'
Hemen ardından kuyruklara bakalım:
kubectl exec rabbitmq-server-0 -c rabbitmq -- \
rabbitmqctl list_queues name messages messages_unacknowledged consumers
name messages unacked consumers
media.thumbnail 0 0 4 ← hızlı kanal: çoktan bitti
media.subtitle 1473 20 2 ← orta hız
media.transcode 1980 10 2 ← YAVAŞ: neredeyse hiçbiri işlenmedi
Ayrı kuyruklar olmasaydı, transcode işleri thumbnail işlerinin arkasını tıkardı.
Bir dakika sonra tekrar bakalım:
media.thumbnail 0 0 4
media.subtitle 0 0 2 ← bitti, ölçek geri indi
media.transcode 490 40 8 ← max worker'da, drenaj sürüyor
Transcode kanalı kendiliğinden 2 worker’dan 8’e çıktı. Otomatik ölçekleyici kuyruk derinliğine bakıyor ve kapasitesinden fazla mesaj bekliyorsa büyüyor, boşta kalırsa küçülüyor.
Grafanadan bir görüntü;

Mavi çizgi media.transcode: yük altında 20.850 mesaja kadar tırmanıyor ve saatlerce değil, dakikalar içinde eriyor. Sarı media.thumbnail neredeyse hep tabanda aynı olayları alıyor ama 30 milisaniyede bitirdiği için hiç birikmiyor. Sağdaki küçük tepe, birazdan göreceğimiz senaryo.
Worker sayısı da kendiliğinden hareket ediyor:

Transcode (mavi) 2 → 3 → 5 → 8’e çıkıyor ve maksimumda kalıyor. Subtitle (yeşil) 2 → 4 arasında gidip geliyor. Thumbnail (sarı) hiç büyümüyor çünkü ihtiyacı yok. Sağdaki sıfıra iniş, worker’ları durdurduğumuz an.
Senaryo 3 — Kararsız sağlayıcı: retry → DLQ → x-death
Altyazı sağlayıcısı asset’lerin ~%10’unda patlıyor. 2000 mesajlık yükün ardından:
kubectl exec rabbitmq-server-0 -c rabbitmq -- rabbitmqctl list_queues name messages | grep dlq
media.thumbnail.dlq 0
media.transcode.dlq 0
media.subtitle.dlq 175 ← ~%8.7
Her kanalın kendi DLQ’su olmasının değeri burada. Tek ortak bir DLQ olsaydı “bir şeyler bozuk” derdiniz; şimdi doğrudan “altyazı hattı bozuk” diyebiliyorsunuz.
Peki bu mesajlar neden düştü? Mesaj dead-letter edilirken RabbitMQ başlıklarına x-death bilgisini ekler:
{
"assetId": "a-14-b941d886",
"xDeath": [
{
"queue": "media.subtitle",
"reason": "rejected",
"count": 1,
"exchange": "media.topic",
"routingKeys": ["media.all"]
}
]
}
Mesaj nereden geldi, neden düştü, kaç kez. reason: "rejected" demek “tüketici retry bütçesini tüketti ve mesajı reddetti” demek.
Yönetim panelinde bir DLQ mesajına baktığımızda tüm mevzu başlıklarda duruyor:

x-death bloğu: mesaj media.topic exchange’inden media.subtitle kuyruğuna gelmiş, media.all routing key’iyle yayınlanmış, rejected sebebiyle bir kez dead-letter edilmiş. Altındaki x-first-death-* ve x-last-death-* alanları aynı bilgiyi sorgulanması kolay biçimde tekrarlıyor.
Kanal başına DLQ’nun değeri de grafikte görünüyor:

Yalnızca media.subtitle.dlq tırmanıyor (2.879), diğer ikisi tabanda sıfır. Tek ortak bir DLQ olsaydı bu grafik tek bir çizgi olurdu ve hangi hattın bozuk olduğunu söyleyemezdik.
Senaryo 4 — Aynı DLQ, farklı sebep
DLQ’ya düşmenin ikinci bir yolu daha var: tüketici çökerse. Pod OOM yerse, deploy sırasında SIGKILL alırsa ya da node tahliye edilirse, mesaj ack’lenmeden kuyruğa döner ve teslim sayacı artar. x-delivery-limit dolduğunda mesaj dead-letter edilir:
{ "assetId": "crash-demo", "reason": "delivery_limit", "count": 1 }
x-death.reason | Anlamı | Nereye bakmalı |
|---|---|---|
rejected | Uygulama pes etti | Koda / dış servise |
delivery_limit | Mesaj çökme yüzünden çok kez geri döndü | Altyapıya / pod sağlığına |
Senaryo 5 — requeue fırtınası
İşlenemeyen bir mesajı kuyruğa geri koymak (nack(requeue=true))
“şimdi olmadı, birazdan tekrar denerim.” Ne olduğuna bakalım.
Bir kanalı bu moda alıp tek bir mesaj gönderdim ve 45 saniye bekledim:
45 saniyede yeniden teslim sayısı: 20183
aynı sürede işlenen mesaj (subtitle): 22981 ← hiç değişmedi
kuyruktaki mesaj sayısı: 1 ← hâlâ orada
45 saniyede 20.183 yeniden teslim, sıfır iş. Mesaj hâlâ kuyrukta, DLQ boş.
Beklentim x-delivery-limit‘in devreye girip mesajı DLQ’ya atmasıydı. Girmedi. Ölçüm şunu gösterdi:
Açık nack(requeue=true) çağrıları quorum queue’nun teslim sayacını artırmıyor. 3000’den fazla yeniden teslimden sonra mesaj hâlâ kuyruktaydı. Yani x-delivery-limit sizi requeue döngüsünden kurtarmıyor. yalnızca gerçek tüketici çökmelerine karşı çalışıyor (bir önceki senaryo)
Bu yüzden bu projede başarısız mesajlar her zaman requeue=false ile DLQ’ya park edilir.

Bu grafiğin okunuşu şu: broker saniyede 334 mesaj dağıtıyor, uygulama son hızda meşgul görünüyor, ve hiçbir iş bitmiyor. Sadece “mesaj hızı” metriğine bakan bir izleme kurulumu burada hiçbir şey fark etmez.
Senaryo 6 — DLQ’dan geri oynatmak (replay)
Altyazı servisindeki sorun düzeldi diyelim. DLQ’daki mesajları silmek istemiyoruz — o videoların altyazısı hâlâ üretilmedi. Yeniden işlemek gerekiyor.
Akla gelen ilk yol: mesajı media.topic‘e geri bas. Ama üç kuyruk da media.* ile bağlı olduğu için mesaj üçüne birden düşer:
media.subtitle.dlq → media.topic → “media.*” eşleşir →
├── media.thumbnail ← video BOŞUNA yeniden thumbnail’lanır
├── media.transcode ← video BOŞUNA yeniden transcode edilir (saatlerce CPU)
└── media.subtitle ← istediğimiz tek şey buydu
Doğru yol: default exchange üzerinden doğrudan tek kuyruğa yazmak.
ch.publish('', 'media.subtitle', msg.content, { persistent: true, headers });
Default exchange ('') özel bir kuraldır: routing key neyse aynı isimli kuyruğa gönderir. Exchange yok, binding yok, desen yok.
--- ÖNCE --- --- SONRA (5 mesaj replay) ---
thumbnail 2001 thumbnail 2001 ← değişmedi
transcode 2001 transcode 2001 ← değişmedi
subtitle 1826 subtitle 1826
Senaryo 7 — Geri basınç: kuyruğun varlık sebebi
Worker’ları durduralım ama alımı durdurmayalım:
curl -X POST localhost:8080/admin/workers/pause
curl -X POST localhost:8080/media/burst -d '{"count":3000}'
name messages unacked consumers
media.thumbnail 3000 0 0 ← Ready dolu, Unacked 0, Consumer 0
media.transcode 3000 0 0
media.subtitle 3000 0 0
Uygulama çalışıyor, HTTP isteklerini kabul ediyor, mesajları yayınlıyor sadece tüketmiyor. Kuyruk yükü emiyor.
Devam ettirelim:
curl -X POST localhost:8080/admin/workers/resume
media.thumbnail 0 0 4 ← anında drene oldu
media.subtitle 0 0 2
media.transcode 2780 10 2 ← yavaş kanal, drenaj sürüyor
Duraklatma boyunca hiçbir istek başarısız olmadı, hiçbir mesaj kaybolmadı. Sistem çökmek yerine gecikti. Kuyruğun bütün varlık sebebi budur.

Sağdaki son bölüme bak: duraklatma anında Unacked sıfıra iniyor (elimizdeki işler bitiyor, yenisi gelmiyor) ama Ready tırmanıyor (yayın devam ediyor, kuyruk emiyor). Devam ettirdiğimizde ikisi de tersine dönüyor. İki çizginin bu ters hareketi, kuyruğun ne işe yaradığını tek bir görselde anlatıyor.
İşte yük testi sırasındaki kuyruk görselleri:

Dört panel bir arada: Ready, Unacked, yayın hızı ve teslim/ack hızı. Tek yayın yaptık, broker üçe kopyaladı, dolayısıyla üçünün yayın hızı birebir aynı.
Ve uygulama panosu:

Retry ve DLQ oranını yakından görmek gerekirse:

k6 panosu da sonuçları Prometheus üzerinden alıyor:

23.539 istek, tepe 200 req/s, p95 = 69,9 ms. “HTTP request failures” panelinin “No data” demesi bir hata değil k6 yalnızca başarısız istek olduğunda o seriyi üretiyor hiç hata olmadığı için seri hiç oluşmamış.
Prometheus’ta hızlı kontrol
Panolar bir işe yaramadan önce hedeflerin toplandığını doğrulamak gerekir:

podMonitor/default/rabbitmq/0 → 3/3 up: kümedeki üç RabbitMQ pod’unun her biri kendi metriklerini veriyor. serviceMonitor/default/mediaflow/0 → 1/1 up: uygulama da kendi metriklerini yayınlıyor. Bu ekran yeşil değilse panolar boş görünür ve saatlerce “dashboard bozuk” diye aranır.
Yazı içerisinde Kubernetes ortamında kullanılan RabbitMQ Cluster Operator YAML dosyalarını, K6 yük testi senaryolarını ve monitoring yapılandırmalarını, bağlantısını paylaştığım GitHub reposundan aldım.
Yazıyı gereksiz yere uzatmamak adına bu yapılandırmaların detaylarına ve kurulum adımlarına ayrıca yer vermedim. İhtiyaç duymanız hâlinde ilgili repoyu inceleyerek bu kaynaklardan siz de faydalanabilirsiniz.
Repo içerisinde ayrıca Java Spring Boot tabanlı örnek bir mimari de yer almaktadır.
Hüseyin Babal GitHub