Takip et

429 Retries ve Çift Yüklenen Batch İşleri Engelleme

HTTP 429 (Çok Fazla İstek) hataları sonrasında yapılan otomatize retry işlemleri, arka planda başarıyla tamamlanmış batch işlerinin tekrarlanmasına ve ciddi veri kirliliğine yol açabilir.

429 Retries ve Çift Yüklenen Batch İşleri Engelleme

HTTP 429 (Çok Fazla İstek) hataları sonrasında yapılan otomatize retry işlemleri, arka planda başarıyla tamamlanmış batch işlerinin tekrarlanmasına ve ciddi veri kirliliğine yol açabilir. Bu makalede, bu kronik mimari sorunu idempotent tasarımlar, Akıllı Yeniden Deneme (Smart Retry) mekanizmaları ve dağıtık kilitler kullanarak nasıl kökten çözeceğinizi adım adım inceliyoruz.

HTTP 429 Hatası ve Batch İşlerde Retry Felaketi Nedir?

Mikrohizmet mimarilerinde (microservices architecture) veya dış API entegrasyonlarında toplu veri işleme (batch processing) yaparken sıklıkla Oran Sınırlaması (Rate Limiting) mekanizmaları ile karşılaşırız. Hedef sunucu, sistem kaynaklarını korumak amacıyla belirli bir zaman diliminde kabul edebileceği istek sayısını sınırlar. Bu sınır aşıldığında ise istemciye HTTP 429 Too Many Requests yanıtı döndürülür. Teoride bu durum son derece sağlıklıdır; ancak işin içine otomatik yeniden deneme (retry) mekanizmaları girdiğinde süreç bir felakete dönüşebilir.

Özellikle arka plan işlerinde (background jobs) istemci yazılımları, HTTP 429 yanıtını aldıklarında isteğin tamamen başarısız olduğunu varsayar. Oysa gerçek dünyada ağ iletişimi tek yönlü değildir. Sunucu isteği almış, veri tabanı işlemini (database transaction) başarıyla tamamlamış ancak yanıtı döndürürken oran sınırına takılmış veya bir zaman aşımı (timeout) yaşamış olabilir. İstemci aynı batch yükünü tekrar gönderdiğinde, eğer hedef sistem eşgüçlü (idempotent) tasarlanmamışsa, aynı veri kümesi veri tabanına mükerrer (duplicate) olarak işlenir.

Bu durum yazılım sistemlerinde şu kritik sorunlara yol açar:

  • Veri Bütünlüğünün Bozulması: Aynı verinin birden fazla kez yazılmasıyla oluşan mükerrer kayıtlar.
  • Mali Kayıplar: Bakiye yüklemeleri, fatura kesim süreçleri veya puan dağıtımlarında çift işlem yapılması.
  • Sistem Yükünün Katlanması: Başarısız sanılan ama aslında çalışan işlerin tekrar tekrar gönderilmesi sonucu oluşan gürültülü sürü (thundering herd) etkisi.

Bir Vaka Analizi: E-Ticaret Sadakat Puanı Batch İşi

Geliştirdiğimiz bir e-ticaret platformunda, gece yarısı çalışan bir batch işi (cron job) aracılığıyla son 24 saatte alışveriş yapan kullanıcıların hesaplarına tanımlanacak sadakat puanlarını işliyorduk. Sistem, her biri 500 kullanıcı içeren paketler halinde API’ye istek atıyordu. Mimarimizde Axios tabanlı bir HTTP istemcisi ve başarısız isteklerde 3 kez yeniden deneme yapan varsayılan bir retry politikası (retry policy) bulunuyordu.

Bir gece, veri tabanı yoğunluğundan dolayı API Gateway katmanımız yanıt sürelerini uzattı ve istemciye HTTP 429 dönmeye başladı. Ancak asıl tehlike şuydu: API Gateway HTTP 429 yanıtını istemciye iletmeden hemen önce, arka plandaki işçi servis (worker service) veri tabanı işlemini çoktan tamamlamıştı. İstemci 429 kodunu görünce otomatik olarak retry mekanizmasını tetikledi ve aynı 500 kişilik paketi tekrar gönderdi.

Sabah karşılaştığımız tablo oldukça ürkütücüydü:

Metrik / Durum Beklenen Değer Gerçekleşen Değer Sonuç
İşlenen Paket Sayısı 20 Paket 20 Paket Başarılı gibi görünüyor
Toplam İstek Sayısı 20 İstek 68 İstek 48 Mükerrer Retry İsteği
Dağıtılan Toplam Puan 100.000 Puan 240.000 Puan %140 Fazla Puan Dağıtımı

Bu kriz bize açıkça gösterdi ki; başarısız görünen bir HTTP isteği, aslında sunucuda başarılı olmuş olabilir. Ağ katmanındaki belirsizliği hesaba katmadan yapılan basit retry stratejileri, sistemin en büyük düşmanıdır.

Ağ İletişiminde Yanılsama: İstek Başarılı Ama Yanıt Hatayla mı Döndü?

Dağıtık sistemlerde (distributed systems) en zorlayıcı problemlerden biri “Ağ Bölünmesi” (Network Partition) ve belirsizlik durumudur. Bir HTTP isteği başlatıldığında süreç üç ana aşamadan geçer: İstemcinin isteği göndermesi, sunucunun isteği işlemesi ve sunucunun yanıtı geri göndermesi. Ağ üzerindeki herhangi bir kesinti veya oran sınırlayıcı (rate limiter) müdahalesi genellikle üçüncü aşamada, yani yanıt dönerken gerçekleşir.

Sunucu tarafında işlem COMMIT edilmiş olsa dahi, paket istemciye ulaşmadığı için istemci uygulamanın durumunu FAILED olarak işaretler. Bu noktada korumasız bir döngü, veriyi tekrar basmaya başlayacaktır. Bu yüzden çözümü sadece istemci tarafında değil, sunucu ve istemci arasındaki mimari sözleşmede (contract) aramamız gerekir.

Çözümün Kalbi: Idempotency (Eşgüçlülük) Mimarisi

Mükerrer işleme sorununu çözmenin temel taşı Eşgüçlülük (Idempotency) ilkesidir. Matematiksel ve bilgisayar bilimleri anlamında idempotency; bir işlemin bir kez uygulanması ile birden fazla kez uygulanması durumunda sistemde aynı nihai sonucun üretilmesidir. HTTP metotlarından GET, PUT ve DELETE doğası gereği idempotent kabul edilirken, POST istekleri varsayılan olarak idempotent değildir.

Batch işlerimizde POST isteklerini idempotent hale getirmek için Idempotency Key (Eşgüçlülük Anahtarı) desenini uyguladık. Bu yöntemde istemci, gönderdiği her batch paketi için benzersiz bir anahtar (örneğin bir UUID v4 veya paketteki verilerin karması/hash değeri) üretir ve bunu HTTP başlığında (header) sunucuya iletir.

Sunucu tarafındaki süreç şu şekilde işler:

  1. İstek geldiğinde HTTP başlığındaki X-Idempotency-Key değeri okunur.
  2. Bu anahtar hızlı bir veri deposunda (örneğin Redis) kontrol edilir.
  3. Eğer anahtar daha önce işlenmişse ve başarılı bir yanıt üretilmişse, sunucu asıl iş mantığını (business logic) hiç çalıştırmadan önbellekteki yanıtı doğrudan döndürür.
  4. Eğer anahtar ilk defa geliyorsa, işlem yürütülür, sonuç anahtar ile ilişkilendirilerek depolanır ve yanıt dönülür.

Retry Stratejilerini İyileştirme: Exponential Backoff ve Jitter

Sadece Idempotency Key kullanmak yeterli değildir. Eğer istemci bir HTTP 429 aldığında anında ve sürekli retry yapmaya devam ederse, hedef sunucunun toparlanmasına asla izin vermez. Bu duruma sistem mimarisinde “Gürültülü Sürü Problemi” (Thundering Herd Problem) adı verilir.

Bunu önlemek için iki temel algoritmayı birleştirmeliyiz:

1. Üstel Bekleme (Exponential Backoff): Her başarısız denemeden sonra bekleme süresini katlayarak artırma yöntemidir. Örneğin ilk başarısızlıkta 1 saniye, ikincisinde 2 saniye, üçüncüsünde 4 saniye beklenir.

2. Rastgele Sapma (Jitter): Bekleme sürelerinin tamamen aynı olmasını engelleyen küçük rastgele zaman dilimleridir. Tüm işçilerin (workers) aynı saniyede tekrar istek atmasını engeller ve trafik yükünü zamana yayar.

Matematiksel olarak Jitter içeren bekleme süresi formülü şu şekildedir:

Bekleme Süresi = Min(Max_Bekleme, Taban_Süre * (2 ^ Deneme_Sayısı)) + Rastgele_Sapma

Kod Uygulaması: İdempotent Batch İşleyici Örneği

Aşağıdaki Node.js ve TypeScript örneğinde, hem X-Idempotency-Key başlığını kullanan hem de üstel bekleme ve jitter algoritmasına sahip güvenli bir batch HTTP istemcisi yer almaktadır. Kod bloklarımız temiz ve standart yapıda kurgulanmıştır.

import axios, { AxiosError, AxiosResponse } from 'axios';
import { crypto } from 'crypto';

interface BatchPayload {
  batchId: string;
  items: Array<{ userId: string; points: number }>;
}

async function sendBatchWithIdempotentRetry(
  payload: BatchPayload,
  maxRetries: number = 5
): Promise<AxiosResponse> {
  const url = 'https://api.sirketiniz.com/v1/loyalty/batch-points';
  
  // Determinastik veya benzersiz anahtar üretimi
  const idempotencyKey = payload.batchId || crypto.randomUUID();

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await axios.post(url, payload, {
        headers: {
          'Content-Type': 'application/json',
          'X-Idempotency-Key': idempotencyKey
        },
        timeout: 5000 // 5 saniye zaman aşımı
      });

      return response; // Başarılı ise sonucu dön
    } catch (error) {
      const axiosError = error as AxiosError;
      const isRateLimited = axiosError.response?.status === 429;
      const isServerError = axiosError.response?.status && axiosError.response.status >= 500;
      
      // Son denemeyse veya retry yapılamaz bir hataysa hatayı fırlat
      if (attempt === maxRetries - 1 || (!isRateLimited && !isServerError)) {
        throw error;
      }

      // Exponential Backoff + Jitter Hesaplaması
      const baseDelay = 1000; // 1 saniye
      const exponentialDelay = baseDelay * Math.pow(2, attempt);
      const jitter = Math.random() * 1000; // 0-1000ms arası rastgele sapma
      const totalDelay = Math.min(exponentialDelay + jitter, 30000); // Maksimum 30 sn

      console.warn(İstek başarısız (${axiosError.response?.status}). ${totalDelay.toFixed(0)}ms sonra tekrar deneniyor... (Deneme: ${attempt + 1}));
      
      await new Promise((resolve) => setTimeout(resolve, totalDelay));
    }
  }

  throw new Error('Maksimum retry sınırına ulaşıldı.');
}

Yukarıdaki kod bloğunda gördüğünüz gibi, istemci kaç kez retry yaparsa yapsın, sunucuya her zaman aynı X-Idempotency-Key değerini gönderir. Bu sayede sunucu 429 hatası sonrası gelen mükerrer istekleri tespit edebilir.

Dağıtık Sistemlerde Çifte Çalışmayı Önleme: Redis Lock

Sadece HTTP seviyesinde önlem almak bazı durumlarda yetersiz kalabilir. Ya aynı batch işi iki farklı arka plan işçisi (worker instance) tarafından eşzamanlı olarak tetiklenirse? Veya ilk istek henüz sunucuda işlenirken (pending durumundayken) istemci zaman aşımına uğrayıp ikinci isteği gönderirse?

Bu gibi durumlar yarış koşuluna (race condition) yol açar. Çözüm, sunucu tarafında Dağıtık Kilitleme (Distributed Locking) mekanizması kurmaktır. Redis tabanlı SETNX (Set if Not Exists) komutu bu tür kilitleme işlemleri için oldukça uygundur.

Sunucu tarafındaki işlem akışı şu adımlarla güvenli hale getirilir:

import Redis from 'ioredis';
const redis = new Redis();

async function processBatchServerSide(idempotencyKey: string, batchData: any) {
  const lockKey = lock:batch:${idempotencyKey};
  const resultKey = result:batch:${idempotencyKey};

  // 1. Önce daha önce tamamlanmış bir sonuç var mı bak
  const cachedResult = await redis.get(resultKey);
  if (cachedResult) {
    return JSON.parse(cachedResult); // İşlemi tekrar yapma, eski sonucu dön!
  }

  // 2. İşlem devam ediyor mu diye kilit (lock) almayı dene (30 saniye TTL)
  const acquiredLock = await redis.set(lockKey, 'PROCESSING', 'PX', 30000, 'NX');
  
  if (!acquiredLock) {
    // Kilit alınamadı, demek ki başka bir worker şu an bu batch'i işliyor
    throw new Error('Bu batch işlemi şu anda başka bir sunucu tarafından işleniyor. Lütfen bekleyin.');
  }

  try {
    // 3. Gerçek Veri Tabanı İşlemleri (Business Logic)
    const dbResult = await executeDatabaseTransaction(batchData);

    // 4. Sonucu önbelleğe kaydet (Örn: 24 saat boyunca sakla)
    await redis.set(resultKey, JSON.stringify(dbResult), 'EX', 86400);

    return dbResult;
  } finally {
    // 5. İşlem bitince kilidi kaldır
    await redis.del(lockKey);
  }
}

Bu mimari yapı sayesinde, HTTP 429 hatası nedeniyle ağda kalan istekler retry edildiğinde, sunucu ya işlemi kilit sayesinde bekletir ya da tamamlanmış sonucu veritabanına dokunmadan mili saniyeler içinde istemciye geri döner.

Mimaride Sağlanan Kazanımlar

Bu mimari dönüşümü tamamladıktan sonra sistemlerimizde elde ettiğimiz somut verileri ve kazanımları şu şekilde sıralayabiliriz:

  • Sıfır Mükerrer Veri: 429 retries kaynaklı çift bakiye yükleme ve mükerrer faturalandırma hataları tamamen sıfırlandı.
  • Düşük Sunucu Yükü: Mükerrer gelen istekler Redis seviyesinde karşılandığı için ana veri tabanı üzerindeki I/O yükü %35 oranında azaldı.
  • Daha Dayanıklı Entegrasyonlar: Anlık trafik dalgalanmalarında (traffic spikes) sistem kilitlenmek yerine kendini düzenleyen (self-healing) bir yapıya kavuştu.

Sonuç ve Değerlendirme

Dağıtık sistemlerde hata kaçınılmazdır. Ağ kesintileri, geçici sunucu yoğunlukları ve HTTP 429 Too Many Requests gibi oran sınırlama yanıtları yazılım geliştirme süreçlerinin doğal bir parçasıdır. Ancak bu hatalara verilen tepkinin kontrolsüz bir retry mekanizması olması, hatanın etkisini katlayarak büyütebilir.

Batch işlerinizi güvene almak istiyorsanız, basit döngüsel retry mantıklarını derhal terk etmelisiniz. Bunun yerine istemci tarafında Exponential Backoff ve Jitter, sunucu tarafında ise Idempotency Key ve Distributed Lock desenlerini bir arada kullanmalısınız. Ancak bu bütünsel yaklaşımla veri kaybı ve veri kirliliği yaşamadan ölçeklenebilir sistemler inşa edebilirsiniz.

Sıkça Sorulan Sorular

1. HTTP 429 hatası aldığımda isteğin sunucuda çalışıp çalışmadığını nasıl anlarım?

İstemci tarafından bunu %100 kesinlikle anlamak imkansızdır. Ağ katmanındaki belirsizlik nedeniyle isteğin sunucuya ulaşıp yanıtın mı yolda kaldığı, yoksa isteğin hiç ulaşmadığı mı bilinemez. Bu belirsizliği yönetmenin tek yolu istemci tarafında her isteğe bir Idempotency-Key eklemek ve sunucunun bu anahtara göre işlem yapmasını sağlamaktır.

2. Idempotency Key değerlerini ne kadar süreyle depolamalıyım?

Bu süre iş mantığınıza (business logic) göre değişir. Genellikle batch işlerin yeniden deneme pencereleri dikkate alınarak Idempotency anahtarları Redis gibi bellek içi veri depolarında 24 ila 72 saat arasında saklanır. Süre dolduğunda anahtar otomatik olarak silinebilir.

3. Exponential Backoff kullanırken maksimum retry sayısı kaç olmalıdır?

Önerilen ideal retry sayısı genellikle 3 ile 5 arasındadır. Çok yüksek retry sayıları (örneğin 10 ve üzeri), sistem üzerindeki birikmiş yükü artırabilir. Eğer bir iş 5 denemeden sonra hala 429 hatası alıyorsa, ilgili batch işini Ölü Mektup Kuyruğuna (Dead Letter Queue – DLQ) taşıyıp insan müdahalesine sunmak daha sağlıklı bir yaklaşımdır.

4. Jitter (Rastgele Sapma) eklemek neden bu kadar önemlidir?

Eğer 100 farklı arka plan işçisi aynı anda 429 hatası alırsa ve sabit bir üstel bekleme süresi (örneğin hepsi tam 2 saniye) kullanırsa, 2 saniye sonra 100 işçi tekrar aynı anda sunucuya yüklenir. Jitter, bu 100 işçinin isteğini 2.1s, 2.4s, 2.05s gibi farklı zaman dilimlerine yayarak sunucunun üzerindeki anlık yük patlamasını engeller.

#Teknoloji #WebGeliştirme #SoftwareArchitecture #Microservices #Backend

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version