Kritik Sistemlerde Güvenli Retry Tasarımı
Retry politikasını hata sınıflandırması, idempotency, katmanlı çoğalma, backoff, jitter, deadline ve trafik bütçesi üzerinden ele alır.
Bir isteğin başarısız olması, aynı isteği hemen yeniden göndermenin güvenli veya yararlı olduğu anlamına gelmez. Hata geçici bir ağ kesintisinden kaynaklanıyorsa yeniden deneme başarı sağlayabilir. Hatanın nedeni hedef sistemdeki aşırı yük ise ek istek üretmek, kurtarılmaya çalışılan sistemi daha da ağırlaştırır.
Başlangıç istek hızı "λ₀", her girişimin başarısız olma olasılığı "p" ve toplam girişim sınırı "A" olsun. Hataların birbirinden bağımsız olduğu basitleştirilmiş durumda bir mantıksal istek için beklenen fiziksel girişim sayısı:
E[N] = 1 + p + p² +... + p^(A-1)
şeklindedir. "p ≠ 1" için:
E[N] = (1 - p^A) / (1 - p)
olur.
Efektif istek yükü yaklaşık olarak:
λeffective = λ₀ × E[N]
biçiminde büyür.
Üç girişime izin verilen bir sistemde hata olasılığı "0,2" ise:
E[N] = 1 + 0,2 + 0,04 = 1,24
elde edilir. Normal trafiğin yüzde 24 üzerinde ek yük oluşur.
Hata olasılığı "0,9" olduğunda ise:
E[N] = 1 + 0,9 + 0,81 = 2,71
olur. Sistem kapasitesinin düşmüş olduğu anda istemciler normal yükün yaklaşık 2,7 katını üretmeye başlar.
Bu formül bağımsız hata varsayımına dayanır. Aşırı yük durumunda hatalar bağımsız değildir, aynı zaman aralığında gönderilen bütün istekler ortak kapasite sorunundan etkilenebilir. Bu durumda yeniden deneme, hata olasılığını azaltmak yerine hata durumunu uzatabilir.
Hangi Hatalar Yeniden Denenmeli?
Bir hata kodunun varlığı tek başına yeniden deneme kararı için yeterli değildir. Hata en az üç sınıftan birine ayrılmalıdır:
geçici hata kalıcı hata sonucu belirsiz hata
Geçici hata, aynı istek daha sonra gönderildiğinde başarılı olabilecek durumdur:
bağlantı kesintisi geçici servis erişilemezliği kısa süreli kilit çatışması anlık kapasite sınırı
Kalıcı hata, istek değiştirilmeden tekrarlandığında aynı sonucu üretmesi beklenen durumdur:
geçersiz parametre yetki eksikliği desteklenmeyen işlem bozuk veri biçimi karşılanmamış ön koşul
gRPC hata modeli bu ayrımı bazı durum kodlarında açıkça ifade eder. "UNAVAILABLE", yalnızca başarısız çağrının backoff ile yeniden denenebileceği geçici durumlar için, "FAILED_PRECONDITION", sistem durumu düzeltilmeden yeniden denenmemesi gereken işlemler için, "ABORTED" ise tüm üst düzey read-modify-write dizisinin yeniden başlatılması gereken çatışmalar için önerilir. Aynı dokümantasyon, idempotent olmayan işlemlerin tekrarının her zaman güvenli olmadığını ayrıca belirtir.
Sonucu belirsiz hata daha zordur. İstemci timeout almış olabilir, ancak sunucu isteği tamamlamış ve yalnızca cevap istemciye ulaşmamış olabilir:
istemci → istek gönderildi sunucu → işlem tamamlandı ağ → cevap kayboldu istemci → timeout
İstemci açısından işlem başarısız görünür. Sunucu açısından yan etki gerçekleşmiştir. Aynı istek yeniden gönderildiğinde işlem iki kez uygulanabilir.
Bu nedenle:
timeout ≠ işlem uygulanmadı
Timeout yalnızca istemcinin belirlenen süre içinde sonucu öğrenemediğini gösterir. AWS'nin dağıtık sistemlere ilişkin teknik açıklaması da timeout veya bağlantı hatasının yan etkinin oluşmadığını garanti etmediğini, güvenli retry için API'nin idempotent tasarlanması gerektiğini vurgular.
İdempotent İşlemler
Bir işlem, aynı mantıksal istek birden fazla kez uygulandığında sunucudaki amaçlanan son durum tek uygulamayla aynı kalıyorsa idempotenttir:
f(f(x)) = f(x)
Bu gösterim işlem semantiğini sadeleştirir, yan etkilerin fiziksel olarak yalnızca bir kez gerçekleştiğini zorunlu olarak ifade etmez.
Örneğin belirli bir anahtara aynı değeri yazmak idempotent olabilir:
UPDATE CONFIGURATION SET VALUE =? WHERE CONFIG_KEY =?
Aynı komut tekrarlandığında son durum değişmez.
Buna karşılık:
UPDATE ACCOUNT SET BALANCE = BALANCE -? WHERE ACCOUNT_ID =?
doğal olarak idempotent değildir. İkinci yürütme bakiyeyi yeniden azaltır.
HTTP semantiğinde bir metodun idempotent olması, aynı isteğin birden fazla uygulanmasının amaçlanan sunucu etkisini tek uygulamayla aynı bırakması şeklinde tanımlanır. RFC 9110, istemcinin idempotent olmayan bir isteği otomatik olarak ancak işlemin uygulama düzeyinde idempotent olduğunu bildiğinde veya ilk isteğin uygulanmadığını kesin olarak belirleyebildiğinde yeniden denemesi gerektiğini belirtir.
Doğal olarak idempotent olmayan işlemler idempotency key ile güvenli hale getirilebilir:
requestId = 7f613a...
Sunucu, aynı mantıksal işlemin bütün denemelerinde değişmeyen bu anahtarı saklar:
(requestId, requestHash, result, status)
Aynı "requestId" yeniden geldiğinde:
- İlk işlem tamamlanmışsa önceki sonuç döndürülür.
- İlk işlem devam ediyorsa yeni işlem başlatılmaz.
- Aynı anahtar farklı içerikle kullanılmışsa istek reddedilir.
- İlk girişim commit edilmemişse işlem kontrollü biçimde yeniden başlatılır.
Anahtar her retry girişiminde yeniden üretilirse koruma ortadan kalkar:
yanlış: attempt 1 → requestId A attempt 2 → requestId B
doğru: logical operation → requestId A attempt 1 → requestId A attempt 2 → requestId A
İdempotency kaydının süresi de retry penceresinden kısa olmamalıdır. İstemci bir saat boyunca yeniden deneyebiliyorsa sunucunun anahtarı beş dakika sonra unutması, geç retry'nin ikinci bir işlem oluşturmasına neden olabilir.
Katmanlı Sistemlerde Retry Amplification
Bir çağrı zinciri düşünelim:
istemci → API → iş servisi → veri servisi → repository → veritabanı
Her katman başarısız alt çağrıyı üç kez deniyorsa veritabanına ulaşabilecek maksimum girişim sayısı:
Amax = 3⁵ = 243
olur.
Tek bir kullanıcı isteği, en alt bağımlılıkta 243 fiziksel çağrıya dönüşebilir. AWS Builders' Library, beş katmanlı bir çağrı zincirinde her katmanın üç girişim yapması durumunda veritabanı yükünün 243 katına çıkabileceğini ve retry'nin mümkün olduğunca tek bir mimari katmanda uygulanması gerektiğini örnekler.
Bu olaya retry amplification denir. Genel durumda "d" bağımlılık katmanı ve katman başına "A" girişim varsa:
maksimum alt çağrı sayısı = A^d
Retry'nin her katmanda bulunması yalnızca yükü çoğaltmaz. Üst katman yeniden başladığında alt katmanlarda önceden tamamlanmış faydalı çalışmalar da atılabilir:
üst seviye timeout → bütün iş akışını yeniden başlat → daha önce başarılı alt çağrıları tekrar et
Bu nedenle retry sorumluluğu açıkça tek bir seviyeye atanmalıdır. Alt katmanlar ayrıntılı ve sınıflandırılabilir hata döndürmeli, retry bütçesini yöneten katman ise toplam deadline, idempotency ve iş bağlamını bilen katman olmalıdır.
Bazı işlemlerde yalnızca en alt çağrının yeniden denenmesi gerekir. Transaction çatışması gibi durumlarda ise tek SQL komutunu tekrar etmek yeterli olmayabilir, bütün read-modify-write dizisi baştan yürütülmelidir. Hatanın hangi soyutlama seviyesinde retry edileceği, hata kodunun kendisi kadar önemlidir.
Backoff Yükü Zamana Yayar
Sabit aralıkla yeniden deneme:
d₁ = 2 s d₂ = 2 s d₃ = 2 s
geçici kısa hatalarda yeterli olabilir. Ancak çok sayıda istemci aynı anda hata aldıysa hepsi aynı anda tekrar döner:
t = 0 → toplu hata t = 2 → toplu retry t = 4 → ikinci toplu retry
Üstel backoff, bekleme süresini girişim numarasıyla artırır:
dₖ = min(dmax, d₀ × b^k)
Burada:
- "d₀", başlangıç gecikmesidir.
- "b", büyüme katsayısıdır.
- "k", retry girişim numarasıdır.
- "dmax", üst gecikme sınırıdır.
Örneğin:
d₀ = 1 saniye b = 2
için:
1, 2, 4, 8, 16, 32...
saniyelik beklemeler oluşur.
Üst sınır kullanılmazsa retry süresi pratik olmayan değerlere ulaşabilir. Üst sınır kullanıldığında ise bütün istemciler bir süre sonra aynı sabit periyotta tekrar denemeye başlayabilir:
32, 32, 32, 32...
Bu nedenle backoff tek başına istemci senkronizasyonunu çözmez. AWS de capped exponential backoff sonrasında istemcilerin üst sınır oranında senkron biçimde retry üretmeye devam edebileceğini belirtir.
Ayrıca backoff kapasite üretmez. Hedef sistem saniyede 100 istek işleyebiliyor ve kalıcı yük 150 istek ise çağrıları geciktirmek yalnızca kuyruğun şeklini değiştirir:
λarrival > μservice
olduğu sürece backlog büyümeye devam eder.
Backoff yalnızca şu durumda iyileşme sağlar:
geciktirilmiş retry anında hedef sistemin kullanılabilir kapasitesi geri dönmüşse
Jitter ve Deterministik Dağılım
Aynı backoff formülünü kullanan binlerce istemci aynı hata anından başladığında girişim zamanları korelasyonlu kalır. Jitter, hesaplanan gecikmeye kontrollü dağılım ekler.
Full jitter yaklaşımı:
capₖ = min(dmax, d₀ × 2^k) delayₖ ~ Uniform(0, capₖ)
şeklinde ifade edilebilir.
Equal jitter yaklaşımında gecikmenin yarısı sabit, kalan yarısı rastgeledir:
delayₖ = capₖ / 2 + Uniform(0, capₖ / 2)
Amaç bütün istemcilerin aynı milisaniyede tekrar dönmesini engellemektir. AWS, eşzamanlı hataların aynı backoff zamanına hizalanması durumunda çatışmanın sürdüğünü, jitter'ın retry girişimlerini zamana dağıttığını belirtir.
Rastgelelik her sistemde aynı ölçüde uygun değildir. Üretim hatalarının tekrar oynatılabilir ve desenlerin gözlemlenebilir olması gereken yapılarda jitter deterministik üretilebilir:
seed = hash(clientId, logicalRequestId, attempt)
ve:
delay = deterministicUniform(seed, 0, cap)
hesaplanabilir.
Böylece farklı istemciler zamana dağılır, fakat aynı istemci ve aynı mantıksal istek tekrar oynatıldığında aynı gecikme dizisi elde edilir. AWS de periyodik görevlerde host başına her çalıştırmada değişen tamamen rastgele jitter yerine, aynı host için kararlı bir dağılımın operasyonel desenleri görünür tuttuğunu ifade eder.
Tek process ve tek thread çalışan bir görevde rastgele jitter her zaman gerekli değildir. Örneğin yalnızca bir veritabanı bağlantısı ve sıralı üç girişim varsa:
0 saniye 2 saniye 10 saniye
gibi deterministik bir dizi yeterli olabilir. Jitter'ın asıl değeri, bağımsız istemcilerin veya scheduler örneklerinin aynı bağımlılığa eşzamanlı yük bindirdiği yapılarda ortaya çıkar.
Timeout, Deadline ve Retry Bütçesi
Her girişime ayrı timeout vermek, toplam işlemin sınırsız uzamasını engellemez:
attempt timeout = 30 saniye attempt count = 3 backoff = 2 + 10 saniye
Bu politikanın yaklaşık toplam süresi:
30 + 2 + 30 + 10 + 30 = 102 saniye
olabilir.
İşlemin dış sözleşmesi 60 saniyeyse üçüncü girişimin başlaması anlamsızdır. Retry sistemi yalnızca girişim sayısını değil, toplam deadline bütçesini yönetmelidir:
remaining = deadline - currentTime
Yeni girişim ancak şu koşulda başlatılmalıdır:
remaining > backoff + minimumUsefulAttemptTime
Aksi durumda sonuç üretme ihtimali olmayan bir çağrı başlatılır ve hedef sistem gereksiz yere meşgul edilir.
gRPC, deadline'ı istemcinin artık sonucu beklemeyeceği mutlak sınır olarak tanımlar, varsayılan olarak deadline bulunmadığında çağrının süresiz bekleyebileceğini belirtir. Zincirleme RPC'lerde geçen süre düşülerek deadline'ın alt servislere taşınması, süresi dolmuş üst isteğe bağlı işlerin çalışmaya devam etmesini önlemeyi amaçlar.
Retry bütçesi şu unsurları birlikte kapsamalıdır:
maksimum girişim sayısı toplam deadline toplam backoff süresi toplam izin verilen retry yükü
Yalnızca girişim sayısı sınırlı bir politika, uzun timeout'larda kabul edilemez gecikme üretebilir. Yalnızca toplam süre sınırlı bir politika ise çok kısa timeout nedeniyle hedefe aşırı sayıda istek gönderebilir.
Token Bucket ile Retry Bütçesi
Bir servis hata vermeye başladığında bütün isteklerin aynı sayıda retry yapmasına izin vermek, hata oranı yükseldikçe retry trafiğini de yükseltir. Bunun yerine sistem genelinde sınırlı bir retry bütçesi tanımlanabilir.
Örneğin token bucket:
capacity = B refill rate = r token/saniye retry cost = 1 token
olarak yapılandırılır.
Her retry bir token tüketir. Token kalmadığında:
- İlk istekler normal biçimde gönderilebilir.
- Retry girişimleri reddedilir veya düşük sabit oranla sınırlandırılır.
- Hedef sistemin iyileşmesi için trafik payı bırakılır.
Bu yapı, retry'nin ana trafiğin belirli bir oranını aşmamasını sağlar. AWS, circuit breaker kaynaklı keskin mod değişimleri yerine retry sayısını yerel token bucket ile sınırlayarak token tükendiğinde retry'leri sabit oranla yürütme yaklaşımını kullandığını belirtir.
Basit bir oran bütçesi de kullanılabilir:
retryRate ≤ α × originalRequestRate
"α = 0,1" ise retry trafiği özgün trafiğin yüzde 10'unu aşmaz.
Bu durumda hata oranı yükseldiğinde bazı istekler retry edilmeden başarısız olur. Bu davranış ilk bakışta erişilebilirliği düşürüyor gibi görünür. Ancak kapasitesi çökmekte olan bağımlılığı koruyarak daha fazla özgün isteğin başarılı olmasını sağlayabilir.
Retry Politikası Bir Zamanlama Dizisi Değildir
Aşağıdaki yapı tek başına retry politikası değildir:
deneme 1 → hemen deneme 2 → 2 saniye sonra deneme 3 → 10 saniye sonra
Bu yalnızca gecikme dizisidir. Tam bir politika şu sorulara kesin cevap vermelidir:
Hangi hata sınıfları yeniden denenecek? İşlem idempotent mi? Aynı mantıksal işlem nasıl tanınacak? Retry hangi mimari katmanda yapılacak? Toplam deadline nedir? Her girişimin timeout'u nedir? Backoff ve jitter nasıl hesaplanacak? Retry yükü hangi bütçeyle sınırlandırılacak? Sunucu pushback veya Retry-After bilgisi dikkate alınacak mı? Başarısız retry nasıl gözlemlenecek?
gRPC'nin yerleşik retry mekanizması da yalnızca hata oluşmasına bakmaz, retry edilebilir durum kodu, girişim sınırı ve exponential backoff koşullarını birlikte değerlendirir. Ayrıca yanıt başlığı alındığında çağrıyı commit edilmiş sayarak sonraki otomatik retry'leri durdurur ve retry throttling ile server pushback mekanizmaları sağlar.
İzlenmesi gereken metrikler de yalnızca son başarı oranı değildir:
ilk girişimde başarı oranı retry sonrası başarı oranı girişim sayısı dağılımı hata sınıfına göre retry sayısı toplam retry yükü retry kaynaklı ek gecikme deadline nedeniyle iptal edilen girişimler idempotency key çakışmaları en eski bekleyen işin yaşı
İlk girişimde başarı oranı düşerken toplam başarı oranı sabit kalıyorsa retry, altyapı bozulmasını geçici olarak gizliyor olabilir. Retry sonrası başarı oranı da düşüyorsa ek girişimler yalnızca yük üretiyor demektir.
Yeniden deneme, hata toleransının otomatik karşılığı değildir. Doğru sınıflandırılmış geçici hatalarda ve idempotent işlemlerde erişilebilirliği artırabilir. Aşırı yük, kalıcı hata veya sonucu belirsiz yan etkiler söz konusu olduğunda ise sistemin kendi kendine uyguladığı dağıtık bir trafik saldırısına dönüşebilir.
Kritik sistemlerde asıl hedef her isteği mümkün olduğunca çok kez denemek değil, her retry girişiminin hala başarı üretme ihtimali bulunduğunu ve sistemin toplam kurtarma kapasitesini azaltmadığını garanti etmektir.