Ders 14 / 25
Ölü Mektup Kuyrukları
Hiçbir zaman başarılı olmayacak iletilerin yalıtılması: eşiksiz yeniden denemenin boşa deneme sayısını sınırsız büyütmesi, deneme sayacı ile eşiğin kuyruğu boşaltması, ölü mektup kaydının taşıması gereken alanlar, nedeni giderilen iletilerin geri sürülmesi ve eşik değerinin geçici hatayı yanlış yalıtmayla ödünleşmesi.
İçindekiler
Şimdiye kadarki bütün ölçümlerde bir varsayım sessizce sürdü: yeniden denenen iş sonunda başarılı oluyor. Çökme geçiciydi, ikinci deneme işi bitiriyordu. Gerçekte bazı iletiler ikinci denemede de, onuncu denemede de başarısız olur — gövdesi çözülemez, atıfta bulunduğu üye kaydı silinmiştir, taşıdığı iş kuralı onu hiçbir zaman kabul etmeyecektir.
En az bir kez kipinde böyle bir ileti kuyruktan hiç çıkmaz. Bu ders o iletinin ne kadar zarar verdiğini ölçer ve onu kuyruktan çıkarıp yalıtan yapıyı kurar.
Kalıcı Hata ile Geçici Hata
Ayrım yeniden denemenin anlamlı olup olmadığına dayanır. Geçici hata, koşullar değişince ortadan kalkan hatadır: veritabanı bağlantısının kopması, dış servisin kısa süreli erişilemezliği, iki yazmanın çatışması. Yeniden deneme bu hataların doğru yanıtıdır.
Kalıcı hata koşullardan değil iletinin kendisinden gelir. Aynı ileti aynı kodla her denemede aynı sonucu verir; yeniden deneme yalnız hatayı tekrarlar. İki hata türü aynı yolu paylaştığında, kalıcı olanı ayırmanın tek yolu deneme sayısına bakmaktır.
Ölçüm düzeneği iki bağıntı kurar: iletileri ve deneme sayaçlarını tutan iş kuyruğu, yalıtılan iletileri tutan ölü mektup kuyruğu.
# kur.sh — is kuyrugu ve olu mektup kuyrugu bagintilari rm -f kuyruk.db sqlite3 kuyruk.db <<'SQL' CREATE TABLE is_kuyrugu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL, govde TEXT NOT NULL, deneme INTEGER NOT NULL DEFAULT 0, durum TEXT NOT NULL DEFAULT 'bekliyor'); CREATE TABLE olu_mektup (olu_id INTEGER PRIMARY KEY, ileti_id INTEGER NOT NULL, tur TEXT NOT NULL, govde TEXT NOT NULL, deneme INTEGER NOT NULL, son_hata TEXT NOT NULL, kaynak TEXT NOT NULL); SQL sqlite3 kuyruk.db "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
is_kuyrugu olu_mektup
Eşiksiz Yeniden Denemenin Maliyeti
Yirmi gecikme bildirimi işlenecek; ikisinin — 7 ve 13 numaralı iletilerin — atıfta bulunduğu üye kaydı yok, dolayısıyla her denemede başarısız olacaklar. Aşağıdaki koşum aynı iş yükünü üç ayarla çalıştırır: eşiksiz sekiz tur, eşiksiz yirmi dört tur ve üç denemelik eşikle sekiz tur.
// esik.mjs — esiksiz yeniden deneme ile esikli yalitmanin ayni is yukunde karsilastirilmasi import { DatabaseSync } from "node:sqlite"; const KALICI = new Set([7, 13]); // govdesi bozuk iletiler: her denemede basarisiz const ADET = 20; function tazele(db) { db.exec("DELETE FROM is_kuyrugu; DELETE FROM olu_mektup"); for (let n = 1; n <= ADET; n++) db.prepare("INSERT INTO is_kuyrugu (ileti_id, tur, govde) VALUES (?,?,?)") .run(n, "gecikme_bildirimi", JSON.stringify({ oduncId: n })); } function isle(i) { if (KALICI.has(i.ileti_id)) throw new Error(`uye bulunamadi (odunc ${i.ileti_id})`); } function kosum(db, esik, turSayisi) { // esik = 0 -> yalitim yok tazele(db); let teslim = 0, basarili = 0, bosa = 0; for (let tur = 1; tur <= turSayisi; tur++) { for (const i of db.prepare("SELECT * FROM is_kuyrugu WHERE durum='bekliyor' ORDER BY ileti_id").all()) { teslim += 1; const deneme = i.deneme + 1; db.prepare("UPDATE is_kuyrugu SET deneme=? WHERE ileti_id=?").run(deneme, i.ileti_id); try { isle(i); db.prepare("UPDATE is_kuyrugu SET durum='islendi' WHERE ileti_id=?").run(i.ileti_id); basarili += 1; } catch (e) { bosa += 1; if (esik > 0 && deneme >= esik) { db.prepare(`INSERT INTO olu_mektup (ileti_id, tur, govde, deneme, son_hata, kaynak) VALUES (?,?,?,?,?,'is_kuyrugu')`) .run(i.ileti_id, i.tur, i.govde, deneme, e.message); db.prepare("UPDATE is_kuyrugu SET durum='olu' WHERE ileti_id=?").run(i.ileti_id); } } } } const say = (s) => db.prepare(s).get().n; return { teslim, basarili, bosa, derinlik: say("SELECT count(*) AS n FROM is_kuyrugu WHERE durum='bekliyor'"), olu: say("SELECT count(*) AS n FROM olu_mektup") }; } const db = new DatabaseSync("kuyruk.db"); console.log("kip".padEnd(10) + " tur teslim basarili bosa deneme kalan derinlik olu mektup"); for (const [ad, esik, tur] of [["esiksiz", 0, 8], ["esiksiz", 0, 24], ["esik=3", 3, 8]]) { const s = kosum(db, esik, tur); console.log(ad.padEnd(10) + `${String(tur).padStart(5)}${String(s.teslim).padStart(8)}${String(s.basarili).padStart(10)}` + `${String(s.bosa).padStart(12)}${String(s.derinlik).padStart(16)}${String(s.olu).padStart(12)}`); }
node esik.mjs
kip tur teslim basarili bosa deneme kalan derinlik olu mektup esiksiz 8 34 18 16 2 0 esiksiz 24 66 18 48 2 0 esik=3 8 24 18 6 0 2
İlk iki satır aynı iş yükünün iki farklı süredeki görüntüsüdür ve tek farkları boşa deneme sayısıdır: sekiz turda 16, yirmi dört turda 48. Başarılı iş her ikisinde de 18’de sabit, kalan derinlik her ikisinde de 2. İki bozuk ileti hiçbir zaman bitmediği için her turda yeniden teslim edilir; harcanan kapasite tur sayısıyla doğru orantılı büyür ve bir üst sınırı yoktur.
Üçüncü satır eşiğin ne yaptığını gösteriyor. Boşa deneme 6’da durdu — iki ileti üçer kez denendi ve kuyruktan çıkarıldı. Toplam teslim 34 yerine 24. Kazanç yalnız sayıda değil: kalan derinlik ikiden sıfıra indi, yani kuyruk bitmiş durumdadır. İki bozuk ileti kaybolmamış, ayrı bir yere alınmıştır.
Önceki dersin bölümlenmiş düzeninde aynı iletinin maliyeti daha da yüksektir. Bozuk ileti sırayı koruyan bir bölümün başındaysa, arkasındaki bütün iletiler o bölümde bekler; sıra garantisi hatalı iletinin atlanmasına izin vermez. Eşik, bölümü yeniden akıtan tek düzenektir.
Ölü Mektup Kaydı Neyi Taşımalı
Eşiği aşan iletinin gideceği yere ölü mektup kuyruğu (dead-letter queue) denir. Kuyruk olmasının nedeni, orada duran şeyin bir günlük kaydı değil, hâlâ işlenebilir bir ileti olmasıdır. Bu yüzden kayıt en az beş alanı taşır: iletinin gövdesi olduğu gibi, türü, kaç kez denendiği, son hatanın metni ve hangi kuyruktan geldiği. Gövde eksikse ileti yeniden sürülemez; hata metni yoksa nedeni araştırmak için denemeyi tekrarlamak gerekir.
Aşağıdaki koşum önce iletileri yalıtır, kayıtları yazdırır, sonra nedeni giderir ve yalıtılanları iş kuyruğuna geri sürer.
// yeniden-surme.mjs — olu mektup kaydinin icerigi ve neden giderildikten sonra geri surulmesi import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kuyruk.db"); let bozuk = new Set([7, 13]); const isle = (i) => { if (bozuk.has(i.ileti_id)) throw new Error(`uye bulunamadi (odunc ${i.ileti_id})`); }; db.exec("DELETE FROM is_kuyrugu; DELETE FROM olu_mektup"); for (let n = 1; n <= 20; n++) db.prepare("INSERT INTO is_kuyrugu (ileti_id, tur, govde) VALUES (?,?,?)") .run(n, "gecikme_bildirimi", JSON.stringify({ oduncId: n })); function tuket(esik = 3) { let islenen = 0; for (let tur = 1; tur <= esik; tur++) for (const i of db.prepare("SELECT * FROM is_kuyrugu WHERE durum='bekliyor' ORDER BY ileti_id").all()) { const deneme = i.deneme + 1; db.prepare("UPDATE is_kuyrugu SET deneme=? WHERE ileti_id=?").run(deneme, i.ileti_id); try { isle(i); db.prepare("UPDATE is_kuyrugu SET durum='islendi' WHERE ileti_id=?").run(i.ileti_id); islenen += 1; } catch (e) { if (deneme >= esik) { // esik asildi: ileti yalitilir db.prepare(`INSERT INTO olu_mektup (ileti_id, tur, govde, deneme, son_hata, kaynak) VALUES (?,?,?,?,?,'is_kuyrugu')`) .run(i.ileti_id, i.tur, i.govde, deneme, e.message); db.prepare("UPDATE is_kuyrugu SET durum='olu' WHERE ileti_id=?").run(i.ileti_id); } } } return islenen; } const say = (s) => db.prepare(`SELECT count(*) AS n FROM ${s}`).get().n; console.log(`birinci gecis: islenen=${tuket()} bekleyen=${say("is_kuyrugu WHERE durum='bekliyor'")}`); for (const r of db.prepare("SELECT * FROM olu_mektup ORDER BY ileti_id").all()) console.log(` ileti=${r.ileti_id} tur=${r.tur} deneme=${r.deneme} kaynak=${r.kaynak} ` + `govde=${r.govde} son_hata=${r.son_hata}`); bozuk = new Set(); // neden giderildi: eksik uye kayitlari tamamlandi const geri = db.prepare("SELECT * FROM olu_mektup ORDER BY ileti_id").all(); for (const r of geri) { db.prepare("UPDATE is_kuyrugu SET durum='bekliyor', deneme=0 WHERE ileti_id=?").run(r.ileti_id); db.prepare("DELETE FROM olu_mektup WHERE olu_id=?").run(r.olu_id); } console.log(`geri surulen=${geri.length} ikinci gecis: islenen=${tuket()}`); console.log(`sonuc: islenen=${say("is_kuyrugu WHERE durum='islendi'")} ` + `bekleyen=${say("is_kuyrugu WHERE durum='bekliyor'")} olu mektup=${say("olu_mektup")}`);
node yeniden-surme.mjs
birinci gecis: islenen=18 bekleyen=0
ileti=7 tur=gecikme_bildirimi deneme=3 kaynak=is_kuyrugu govde={"oduncId":7} son_hata=uye bulunamadi (odunc 7)
ileti=13 tur=gecikme_bildirimi deneme=3 kaynak=is_kuyrugu govde={"oduncId":13} son_hata=uye bulunamadi (odunc 13)
geri surulen=2 ikinci gecis: islenen=2
sonuc: islenen=20 bekleyen=0 olu mektup=0
Ölü mektup kaydının kendisi hatanın tanısını veriyor: aynı türden iki ileti, aynı hata metni, üçer deneme. Eksik üye kayıtları tamamlandıktan sonra iki ileti iş kuyruğuna geri sürüldü, deneme sayaçları sıfırlandı ve ikinci geçişte işlendi. Yirmi iletinin yirmisi de sonunda işlenmiş oldu.
Yeniden sürme (redrive) işleminin iki koşulu vardır. Deneme sayacı sıfırlanmalıdır, yoksa ileti eşiği zaten aşmış olduğu için ilk hatada yeniden yalıtılır. Ve nedenin gerçekten giderilmiş olması gerekir; giderilmeden yapılan geri sürme, aynı iletileri aynı sayıda denemeyle ölü mektup kuyruğuna geri gönderir. İkinci koşul, geri sürmenin bir düğmeye değil, bir tanıya bağlanmasını zorunlu kılar.
Eşik Değerini Seçmek
Eşik iki hatayı birbirinden ayırt edemez; yalnız deneme sayısına bakar. Bu yüzden değeri iki kusur arasında bir seçimdir: küçük eşik, dördüncü denemede geçecek geçici bir hatayı kalıcı sanıp yalıtır; büyük eşik, kalıcı hatalı iletiyi gereğinden çok kez dener.
Aşağıdaki koşum yirmi iletiden ikisini kalıcı bozuk, beşini üçüncü denemede geçen, dördünü beşinci denemede geçen iletiler olarak kurar ve eşiği bir ile sekiz arasında değiştirir.
// esik-secimi.mjs — esik degeri: gecici hatayi yanlis yalitmak ile bosa deneme arasindaki secim const ILETILER = []; // gerekli = kacinci denemede basarili olur, 0 = hicbir zaman for (let n = 1; n <= 20; n++) { const gerekli = n === 7 || n === 13 ? 0 : (n % 5 === 0 ? 5 : (n % 3 === 0 ? 3 : 1)); ILETILER.push({ ileti_id: n, gerekli }); } function kosum(esik) { let bosa = 0, yanlisYalitilan = 0, kaliciYalitilan = 0, islenen = 0; for (const i of ILETILER) { for (let deneme = 1; deneme <= esik; deneme++) { if (i.gerekli !== 0 && deneme >= i.gerekli) { islenen += 1; break; } bosa += 1; if (deneme === esik) { // esik doldu: ileti olu mektup kuyruguna if (i.gerekli === 0) kaliciYalitilan += 1; else yanlisYalitilan += 1; } } } return { islenen, bosa, yanlisYalitilan, kaliciYalitilan }; } console.log(`ileti=${ILETILER.length} kalici bozuk=${ILETILER.filter((i) => i.gerekli === 0).length} ` + `3 denemede gecen=${ILETILER.filter((i) => i.gerekli === 3).length} ` + `5 denemede gecen=${ILETILER.filter((i) => i.gerekli === 5).length}`); console.log("esik islenen bosa deneme yanlis yalitilan kalici yalitilan"); for (const esik of [1, 2, 3, 5, 8]) { const s = kosum(esik); console.log(`${String(esik).padStart(4)}${String(s.islenen).padStart(9)}${String(s.bosa).padStart(13)}` + `${String(s.yanlisYalitilan).padStart(18)}${String(s.kaliciYalitilan).padStart(18)}`); }
node esik-secimi.mjs
ileti=20 kalici bozuk=2 3 denemede gecen=5 5 denemede gecen=4 esik islenen bosa deneme yanlis yalitilan kalici yalitilan 1 9 11 9 2 2 9 22 9 2 3 14 28 4 2 5 18 36 0 2 8 18 42 0 2
Tablo iki yönde de tek yönlü. Eşik büyüdükçe yanlış yalıtılan ileti sayısı 9’dan 0’a iniyor, boşa deneme sayısı 11’den 42’ye çıkıyor. İşlenen iş beşinci eşikte doyuma ulaşıyor: sekize çıkarmak hiçbir ileti kazandırmıyor, yalnız iki kalıcı bozuk iletiyi üçer kez daha deniyor.
Bu, eşiğin nasıl seçileceğini de söyler. Doğru değer, işin gerçekten ihtiyaç duyduğu en yüksek deneme sayısının biraz üstüdür; ondan sonrası saf israftır. “Gerçekten ihtiyaç duyulan deneme sayısı” tahmin edilmez, ölçülür — başarılı işlerin deneme sayısı dağılımı bu değeri doğrudan verir.
Yalıtmak Çözmek Değildir
Ölü mektup kuyruğunun tehlikesi, sorunu görünmezleştirmesidir. İletiler oradadır, kuyruk temizdir, ölçütler sağlıklı görünür — ve üç üye bildirimini hiç almamıştır. Yalıtım, kaybı sessiz olmaktan çıkarır ancak izlenirse.
Üç kural bunu sağlar. Ölü mektup kuyruğunun derinliği izlenen bir ölçüttür ve sıfırdan farklı olması bir uyarı üretir; kuyruk derinliğinden farklı olarak burada “normal” bir taban değeri yoktur. Her kaydın bir sahibi vardır: iletiyi hangi ekip üretiyorsa yalıtılan iletiyi de o inceler. Ve kayıtların bir yaşı vardır — incelenmeden aylarca duran ölü mektup, incelenmeyecek demektir.
Yalıtılan iletilerin kümelenmesi de bir tanı aracıdır. Aynı hata metnini taşıyan çok sayıda kayıt tek bir kök nedene işaret eder ve tek tek incelenmesi gerekmez; dağınık hatalar ise her birinin ayrı olduğunu söyler. Ölü mektup kaydında hata metninin saklanmasının asıl karşılığı budur.
Özet
- Geçici hata koşullar değişince kaybolur ve yeniden denemeye yanıt verir; kalıcı hata iletinin kendisinden gelir ve her denemede aynı sonucu üretir.
- Eşiksiz yeniden denemede iki bozuk ileti sekiz turda 16, yirmi dört turda 48 boşa deneme üretti; başarılı iş her ikisinde de 18’de kaldı ve harcanan kapasitenin üst sınırı yoktu.
- Üç denemelik eşikle boşa deneme 6’da durdu, kuyruk boşaldı ve iki ileti ölü mektup kuyruğuna alındı; bölümlenmiş düzende eşik, bozuk iletinin durdurduğu bölümü yeniden akıtır.
- Ölü mektup kaydı gövdeyi, türü, deneme sayısını, son hata metnini ve kaynak kuyruğu taşır; neden giderildikten sonra deneme sayacı sıfırlanarak geri sürülen iki ileti işlendi.
- Eşik büyüdükçe yanlış yalıtılan ileti 9’dan 0’a indi, boşa deneme 11’den 42’ye çıktı; doğru değer, başarılı işlerin ihtiyaç duyduğu en yüksek deneme sayısının biraz üstüdür.
Sonraki Adım
Eşik, kaç kez deneneceğini söyledi; ne zaman deneneceğini söylemedi. Önceki ölçümlerde 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; üstelik aynı anda başarısız olan yüzlerce tüketici aynı anda yeniden deneyerek yükü tek noktada toplar. Sonraki ders bekleme sürelerinin nasıl hesaplanacağını kurar: üstel geri çekilme, üst sınır ve yeniden deneme dalgasını dağıtan sarsıntı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.