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:
- İstek geldiğinde HTTP başlığındaki
X-Idempotency-Keydeğeri okunur. - Bu anahtar hızlı bir veri deposunda (örneğin Redis) kontrol edilir.
- 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.
- 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