İçeriğe geç
academia.sh

Ders 15 / 25

Zehirli Mesaj ve Yeniden Deneme

Tüketiciyi düşüren iletinin yalıtılması ve bekleme sürelerinin hesaplanması: deneme sayacının teslim anında artırılmasının zorunluluğu, anında–sabit–üstel çizelgelerin toplam bekleme ve deneme bütçesi karşılaştırması, sarsıntının yeniden deneme dalgasının tepesini düşürmesi ve görünmezlik süresiyle geri çekilmenin uyumu.

İçindekiler

Önceki ders eşiğin kaç kez deneneceğini söylediğini, ne zaman deneneceğini söylemediğini belirtti. Bu dersin iki konusu o boşluktan çıkıyor. Birincisi, eşiğin işlemediği bir hata türü: iletiyi işleyen tüketiciyi ayakta bırakmayan ileti. İkincisi, denemeler arasındaki beklemenin nasıl hesaplanacağı.

İkisi de aynı iş üzerinde ölçülecek: giden kutusundan alınan gecikme bildirimleri.

Zehirli Mesaj

Önceki dersteki bozuk iletiler nazik davranıyordu: bir hata fırlatıyor, tüketici hatayı yakalıyor, deneme sayacını artırıyor ve sonraki iletiye geçiyordu. Zehirli mesaj (poison message) bunu yapmaz. İşlenmesi tüketici sürecini sonlandırır — belleği tüketen bir gövde, sonsuz döngüye giren bir çözümleme, yakalanamayan bir başarısızlık. Süreç öldüğü için hata işleme kodu hiç çalışmaz.

Sonucu ölçülebilir bir kısır döngüdür. Süreç yeniden başlar, kuyruktan aynı iletiyi alır, yine ölür. Deneme sayacı hata yakalandığında artırılıyorsa hiç artmaz; eşik hiçbir zaman dolmaz; ölü mektup kuyruğu hiçbir zaman devreye girmez.

Aşağıdaki koşum beş iletilik bir kuyrukta üçüncü iletiyi öldürücü yapar ve sayacın iki farklı konumunu karşılaştırır: hata yakalandığında artan sayaç ile teslim anında, iş başlamadan artan sayaç.

// zehirli-sayac.mjs — sureci olduren ileti: sayac hangi anda artarsa yalitim calisir
const OLDURUCU = new Set([3]);            // bu ileti tuketici surecini sonlandiriyor
const ESIK = 3;

// Tek bir surec omru: oldurucu iletide donus yapar, arkasindaki kod hic calismaz.
function surecOmru(kuyruk, sayacTeslimde, olu) {
  let teslim = 0;
  for (const i of kuyruk.filter((m) => m.durum === "bekliyor")) {
    teslim += 1;
    if (sayacTeslimde) {                  // sayac isten once, teslim aninda artar
      i.deneme += 1;
      if (i.deneme >= ESIK) { i.durum = "olu"; olu.push(i.id); continue; }
    }
    if (OLDURUCU.has(i.id)) return { teslim, oldu: true };
    i.durum = "islendi";                  // sayac hata yakalaninca artacakti; surec olunce artmaz
  }
  return { teslim, oldu: false };
}

function kosum(sayacTeslimde, enCokBaslatma = 12) {
  const kuyruk = [1, 2, 3, 4, 5].map((id) => ({ id, deneme: 0, durum: "bekliyor" }));
  const olu = [];
  let teslim = 0, olum = 0, baslatma = 0;
  for (let b = 1; b <= enCokBaslatma; b++) {
    baslatma = b;
    const r = surecOmru(kuyruk, sayacTeslimde, olu);
    teslim += r.teslim;
    if (r.oldu) olum += 1;
    if (kuyruk.every((m) => m.durum !== "bekliyor")) break;
  }
  return { baslatma, teslim, olum, olu: olu.length,
           islenen: kuyruk.filter((m) => m.durum === "islendi").length,
           kalan: kuyruk.filter((m) => m.durum === "bekliyor").length };
}

console.log("sayac konumu".padEnd(22) + "baslatma  teslim  surec olumu  islenen  olu mektup  kalan");
for (const [ad, teslimde] of [["hata yakalaninca", false], ["teslim aninda", true]]) {
  const s = kosum(teslimde);
  console.log(ad.padEnd(22) +
    `${String(s.baslatma).padStart(8)}${String(s.teslim).padStart(8)}${String(s.olum).padStart(13)}` +
    `${String(s.islenen).padStart(9)}${String(s.olu).padStart(12)}${String(s.kalan).padStart(7)}`);
}
node zehirli-sayac.mjs
sayac konumu          baslatma  teslim  surec olumu  islenen  olu mektup  kalan
hata yakalaninca            12      14           12        2           0      3
teslim aninda                3       7            2        4           1      0

İlk satırda on iki başlatmanın on ikisi ölümle bitti, yalnız iki ileti işlendi ve üç ileti kuyrukta kaldı. On iki sayısı bir sonuç değil, koşumun üst sınırıdır: döngü orada durdurulduğu için durdu, kendiliğinden değil. Ölü mektup sütunu sıfır — eşik vardı, ama sayaç hiç artmadığı için hiçbir zaman dolmadı.

İkinci satırda üç başlatma yetti. Zehirli ileti iki kez teslim edildi, üçüncü teslimde sayaç eşiğe ulaştığı için iş hiç başlatılmadan yalıtıldı; arkasındaki iki ileti de işlendi. Kuyruk boşaldı.

Kural tek cümledir: deneme sayacı, işin sonucundan bağımsız olarak teslim anında artırılmalıdır. Sayacı hata yoluna koymak, yalnız hata yolunun çalıştığı durumlar için koruma sağlar. Önceki derste kurulan deneme alanının işten önce yazılmasının nedeni budur ve aynı gerekçe kuyruğun teslim sayacı için de geçerlidir: 02. derste ölçtüğümüz teslim sayısı al çağrısında artıyordu, işin bitişinde değil.

Yalıtım zehirli mesajın maliyetini sınırlar ama sıfırlamaz. İki süreç ölümü olmuştur ve o süreçlerin o sırada elinde tuttuğu başka işler yarım kalmıştır. Bu yüzden zehirli mesaj riski taşıyan işlerde ikinci bir savunma kullanılır: gövdenin işlenmeden önce doğrulanması. Doğrulama Katmanları dersinde konan ölçüt burada da geçerlidir — biçimi bozuk gövde işe hiç girmez, ölü mektup kuyruğuna doğrudan alınır.

Bekleme Süresinin Hesabı

Şimdiye kadarki koşumlarda denemeler arka arkaya yapıldı, aralarında hiç bekleme yoktu. Geçici hataların çoğu bir kaynağın aşırı yüklenmesinden doğar ve beklemeden yapılan yeniden deneme o kaynağı daha da yükler. Bekleme süresini veren kurala geri çekilme çizelgesi denir.

// geri-cekilme.mjs — bekleme cizelgeleri ve bir deneme butcesine sigan deneme sayisi
const TABAN = 500, TAVAN = 30_000;                 // ms
const CIZELGE = {
  "aninda": () => 0,
  "sabit 2 sn": () => 2000,
  "ustel": (n) => TABAN * 2 ** (n - 1),
  "ustel + tavan": (n) => Math.min(TAVAN, TABAN * 2 ** (n - 1)),
};
const ADLAR = Object.keys(CIZELGE);

console.log("deneme" + ADLAR.map((a) => a.padStart(15)).join(""));
for (let n = 1; n <= 8; n++)
  console.log(String(n).padStart(6) + ADLAR.map((a) => String(CIZELGE[a](n)).padStart(15)).join(""));

const toplam = (ad, adet) => Array.from({ length: adet }, (_, i) => CIZELGE[ad](i + 1))
  .reduce((t, x) => t + x, 0);
console.log("toplam" + ADLAR.map((a) => String(toplam(a, 8)).padStart(15)).join(""));

const BUTCE = 5 * 60 * 1000;                       // 5 dakikalik zaman butcesi
console.log("\ncizelge".padEnd(16) + "5 dk icinde sigan deneme  butce dolunca gecen sure");
for (const ad of ADLAR) {
  let gecen = 0, deneme = 0;
  while (gecen + CIZELGE[ad](deneme + 1) <= BUTCE && deneme < 1000) {
    deneme += 1; gecen += CIZELGE[ad](deneme);
  }
  console.log(ad.padEnd(16) + String(deneme === 1000 ? "sinirsiz" : deneme).padStart(24) +
              String(gecen).padStart(25) + " ms");
}
node geri-cekilme.mjs
deneme         aninda     sabit 2 sn          ustel  ustel + tavan
     1              0           2000            500            500
     2              0           2000           1000           1000
     3              0           2000           2000           2000
     4              0           2000           4000           4000
     5              0           2000           8000           8000
     6              0           2000          16000          16000
     7              0           2000          32000          30000
     8              0           2000          64000          30000
toplam              0          16000         127500          91500

cizelge        5 dk icinde sigan deneme  butce dolunca gecen sure
aninda                          sinirsiz                        0 ms
sabit 2 sn                           150                   300000 ms
ustel                                  9                   255500 ms
ustel + tavan                         14                   271500 ms

Dört çizelge dört ayrı davranış üretiyor. Anında yeniden deneme hiç beklemez: zaman bütçesi hiçbir zaman dolmaz, dolayısıyla denemeye sınırı ancak eşik koyabilir. Kaynağın yükünü azaltmak bir yana, hata anında yükü yoğunlaştırır.

Sabit aralık öngörülebilirdir ama iki uçta da yanlıştır: bir saniyede toparlanacak bir hata için iki saniye beklemek gereksizdir, bir dakika sürecek bir kesintide iki saniyede bir denemek kaynağı yüz elli kez boşuna yorar.

Üstel geri çekilme (exponential backoff) her denemede bekleme süresini katlayarak ikisini de çözer: hata kısa sürerse erken denemeler onu yakalar, uzun sürerse denemeler seyrelir. Ödediği bedel yedinci satırda görünüyor — bekleme 32 saniyeye çıkmış. Bu yüzden bir tavan konur; tavanlı sürümde toplam bekleme 127,5 saniyeden 91,5 saniyeye iner ve beş dakikalık bütçeye dokuz yerine on dört deneme sığar. Tavan, seyrelmenin sınırsızlaşmasını önler.

Frontend Mimarisi kursunun Yeniden Deneme ve Geri Çekilme dersinde aynı çizelgeler istemci tarafı için kurulmuştu. Tüketici tarafındaki fark, bekleyen tarafın bir kullanıcı değil bir ileti olmasıdır: bekleme süresince kuyruk derinliği artmaya devam eder ve gecikme yalnız o iletiye değil, arkasındaki bütün işlere yansır.

Sarsıntı

Geri çekilme çizelgesi tek bir tüketici için doğru olsa da, çok sayıda tüketici aynı anda başarısız olduğunda yeni bir sorun doğurur. Hepsi aynı çizelgeyi kullandığı için hepsi aynı anda yeniden dener; kaynak toparlanmaya çalışırken art arda gelen dalgalarla karşılaşır.

Çözüm bekleme süresine rastgelelik katmaktır. Buna sarsıntı (jitter) denir. Tam sarsıntı (full jitter) beklemeyi sıfır ile hesaplanan süre arasında rastgele seçer; ilişkisiz sarsıntı (decorrelated jitter) her adımda bir önceki beklemeye bağlı, daha geniş bir aralıktan seçer.

Aşağıdaki koşum aynı anda başarısız olan iki yüz tüketiciyi dört deneme boyunca izler ve yeniden deneme anlarını yüz milisaniyelik kovalara böler. Üreteç sabit tohumludur; sayılar her koşumda aynıdır.

// sarsinti.mjs — ayni anda basarisiz olan 200 tuketicinin yeniden deneme dalgasi
const uretec = (tohum) => { let x = tohum; return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648; };
const TUKETICI = 200, DENEME = 4, KOVA = 100;      // kova genisligi 100 ms
const TABAN = 500, TAVAN = 30_000;

function dalga(kip, tohum = 20250720) {
  const rast = uretec(tohum);
  const anlar = [];
  for (let t = 0; t < TUKETICI; t++) {
    let an = 0, onceki = TABAN;
    for (let n = 1; n <= DENEME; n++) {
      const taban = Math.min(TAVAN, TABAN * 2 ** (n - 1));
      if (kip === "sarsintisiz") an += taban;
      else if (kip === "tam sarsinti") an += Math.round(rast() * taban);          // [0, taban]
      else {                                                                       // iliskisiz
        onceki = Math.min(TAVAN, Math.round(TABAN + rast() * (onceki * 3 - TABAN)));
        an += onceki;
      }
      anlar.push(an);
    }
  }
  const kova = new Map();
  for (const a of anlar) { const k = Math.floor(a / KOVA); kova.set(k, (kova.get(k) ?? 0) + 1); }
  return { istek: anlar.length, tepe: Math.max(...kova.values()), doluKova: kova.size,
           sonKova: Math.max(...kova.keys()) };
}

console.log("cizelge".padEnd(20) + "toplam istek  en yogun 100 ms  dolu kova  son kova");
for (const kip of ["sarsintisiz", "tam sarsinti", "iliskisiz sarsinti"]) {
  const s = dalga(kip);
  console.log(kip.padEnd(20) + String(s.istek).padStart(12) + String(s.tepe).padStart(17) +
              String(s.doluKova).padStart(11) + String(s.sonKova).padStart(10));
}
node sarsinti.mjs
cizelge             toplam istek  en yogun 100 ms  dolu kova  son kova
sarsintisiz                  800              200          4        75
tam sarsinti                 800               69         65        67
iliskisiz sarsinti           800               28        163       389

Üç çizelgede de toplam istek sayısı aynı: sekiz yüz. Değişen tek şey bu isteklerin zamana nasıl yayıldığıdır. Sarsıntısız çizelgede sekiz yüz istek yalnız dört kovaya sığıyor ve her kovada iki yüz istek var — iki yüz tüketicinin tamamı aynı yüz milisaniyede kapıyı çalıyor. Tam sarsıntı tepeyi 69’a indiriyor ve isteği altmış beş kovaya yayıyor. İlişkisiz sarsıntı tepeyi 28’e indiriyor, ama son kova 389 — yani yayılım otuz dokuz saniyeye uzuyor.

Ödünleşme buradadır. Sarsıntı, kaynağın gördüğü ani yükü düşürür; karşılığında bazı iletilerin yeniden denenmesi geç kalır. Yükün tepesi kaynağı yeniden düşürecek büyüklükteyse sarsıntı zorunludur; işin gecikmeye duyarlılığı yüksekse yayılım dar tutulur.

Deneme Bütçesi ve Görünmezlik Süresi

Yeniden deneme kuralı iki sınırla birlikte tanımlanır. Deneme bütçesi (retry budget) sayıya bakar: eşik. Zaman bütçesi süreye bakar: iletinin ilk teslimden itibaren ne kadar süre sonra ölü mektup kuyruğuna alınacağı. İkisi birlikte gerekir, çünkü tek başına eşik geri çekilmeyle birleştiğinde süreyi öngörülemez kılar — tavanlı üstel çizelgede on dört deneme dört buçuk dakikadan uzun sürüyordu, tavansızda dokuz deneme aynı süreyi buluyordu.

İkinci uyum kuyruğun kendisiyledir. 02. derste kurulan görünmezlik süresi, alınan iletinin ne kadar süre başkasına verilmeyeceğini söylüyordu. Tüketici geri çekilme süresi boyunca iletiyi elinde tutup bekliyorsa, bu bekleme görünmezlik süresinden uzun olduğu anda ileti başka bir tüketiciye görünür olur ve aynı iş koşut yürür. Doğru yerleşim, tüketicinin beklerken iletiyi elinde tutmamasıdır: ileti kuyruğa geri bırakılır ve yeniden görünür olacağı an geri çekilme süresi kadar ileriye alınır. Böylece bekleme kuyruğun kendi zaman düzleminde tutulur ve tüketici süreci bu sırada başka iş yapar.

Üçüncü bir sınır hata oranıyla ilgilidir. Bir kaynak toptan erişilemez hâle geldiğinde her ileti aynı hatayı alır ve yeniden denemeler tek başına bir yük kaynağına dönüşür. Bu durumda tek tek iletilerin çizelgesi değil, tüketicinin tamamının durması gerekir; yukarıda anılan Yeniden Deneme ve Geri Çekilme dersinde tanıtılan devre kesici (circuit breaker) kalıbının çözdüğü sorun budur.

Özet

  • Zehirli mesaj yalnız başarısız olan ileti değildir; tüketici sürecini sonlandırdığı için hata işleme kodunu hiç çalıştırmaz.
  • Sayaç hata yakalandığında artırıldığında on iki başlatmanın on ikisi ölümle bitti, iki ileti işlendi ve üç ileti kuyrukta kaldı; teslim anında artırıldığında üç başlatma yetti, ileti üçüncü teslimde yalıtıldı ve kuyruk boşaldı.
  • Tavanlı üstel geri çekilme sekiz denemede toplam beklemeyi 127,5 saniyeden 91,5 saniyeye indirdi ve beş dakikalık bütçeye dokuz yerine on dört deneme sığdırdı.
  • İki yüz tüketicinin sekiz yüz yeniden denemesi sarsıntısız çizelgede dört kovaya yığıldı ve tepe 200 oldu; tam sarsıntı tepeyi 69’a, ilişkisiz sarsıntı 28’e indirdi ve yayılımı otuz dokuz saniyeye uzattı.
  • Deneme bütçesi ile zaman bütçesi birlikte tanımlanır; geri çekilme süresi görünmezlik süresini aşmamalı, bekleme tüketicide değil kuyruğun zaman düzleminde tutulmalıdır.

Sonraki Adım

Mesaj artık dayanıklıdır. Kalıcı bir yere yazılıyor, birden çok ilgiliye ulaşıyor, sırası gerektiği yerde korunuyor, tekrarı etkisiz kılınıyor, işlenemeyeni yalıtılıyor ve yeniden denemesi ölçülü bir çizelgeye bağlanıyor. Bütün bu ölçümlerde bir taraf ise hep verili kabul edildi: iletiyi işleyen tüketici. Kaç tane işçi süreci vardır, biri saniyede kaç ileti bitirir, sayıları artırıldığında ne kazanılır, üretim hızı kapasitelerini aştığında ne olur? Sonraki konu tüketiciyi bir ölçekleme birimi olarak ele alır ve İşçi Süreçleri dersiyle başlar.

İ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