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.