Yeniden Deneme Modeli: Dayanıklı Mikro Hizmetler Oluşturma
Mikro hizmet mimarisinde hizmetler, bellek içi çağrılar yerine ağ üzerinden iletişim kurar. Bu ayırma, büyük yatay ölçeklendirmeye ve bağımsız dağıtımlara olanak tanırken aynı zamanda büyük bir güvenlik açığını da beraberinde getirir: ağ güvenilmezdir.
Herhangi bir anda, bir aşağı akış hizmetinde kısa bir ağ arızası, geçici bir CPU artışı, hızlı bir veritabanı kilit çekişmesi veya güncellemenin sürekli olarak yeniden başlatılması yaşanabilir. Bu geçici arızalar geçici arızalar olarak bilinir.
Hizmetiniz hemen bir hata verirse ve aşağı akış çağrısının başarısız olduğu anda istekte başarısız olursa, hassas bir kullanıcı deneyimi yaratırsınız. Bunun yerine, bu geçici hataların çoğu, bir süre bekleyip tekrar deneyerek otomatik olarak çözülebilir. Yeniden Deneme Modeli burada devreye giriyor.
Bu kılavuzda Yeniden Dene modelini, genel olarak nasıl çalıştığını, basit uygulamaların tehlikelerini ve bunun Java (Resilience4j) ve Go’da doğru şekilde nasıl uygulanacağını inceleyeceğiz.
Gerçek Dünya Analojisi: Meşgul Hattı Tekrar Aramak
Bir arkadaşınızı aramaya çalıştığınızı hayal edin. Numarayı çevirirsiniz ancak şu anda başka bir görüşmede oldukları için meşgul sinyali alırsınız.
Derhal pes edip irtibatlarını siliyor ve onlarla bir daha asla konuşamayacağınızı mı varsayıyorsunuz? Tabii ki değil. Telefonu kapatıyorsunuz, bir dakika bekliyorsunuz ve numaralarını tekrar çeviriyorsunuz. Eğer hala meşgullerse, tekrar denemeden önce beş dakika bekleyebilirsiniz.
Sonunda aramaları sona erer ve yeniden denemeniz başarılı olur.
Mikro hizmetlerde:
- Çağrı bir aşağı akış hizmetine yapılan API isteğidir.
- Meşgul Sinyali geçici bir ağ hatası veya
503 Service Unavailableyanıtıdır. - Yeniden Arama bir yeniden deneme girişimidir.
- Bekleme Süresi geri çekilme süresidir.
Tehlike: Naif Yeniden Denemeler ve “Yeniden Deneme Fırtınaları”
Yeniden deneme mekanizmasını uygulamak ilk bakışta önemsiz görünebilir: HTTP çağrınızı bir for döngüsüne sarmanız ve başarılı olana kadar denemeye devam etmeniz yeterlidir. Bununla birlikte, basit bir yeniden deneme uygulaması, küçük bir kesintiyi kolayca sistem çapında felaketle sonuçlanabilecek bir kesintiye dönüştürebilir.
Ani bir trafik dalgası altında zorluk çeken bir alt hizmet düşünün. Veritabanı %99 CPU kullanımıyla çalışıyor ve istekler zaman aşımına uğramaya başlıyor.
100 istemci hizmetinin tümü bir zaman aşımı tespit ederse ve beklemeden hemen 3 kez yeniden denerse, zaten boğucu olan aşağı akış hizmetine çarpan trafik hacmi aniden üç katına çıkacak. Trafiğin bu ani artışı Yeniden Deneme Fırtınası (veya Gürleyen Sürü sorunu) olarak bilinir.
Aşağı akış hizmetinin iyileşmesine yardımcı olmak yerine, yeniden denemeleriniz onu aşağı itmeye devam edecek ve asla yetişmesini engelleyecektir.
Çözüm: Gerileme ve Titreşim
Yeniden deneme fırtınalarını önlemek için dirençli bir sistemin iki temel stratejiden yararlanması gerekir: Geri çekilme ve Sarsıntı.
1. Geri Alma Stratejileri
Backoff, bir istemcinin daha sonra yeniden deneme girişiminde bulunmadan önce ne kadar beklemesi gerektiğini belirler.
- Sabit Geri Alma: İstemci, denemeler arasında sabit bir süre (ör. tam olarak 200 ms) bekler. Basit olmasına rağmen yine de senkronize trafik artışları riskini taşır.
- Üstel Gerileme: Bekleme süresi her başarısız denemede katlanarak artar (ör. 100 ms, 200 ms, 400 ms, 800 ms). Bu, arıza devam ettikçe aşağı akış hizmetine iyileşmesi için giderek daha fazla zaman kazandırır.
2. Titreşim (Rastgelelik)
Üstel geri çekilme durumunda bile, bir ağ kesintisi 1.000 isteğin tam olarak aynı anda başarısız olmasına neden olursa, 1.000 istemcinin tümü tam olarak aynı geri çekilme gecikmesini hesaplayacaktır. Sonuç olarak, hepsi aynı anda dalgalar halinde yeniden deneyecek ve senkronize ani artışlarla aşağı akış hizmetine ulaşacak.
Jitter, geri çekilme gecikmesine rastgele değişkenlik ekleyerek bu sorunu çözer.
Without Jitter (Synchronized Waves):
Time: 0ms -> [1000 requests fail]
Time: 100ms -> [1000 retries hit simultaneously]
Time: 200ms -> [1000 retries hit simultaneously]
With Jitter (Distributed Traffic):
Time: 0ms -> [1000 requests fail]
Time: 92ms -> [85 retries]
Time: 105ms -> [120 retries]
Time: 118ms -> [95 retries]
... (Traffic is smoothed out over time)
Yeniden denemeleri rastgele bir aralığa yayarak, aşağı akış hizmetindeki yük yumuşatılır ve sorunsuz bir şekilde iyileşmesine olanak sağlanır.
Yeniden Denemenin Altın Kuralı: Bağımsızlık
Herhangi bir API uç noktasına yeniden denemeler uygulamadan önce kritik bir soruyu sormanız gerekir: Bu işlem bağımsız mı?
idempotent işlemi, birden fazla aynı isteğin yapılmasının, tek bir istek yapılmasıyla tam olarak aynı etkiye sahip olduğu işlemdir.
- Idempotent: Bir kullanıcı profilini okumak (
GET /users/123), bir e-posta alanının tamamını güncellemek (PUT /users/123/email) veya bir öğeyi silmek (DELETE /items/456). - Idempotent Olmayan: Yeni bir sipariş oluşturma (
POST /orders) veya kredi kartı ücretini işleme koyma (POST /payments).
Bir müşteriden 50 ABD doları tahsil etmek için POST /payments numaralı telefonu aradığınızı varsayalım. Alt ödeme ağ geçidi isteği alır, kredi kartından başarıyla çekim yapar, ancak daha sonra size 200 OK yanıtını gönderemeden bir ağ kesintisi meydana gelir.
Hizmetiniz bir zaman aşımı kaydeder, aramanın başarısız olduğunu varsayar ve otomatik olarak yeniden dener. Ödeme ağ geçidi yinelenen istekleri karşılayacak şekilde tasarlanmadıysa müşteriden iki kez ücret alınacaktır.
[!UYARI] Aşağı akış hizmeti Idempotency Keys‘i (yinelenen işlemleri algılamak ve atmak için kullanılan benzersiz istek tanımlayıcıları) desteklemediği sürece, eş zamanlı olmayan işlemleri asla yeniden denemeyin.
Uygulama Örnekleri
Java ve Go’da Yeniden Deneme Deseninin nasıl uygulanacağına bakalım.
1. Java (Resilience4j ve Spring Boot)
Resilience4j, sağlam, yüksek düzeyde yapılandırılabilir bir yeniden deneme modülü sağlar. Harici bir faturalandırma hizmeti için bunu nasıl yapılandıracağınız aşağıda açıklanmıştır.
Yapılandırma (application.yml)
resilience4j.retry:
instances:
billingService:
maxAttempts: 3
waitDuration: 100ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2.0
enableRandomizedWait: true
randomizedWaitFactor: 0.5
retryExceptions:
- org.springframework.web.client.ResourceAccessException
- io.netty.channel.ConnectTimeoutException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException # E.g., 400 Bad Request shouldn't be retried
Kod Uygulaması
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class PaymentProcessor {
private final RestTemplate restTemplate;
public PaymentProcessor(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// Apply the configured retry policy with a fallback method
@Retry(name = "billingService", fallbackMethod = "billingFallback")
public String chargeUser(BillingRequest request) {
return restTemplate.postForObject("http://billing-service/charge", request, String.class);
}
// Executed when all retry attempts fail
public String billingFallback(BillingRequest request, Throwable throwable) {
return "Billing service is currently unavailable. Your transaction will be queued.";
}
}
2. Git (Golang)
Go’da, standart kitaplık zamanlayıcılarını kullanarak üstel geri çekilme ve rastgele titreşim içeren zarif bir yeniden deneme koşucusu yazabiliriz.
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
// RetryConfig holds the policies for our retry attempts
type RetryConfig struct {
MaxAttempts int
MinBackoff time.Duration
MaxBackoff time.Duration
}
// Execute runs the operation using exponential backoff with full jitter
func Execute(ctx context.Context, config RetryConfig, operation func() error) error {
var err error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
err = operation()
if err == nil {
return nil // Success!
}
if attempt == config.MaxAttempts {
break
}
// Calculate exponential backoff
// wait = min(MaxBackoff, MinBackoff * 2^(attempt-1))
backoff := config.MinBackoff * (1 << (attempt - 1))
if backoff > config.MaxBackoff || backoff <= 0 {
backoff = config.MaxBackoff
}
// Apply Full Jitter: wait randomly between 0 and backoff
jitter := time.Duration(rand.Int63n(int64(backoff)))
fmt.Printf("[Attempt %d/%d] Failed: %v. Retrying in %v...\n", attempt, config.MaxAttempts, err, jitter)
select {
case <-time.After(jitter):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("operation failed after %d attempts: %w", config.MaxAttempts, err)
}
func main() {
config := RetryConfig{
MaxAttempts: 4,
MinBackoff: 100 * time.Millisecond,
MaxBackoff: 2000 * time.Millisecond,
}
// Mock function that fails 3 times and succeeds on the 4th
attempts := 0
mockAPI := func() error {
attempts++
if attempts < 4 {
return errors.New("network timeout (503)")
}
return nil
}
ctx := context.Background()
err := Execute(ctx, config, mockAPI)
if err != nil {
fmt.Printf("Final Outcome: %v\n", err)
} else {
fmt.Println("Final Outcome: Successfully connected on attempt", attempts)
}
}
Yeniden Deneme ve Devre Kesici: Hangisi Ne Zaman Kullanılmalı?
Geliştiriciler genellikle Yeniden Deneme Modeli ile Devre Kesici Modeli’ni karıştırırlar. Her ikisi de sistem dayanıklılığını artırmayı amaçlasa da temelde farklı türdeki arızalarla ilgilenir:
| Metrik | Deseni Yeniden Dene | Devre Kesici Deseni |
|---|---|---|
| Birincil Hedef | Geçici (kısa süreli) arızalardan otomatik olarak kurtarır. | Kalıcı (uzun ömürlü) arızaların arayanı devre dışı bırakmasını önler. |
| Strateji | Hemen veya kısa bir geri çekilme süresinden sonra tekrar deneyin. | Hedef hizmeti çağırmadan hemen arızayı giderin. |
| Aşağı Yöndeki Etki | Aşağı akış hizmetindeki yükü geçici olarak artırır. | Aşağı akış hizmetini trafikten koruyarak iyileşmesine olanak tanır. |
| Tipik Tetikleyiciler | Kısa ağ bağlantısı kesintileri, veritabanı zaman aşımları, soket kesintileri. | Aşağı akış hizmeti tamamen kapalı, sürekli 500’ler veya zaman aşımları döndürüyor. |
Güç Çifti: Her ikisini de Birleştirmek
Üretim sistemlerinde bu iki model birlikte kullanılmak üzere tasarlanmıştır.
Aşağı yönde bir istekte bulunduğunuzda, bu isteğin önce Devre Kesici‘den ve ardından Yeniden Dene sarmalayıcısından geçmesi gerekir.
Geçici bir aksaklık meydana gelirse, Yeniden Deneme mekanizması bunu durdurur ve yeniden dener. Ancak aşağı yöndeki hizmet ölüyse arızalar birikecektir. Devre Kesici ardışık arızaları algılar, “açma” yapar ve gelecekteki tüm çağrıları engeller.
Artık hizmeti aramaya çalıştığınızda Devre Kesici, Yeniden Dene döngüsünü tamamen atlayarak ve bilgi işlem kaynaklarınızdan tasarruf ederek hemen hızlı bir şekilde arızalanır.
Yeniden Deneme Modeli için En İyi Uygulamalar
- Yalnızca Geçici Hataları Yeniden Dene: HTTP durum kodlarını inceleyin.
503 Service Unavailable,429 Too Many Requests(varsaRetry-Afterbaşlıklarına saygı göstererek) ve ağ zaman aşımlarını yeniden deneyin.400 Bad Request,401 Unauthorizedveya404 Not Foundyeniden denemeyin; bunlar yeniden denemede asla başarılı olmaz. - Sarsıntı Uygula: Rastgele bir titreşim yaratmadan asla sabit bir yeniden deneme zamanlayıcısı kullanmayın.
- Maksimum Deneme Sayısını Sınırlayın: Makul bir eşikten sonra (genellikle 3 ila 5 deneme) yeniden denemeyi durdurun. Sınırsız yeniden deneme, kaynakları tüketir ve gecikmeyi azaltır.
- Basamaklı Yeniden Denemelerde Dikkatli Olun: A Hizmeti B Hizmetini (3 kez yeniden deneyen) ararsa ve B Hizmeti C Hizmetini (3 kez yeniden deneyen) ararsa, C’deki bir hata $3 \times 3 = 9$ basamaklı çağrılarla sonuçlanabilir. Yeniden denemeleri yalnızca en anlamlı oldukları sınırlarda uygulayın.
- Her Zaman Kesin Zaman Aşımları Ayarlayın: Ağ zaman aşımlarınızın, geri çekilme dönemlerinizden daha kısa olduğundan emin olun, böylece ileti dizileri süresiz olarak tutulmaz.
Çözüm
Yeniden Deneme Modeli, mikro hizmet mimarilerinde ağ güvenilmezliğine karşı güçlü bir ilk savunma hattıdır. Üstel Gerileme, Sarsıntı ve Devre Kesici ile eşleştirildiğinde, sistemlerinizi kademeli kesintilerden koruyabilir ve kullanıcılarınız için sorunsuz, kendi kendini onaran bir deneyim oluşturabilirsiniz.