İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat