Retry Pattern ve Exponential Backoff
Dağıtık sistemlerde, ağ çağrıları, veritabanı işlemleri ve harici servis çağrıları her zaman başarılı olmaz. Geçici ağ kesintileri, anlık yüklenmeler veya rate limiting gibi nedenlerle bir istek başarısız olabilir. Ancak bu hataların çoğu geçici (transient) niteliktedir; kısa bir süre sonra aynı istek başarılı olabilir. İşte Retry Pattern, bu geçici hataların üstesinden gelmek için başarısız işlemlerin belirli bir politika ile yeniden denenmesini sağlar.
1. Retry Pattern Nedir?
Retry Pattern, bir işlem başarısız olduğunda, bu işlemi belirli bir sayıda veya belirli bir süre boyunca yeniden denemeyi öneren bir dayanıklılık desenidir. Amaç, geçici hataların kullanıcıya yansımasını engellemek ve sistemin kendi kendini iyileştirmesini sağlamaktır.
Temel Akış:
text
İstek Gönder → Başarısız → Bekle → Tekrar Dene → Başarısız → Bekle → Tekrar Dene → ...
Önemli: Retry Pattern, geçici hatalar için tasarlanmıştır. Kalıcı hatalar (400 Bad Request, 401 Unauthorized, 404 Not Found) için retry yapmak anlamsızdır ve kaynak israfına yol açar.
2. Hangi Hatalar Retry Edilmeli?
| HTTP Durum Kodu | Retry Yapılmalı mı? | Açıklama |
|---|---|---|
| 2xx (200, 201, 204) | ❌ Hayır | Başarılı, retry gerekmez |
| 400 Bad Request | ❌ Hayır | Kalıcı hata, istek formatı bozuk |
| 401 Unauthorized | ❌ Hayır | Kimlik doğrulama hatası |
| 403 Forbidden | ❌ Hayır | Yetki hatası |
| 404 Not Found | ❌ Hayır | Kaynak yok |
| 408 Request Timeout | ✅ Evet | Geçici ağ sorunu |
| 429 Too Many Requests | ✅ Evet | Rate limit, Retry-After'a uyulmalı |
| 500 Internal Server Error | ✅ Evet | Sunucu hatası, geçici olabilir |
| 502 Bad Gateway | ✅ Evet | Proxy/servis geçici olarak kullanılamıyor |
| 503 Service Unavailable | ✅ Evet | Servis bakımda veya aşırı yüklü |
| 504 Gateway Timeout | ✅ Evet | Yukarı akış servisi zaman aşımına uğradı |
| Bağlantı Hatası | ✅ Evet | Ağ sorunu, geçici olabilir |
Genel Kural: 5xx hataları ve ağ hataları retry edilir; 4xx hataları (429 hariç) retry edilmez.
3. Retry Stratejileri (Backoff Algoritmaları)
Retry denemeleri arasındaki bekleme süresini belirleyen stratejilere backoff (geri çekilme) algoritmaları denir.
A. No Backoff (Anında Retry)
Her başarısız denemeden sonra hemen tekrar denenir. Sunucu aşırı yüklüyse durumu daha da kötüleştirir (thundering herd).
B. Fixed Delay (Sabit Bekleme)
Her denemede sabit bir süre beklenir. Basittir ancak uzun süren kesintilerde etkisizdir.
C. Linear Backoff (Doğrusal Geri Çekilme)
Bekleme süresi her denemede sabit bir miktar artar: 1sn, 2sn, 3sn...
D. Exponential Backoff (Üstel Geri Çekilme)
Bekleme süresi her denemede katlanarak artar:
text
Deneme 1 → 1sn → Deneme 2 → 2sn → Deneme 3 → 4sn → Deneme 4 → 8sn
Avantaj: Sunucuya nefes alma süresi tanır; aşırı yüklenmeyi önler.
Dezavantaj: Toplam bekleme süresi hızla artar.
E. Exponential Backoff + Jitter (Rastgelelik)
Üstel geri çekilmeye rastgele bir değer eklenir. Bu, aynı anda başarısız olan binlerce isteğin aynı anda yeniden denenmesini (thundering herd) önler.
text
Deneme 1 → 1sn + random(0-500ms) → Deneme 2 → 2sn + random(0-1sn) → ...
Jitter Türleri:
-
Full Jitter:
sleep = random(0, min(cap, base * 2^attempt)) -
Equal Jitter:
sleep = (base * 2^attempt) / 2 + random(0, (base * 2^attempt) / 2)
4. Retry Pattern'ın Riskleri
A. Thundering Herd (Derebeyi Sürüsü)
Aynı anda binlerce istek başarısız olursa ve hepsi aynı anda yeniden denenirse, sunucu daha da çöker. Çözüm: Jitter kullanın.
B. Retry Storm (Retry Fırtınası)
Bir servis yavaşladığında, üst katmanlardaki servisler sürekli retry yaparsa, yük katlanarak artar. Çözüm: Circuit Breaker ile birlikte kullanın.
C. Idempotency (Yinelenebilirlik) İhlali
Retry edilen bir işlem, yan etkileri nedeniyle istenmeyen sonuçlar doğurabilir (ör. çift ödeme). Çözüm: İşlemleri idempotent yapın (Idempotency Key kullanın).
D. Toplam Gecikme Artışı
Üstel geri çekilme ile toplam bekleme süresi hızla artabilir. Çözüm: Maksimum bekleme süresi (cap) ve maksimum deneme sayısı belirleyin.
5. .NET'te Polly ile Retry Uygulaması
A. Basit Retry (Fixed Delay)
csharp
using Polly;
var retryPolicy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: _ => TimeSpan.FromSeconds(2),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} - {timespan} sonra denenecek.");
});
var response = await retryPolicy.ExecuteAsync(async () =>
await _httpClient.GetAsync("https://api.example.com/data"));
B. Exponential Backoff
csharp
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2, 4, 8, 16, 32 saniye
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount}: {timespan.TotalSeconds}sn bekleniyor.");
});
C. Exponential Backoff + Jitter
csharp
var random = new Random();
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: retryAttempt =>
{
var exponential = Math.Pow(2, retryAttempt);
var jitter = random.NextDouble();
return TimeSpan.FromSeconds(exponential + jitter);
});
D. Polly v8 ile Resilience Pipeline
csharp
using Polly;
using Polly.Retry;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(30),
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.TooManyRequests ||
(int)r.StatusCode >= 500),
OnRetry = args =>
{
Console.WriteLine($"Retry {args.AttemptNumber}: {args.RetryDelay.TotalSeconds}sn bekleniyor.");
return default;
}
})
.Build();
6. Retry-After Başlığına Uyum
429 Too Many Requests veya 503 Service Unavailable yanıtlarında sunucu, Retry-After başlığı ile ne zaman tekrar deneneceğini bildirebilir. Bu süreye uymak, sunucuya saygılı davranmanın ve rate limit'i aşmamanın anahtarıdır.
csharp
var retryPolicy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: (retryCount, outcome, context) =>
{
if (outcome.Result?.Headers.RetryAfter?.Delta is TimeSpan retryAfter)
return retryAfter;
return TimeSpan.FromSeconds(Math.Pow(2, retryCount));
});
7. HTTP Client Factory ile Retry Entegrasyonu
csharp
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddResilienceHandler("retry-pipeline", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(10)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 8,
BreakDuration = TimeSpan.FromSeconds(30)
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
8. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Sadece geçici hataları retry edin | 4xx hataları (429 hariç) retry etmeyin |
| Exponential Backoff + Jitter kullanın | Thundering herd'i önlemek için jitter şarttır |
| Maksimum retry sayısı belirleyin | Sonsuz retry, kaynak israfına yol açar |
| Maksimum bekleme süresi (cap) koyun | Üstel geri çekilme hızla büyür; MaxDelay ile sınırlayın |
| Idempotency sağlayın | Retry edilen işlemler idempotent olmalı |
| Retry-After başlığına uyun | Sunucu ne zaman deneyeceğinizi söylüyorsa, ona uyun |
| Circuit Breaker ile birleştirin | Sürekli başarısız olan servisi geçici olarak devre dışı bırakın |
| Toplam timeout belirleyin | Retry'ler toplamda bir timeout'u aşmamalı |
| Loglayın ve izleyin | Retry sayılarını ve başarı oranlarını izleyin |
9. Retry Pattern vs. Diğer Dayanıklılık Desenleri
| Desen | Amaç | Retry ile İlişkisi |
|---|---|---|
| Retry | Geçici hatayı aşmak için tekrar dene | Temel desen |
| Circuit Breaker | Hatalı servisi geçici olarak devre dışı bırak | Retry'ı tamamlar; sürekli başarısız olan servise retry yapılmasını engeller |
| Bulkhead | Kaynak izolasyonu | Retry'ların tüm kaynakları tüketmesini engeller |
| Timeout | Uzun süren işlemleri iptal et | Retry'ın sonsuza kadar sürmesini engeller |
| Fallback | Hata durumunda yedek yanıt dön | Retry başarısız olduğunda devreye girer |
10. Gerçek Dünya Senaryosu
Senaryo: Bir e-ticaret uygulaması, ödeme sağlayıcısına istek gönderiyor. Sağlayıcı zaman zaman yavaşlıyor veya geçici olarak kullanılamıyor.
Çözüm: Exponential backoff + jitter + circuit breaker + idempotency key.
csharp
public class PaymentService
{
private readonly HttpClient _httpClient;
private readonly ResiliencePipeline _pipeline;
public PaymentService(HttpClient httpClient)
{
_httpClient = httpClient;
_pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 4,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r =>
(int)r.StatusCode >= 500 ||
r.StatusCode == HttpStatusCode.TooManyRequests)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(60)
})
.AddTimeout(TimeSpan.FromSeconds(20))
.Build();
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, string idempotencyKey)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments")
{
Content = JsonContent.Create(request)
};
httpRequest.Headers.Add("Idempotency-Key", idempotencyKey);
var response = await _pipeline.ExecuteAsync(async token =>
await _httpClient.SendAsync(httpRequest, token));
if (!response.IsSuccessStatusCode)
throw new PaymentException($"Ödeme başarısız: {response.StatusCode}");
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
}
Bu yapılandırma sayesinde:
-
Geçici hatalar otomatik olarak yeniden denenir.
-
Jitter ile thundering herd önlenir.
-
Circuit breaker ile sürekli başarısız olan sağlayıcı geçici olarak devre dışı bırakılır.
-
Idempotency key ile çift ödeme engellenir.
Sonuç
Retry Pattern ve Exponential Backoff, dağıtık sistemlerde dayanıklılığın temel yapı taşlarıdır. Geçici hataların üstesinden gelmek için sadece geçici hataları retry edin, exponential backoff + jitter kullanın, maksimum deneme ve bekleme süresi belirleyin, idempotency sağlayın ve Retry-After başlığına uyun. Polly, .NET ekosisteminde bu politikaları uygulamak için olgun ve güçlü bir kütüphanedir. Circuit Breaker, Bulkhead, Timeout ve Fallback ile birlikte kullanıldığında, sisteminiz dalgalanmalara ve arızalara karşı çok daha dirençli hale gelir. Unutmayın: Retry, bir sihirli değnek değildir; yanlış kullanıldığında sorunu büyütür. Doğru yapılandırıldığında ise sisteminizin kendi kendini iyileştirmesini sağlar.