İçeriğe geç
Can Uğurlu
Geri dön

Retry stratejisi: üstel geri çekilme ve jitter

Sabit aralıklı retry, bir kesintiyi kendi kendine yarattığınız bir yığılmaya çeviriyor. Karşı taraf beş saniyeliğine yavaşladığında, her çağıran beş saniye sonra aynı anda tekrar vuruyor ve zaten zorlanan servisi bitiriyor.

Doğru retry üç şeyle çalışır: üstel geri çekilme, jitter, bir üst sınır.

Üstel geri çekilme

$delays = [1, 2, 4, 8, 16]; // saniye

foreach ($delays as $attempt => $delay) {
    try {
        return $client->post($endpoint, $payload);
    } catch (ConnectionException|ServerException $e) {
        if ($attempt === array_key_last($delays)) {
            throw $e;
        }
        sleep($delay);
    }
}

Her deneme arasındaki bekleme bir öncekinin iki katı. İlk hata anlık bir titreşim olabilir, bir saniye sonra tekrar denemek makul. Beşinci hataya kadar geldiyseniz, sorun muhtemelen kalıcı ve on altı saniye beklemek hem karşı tarafa nefes alma payı veriyor hem de gereksiz yükü azaltıyor.

Geri çekilme tek başına yetmez

Sorun şu: aynı anda başarısız olan her şey, aynı geri çekilme formülünü kullandığı için aynı anda tekrar deniyor. Yüz farklı worker aynı API’ye aynı saniyede başarısız oldu, hepsi bir saniye sonra aynı saniyede tekrar deniyor. İkinci deneme dalgası ilkinden daha büyük bir yığılma yaratıyor.

Çözüm jitter: bekleme süresine rastgele bir sapma eklemek.

$baseDelay = min(60, pow(2, $attempt));
$jitter = random_int(0, (int) ($baseDelay * 1000 * 0.5)) / 1000;
$delay = $baseDelay + $jitter;

sleep((int) $delay);

$baseDelay üstel geri çekilmenin hesapladığı süre, 60 saniyede tavanlanmış. $jitter bu sürenin yarısı kadar bir aralıkta rastgele bir ek. Yüz worker artık aynı saniyede değil, bir aralığa yayılmış saniyelerde tekrar deniyor. Karşı tarafa gelen yük tek bir dalga yerine dağılıyor.

Hangi hata retry hak ediyor

Her hata aynı muameleyi hak etmiyor.

Retry edilir: zaman aşımı (timeout), 429 Too Many Requests, 5xx sunucu hataları. Bunlar geçici: sunucu meşgul, ağ kesildi, bir sonraki denemede farklı sonuç çıkabilir.

Retry edilmez: 400 Bad Request, 422 Unprocessable Entity, 401 Unauthorized. Bunlar isteğin kendisiyle ilgili, sunucunun anlık durumuyla değil. Gönderdiğiniz veri geçersizse, aynı veriyi beş kez daha göndermek beş kez daha aynı hatayı üretir. Token süresi dolmuşsa, retry değil yenileme gerekiyor.

match (true) {
    $response->status() === 429,
    $response->status() >= 500 => $this->retry($attempt),
    $response->status() === 401 => $this->refreshTokenAndFail(),
    default => throw new UnrecoverableApiError($response->body()),
};

Bu ayrımı yapmadan her hatayı retry etmek, kalıcı bir hatayı beş kat gürültüye çeviriyor ve karşı tarafın loglarında sizi şüpheli trafik gibi gösteriyor.

Retry-After varsa ona uyun

HTTP/1.1 429 Too Many Requests
Retry-After: 30

Sunucu Retry-After başlığını gönderdiğinde, ne kadar bekletmesi gerektiğini zaten söylemiş oluyor. Kendi üstel geri çekilme formülünüzü buna rağmen çalıştırmak, sunucunun verdiği bilgiyi yok saymak. Başlık varsa onu okuyun, yoksa kendi formülünüze dönün.

$retryAfter = $response->header('Retry-After');
$delay = $retryAfter ? (int) $retryAfter : $baseDelay + $jitter;

Retry bütçesi

Deneme sayısına ya da toplam süreye bir üst sınır konmadan retry, başarısız bir işi günlerce beklemede tutabilir.

public int $tries = 5;
public int $maxExceptions = 3;
public int $backoff = 30;

Beş denemeden sonra iş failed_jobs tablosuna düşmeli, sonsuza kadar tekrar denenmemeli. Bir üst limit olmadan, geçici bir hata sanılan şey aslında kalıcıysa iş kuyrukta sonsuza kadar dönüyor, hiç bitmiyor ve hiç fark edilmiyor.

Idempotency retry’ı güvenli yapan şey

Retry’ın öncülü şu: aynı isteği iki kez göndermek, bir kez göndermekle aynı sonucu vermeli. Bu doğru değilse retry güvenli değil. İlk istek aslında sunucuya ulaştı ama yanıt kaybolduysa, retry aynı işlemi ikinci kez tetikliyor.

$client->post('/orders', [
    'idempotency_key' => $order->uuid,
    'payload' => $payload,
]);

idempotency_key sunucu tarafında saklanıyor. Aynı anahtarla ikinci bir istek geldiğinde sunucu işlemi tekrar çalıştırmıyor, ilk isteğin sonucunu döndürüyor. Bu anahtar olmadan bir ödeme ya da sipariş oluşturma isteğini retry etmek, aynı işlemi iki kez yaratma riski taşıyor.

Özet

Sabit aralık yerine üstel geri çekilme kullanın, üzerine jitter ekleyin. Sadece geçici hataları (timeout, 429, 5xx) retry edin. Retry-After varsa onu esas alın. Deneme sayısına üst sınır koyun ve retry edilen her işlemi idempotent yapın.


Bu yazıyı paylaş:

Önceki Yazı
Tarihi UTC sakla, gösterimde çevir
Sonraki Yazı
Cache-Control başlıklarını nginx'te doğru yazmak