RabbitMQ’da Dayanıklı Mesaj İşleme: Quorum Queue, DLQ, Retry

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:

  1. Kullanıcı 2+ saniye bekliyor. Oysa onu ilgilendiren tek şey videonun alınmış olması.
  2. Bir bölüm koparsa zincir kopuyor. Altyazı servisi çökmüşse yükleme başarısız olur. Video yüklemeyi engeller.
  3. 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.dlq derinliğ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+C yapmaktan 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"
}
concurrencyAynı kuyruk üzerinde kaç consumer paralel çalışabilir?
prefetchHer 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                   │
#MekanizmaCevapladığı soruOlmazsa
①Publisher confirmBroker mesajı kabul etti mi?“Gönderdim” sanırsınız, mesaj hiç ulaşmamıştır
②Publisher returnMesaj bir kuyruğa yönlendirilebildi mi?Yanlış routing key sessizce çöpe gider
③Consumer retryGeçici hata düzelir mi?İlk ağ hıçkırığında mesaj DLQ’ya düşer
④DLQArtı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.reasonAnlamıNereye bakmalı
rejectedUygulama pes ettiKoda / dış servise
delivery_limitMesaj çö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

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir