Ders 05 / 25
Sıcak Anahtar ve Sürü Etkisi
Sıcak bir anahtarın girdisi düştüğünde eşzamanlı yüz isteğin kaynağa yığılmasının ölçülmesi ve tek uçuş kilidiyle bunun tek üretime indirilmesi; yeniden doğrulayarak sunmanın bekleme süresini sıfırlaması, yaşam süresinin dağıtılmasıyla eşzamanlı süre sonunun önlenmesi ve sıcak anahtarın bölümlere yayılması.
İçindekiler
Önceki derslerin bütün ölçümleri tek bir sırada yapıldı: bir istek ıska alır, kaynağa gider, girdiyi koyar, sıradaki istek isabet alır. Bu sıra, isteklerin birbirini beklediğini varsayar. Gerçek bir sunucuda varsaymaz.
Kütüphane sisteminin en çok okunan anahtarı Merkez şubesinin stok sayısıdır. O girdi düştüğünde, düşme anıyla yeni girdinin konması arasında geçen sürede gelen her istek ıska alır ve her biri kaynağa gider. Bu davranışa sürü etkisi (cache stampede) denir ve sonucu şudur: kaynak, en çok korunduğu anahtarda en yüksek yükü, tam da önbelleğin boşaldığı anda görür. Bu ders davranışı ölçer ve üç önlemi karşılaştırır.
Yüz İsteğin Aynı Anda Gelmesi
Ölçüm düzeneğinde kaynak erişimi eşzamansız bir çağrıdır: sorgu ile gidiş-dönüş birlikte 30 milisaniye sürer. Sayılan şey süre değil, kaynağa giden çağrı sayısıdır.
// suru.mjs — sicak anahtarin girdisi dustugu anda 100 istek birden geliyor import { setTimeout as bekle } from "node:timers/promises"; const GECIKME = 30; // kaynak erisimi: sorgu + gidis-donus const sayac = { kaynak: 0 }; async function kaynaktanUret(anahtar) { // sube stok sayisi: pahali toplama sorgusu sayac.kaynak += 1; await bekle(GECIKME); return { anahtar, raftaki: 137 }; } const ANAHTAR = "kutuphane:b1:n1:ksube-1:stok"; const ISTEK = 100; async function kosu(ad, kur) { sayac.kaynak = 0; const olcu = { bekleyen: 0, bayat: 0 }; const oku = kur(olcu); const t = performance.now(); await Promise.all(Array.from({ length: ISTEK }, () => oku(ANAHTAR))); const sure = performance.now() - t; return `${ad.padEnd(28)}${String(sayac.kaynak).padStart(14)}${String(olcu.bekleyen).padStart(16)}` + `${String(olcu.bayat).padStart(12)}${(String(Math.round(sure / 10) * 10) + " ms").padStart(12)}`; } // 1) Duz yaninda okuma: girdi yok, herkes kaynaga gider. const duz = (olcu) => { const kutu = new Map(); return async (a) => { if (kutu.has(a)) return kutu.get(a); olcu.bekleyen += 1; const d = await kaynaktanUret(a); kutu.set(a, d); return d; }; }; // 2) Tek ucus: ayni anahtar icin tek uretim; digerleri ayni sozu bekler. const tekUcus = (olcu) => { const kutu = new Map(); const ucus = new Map(); return async (a) => { if (kutu.has(a)) return kutu.get(a); olcu.bekleyen += 1; if (!ucus.has(a)) { ucus.set(a, kaynaktanUret(a).then((d) => { kutu.set(a, d); ucus.delete(a); return d; })); } return ucus.get(a); }; }; // 3) Yeniden dogrulayarak sunma: suresi dolmus girdi hemen donulur, yenileme arkada baslar. const yenidenDogrulayarakSun = (olcu) => { const kutu = new Map([[ANAHTAR, { deger: { anahtar: ANAHTAR, raftaki: 131 }, taze: false }]]); const ucus = new Map(); return async (a) => { const girdi = kutu.get(a); if (girdi && !girdi.taze && !ucus.has(a)) { // yenilemeyi baslat, bekleme ucus.set(a, kaynaktanUret(a).then((d) => { kutu.set(a, { deger: d, taze: true }); ucus.delete(a); })); } if (girdi) { if (!girdi.taze) olcu.bayat += 1; return girdi.deger; } olcu.bekleyen += 1; return kaynaktanUret(a); }; }; console.log(["strateji", "kaynak cagrisi", "bekleyen istek", "bayat yanit", "sure~"] .map((b, i) => (i === 0 ? b.padEnd(28) : b.padStart([0, 14, 16, 12, 12][i]))).join("")); console.log(await kosu("duz", duz)); console.log(await kosu("tek ucus", tekUcus)); console.log(await kosu("yeniden dogrulayarak sunma", yenidenDogrulayarakSun));
strateji kaynak cagrisi bekleyen istek bayat yanit sure~ duz 100 100 0 30 ms tek ucus 1 100 0 30 ms yeniden dogrulayarak sunma 1 0 100 0 ms
Süre sütunu on milisaniyeye yuvarlanmıştır ve makineye bağlıdır; ilk üç sütun değildir.
Düz kurulumda yüz istek yüz kaynak çağrısı üretti. Kod hatalı değil: her istek önbelleğe baktı, bulamadı, kaynağa gitti. Kusur, ıska ile girdinin konması arasındaki boşlukta yatar. Bu boşluk kaynak erişimi kadar sürer ve o süre boyunca gelen her istek aynı işi yeniden başlatır. Yükün büyüklüğü isteklerin gelme hızıyla belirlenir: saniyede bin istek alan bir uç noktada 30 milisaniyelik boşluk otuz eşzamanlı sorgu demektir.
Tek uçuş (single-flight) kilidi kaynak çağrısını bire indirdi. Kurulum basittir: üretim başlatılmadan önce anahtar bir uçuş tablosuna yazılır ve sonraki istekler aynı sözü bekler. Yüz isteğin hepsi yine bekledi — sonuçta veri henüz yoktu — ama kaynak tek sorgu gördü. Bu, sürü etkisinin doğrudan çözümüdür.
Yeniden doğrulayarak sunma (stale-while-revalidate) farklı bir ödün verir. Süresi dolmuş girdi silinmez; istek gelince eski değer hemen döndürülür ve yenileme arka planda başlatılır. Bekleyen istek sayısı sıfıra indi, karşılığında yüz yanıt bayat veri taşıdı. Frontend Kalitesi kursundaki Önbellek Stratejileri dersinde tarayıcı tarafında tanıtılan aynı kalıp, sunucu tarafında da aynı ödünü verir: bekleme yerine bayatlık.
Üç satır arasındaki seçim veriye bağlıdır. Şube stok sayısı birkaç saniye eski olabilir; orada yeniden doğrulayarak sunma en iyi seçimdir. Ödünç verme sırasında yapılan uygunluk denetimi bayat veriyle çalışamaz; orada tek uçuş kullanılır.
Tek uçuş tablosunun kapsamı da ayrı bir karardır. Süreç içindeki tablo yalnız o örneği korur; dört uygulama örneği çalışıyorsa kaynağa bir yerine dört çağrı gider. Kaynağı kesin olarak tek çağrıya indirmek için kilidin paylaşılan depoda tutulması gerekir; bu, doğruluğu artırırken her ıskaya bir ağ turu ekler.
Süre Sonlarının Aynı Ana Düşmesi
Sürü etkisinin ikinci biçimi tek bir anahtarda değil, aynı anda yüklenmiş bir anahtar kümesinde ortaya çıkar. Bir uygulama örneği yeniden başlatıldığında ya da bir nesil sayacı artırıldığında yüzlerce anahtar aynı adımda önbelleğe girer. Hepsine aynı yaşam süresi verilirse hepsi aynı adımda düşer.
// sicak-anahtar.mjs — es zamanli sure sonu ve tek dugume yigilan sicak anahtar import { createHash } from "node:crypto"; const ozet = (m) => parseInt(createHash("sha256").update(m).digest("hex").slice(0, 8), 16); console.log("--- 200 anahtar ayni adimda yuklendi, omur=100"); for (const [ad, omur] of [["sabit omur", () => 100], ["dagitilmis omur", (a) => 100 + (ozet(a) % 40)]]) { const adimBasi = new Map(); for (let i = 1; i <= 200; i++) { const anahtar = `kutuphane:b1:n1:ksube-1:kitap:${i}`; const adim = omur(anahtar); adimBasi.set(adim, (adimBasi.get(adim) ?? 0) + 1); } const enYuksek = Math.max(...adimBasi.values()); console.log(`${ad.padEnd(18)} sure sonu adimi=${adimBasi.size} tek adimdaki en yuksek iska=${enYuksek}`); } console.log("--- 4 bolumlu paylasilan onbellek, isteklerin %60'i tek anahtarda"); const SICAK = "kutuphane:b1:n1:ksube-1:stok"; const istekler = Array.from({ length: 1000 }, (_, i) => i % 10 < 6 ? SICAK : `kutuphane:b1:n1:ksube-${(i % 3) + 2}:kitap:${i % 40}`); function dagilim(donustur) { const bolum = [0, 0, 0, 0]; for (const [i, a] of istekler.entries()) bolum[ozet(donustur(a, i)) % 4] += 1; return bolum; } const yaz = (ad, b) => console.log(`${ad.padEnd(22)}${b.map((n) => String(n).padStart(8)).join("")}` + ` en yuklu bolum=%${((Math.max(...b) / 1000) * 100).toFixed(0)}`); yaz("tek anahtar", dagilim((a) => a)); yaz("4 kopyaya bolunmus", dagilim((a, i) => (a === SICAK ? `${a}#${i % 4}` : a))); yaz("8 kopyaya bolunmus", dagilim((a, i) => (a === SICAK ? `${a}#${i % 8}` : a)));
--- 200 anahtar ayni adimda yuklendi, omur=100 sabit omur sure sonu adimi=1 tek adimdaki en yuksek iska=200 dagitilmis omur sure sonu adimi=40 tek adimdaki en yuksek iska=11 --- 4 bolumlu paylasilan onbellek, isteklerin %60'i tek anahtarda tek anahtar 150 116 83 651 en yuklu bolum=%65 4 kopyaya bolunmus 300 416 83 201 en yuklu bolum=%42 8 kopyaya bolunmus 300 341 158 201 en yuklu bolum=%34
Sabit ömürde iki yüz anahtarın tamamı tek bir adımda düştü. Yaşam süresine anahtardan türetilen küçük bir sapma eklendiğinde süre sonları kırk adıma yayıldı ve tek adımdaki en yüksek ıska sayısı 11’e indi. Sapmanın anahtardan türetilmesi önemlidir: rastgele bir sayı kullanılırsa aynı anahtar farklı örneklerde farklı sürelerde düşer ve davranış yinelenemez hâle gelir.
Sıcak Anahtarın Kendisi
Çıktının ikinci yarısı ayrı bir sorunu ölçüyor. Paylaşılan bir önbellek birden çok düğüme bölündüğünde anahtar, özetine göre bir bölüme düşer. İsteklerin %60’ı tek bir anahtardaysa o anahtarın bölümü isteklerin %65’ini görür; kalan üç bölüm boşta kalır. Bölüm sayısını artırmak bu dağılımı düzeltmez, çünkü sorun anahtar sayısının azlığında değil, tek bir anahtarın ağırlığındadır.
Çözüm anahtarı çoğaltmaktır: sıcak anahtar sabit sayıda kopyaya bölünür ve istek bunlardan birine yönlendirilir. Dört kopyada en yüklü bölüm %65’ten %42’ye, sekiz kopyada %34’e indi. Dağılım eşit değildir — özet işlevi kopyaları bölümlere eşit dağıtmaz — ama yığılma kırılmıştır.
Çoğaltmanın bedeli iki tanedir. Aynı değer birden çok kez saklanır ve geçersizleştirme artık tek anahtarı değil, bütün kopyaları düşürmelidir. İkincisi, kopya sayısı sabit olduğu için önceki dersteki nesil sayacı burada da işe yarar: nesil artırıldığında bütün kopyalar tek hamlede erişilemez olur.
Sıcak anahtarın üçüncü ve en ucuz önlemi, uygulama örneğinin içinde kısa ömürlü bir yerel kopya tutmaktır. İlk derste ölçülen katman zincirinin sunucu halkası budur: paylaşılan önbelleğin önünde duran birkaç saniyelik yerel bir girdi, sıcak anahtara giden ağ turlarının çoğunu keser.
Özet
- Sıcak bir anahtarın girdisi düştüğünde ıska ile girdinin konması arasındaki boşlukta gelen her istek kaynağa gider: yüz eşzamanlı istek yüz kaynak çağrısı üretti.
- Tek uçuş kilidi aynı anahtar için üretimi bire indirir; yüz istek yine bekledi ama kaynak tek sorgu gördü. Kilit süreç içindeyse koruma örnek sayısı kadar zayıflar.
- Yeniden doğrulayarak sunma bekleyen istek sayısını sıfıra indirir ve karşılığında bayat yanıt döndürür; seçim, verinin bayatlık toleransına bağlıdır.
- Aynı anda yüklenen iki yüz anahtar sabit ömürle tek adımda düştü; ömre anahtardan türetilen bir sapma eklendiğinde tek adımdaki en yüksek ıska 200’den 11’e indi.
- Bölümlere ayrılmış paylaşılan bir önbellekte tek sıcak anahtar bir bölüme %65 yük bindirdi; anahtar sekiz kopyaya bölündüğünde en yüklü bölüm %34’e indi.
Sonraki Adım
Bu derse kadar bütün önbellekler uygulamanın içinde kuruldu ve bütün kararlar kodda verildi. Yanıt istemciye gönderildiğinde ise önbellekleme kararı artık kodun elinde değildir: tarayıcı, aradaki paylaşılan önbellekler ve kenar katmanı, yalnız yanıtın başlıklarında yazana bakar. Bu, ilk derste “denetlenemez” diye anılan katmanla konuşmanın tek yoludur ve kendi sözlüğü vardır — hangi yanıt saklanabilir, ne kadar taze sayılır, saklanan kopyanın hâlâ geçerli olup olmadığı kaynağa sorulmadan nasıl anlaşılır. Sonraki ders bu sözlüğü bir HTTP sunucusu ile gerçek istekler üzerinde kurar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.