Mikroservis Mimarisi & Dayanıklılık 7 dk okuma 3 okunma

Retry Pattern ve Exponential Backoff

Retry Pattern, geçici hataların üstesinden gelmek için başarısız işlemleri belirli bir politika ile yeniden dener. Exponential Backoff, her denemede bekleme süresini katlayarak artırarak sistemi aşırı yüklenmeye karşı korur. Jitter eklenmesi, aynı anda yeniden deneme fırtınasını (thundering herd) önler. Idempotency ve maksimum deneme sayısı kritik öneme sahiptir.

Retry Pattern ve Exponential Backoff

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.

Tüm yazılar Soru ya da yorumun mu var? Yaz