Kayas Commerce reklamı - sol taraf
Kayas Commerce reklamı - sağ taraf
RabbitMQ Reliability — Part 3: Retry, DLQ, Exponential Backoff & Jitter

RabbitMQ Reliability — Part 3: Retry, DLQ, Exponential Backoff & Jitter

Web Development

Yazı serimizin ilk bölümünde Publisher’ın mesajı Broker’a güvenilir şekilde ulaştırması sağlamış, ikinci bölümünde ise duplicate mesajların consumer tarafında idempotency ile kontrol altına alınmasını sağlamıştık. Dolayısıyla Publisher → Broker → Consumer akışını temel olarak tamamlamıştık

Yazı serimizin son bölümü olacak olan bu bölümde ise “consumer aldığı mesajları işleyemezse ne olur?” sorusuna cevap bulmaya çalışacağız.

1. Consumer Mesajı İşleyemezse Ne Olur?

Consumer’lar her zaman istediğimiz şekilde çalışmayabilir. Bazen database’e olan erişim geçici olarak kesilebilir, bazen ilgili consumer’ın kullandığı harici servis cevap vermeyi durdurabilir veya network kaynaklı çeşitli problemler yaşanabilir.

Örneğin:

  • Database kısa süreliğine erişilemez durumda olabilir.
  • Harici bir API timeout verebilir.
  • Network problemi yaşanabilir.
  • Consumer içerisinde beklenmeyen bir exception oluşabilir.

Bu gibi durumlarda mesajın kendisinde herhangi bir problem olmayabilir. Problem yalnızca consumer’ın mesajı o anda sağlıklı şekilde işleyememesidir. Dolayısıyla böyle bir mesajı doğrudan kaybetmek veya tamamen başarısız kabul etmek yerine, bir süre sonra tekrar işlenmesini isteyebiliriz.

Örneğin database’e erişim 10 saniyeliğine kesilmişse, aynı mesaj birkaç saniye sonra tekrar işlendiğinde işlem başarılı olabilir. Benzer şekilde geçici olarak cevap vermeyen bir harici servis kısa bir süre sonra tekrar kullanılabilir hale gelebilir.

Bu nedenle mesajlaşma sistemlerinde başarısız olan bir işlemi belirli kurallar çerçevesinde yeniden denemek isteyebiliriz.

İşte bu yeniden deneme yaklaşımına Retry diyoruz. Fakat burada yeni bir soru ortaya çıkıyor:

Mesaj başarısız olduğu anda tekrar denenmeli mi?

İlk bakışta mantıklı görünen bu yaklaşım, bazı durumlarda sistemi daha da probleme sokabilir.

2. Retry Nedir ve Neden Immediate Retry Her Zaman Doğru Değildir?

Retry, başarısız olan bir işlemin belirli koşullar altında tekrardan denenmesidir. RabbitMQ tarafında da bir mesaj sağlıklı şekilde işlenemediğinde bu mesajın consumer’a tekrardan ulaştırılmasını sağlamak için ilgili mesajı queue’ya göndeririz. Queue’ya geri gönderilen mesajlar tekrardan consumer’lara iletilir.

Bunu en basit şekilde alttaki kod örneği ile gerçekleştirebiliriz:

await channel.BasicNackAsync( 
deliveryTag: args.DeliveryTag,
multiple: false,
requeue: true);

Burada ‘requeue: true’ dediğimizde, consumer’a iletilen mesajın tekrardan queue’ya yazılmasını istediğimizi belirtiyoruz. Böylece bu mesaj, o an consumer tarafından işlenemese dahi tekrardan queue’ya yazılarak bir süre sonra consumer tarafından işlenebilir hale gelecektir. Bu akışı daha anlaşılabilir hale getirmek adına alttaki görseli inceleyebiliriz.

Bu görselde anlayacağımız üzere queue’daki mesaj consumer’a iletiliyor fakat bir exception yaşandığından dolayı ilgili mesaj NACK ile birlikte requeue ediliyor. İlgili mesajın NACK edilmesi ‘Requeue : true’ parametresi ile yapıldığı için mesajı tekrardan queue’ya koyuyoruz.

Press enter or click to view image in full size

Consumer sağlıklı şekilde çalışır hale geldikten sonra gelen mesajı başarıyla alıyor.

Requeue edilen mesaj queue’dan consumer’a tekrardan iletiliyor. Bu sırada consumer’ın kullandığı dış servis sağlıklı şekilde çalışmaya başladığı için ilgili mesaj başarıyla işleniyor.

Peki, ya consumer uzun bir süre sağlıklı şekilde çalışamazsa ne olacak? Bu süreç içerisinde broker ve consumer arasındaki trafik nasıl olacak?

İsterseniz şöyle bir kurgumuz olsun:

Queue’da hazır olarak bekleyen 10.000 mesajımız olsun ve bu mesajlar consumer tarafından tüketiliyor olsun. İlgili consumer’ın bir dış servise bağımlılığı olduğunu ve bu dış servisin 5 saniyeliğine erişilemez durumda olduğunu farzedelim.

Bu 5 saniyelik süreçte broker ve queue arasında nasıl bir etkileşim olacak?

Press enter or click to view image in full size

5 saniyelik süreçte broker ile consumer arasındaki etkileşim

Üstteki görselde neler olacağını temsil etmeye çalıştım. Sizin de göreceğiniz üzere bu 5 saniyelik süreçte broker tarafından queue’ya toplamda 60.000 mesaj gönderilmiş. Aynı şekilde consumer tarafından broker’a da 60.000 mesajın requeue edilmesi bildirilmiş.

Bu 5 saniyelik süreçte, consumer’ın dış servis bağımlılığında bir problem olmasına rağmen sistemdeki component’lar gereksiz yere işlemler yapıyor, CPU tüketiyor, hatalar fırlatıyor ve loglar üretiyor. Kısacası kaynaklarımız bu durumdan ciddi şekilde etkileniyor.

Bu örnekte, consumer’ın mesajı işleyemediği durumlarda mesajı direkt requeue etme işleminin verimli olamayabileceğini gözlemliyoruz.

Peki burada ne yapabiliriz diye düşünecek olursak aklımıza ilk gelen fikir muhtemelen şu olacaktır:

“Bu kadar sık denemek yerine daha geniş aralıklar ile deneyelim.”

İsterseniz bu fikri, yani ‘Delayed Retry’ı inceleyerek devam edelim.

3. Delayed Retry, TTL ve Dead Letter Exchange

Bir önceki bölümde, consumer’ın işleyemediği mesajların doğrudan requeue edilmesinin her zaman sağlıklı bir yaklaşım olmadığını gördük. Özellikle problemin birkaç saniye boyunca devam ettiği durumlarda mesajların sürekli olarak consumer’a iletilmesi, işlenememesi ve tekrar queue’ya alınmasının sistem üzerinde gereksiz kaynak tüketimine sebep olabileceğinden de bahsetmiştik.

Delayed Retry’da yapmak istediğimiz şey oldukça basit:

Başarısız olan mesajı hemen tekrar denemek yerine, belirli bir süre beklettikten sonra tekrar deneyelim.

İsterseniz test sonucunda iki farklı retry yöntemini karşılaştırabilmek adına yine aynı örnek ile devam edelim. Requeue durumunda mesajı tekrar consumer’a göndermek yerine 5 saniye bekletip tekrar deneyelim.

Press enter or click to view image in full size

Mesajları 5'er saniye ara ile requeue etme senaryosu

Üstteki görselden de akışımızı anlayabiliriz.

Burada dikkat ederseniz immediate retry yaklaşımından farklı olarak başarısız olan mesajı doğrudan ana queue’ya geri göndermiyor, ayrı bir retry queue’ya yönlendiriyoruz.

Bu noktada anlamlandıramadığımız 2 farklı soru olabilir.

1. Neden ayrı bir queue kullandık?

2. Mesajın queue içerisinde kaç saniye duracağını nasıl tanımlıyoruz?

Bu sorulara verilecek cevapların temelini oluşturacak olan TTL (Time To Live) kavramı ile devam etmek istiyorum. Bu kavramın ne olduğunu ve nasıl çalıştığını anladığımızda üstteki sorulara doğrudan kendimiz cevap verebilir hale geleceğiz.

TTL Nedir?

TTL, bir mesajın queue içerisinde ne kadar süre tutulabileceğini belirlememizi sağlar. TTL dolduğunda ilgili mesaj queue’dan silinebilir hale gelir.

TTL değerini iki farklı kapsam da tanımlayabiliriz.

Bunlardan ilki, mesaj başına TTL tanımlaması yapmaktır. Bu yaklaşımda her bir mesaja ayrı bir TTL değeri atanır ve ilgili mesajların ömrü o TTL değeri kadardır.

Örneğin bir mesajın TTL değerini ‘5000 ms’ saniye olarak atadığımızda, 5 saniye sonra bu mesaj queue’dan silinebilir hale gelecektir.

Not: TTL süresi dolan mesajlar queue’dan direkt silinmez. Queue’nun başında yapılacak olan TTL kontrolü sonrasında silinirler. Dolayısıyla “TTL süresi biten bir mesajlar queue’dan direkt silinir” demek çok doğru değil.

Bir diğer tanımlama şeklimiz de queue bazlı TTL tanımlaması yapmaktır. Bu yaklaşımda ise her bir queue’ya, içereceği mesajların ne kadar ömrü olacağı en baştan tanımlanır. Dolayısıyla bu tür queue’lara mesajlar gönderildiğinde, o mesajların TTL değerleri aslında queue’nun TTL değeri olacaktır.

Bu görselden de hatırlayacağımız üzere “5s-retry-queue” sunda ilgili queue nun TTL değerini 5 saniye olarak ayarlamıştık. Böylece mesajların 5 saniye sonra retry queue’dan silinmesini sağlamıştık.

Tamam, TTL’in iki şekilde tanımlandığını gördük. TTL süresi dolan mesajlar queue’dan silinecekti. Peki TTL değeri dolduğundan dolayı queue’dan silinen mesajlar nereye gidecek?

Bizim istediğimiz şey mesajın silinmesi değil.

Amacımız mesajı bir süre beklettikten sonra tekrar ana queue’ya göndererek consumer tarafından yeniden işlenmesini sağlamak.

Tam olarak bu noktada mesajların ana queue’ya yönlendirilmesinde önemli rol alacak Dead Letter Exchange devreye giriyor.

Dead Letter Exchange

RabbitMQ’da bir mesaj belirli nedenlerden dolayı bulunduğu queue’dan çıkarıldığında, mesaj başka bir exchange’e yönlendirilebilir.

Bu exchange’e Dead Letter Exchange, yani kısaca DLX denir. Bu exchange’e yönlendirilen mesajlar da ilgili routing key değeri ile birlikte ilgili queue’ya yönlendirilir.

Böylece akışımız şu hale gelir:

order-created-queue

consumer

fail

5s-retry-queue

TTL = 5s

DLX

order-created-queue

consumer

İsterseniz burada biraz duralım ve ilk baştaki örneğimizi düşünelim. Bizim istediğimiz şey tam olarak şuydu :

“ Mesajlar direkt requeue olmasın, 5 saniye bekledikten sonra requeue olsun”.

O zaman kendi kendimize şu çıkarımı yapabiliriz: “Biz de consumer olarak bir mesajı işleyemediğimizde direkt requeue etmek yerine, o mesajı bir retry-queue’ya publish edelim. Retry-queue’ya gönderilen mesaj TTL’i dolana kadar beklesin. TTL dolduğunda mesajı, o queue’da DLX olarak tanımlanmış olan order-created-exchange’e yönlendirelim. Böylece işleyemediğim her bir mesaj 5 saniye bekledikten sonra tekrardan queue’ya yönlendirilsin.”

Farkettiyseniz bu noktaya kadar TTL ve DLX’in ne olduğunu öğrendik. Sonrasında ise öğrendiklerimizle üstteki çıkarımı yapabildik.

Dolayısıyla TTL’i anlatırken kendi kendimize sormuş olduğumuz şu iki soruyu kendimiz cevaplamış olduk :

1. Neden ayrı bir queue kullandık?

2. Mesajın queue içerisinde kaç saniye duracağını nasıl tanımlıyoruz?

Yazımızın bu noktasına kadar Immediate Retry’ın ne olduğunu ve ne gibi etkiler yaratabileceğini gördük. Yarattığı etkileri azaltmak için de Delayed Retry yaklaşımını kullandık.

Her iki yaklaşımın da temel amacı, o anda sağlıklı şekilde işlenemeyen bir mesajın daha sonra tekrar işlenebilmesini sağlamaktır.

Buradaki varsayımımız oldukça basit:

Consumer’ın veya bağımlı olduğu servisin yaşadığı problem geçici olabilir.

Peki problem geçici değilse?

Consumer 5 saniye sonra da, 10 saniye sonra da, 1 dakika sonra da aynı mesajı işleyemiyorsa ne olacak?

Press enter or click to view image in full size

Queue çok fazla mesaj alıyor fakat bu mesajlar işlenemiyor.

Queue çok fazla mesaj alıyor fakat bu mesajlar işlenemiyor.

Üstteki senaryoda queue’ya yoğun şekilde mesaj gelmeye devam ederken consumer bu mesajları başarılı şekilde işleyemiyor. Başarısız olan mesajlar retry mekanizması üzerinden tekrar dolaşıma giriyor.

Consumer’ın sağlıklı şekilde hizmet veremediği süre uzadıkça işlenmeyi bekleyen ve tekrar denenen mesajların sayısı da artmaya devam ediyor.

Dolayısıyla yalnızca retry mekanizması oluşturmak yeterli değildir.

Bir noktada şu soruya cevap vermemiz gerekir:

Bir mesajı kaç defa retry edeceğiz?

Çünkü başarısız olan bir mesajı sonsuza kadar tekrar denemek yerine, belirli bir deneme sayısından sonra normal akıştan çıkarmamız gerekir.

Bu noktada karşımıza Retry Count, maksimum deneme sayısı ve başarısız mesajları ayrı bir yerde tutmamızı sağlayacak Dead Letter Queue kavramları çıkıyor.

4. Retry Count, Maksimum Deneme ve Dead Letter Queue

Bir önceki bölümde kendi kendimize şu soruyu sormuştuk:

Bir mesajı kaç defa retry edeceğiz?

Aslında bu sorunun cevabı oldukça önemli. Çünkü mesajın her başarısız olduğunda tekrar tekrar dolaşıma sokulması, bir noktadan sonra retry mekanizmasının kendisinin problem haline gelmesine sebep olabilir.

Örneğin bir consumer’ın işleyemediği bir mesajın döngüsü alttaki gibi olacaktır.

Message

Consumer

Fail

Retry #1

Fail

Retry #2

Fail

Retry #3

Fail

...

Burada fark edeceğiniz üzere retry mekanizmasının ne zaman sonlanacağına dair herhangi bir kuralımız yok. Bu yüzden bu döngü sonsuza kadar devam edecektir. Bu döngünün belirli bir süre sonra sonlanması için ilk yapmamız gereken şeylerden biri, ilgili mesajın daha önce kaç defa retry edildiğini bilebilmek.

Retry Count

Retry Count, bir mesajın kaç defa yeniden işlenmeye çalışıldığını takip etmemizi sağlar.

‘x-death’ örneği

RabbitMQ, mesajın header’ına “x-death” bilgisini ekler. Bu bilgi içerisinde ilgili mesajın hangi queue’dan, hangi sebeple ve kaç defa dead-letter edildiği gibi bilgileri içerir.

Bu bilgi ile birlikte consumer mesajı her aldığında, ilgili mesajın daha önce kaç defa retry edildiğini bilebilir hale gelir.

Maksimum Retry Sayısı

Bazı durumlarda mesajların sağlıklı şekilde işlenememesi geçici bir durumdur. Şuanda işlenemeyen mesaj 1 saniye sonra sağlıklı şekilde işlenebilir. Bazı durumlarda ise ilgili mesaj uzun sürelerce işlenemeyebilir.

Mesajın uzun süre boyunca işlenememesinin component’larımız üzerinde oluşturacağı etkiyi engellemek için retry mekanizmalarında bir maksimum retry sayısı belirleyebiliriz.

Örneğin uygulamamızda şu şekilde bir kural tanımladığımızı düşünelim:

Max Retry Count = 3

Bu durumda sağlıklı bir şekilde işlenemeyecek olan mesajın akışı şu şekilde olacaktır:

İlk İşleme

Fail

Retry #1

Fail

Retry #2

Fail

Retry #3

Fail

Mesaj üçüncü retry sonrasında hâlâ başarılı şekilde işlenemiyorsa artık şu soruyu sormamız gerekir:

Bu mesajı bir kez daha retry etmenin bize gerçekten bir faydası var mı?

Bazı durumlarda cevap evet olabilir; fakat belirlediğimiz retry politikasına göre bu noktada artık mesajın normal retry döngüsünden çıkarılması gerekir.

Çünkü aksi durumda başarısız olan mesaj sürekli olarak:

Queue → Consumer → Fail → Retry Queue → Queue

döngüsünde dolaşmaya devam eder.

Bu durum yalnızca kaynak tüketimini artırmakla kalmaz, aynı zamanda gerçekten işlenebilir mesajların da aynı altyapıyı paylaşmasına sebep olur. Aynı altyapıyı paylaşan bir yapıda belki hiç işlenmeyecek mesajlar, işlenebilecek mesajların önüne geçip ilgili mesajların daha geç işlenmesine de sebebiyet verebilir. Dolayısıyla bir mesajın kaç defa retry edilebilir olduğunun sınırlandırılmasının bize birden fazla yararının olacağını görebiliyoruz.

Bu zamana kadar consumer tarafında yaşanan bir problemden dolayı işlenemeyen mesajları retry mekanizmamız ile tekrardan döngüye sokarak işlenebilmesini sağladık.

Peki mesajlarımızın işlenememesini sebebi consumer değil de mesajın bizzat kendisiyse?

Örneğin mesajın payload’ında bir problem varsa, zorunlu bir alan eksikse veya ilgili business rule mesajın işlenmesine izin vermiyorsa aynı mesajı 10 defa da, 100 defa da retry etsek sonuç büyük ihtimalle değişmeyecektir.

Dolayısıyla problemin ana nedeni mesajın kendisi olduğu durumlarda, ilgili mesajın retry edilmesinin bir mantığı olmayacaktır.

O zaman bu mesajı ne yapacağız, mesajın kendisinde hata varsa bu mesaj direkt yok mu sayılmalıdır? Tabii ki hayır.

Bu mesaj bizim için hala önemli olabilir. Mesajın neden işlenemediğini incelemek, loglarla karşılaştırmak veya problem giderildikten sonra tekrar işlemek isteyebiliriz. Bu sebeple bu gibi problem yaşayan mesajları Dead Letter Queue adını verdiğimiz queue’larda tutabiliriz.

Dead Letter Queue Nedir?

Dead Letter Queue, normal mesaj akışı içerisinde başarılı şekilde işlenemeyen mesajları ayrı bir yerde tutmak için kullandığımız queue’dur.

Örneğin akışımızı şöyle kurgulayabiliriz:

order-created-queue

consumer

fail

retry-queue

retry #1

fail

retry #2

fail

retry #3

fail

DLQ

Böylece belirlediğimiz maksimum retry sayısından sonra mesaj artık tekrar tekrar normal akışa dahil edilmez.

Bunun yerine ayrı bir queue içerisinde tutulur.

Örneğin:

order-created-dead-letter-queue

şeklinde bir queue tanımlayabiliriz.

Bu queue içerisinde bulunan mesajlar artık bizim için şu anlama gelir:

“Bu mesaj normal retry mekanizması ile başarılı şekilde işlenemedi. İncelenmesi gerekiyor.”

İncelenmesi gerektiğini düşündüğümüz mesajlar dead-letter-queue vesilesiyle daha sonra inceleyebilir, hataları düzeltip tekrardan publish edebiliriz.

Yazımınızın bu noktasına kadar öğrendiklerimizle artık:

  • Mesaj başarısız olduğunda retry edebiliyoruz.
  • Retry işlemini belirli aralıklarla gerçekleştirebiliyoruz.
  • Kaç defa retry edildiğini takip edebiliyoruz.
  • Belirli bir limitten sonra mesajı DLQ’ya gönderebiliyoruz.

Şuanda geldiğimiz noktanın gayet iyi olduğunu düşünüyorum. İlk versiyonumuzda immediate retry ile birlikte çokça retry yapıyorduk. Sonrasında delayed retry ile birlikte bu retry etme sıklığını genişleterek kaynak tüketimini azalttık. Bununla birlikte mesajların retry edilmesini sınırlandırarak işleyenemeyecek olan mesajları main queue’dan çıkartıp ayrı bir queue’da muhafaza ettik.

İsterseniz şuandaki yapımızı biraz daha inceleyelim.

Maksimum retry sayısının 3 olduğu ve Delayed Retry kullandığımız bir senaryoda yaşanan olası problem 15 saniye içerisinde çözüldüğü takdirde ilgili mesaj başarıyla işlenebiliyor.

5 saniye
5 saniye
5 saniye

Problem 30 saniye içerisinde çözülecekse ne yapacağız?

Şuandaki çözüm yöntemimize göre mesajımız dead-letter-queue’ya gidecek ve bizim manuel olarak ilgili mesajı tekrardan publish etmemiz gerekecek değil mi?

Burada nasıl bir çözüm yöntemi uygulayabiliriz diye düşünelim ve aklımıza gerçek hayattan şu senaryoyu getirelim:

“Birisini telefonla arayıp ulaşamadığımızda tekrardan ararız, yine ulaşamazsak biraz bekler sonrasında ararız. Eğer tekrardan ulaşamazsak şansımızı saatler sonrasında deneriz.”

Gerçek hayatta uyguladığımız bu çözüm yöntemini burada da uygulabilir miyiz ?

İşte bu düşünce bizi Exponential Backoff yaklaşımına götürüyor.

5. Exponential Backoff ve Jitter

Exponential Backoff, her başarısız retry denemesinden sonra bir sonraki deneme için beklenecek sürenin kademeli olarak arttırılması yaklaşımıdır.

Örneğin retry sürelerimizi şu şekilde belirlediğimizi düşünelim:

Retry #1 → 5 saniye
Retry #2 → 10 saniye
Retry #3 → 20 saniye

Burada mesaj her başarısız olduğunda aynı süre boyunca beklemek yerine, bir sonraki retry için daha uzun süre bekliyor.

Akışımız kabaca şu şekilde olur:

Message

Consumer

Fail

5 saniye bekle

Retry #1

Fail

10 saniye bekle

Retry #2

Fail

20 saniye bekle

Retry #3

Aslında burada yaptığımız şey, bir önceki bölümde verdiğimiz telefon örneğinin karşılığıdır.

Örneğimiz şuydu:

İlk denemede ulaşamadığımızda kısa bir süre bekleriz. Tekrar ulaşamazsak bu sefer biraz daha uzun bekleriz. Başarısızlık devam ettikçe denemelerimizin arasındaki süreyi arttırırız.

Exponential Backoff yaklaşımında da temel düşünce aynı.

Bu yaklaşım ile hem consumer’ın bağımlı olduğu servise toparlanması için daha fazla zaman tanıyoruz hem de sürekli aynı sıklıkta retry yaparak sistemi gereksiz şekilde yormamış oluruz.

Bekleme süresini alttaki mantığa göre hesaplayalım

delay = baseDelay × 2^retryCount

Örneklerimizde baseDelay’i 5 saniye olarak kullanmıştık, burada da aynı değer ile devam edelim:

Retry #0 → 5 × 2⁰ = 5 saniye
Retry #1 → 5 × 2¹ = 10 saniye
Retry #2 → 5 × 2² = 20 saniye
Retry #3 → 5 × 2³ = 40 saniye

Tabii ki bu sürenin sonsuza kadar büyümesini de istemeyiz. Retry sayımız düşük oluğunda retry süresi ile ilgili bir problem olmayabilir. Retry sayısı eğer artarsa bu süre için üst sınır tanımlamak isteyebiliriz.

Örneğin:

5s → 10s → 20s → 40s → 60s → 60s

şeklinde bir üst sınır koyabiliriz.

Burada retry sürelerini nasıl belirlediğimizi gördük. Peki bunu nasıl uygulayabiliriz biraz buna bakalım.

Delayed Retry case

Hatırlayacağınız üzere Delayed Retry yaklaşımında 1 adet retry-queue’muz vardı ve bu queue ile 5 saniyelik gecikme sağlıyorduk. Exponential Backoff yaklaşımında ise sahip olmamız gereken queue sayısı, maksimum retry sayısı ile doğrudan ilişkilidir. Maksimum retry sayımızı 3 olarak belirlersek 3 farklı retry süremiz olmalı. Bu süreler de 5, 10 ve 20 olacaktır. Bu sebeple 3 farklı retry süresi için 3 adet retry queue’su oluşturacağız.

Press enter or click to view image in full size

Exponential Backoff’ta oluşturduğumuz queue’lar

Bu senaryoda Consumer kendisine iletilen mesajın kaç defa retry edildiğine göre o mesajı ilgili retry-queue’lara publish edecek. Retry-queue’ların da dead-letter-exchange’leri order-created-exchange olduğu için mesajlar TTL sonunda ana queue’mıza yönlendirilecektir.

Exponential Backoff yöntemimizde temel olarak bu şekildeydi. İsterseniz bu 3 farklı yöntemin broker-consumer arasındaki trafiğe nasıl etki ettiğini gözlemlerek devam edelim.

Immediate Retry’da bekleme sürecinde broker-consumer arasındaki iletişim şu şekildeydi:

Press enter or click to view image in full size

Immediate Retry

Görüldüğü üzere 30 saniyelik bir kesintide onlarca mesaj döngüsü oluşuyor.

Delayed Retry’a baktığımızda ise:

Press enter or click to view image in full size

Delayed Retry

Burada ise Immediate Retry’daki sıkı retry işlemlerinin biraz daha azaltılmış halini görüyoruz. Immediate retry’a göre kaynakların daha iyi kullanıldığı bir yöntem olduğunu düşünüyorum.

Son olarak da Exponential Backoff kullandığımız retry’a bakalım:

Press enter or click to view image in full size

Exponential Backoff

Exponential Backoff ta ise broker ile consumer arasında sadece 4 adet cycle oluşmuş. Kaynakların en verimli şekilde kullanıldığı yöntemin bu olduğunu görebiliriz.

Üç yaklaşımı da alttaki görselden çok daha iyi inceleyebiliriz.

Press enter or click to view image in full size

3 yöntemin tek görseli

Bu üç yöntemde de broker-consumer arasındaki döngü sayılarının nasıl değiştiğini ve bu değişimin kaynaklarımızı nasıl etkileyebileceğini gördük.

Bu noktada dikkat çekmek istediğim bir konu var.

Dikkat edersek şimdiye kadar kullandığımız retry yaklaşımlarında mesajlarımızın büyük bir kısmı belirli zamanlarda toplu şekilde tekrar publish ediliyor.

Örneğin elimizde 10.000 mesaj olduğunu düşünelim. Bu mesajların tamamı aynı anda başarısız olduysa ve aynı retry süresine sahipse, 10 saniye sonra bu 10.000 mesajın tamamı tekrar aynı anda dolaşıma girecektir.

Peki böylesine büyük bir yükün tek bir anda sisteme geri dönmesi sizce bizi nasıl etkiler?

Thundering Herd Problem

Bir retry zamanı geldiğinde binlerce mesajın aynı anda tekrar publish edildiğini düşünelim. Bu mesajların tamamı kısa bir zaman aralığında consumer’lara ulaşmaya başlayacaktır.

İlk bakışta bunda bir problem yokmuş gibi görünebilir. Sonuçta amacımız zaten mesajları tekrar işlemek.

Fakat binlerce mesajın aynı anda consumer’lara ulaşması, sistem üzerinde ani bir yük oluşturabilir. Consumer tarafında CPU kullanımı artabilir, database’e veya harici servislere aynı anda çok sayıda istek gönderilebilir. Eğer problemimizin sebebi zaten kaynakların yoğun şekilde kullanılmasıysa binlerce mesajı retry etmemiz problemli olan yapıya bir darbe daha vurabilir.

Bahsettiğimiz problemli döngünün akışı şu şekilde:

Servis yoğunlaştı

Mesajlar başarısız oldu

Belirli bir süre beklendi

Binlerce mesaj aynı anda retry edildi

Servis tekrar yoğunlaştı

Mesajlar tekrar başarısız oldu

Burada kaynaklarımızı sıkıntıya sokan problem Thundering Herd Problemidir.

İsterseniz bir diğer olan Jitter bölümünde bu problemi çözmeye çalışalım.

Jitter

Bir önceki bölümde gördüğümüz üzere problem, yalnızca retry işlemi yapmak değil; çok sayıda mesajın aynı anda retry edilmesiydi.

Örneğin 10.000 mesajın tamamının 5saniye sonra tekrar işlenmek üzere beklediğini düşünelim.

Press enter or click to view image in full size

Bu durumda Exponential Backoff kullanıyor olsak bile bütün mesajlar aynı bekleme süresine sahip olduğu için sistem üzerinde yine ani bir yük oluşabilir.

Press enter or click to view image in full size

Jitter kullanımı

İşte Jitter yaklaşımında yapmak istediğimiz şey, hesaplanan retry süresine küçük miktarda rastgelelik eklemektir.

Örneğin normalde retry süremiz 5 saniye olsun. Jitter uyguladığımızda bu süre şu aralıkta olur:

4.5 - 5.5 saniye

Böylece aynı anda başarısız olan mesajların tamamı tam olarak 5. saniyede tekrar işlenmek yerine farklı zamanlarda retry edilir.

Örneğin:

Message #1 → 4.6 saniye
Message #2 → 4.9 saniye
Message #3 → 5.2 saniye
Message #4 → 5.4 saniye

şeklinde farklı bekleme süreleri oluşabilir.

Bu sayede bütün retry işlemlerini tek bir ana yığmak yerine zamana yaymış olur ve kaynaklarımızı daha iyi bir şekilde tüketmiş oluruz.

Jitter’ı Exponential Backoff ile birlikte düşündüğümüzde ise şöyle bir yapı elde edebiliriz:

Retry #1 → 4.5 - 5.5 saniye
Retry #2 → 9.5 - 10.5 saniye
Retry #3 → 19.5 - 20.5 saniye

Burada Exponential Backoff ile birlikte her başarısız denemeden sonra bekleme süresini artırırken; Jitter ile de bu süreye küçük bir rastgelelik ekleyerek mesajların aynı anda retry edilmesini azaltmaya çalıştık. Gün sonunda sistem üzerindeki ani yükü azalttık, consumer’ların ve daha kontrollü şekilde trafik almasını sağladık diyebiliriz.

Bu yazı serisinde RabbitMQ öğrenme sürecimde merak ettiğim ve öğrenmekten keyif aldığım başlıkları yazıya dökmek istedim. Umarım bilgilendirici bir yazı serisi olmuştur.

İyi okumalar dilerim.



John Doe
Article by

Görkem Kaya

Computer Engineering Student

Comments 0

You cannot comment because you are not logged in.


Log in to comment.

Give us your feedback

✓ Your feedback has been sent successfully. Thank you!

Important Notice

Some post images may not display properly. This issue occurred because I didn't mount volumes to the container. As a result, some posts may be missing their images. We apologize for the inconvenience and are working to resolve this.

Our Current Progress & Plans

📋 Planned Features

Advanced Analytics Planned

A detailed reporting and statistics system

Estimated: Q4 2025

🐛 Known Issues

Rich text editor problem Bug

Error that may occur while creating a post

Priority: High
About storing the content of the post. Bug

Storing the entire post content as binary is a problem. We'll work on it.

Priority: High

✅ Recent Updates

Feedback System Completed

User feedback form and admin panel added

July 18, 2025