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

Retry Pattern ve Exponential Backoff

Retry Pattern (Yeniden Deneme Deseni), geçici (transient) hataların üstesinden gelmek için başarısız işlemlerin belirli bir politika ile yeniden denenmesini sağlar. Exponential Backoff (Üstel Geri Çekilme), her denemede bekleme süresini katlayarak artırarak sistemi aşırı yüklenmeye karşı korur. Bu yazıda, retry stratejileri, backoff algoritmaları (sabit, üstel, jitter), idempotency gereksinimi, .NET'te Polly ile uygulama ve en iyi pratikler ele alınır.

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, sunucu yeniden başlatmaları 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 (Yeniden Deneme Deseni), 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ı, retry faydasız.
403 Forbidden ❌ Hayır Yetki hatası, retry faydasız.
404 Not Found ❌ Hayır Kaynak yok, retry faydasız.
408 Request Timeout ✅ Evet Geçici ağ sorunu.
429 Too Many Requests ✅ Evet Rate limit, Retry-After başlığına 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 geçici olarak bakımda veya aşırı yüklü.
504 Gateway Timeout ✅ Evet Yukarı akış servisi zaman aşımına uğradı.
Bağlantı Hatası (Socket Exception) ✅ 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.

text

Deneme 1 → Hemen → Deneme 2 → Hemen → Deneme 3

Dezavantaj: Sunucu aşırı yüklüyse, anında retry durumu daha da kötüleştirir (thundering herd).

B. Fixed Delay (Sabit Bekleme)
Her denemede sabit bir süre beklenir.

text

Deneme 1 → 2sn → Deneme 2 → 2sn → Deneme 3

Avantaj: Basit.
Dezavantaj: Uzun süren kesintilerde etkisizdir.

C. Linear Backoff (Doğrusal Geri Çekilme)
Bekleme süresi her denemede sabit bir miktar artar.

text

Deneme 1 → 1sn → Deneme 2 → 2sn → Deneme 3 → 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 → Deneme 5 → 16sn

Avantaj: Sunucuya nefes alma süresi tanır; aşırı yüklenmeyi önler.
Dezavantaj: Toplam bekleme süresi hızla artar; kullanıcı deneyimini olumsuz etkileyebilir.

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)

  • Decorrelated Jitter: sleep = min(cap, random(base, prevSleep * 3))


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ı

Polly, .NET ekosisteminde retry politikalarını uygulamak için en yaygın kullanılan kütüphanedir.

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(); // 0 ile 1 arası rastgele
            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), // Maksimum bekleme süresi (cap)
        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();

var result = await pipeline.ExecuteAsync(async token =>
    await _httpClient.GetStringAsync("https://api.example.com/data", token));

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));
        },
        onRetryAsync: async (outcome, timespan, retryCount, context) =>
        {
            Console.WriteLine($"Rate limit - {timespan.TotalSeconds}sn bekleniyor.");
        });

7. HTTP Client Factory ile Retry Entegrasyonu

csharp

// Program.cs
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. Retry Pattern En İyi Pratikler

Pratik Açıklama
Sadece Geçici Hataları Retry Edin 4xx hataları (429 hariç) retry etmeyin; kalıcı hatalardır.
Exponential Backoff + Jitter Kullanın Thundering herd'i önlemek için jitter şarttır.
Maksimum Retry Sayısı Belirleyin Sonsuz retry, kaynak israfına ve gecikmeye 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ı; aksi halde çift işlem riski.
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 İzleyin Retry sayılarını ve başarı oranlarını izleyin.
Test Edin Farklı hata senaryolarında retry davranışını test edin.

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 (Payment Gateway) 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)
        };
        // Idempotency Key - aynı istek tekrar edilirse çift ödeme engellenir
        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