Ders 16 / 22
Yeniden Deneme ve Geri Çekilme
Hangi hatanın kendiliğinden yeniden deneneceği, üstel geri çekilmenin ve sarsıntının hesabı, sunucunun bekleme önerisi, deneme bütçesi ve üstüste başarısızlıkta isteği hiç göndermeyen devre kesici.
İçindekiler
Önceki ders basarisiz olayını tek bir kapı olarak ele aldı: hata geldi, görünüm hata
durumuna geçti, kullanıcıya “tekrar dene” düğmesi sunuldu. Oysa bazı hatalar kullanıcıya
hiç gösterilmemelidir. Kuzey Yamaç istasyonuna dağdan bağlanan bir gözlemcinin bağlantısı
saniyeler içinde kopup düzelir; iki yüz milisaniye sonra yeniden denenen istek başarılı
olur ve kullanıcı arada hiçbir şey görmez.
Bu dersin sorusu şudur: hangi hata kendiliğinden yeniden denenir, ne kadar beklenir, kaç kez denenir ve ne zaman denemekten vazgeçilir. Dördü de yanlış cevaplandığında ortaya çıkan sonuç aynıdır — zaten zorlanan bir sunucunun istemciler tarafından daha da zorlanması.
Hangi Hata Yeniden Denenir
Karar iki bilgiye bakar: hatanın sınıfı ve isteğin yöntemi.
Hata sınıfı REST İstemcisi dersinde kuruldu. HTTP hatalarında sunucunun kararı bilinir: 4xx isteğin kendisiyle ilgilidir, aynı isteği tekrarlamak aynı yanıtı üretir. İki istisna vardır — 429 hız sınırını, 5xx sunucunun o anki durumunu bildirir; ikisi de zamanla değişebilir.
Ağ hatalarında sunucunun isteği alıp almadığı bilinmez. Yanıt yolda kaybolmuş olabilir; bu durumda sunucu isteği işlemiştir. Bu yüzden ağ hatasında karar yönteme geçer. İnternet Nasıl Çalışır kursunda tanımlanan güvenli yöntemler durum değiştirmez, tekrarlanması zararsızdır. Ölçüm kaydı gönderen bir istek ise iki kez işlenirse iki kayıt üretebilir; otomatik olarak yeniden denenemez.
Bu kısıtın aşılma yolu vardır: isteğe istemcinin ürettiği bir tekillik anahtarı eklenir, sunucu aynı anahtarla gelen ikinci isteği yeni bir kayıt saymaz. Anahtar gönderiliyorsa durum değiştiren istek de yeniden denenebilir hâle gelir. Bu, istemci ile sunucunun birlikte kararlaştırması gereken bir sözleşmedir; tek taraflı kurulamaz.
Gövde hatası ve iptal hiçbir koşulda yeniden denenmez. Birincisi programın kendi hatasıdır, ikincisi zaten bir vazgeçme kararıdır.
Bekleme Süresinin Hesabı
Hemen tekrar denemek işe yaramaz: sorunun ortadan kalkması için zaman gerekir. Sabit aralıkla denemek de yetmez; sunucu ayağa kalkamamışsa aynı yükü aynı sıklıkla almaya devam eder. Üstel geri çekilme (exponential backoff), her denemede bekleme süresini bir çarpanla büyütür; sorun kısa sürerse hızlı toparlanır, uzun sürerse istemci geri çekilir.
Üstel büyüme tek başına sınırsızdır, bu yüzden bir tavan konur. Asıl eksik parça ise başkadır. Bir sunucu düştüğünde ona bağlı bütün istemciler aynı anda hata alır; hepsi aynı formülü uygularsa aynı anda yeniden dener. Sunucu ayağa kalktığı anda eşzamanlı bir dalga karşılar ve yeniden düşer. Sarsıntı (jitter), bekleme süresine rastgelelik katarak bu dalgayı dağıtır.
// geri-cekilme.mjs — bekleme sureleri ve sarsintinin dagilima etkisi // Belirlenimci sozde rastgele uretec: ayni tohum ayni diziyi verir. function uretec(tohum) { let a = tohum >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const TABAN = 200, CARPAN = 2, TAVAN = 5000; const ustel = (n) => Math.min(TAVAN, TABAN * CARPAN ** n); const rastgele = uretec(20260114); const tamSarsinti = (n) => Math.round(rastgele() * ustel(n)); // Iliskisiz sarsinti: bir sonraki bekleme oncekine bagli araliktan secilir. let onceki = TABAN; const iliskisizSarsinti = () => { onceki = Math.min(TAVAN, Math.round(TABAN + rastgele() * (onceki * 3 - TABAN))); return onceki; }; console.log("deneme sabit üstel üstel+tavan tam sarsıntı ilişkisiz"); for (let n = 0; n < 6; n++) { console.log( String(n + 1).padEnd(8) + String(TABAN).padEnd(8) + String(TABAN * CARPAN ** n).padEnd(8) + String(ustel(n)).padEnd(13) + String(tamSarsinti(n)).padEnd(14) + String(iliskisizSarsinti())); } // --- Ayni anda dusen istemcilerin uyanma dagilimi -------------------------- const ISTEMCI = 24, DENEME = 3; // ucuncu denemenin beklemesi: ustel(2) = 800 ms const kova = (ms) => Math.floor(ms / 200) * 200; const say = (liste) => { const m = new Map(); for (const v of liste) m.set(v, (m.get(v) ?? 0) + 1); return [...m.entries()].sort((a, b) => a[0] - b[0]) .map(([k, s]) => `${String(k).padStart(4)}ms:${"#".repeat(s)}`); }; const sarsintisiz = Array.from({ length: ISTEMCI }, () => kova(ustel(DENEME - 1))); const r2 = uretec(20260114); const sarsintili = Array.from({ length: ISTEMCI }, () => kova(Math.round(r2() * ustel(DENEME - 1)))); console.log("\nsarsıntısız (24 istemci, 3. deneme):"); say(sarsintisiz).forEach((s) => console.log(" " + s)); console.log("sarsıntılı (24 istemci, 3. deneme):"); say(sarsintili).forEach((s) => console.log(" " + s));
deneme sabit üstel üstel+tavan tam sarsıntı ilişkisiz
1 200 200 200 70 384
2 200 400 400 296 1034
3 200 800 800 780 2121
4 200 1600 1600 303 1933
5 200 3200 3200 480 3986
6 200 6400 5000 957 5000
sarsıntısız (24 istemci, 3. deneme):
800ms:########################
sarsıntılı (24 istemci, 3. deneme):
0ms:######
200ms:#####
400ms:#######
600ms:######
İki histogram farkı açıkça gösteriyor. Sarsıntısız hesapta yirmi dört istemcinin tamamı tek bir ana yığılıyor; sarsıntılı hesapta aynı istemciler dört ayrı aralığa dağılıyor. Sunucunun gördüğü tepe yük dörtte birine iniyor.
Tablodaki iki sarsıntı biçimi farklı ödünleşim yapar. Tam sarsıntı, beklemeyi sıfır ile üstel değer arasından seçer; dağılımı en geniş olan yoldur ama bazı denemeler çok erken olur. İlişkisiz sarsıntı, bir sonraki beklemeyi bir öncekine bağlı bir aralıktan seçer; büyüme daha yumuşak ve daha az sıçramalıdır, tabloda altıncı denemede tavana ulaştığı görülüyor. Seçim, sunucunun toparlanma süresine bağlıdır: hızlı toparlanan bir hizmet için tam sarsıntı, yavaş toparlanan için ilişkisiz sarsıntı daha uygundur.
Rastgeleliğin belirlenimci bir üreteçle kurulduğuna dikkat edin. Uygulamada üreteç tohumsuzdur; burada tohum sabit tutulduğu için çıktı her çalıştırmada aynıdır ve tablo doğrulanabilir.
Döngünün Kurulması
Hesap tek başına yetmez; döngünün ne zaman durduğu ve sunucunun söylediklerine ne kadar uyulduğu da tanımlanmalıdır.
// yeniden-deneme.mjs — yerel sunucuya karsi calisan yeniden deneme dongusu import http from "node:http"; let olcumIstegi = 0; const sunucu = http.createServer((istek, yanit) => { const yol = new URL(istek.url, "http://127.0.0.1").pathname; if (yol === "/olcumler" && istek.method === "GET") { olcumIstegi += 1; if (olcumIstegi <= 2) { // ilk iki istek basarisiz yanit.writeHead(503, { "content-type": "application/json" }); return yanit.end(JSON.stringify({ kod: "erisilemez" })); } yanit.writeHead(200, { "content-type": "application/json" }); return yanit.end(JSON.stringify([{ kimlik: "o-114", deger: -4.2 }])); } if (yol === "/olcumler" && istek.method === "POST") { yanit.writeHead(422, { "content-type": "application/json" }); return yanit.end(JSON.stringify({ kod: "dogrulama" })); } if (yol === "/rapor") { yanit.writeHead(429, { "content-type": "application/json", "retry-after": "2" }); return yanit.end(JSON.stringify({ kod: "hiz_siniri" })); } yanit.writeHead(404, { "content-type": "application/json" }); yanit.end(JSON.stringify({ kod: "yol_yok" })); }); await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz)); const TABAN = `http://127.0.0.1:${sunucu.address().port}`; // --- Bekleme hesabi (belirlenimci) ----------------------------------------- function uretec(tohum) { let a = tohum >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const rastgele = uretec(20260114); const bekleme = (n) => Math.round(rastgele() * Math.min(5000, 200 * 2 ** n)); // --- Ilke: hangi sonuc yeniden denenir ------------------------------------- const GUVENLI_YONTEM = new Set(["GET", "HEAD", "OPTIONS"]); function denenirMi(sonuc, yontem) { if (sonuc.tur === "ag") return GUVENLI_YONTEM.has(yontem); // ulasti mi bilinmiyor if (sonuc.tur === "http") return sonuc.durum === 429 || sonuc.durum >= 500; return false; // gövde hatasi, iptal } // --- Dongu ------------------------------------------------------------------ async function yenidenDene(yol, { yontem = "GET", enCokDeneme = 4, uyu }) { const beklemeler = []; for (let deneme = 0; deneme < enCokDeneme; deneme++) { let sonuc; try { const yanit = await fetch(TABAN + yol, { method: yontem }); const govde = await yanit.json(); sonuc = yanit.ok ? { tur: "basari", veri: govde } : { tur: "http", durum: yanit.status, sunucuOnerisi: Number(yanit.headers.get("retry-after") ?? 0) * 1000 }; } catch { sonuc = { tur: "ag" }; } if (sonuc.tur === "basari") return { sonuc, deneme: deneme + 1, beklemeler }; if (!denenirMi(sonuc, yontem)) return { sonuc, deneme: deneme + 1, beklemeler }; if (deneme === enCokDeneme - 1) return { sonuc, deneme: deneme + 1, beklemeler }; // Sunucunun onerisi hesaplanan beklemeden uzunsa oneri kazanir. const sure = Math.max(bekleme(deneme), sonuc.sunucuOnerisi ?? 0); beklemeler.push(sure); await uyu(sure); } } // Sinamada gercek beklemek yerine sureler kaydedilir; uygulamada burada // gercek bir zamanlayici bulunur. const kayitli = []; const sahteUyku = (ms) => { kayitli.push(ms); return Promise.resolve(); }; const yaz = (ad, r) => console.log( ad.padEnd(24), `deneme=${r.deneme}`.padEnd(10), `sonuç=${r.sonuc.tur}${r.sonuc.durum ? "/" + r.sonuc.durum : ""}`.padEnd(16), `beklemeler=[${r.beklemeler.join(", ")}]`); yaz("503, sonra başarılı", await yenidenDene("/olcumler", { uyu: sahteUyku })); yaz("422 (denenmez)", await yenidenDene("/olcumler", { yontem: "POST", uyu: sahteUyku })); yaz("429 (Retry-After: 2)", await yenidenDene("/rapor", { enCokDeneme: 3, uyu: sahteUyku })); yaz("404 (denenmez)", await yenidenDene("/yok", { uyu: sahteUyku })); console.log("sunucuya ulaşan ölçüm isteği:", olcumIstegi); console.log("kaydedilen tüm beklemeler :", kayitli.join(", ")); sunucu.close();
503, sonra başarılı deneme=3 sonuç=basari beklemeler=[70, 184] 422 (denenmez) deneme=1 sonuç=http/422 beklemeler=[] 429 (Retry-After: 2) deneme=3 sonuç=http/429 beklemeler=[2000, 2000] 404 (denenmez) deneme=1 sonuç=http/404 beklemeler=[] sunucuya ulaşan ölçüm isteği: 3 kaydedilen tüm beklemeler : 70, 184, 2000, 2000
İlk satır beklenen davranışı gösteriyor: sunucu iki kez 503 döndü, döngü iki kez bekledi,
üçüncü denemede veri geldi. Kullanıcı bunların hiçbirini görmedi; görünüm hâlâ
yukleniyor durumundaydı.
İkinci ve dördüncü satırlar ilkenin ayırt ediciliğini gösteriyor. Doğrulama hatası ve bulunamadı yanıtı tek denemede bitti; tekrarlamak sonucu değiştirmeyeceği için beklenmedi bile. Yeniden denemenin en sık yapılan hatası, her hatayı denemeye sokmaktır: kullanıcı yanlış bir değer girdiğinde hatayı öğrenmesi üç kat gecikir.
Üçüncü satır sunucunun önerisinin kazandığını gösteriyor. Hesaplanan bekleme birkaç yüz milisaniyeydi; sunucu iki saniye söylediği için iki saniye beklendi. Sunucu kendi toparlanma süresini istemciden iyi bilir, bu yüzden önerisi taban değil tavan olarak değil, alt sınır olarak kullanılır. Öneri makul olmayacak kadar uzunsa denemeden vazgeçmek — kullanıcıya durumu bildirmek — beklemekten iyidir.
Döngünün üç sınırı vardır. Deneme sayısı üst sınırı sonsuz döngüyü engeller. Toplam bütçe, denemelerin toplam süresini sınırlar; kullanıcının bir düğmeye basıp sekiz saniye beklemesi, hata görmesinden kötüdür. Üçüncüsü, kullanıcı tarafından tetiklenen ile arka planda çalışan isteklerin ayrı ilkeleri olmasıdır: kullanıcı bekliyorsa deneme sayısı az ve süre kısa tutulur, arka plan tazelemesi için tersi geçerlidir.
Devre Kesici
Yeniden deneme tek bir isteğin sorunudur. Sunucu tümüyle erişilemez durumdaysa her istek kendi denemelerini yapar ve uygulama, hiçbiri başarılı olmayacak isteklerle hem kullanıcıyı bekletir hem sunucuyu yükler. Devre kesici (circuit breaker) bu durumu bir üst düzeyde tanır: ardışık başarısızlıklar bir eşiği aştığında isteği hiç göndermez.
// devre-kesici.mjs — ustuste basarisizlikta istegin hic gonderilmemesi const ESIK = 3, SOGUMA = 5000; // 3 ardisik hata; 5000 ms sonra deneme izni function devreKesici() { let durum = "kapali", ardisikHata = 0, acilma = 0; return { izinVer(simdi) { if (durum === "acik" && simdi - acilma >= SOGUMA) durum = "yariAcik"; return { izin: durum !== "acik", durum }; }, bildir(basarili, simdi) { if (basarili) { durum = "kapali"; ardisikHata = 0; return durum; } ardisikHata += 1; if (durum === "yariAcik" || ardisikHata >= ESIK) { durum = "acik"; acilma = simdi; ardisikHata = 0; } return durum; }, }; } // Mantiksal saat: gercek zaman yerine adim adim ilerler. const kesici = devreKesici(); const OLAYLAR = [ [ 0, false], [ 200, false], [ 600, false], // ucuncu hata devreyi acar [ 900, false], [ 1500, false], // sogumada: istek gitmez [ 6000, false], // yari acik deneme, basarisiz [ 8000, false], // yeniden sogumada [11500, true ], // yari acik deneme, basarili [11800, true ], ]; let gonderilen = 0, engellenen = 0; console.log("saat izin önceki durum sonuç yeni durum"); for (const [simdi, basarili] of OLAYLAR) { const { izin, durum } = kesici.izinVer(simdi); if (!izin) { engellenen += 1; console.log(String(simdi).padEnd(8) + "hayır " + durum.padEnd(14) + "gönderilmedi".padEnd(13) + durum); continue; } gonderilen += 1; const yeni = kesici.bildir(basarili, simdi); console.log(String(simdi).padEnd(8) + "evet " + durum.padEnd(14) + (basarili ? "başarılı" : "başarısız").padEnd(13) + yeni); } console.log(`gönderilen: ${gonderilen} engellenen: ${engellenen}`);
saat izin önceki durum sonuç yeni durum 0 evet kapali başarısız kapali 200 evet kapali başarısız kapali 600 evet kapali başarısız acik 900 hayır acik gönderilmedi acik 1500 hayır acik gönderilmedi acik 6000 evet yariAcik başarısız acik 8000 hayır acik gönderilmedi acik 11500 evet yariAcik başarılı kapali 11800 evet kapali başarılı kapali gönderilen: 6 engellenen: 3
Üç durum ve geçişleri çıktıdan okunur. Kapalı durumda istekler geçer; ardışık üçüncü hata devreyi açık duruma alır. Açık durumda istek gönderilmez, çağrı hemen ve ağa dokunmadan başarısız döner — dokuz olaydan üçü hiç sunucuya gitmedi. Soğuma süresi dolduğunda devre yarı açık olur ve tek bir deneme isteğine izin verir. Bu denemenin sonucu belirleyicidir: başarısız olursa devre yeniden açılır ve soğuma baştan başlar (6000. anda görülen davranış), başarılı olursa kapanır ve sayaç sıfırlanır.
Devre açıkken çağrı noktası bir yanıt almaz ama bir hata alır. Bu hata, ağ hatasından ayrılmalıdır: kullanıcıya “hizmet şu anda yanıt vermiyor” denir, “tekrar dene” düğmesi de soğuma süresi dolana kadar bir şey yapmayacağı için o süreyi bildirmek anlamlıdır.
Devre kesicinin kapsamı da bir tasarım kararıdır. Tek bir kesici tüm uygulamayı kapsarsa, bir uç noktanın arızası bütün ekranları durdurur. Uygulamada kesici genellikle hizmet ya da uç nokta grubu başına kurulur; ölçüm yazma yolu arızalıyken istasyon listesi okunabilir kalır.
Özet
- Yeniden deneme kararı hata sınıfına ve yönteme bakar: 429 ile 5xx her zaman, ağ hatası yalnız güvenli yöntemlerde denenir; doğrulama hataları, gövde hataları ve iptaller hiç denenmez.
- Durum değiştiren bir istek, ancak istemcinin ürettiği bir tekillik anahtarı sunucuyla kararlaştırılmışsa yeniden denenebilir.
- Üstel geri çekilme beklemeyi her denemede büyütür ve bir tavanla sınırlanır; sarsıntı, aynı anda hata alan istemcilerin aynı anda dönmesini engeller.
- Tam sarsıntı dağılımı en geniş yoldur, ilişkisiz sarsıntı büyümeyi yumuşatır; seçim hizmetin toparlanma süresine bağlıdır.
- Sunucunun bildirdiği bekleme önerisi hesaplanan süreyle karşılaştırılır ve büyük olan uygulanır; deneme sayısının yanında toplam süre bütçesi de sınır koyar.
- Devre kesici ardışık başarısızlıkta isteği hiç göndermez; açık, yarı açık ve kapalı durumları arasında geçer ve uç nokta grubu başına kurulur.
Sonraki Adım
Veri erişimi konusu burada tamamlanıyor: veri alınıyor, saklanıyor, akış hâlinde güncelleniyor, beklenmedik durumlar sayılı bir kümeye indirgeniyor ve ağ kararsızlığı kullanıcıya yansımadan soğuruluyor. Bütün bu akış tek yönlüydü — sunucudan ekrana. Sonraki konu ters yönü kurar: kullanıcının verdiği veri. Ölçüm giriş formu, alanların değerini tutmak, ne zaman doğrulamak, hatayı nasıl bildirmek ve gönderim sırasında ne göstermek sorularını aynı anda sorar. İlk ders bu sorulardan ilkiyle başlıyor: alanın değeri nerede durur — belgede mi, uygulamanın durumunda mı?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.