Ödeme API çağrınız sabah 2'de başarısız oldu. Bu bir ağ arızası mıydı, hız sınırı mıydı, yoksa çökmüş bir sunucu mu? Cevap, yeniden denemenin işlemi kurtarıp kurtarmayacağını veya müşteriyi iki kez ücretlendirip ücretlendirmeyeceğini belirler.
Yeniden denemeler, dağıtılmış sistemlerde en yaygın esneklik modelidir; ancak en sık yanlış uygulananlardan da biridir. Doğru tasarlanmadığında, 30 saniyelik bir kesintiyi 30 dakikaya uzatabilir. Binlerce istemci aynı anda zor durumdaki sunucuya yük bindirir. Doğru tasarlandığında ise geçici arızaları kullanıcı fark etmeden absorbe eder.
Bu kılavuzda üretim sistemleri için güvenilir yeniden deneme mantığını ele alacağız:
- Yeniden denenebilecek HTTP durum kodları
- Tam jitter'lı üstel geri çekilme
-
Retry-Afterbaşlığı - İdempotens anahtarları
- Yeniden deneme bütçeleri
- Devre kesiciler
- Apidog sahte sunucularıyla
429ve503testleri
Neden basit yeniden denemeler kesintileri kötüleştirir?
Saniyede 1.000 istek işleyen bir hizmet düşünün. Hizmet beş saniye boyunca aksıyor ve her istemci isteği hemen üç kez yeniden deniyor. Talep, zaten zorlanan sunucuya yönelik 1.000 RPS'den 4.000 RPS'ye çıkar. Sunucu tamamen çöker ve istemciler yeniden denemeye devam eder.
Bu geri bildirim döngüsüne yeniden deneme fırtınası denir. Sunucu geri geldiğinde oluşan senkronize trafik dalgası ise gürleyen sürü (thundering herd) olarak bilinir. Google SRE kitabı, geri çekilme olmadan yapılan yeniden denemelerin sistemin en az kaldırabileceği anda yükü artırdığını vurgular.
Yeniden deneme fırtınalarının başlıca nedenleri:
- Gecikme olmaması: Anında yeniden denemeler en kötü anda ek yük oluşturur.
- Sabit gecikme: Her istemci bir saniye beklerse hepsi aynı anda geri döner.
Çözüm yeniden denemelerden tamamen vazgeçmek değildir. Yeniden denemeleri seçici biçimde, artan rastgele gecikmelerle ve ek yük için net bir üst sınırla uygulayın.
Bu arızaları yeniden deneyin, şunları asla denemeyin
Önce bir karar tablosu oluşturun. Sunucunun isteğinizi geçersiz olduğu için reddettiği durumlarda yeniden denemek kapasiteyi boşa harcar. Amaç geçici hataları yeniden denemektir.
Yeniden denenebilecek durumlar
| Sinyal | Anlamı |
|---|---|
429 Too Many Requests |
Hız sınırına ulaştınız. Geri çekilin ve daha yavaş istek gönderin. |
502 Bad Gateway |
Yukarı akıştan geçersiz yanıt alındı. Genellikle geçicidir. |
503 Service Unavailable |
Sunucu aşırı yüklenmiş veya yeniden başlatılıyor olabilir. |
504 Gateway Timeout |
Yukarı akış bağımlılığı çok yavaştı. |
| Bağlantı sıfırlamaları, DNS hataları, soket zaman aşımları | İstek sunucuya hiç ulaşmamış olabilir. |
504 Gateway Timeout özel dikkat gerektirir: Ağ geçidi beklemeyi bırakmış olsa bile kaynak isteğiniz işlenmiş olabilir. Bu ayrım, idempotens açısından kritiktir. Ayrıntılar için 504 Gateway Timeout yazısına bakabilirsiniz.
Yeniden denenmemesi gereken durumlar
| Sinyal | Anlamı |
|---|---|
400 Bad Request |
İstek gövdesi hatalı biçimlendirilmiş; tekrar denemede de hatalı olacaktır. |
401 Unauthorized |
Kimlik bilgileri yanlış veya süresi dolmuş. Token'ı yenileyin. |
403 Forbidden |
Yetkiniz yok; yeniden denemek yetki sağlamaz. |
422 Unprocessable Entity |
Doğrulama başarısız oldu. Zamanlamayı değil, veriyi düzeltin. |
Kural: Hata sunucunun durumu veya ağ ile ilgiliyse yeniden deneyin. Hata isteğinizle ilgiliyse hızlıca başarısız olun. 429 iki durumun arasındadır: Yeniden denenebilir, ancak aynı zamanda genel istek hızınızı, önbellekleme veya istemci tarafı kısıtlamasıyla iyileştirmeniz gerektiğini gösterir. API rate limiting yaklaşımını yeniden deneme döngüsünün üstünde ele alın.
Üstel geri çekilme ve jitter
Üstel geri çekilmede her yeniden deneme bir öncekinden daha uzun bekler:
delay = base * 2^retry_count
500 ms taban gecikmeyle bekleme süreleri 0.5s, 1s, 2s, 4s, 8s olur. Gecikmelerin dakikalara dönüşmesini önlemek için bir üst sınır ekleyin:
delay = min(cap, base * 2^retry_count)
Bu yaklaşım yükü azaltır, ancak senkronizasyonu çözmez. Aynı anda başarısız olan 5.000 istemci 0.5s, 1s ve 2s sonra birlikte geri döner. Trafik hâlâ dalgalar hâlindedir.
Tam jitter formülü
Jitter, gecikmeyi rastgeleleştirerek istemcilerin senkronizasyonunu bozar. Tam jitter, sıfır ile üstel tavan arasında rastgele bir gecikme seçer:
delay = random_between(0, min(cap, base * 2^retry_count))
AWS'nin üstel geri çekilme ve jitter analizinde, tam jitter'ın hem toplam çağrı sayısında hem de tamamlanma süresinde başarılı sonuç verdiği gösterilir. İstemcileri pencereye yaymak, sunucu yükünü düzleştirir.
Ölçümleriniz farklı bir yaklaşımı gerektirmediği sürece tam jitter'ı varsayılan desen olarak kullanın.
Sunucu söylediğinde Retry-After değerine uyun
İstemci kendi geri çekilme süresini tahmin eder; ancak sunucu bazen doğru bekleme süresini doğrudan bildirir. 429 ve 503 yanıtlarıyla kullanılabilen Retry-After başlığı saniye veya HTTP tarihi taşıyabilir:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Başlık mevcutsa hesaplanan geri çekilme süresini geçersiz kılın. Sunucu, hız sınırı penceresinin ne zaman sıfırlanacağını veya bakımın ne zaman biteceğini sizden daha iyi bilir.
Yine de şu korumaları koruyun:
-
Retry-Afterdeğerini ayrıştırın. - Değeri kendi üst sınırınızla sınırlayın.
- Maksimum yeniden deneme sayısını uygulayın.
Böylece hatalı veya kötü niyetli bir Retry-After: 86400 değeri işçinizi bir gün boyunca kilitlemez.
İdempotens: POST yeniden denemelerinin ön koşulu
GET, PUT ve DELETE sözleşme gereği idempotenttir; aynı isteği iki kez göndermek sistemi aynı mantıksal durumda bırakır. POST ise idempotent değildir.
Örneğin POST /v1/payments sunucu tarafından işlendikten sonra zaman aşımına uğrarsa, istemcinin yeniden denemesi ikinci bir ödeme oluşturabilir. Fintek API yeniden deneme mantığı geliştirirken bu senaryo özellikle önemlidir.
Çözüm, her mantıksal işlem için benzersiz bir idempotens anahtarı göndermektir:
Idempotency-Key: <client-generated-uuid>
Sunucu ilk yanıtla birlikte anahtarı saklar ve yinelenen isteklerde kaydedilmiş yanıtı döndürür. Stripe'ın idempotent istekleri bu modelin yaygın bir örneğidir.
İki temel kural:
- Aynı işlem, aynı anahtar: Bir ödemenin tüm yeniden denemeleri aynı anahtarı kullanmalıdır.
- Anahtarı döngünün dışında oluşturun: Anahtarı her yeniden denemede üretmeyin. Aksi hâlde her istek yeni bir işlem gibi görünür.
Çağırdığınız API idempotens anahtarlarını desteklemiyorsa idempotent olmayan yazmaları otomatik olarak yeniden denemeyin. Hatayı yüzeye çıkarın ve insan müdahalesi veya uzlaştırma işi için bırakın.
Yeniden deneme bütçeleri ve devre kesiciler
Geri çekilme, yeniden denemelerin zamanını düzenler; ancak sayılarını sınırlamaz. Uzun bir kesintide, iyi jitter uygulanmış istemciler bile ek yük oluşturabilir.
Katmanlı yeniden denemeler bunu daha da büyütür. API ağ geçidi üç kez, hizmet istemcisi de üç kez yeniden denerse tek bir kullanıcı tıklaması dokuz isteğe dönüşebilir.
Yeniden deneme bütçeleri
“İstek başına üç yeniden deneme” yerine kayan bir pencere üzerinde şu tür bir kural uygulayın:
Yeniden denemeler en fazla %10 ek trafik oluşturabilir.
Bütçe tükendiğinde hataları hemen döndürün. Böylece aynı anda kaç isteğin başarısız olduğundan bağımsız olarak yeniden deneme amplifikasyonu sınırlı kalır. Linkerd ve Envoy bu yaklaşımı birinci sınıf yapılandırma olarak destekler.
Devre kesiciler
Her aşağı akış bağımlılığının hata oranını izleyin. Hata oranı eşiği aştığında devre kesici açılır ve çağrılar ağa ulaşmadan anında başarısız olur.
Soğuma süresinden sonra birkaç yoklama isteği gönderilir. Bağımlılık düzelmişse devre kesici kapanır; düzelmemişse açık kalır.
Geri çekilme yığılmayı yavaşlatır, devre kesici ise yükü durdurur. Üretim sistemlerinde ikisini birlikte kullanın.
Python'da üretime hazır örnek
Aşağıdaki örnek yeniden denenebilir durum kodlarını filtreler, tam jitter kullanır, Retry-After değerine uyar, idempotens anahtarını yeniden kullanır ve maksimum yeniden deneme sınırı uygular:
import random
import time
import uuid
import requests
RETRYABLE = {429, 502, 503, 504}
BASE = 0.5 # seconds
CAP = 30.0 # ceiling on any single delay
MAX_RETRIES = 5
def create_payment(payload):
idempotency_key = str(uuid.uuid4()) # one key per logical payment
headers = {"Idempotency-Key": idempotency_key}
for retry_count in range(MAX_RETRIES + 1):
try:
resp = requests.post(
"https://api.acmepay.com/v1/payments",
json=payload, headers=headers, timeout=10,
)
if resp.status_code < 400:
return resp.json()
if resp.status_code not in RETRYABLE:
resp.raise_for_status() # 400/401/403/422: fail fast
retry_after = resp.headers.get("Retry-After")
except (requests.ConnectionError, requests.Timeout):
retry_after = None # network fault: fall through to backoff
if retry_count == MAX_RETRIES:
raise RuntimeError("payment failed after all retries")
if retry_after and retry_after.isdigit():
delay = min(CAP, float(retry_after))
else:
delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
time.sleep(delay)
Dikkat edilmesi gereken noktalar:
- İdempotens anahtarı döngünün dışında, yalnızca bir kez üretilir.
-
Retry-After, hesaplanan geri çekilmenin önüne geçer; ancakCAPile sınırlandırılır. - Yeniden denenemeyen durumlar hemen yükseltilir.
JavaScript kullanıyorsanız axios-retry, retryCondition ve retryDelay kancalarıyla aynı yapıyı sağlar. Karar tablosu yine aynıdır.
Üretimden önce yeniden deneme davranışını test edin
Birçok ekip yalnızca başarılı yolu test eder; 503 yolu gerçek bir kesinti sırasında ilk kez çalışır. Bu yaklaşım yerine Apidog ile başarısızlık senaryolarını kontrollü biçimde simüle edin.
1. Sahte sunucuyla hataları simüle edin
Apidog'un akıllı sahte sunucusuyla /v1/payments gibi bir uç nokta tanımlayın ve yanıtları senaryoya göre değiştirin:
- İlk iki çağrıda
503, üçüncü çağrıda200döndürün. -
Retry-After: 5ile429döndürün. - İstemci zaman aşımını test etmek için 15 saniyelik gecikme ekleyin.
İstemcinizi sahte URL'ye yönlendirin ve her senaryoda yeniden deneme döngüsünün davranışını gözlemleyin.
2. Test senaryolarıyla davranışı doğrulayın
Apidog test senaryolarında istekleri birbirine bağlayın ve şu iddiaları kontrol edin:
- Çağrı sonunda başarılı oluyor mu?
- Toplam süre beklenen geri çekilme aralığında mı?
- İdempotens anahtarı sayesinde tam olarak bir kaynak mı oluşturuldu?
Testi CI'a bağlayarak yeniden deneme mantığını yalnızca kesinti sırasında değil, her değişiklikte çalıştırın.
Bu, “yeniden denemeler ekledik” ile “istemcimizin hız sınırlı veya yarı kapalı bir bağımlılıkta hayatta kaldığını doğruladık” arasındaki farktır. Apidog'u ücretsiz indirin ve yaklaşık on dakika içinde istemcinize karşı çalışan bir sahte sunucu oluşturun.
SSS
429 yanıtını yeniden denemeli miyim?
Evet. Retry-After başlığını okuyun ve en az belirtilen süre kadar bekleyin. Başlık yoksa jitter'lı üstel geri çekilmeye dönün.
Tekrarlanan 429 yanıtlarını normal bir işlem gibi görmeyin. Bunlar istemci tarafı kısıtlaması, önbellekleme veya istek hızının genel olarak azaltılması gerektiğine dair sinyallerdir.
Tam jitter nedir?
Tam jitter, her yeniden deneme gecikmesini sıfır ile üstel tavan arasında tekdüze biçimde rastgele seçer:
random(0, min(cap, base * 2^n))
Bu yaklaşım, çok sayıda istemciden gelen senkronize yeniden deneme dalgalarını önler. AWS simülasyonlarında basit geri çekilme ve eşit jitter'a kıyasla daha az toplam çağrı ve daha kısa tamamlanma süresi sağlamıştır.
POST isteklerini yeniden denemek güvenli midir?
Yalnızca istek pratikte idempotent hâle getirildiyse güvenlidir. POST için bu genellikle sunucunun yinelenen istekleri ayıklamasını sağlayan bir idempotens anahtarı göndermek anlamına gelir.
Anahtar olmadan zaman aşımından sonra yapılan yeniden deneme ödeme, sipariş veya kaydı çoğaltabilir; çünkü sunucu sizin başarısız sandığınız isteği zaten işlemiş olabilir. Yapay zeka ajanlarında hata kurtarma modelleri için de aynı ilkeler geçerlidir: anahtarlı yazmalar, sınırlı yeniden denemeler ve devre kesici.
Kaç kez yeniden denemeliyim?
Üç ila beş deneme çoğu geçici hatayı ele alır. Bunun ötesinde başarı oranı sabitlenirken yük ve gecikme artmaya devam eder.
İstek başına bir üst sınırı global yeniden deneme bütçesiyle eşleştirin. Örneğin yeniden denemeler toplamda en fazla %10 ek trafik oluşturabilir. Bağımlılık son yeniden denemeden sonra hâlâ kapalıysa sorun artık yeniden deneme değil, devre kesici kapsamındadır.
Top comments (1)
Hello Glad to see you, I am Kane Lim from Hong Kong. I have over 10 years of development experience. I am writing this because your post was interesting.
The retry storm and payment duplication scenarios you highlighted are exactly where many production systems fail. I would take the design one step further by treating retries as a distributed systems budget rather than simply an HTTP client feature.
For payment workflows, I prefer combining idempotency keys, exponential full jitter, Retry After, circuit breakers, and an overall deadline enforced by the calling workflow. The deadline is important because five retries can still create unacceptable tail latency if each attempt has its own timeout.
I would also introduce failure classification at the transport layer. A connection reset after request transmission should be treated as an unknown outcome, not automatically as a failed operation. For critical writes, persist the operation state and reconcile asynchronously against the provider using the same idempotency key.
At scale, adaptive concurrency limiting is another valuable layer. Instead of allowing every worker to retry independently, control concurrency based on observed latency, error rate, and rate limit signals. This prevents retry amplification across service boundaries.
One additional metric I strongly recommend is retry amplification ratio: total downstream attempts divided by original logical operations. Tracking this alongside p95 latency and dependency saturation makes retry storms visible before they become outages.
Excellent production focused writeup. I would be happy to exchange ideas on resilient API architecture and distributed systems with you.