Ders 07 / 25
Önbellek Ölçütleri
İsabet oranının hesaplanması ve ortalama yanıt süresine etkisi: kapasite arttıkça isabet oranının çizdiği eğri, tahliye sayısının anlamı, tek bir toplam oranın gizlediği ad alanı ayrışımı, bayt isabet oranı ile istek isabet oranının farkı ve düşük tekrarlı verinin önbellekten çıkarılmasının ölçülen kazancı.
İçindekiler
Konu boyunca her ders bir sayı üretti: kaynağa giden sorgu, bayat okuma, isabet ve ıska, indirilen bayt. Bu sayılar tek tek anlamlıydı ama hiçbiri “önbellek işe yarıyor mu” sorusunu tek başına yanıtlamaz. Anahtar Tasarımı dersinde en yüksek isabet oranı yanlış gerçekleştirime aitti; bu ders, doğru gerçekleştirimlerde bile tek bir oranın neyi gizlediğini gösterir.
Ölçütün tanımı basittir. İsabet oranı, isteklerin önbellekten karşılanan payıdır:
Ortalama yanıt süresi ise iki bileşenden oluşur: her istek önbelleğe bakar, ıska alanlar ayrıca kaynağa gider. Önbellek erişimi , kaynak erişimi ise:
Bu biçim, isabet oranındaki iyileşmenin neden orantısız kazanç verdiğini açıklar. Oran %90’dan %95’e çıktığında iyileşme beş puandır, ama çarpanı 0,10’dan 0,05’e indiği için kaynağa binen yük yarıya iner.
Ölçüm Düzeneği
# kur.sh — olcum icin kitap ve odunc verisi rm -f kutuphane.db sqlite3 kutuphane.db <<'SQL' CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL, basim_yili INTEGER, sube_id INTEGER NOT NULL); CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL, iade_tarihi TEXT); INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 40 + 1), 1950 + n % 70, n % 4 + 1 FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 300) SELECT n FROM s); INSERT INTO odunc SELECT n, n % 300 + 1, n % 200 + 1, CASE WHEN n % 5 = 0 THEN NULL ELSE '2025-12-31' END FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 4000) SELECT n FROM s); CREATE INDEX odunc_uye ON odunc(uye_id); SQL sqlite3 kutuphane.db "SELECT 'kitap=' || (SELECT count(*) FROM kitap) || ' odunc=' || (SELECT count(*) FROM odunc);"
kitap=300 odunc=4000
Ölçüm için konu boyunca parça parça kurulan davranışları tek yerde toplayan bir önbellek gerekir: yaşam süresi, kapasite dolduğunda en son kullanılana göre tahliye ve sayaçlar.
// onbellek.mjs — sure sonu, en son kullanilana gore tahliye ve sayaclarla onbellek export function onbellekAc({ kapasite = 128, omur = Infinity, saat = () => Date.now() } = {}) { const kutu = new Map(); const sayac = { isabet: 0, iska: 0, tahliye: 0, sureSonu: 0, isabetBayt: 0, iskaBayt: 0 }; const boyutu = (d) => Buffer.byteLength(JSON.stringify(d)); return { sayac, al(anahtar) { const girdi = kutu.get(anahtar); if (girdi === undefined) { sayac.iska += 1; return undefined; } if (girdi.sonGecerlilik <= saat()) { kutu.delete(anahtar); sayac.sureSonu += 1; sayac.iska += 1; return undefined; } kutu.delete(anahtar); kutu.set(anahtar, girdi); // en son kullanilan sona tasinir sayac.isabet += 1; sayac.isabetBayt += girdi.boyut; return girdi.deger; }, koy(anahtar, deger) { const boyut = boyutu(deger); sayac.iskaBayt += boyut; kutu.delete(anahtar); kutu.set(anahtar, { deger, boyut, sonGecerlilik: saat() + omur }); while (kutu.size > kapasite) { kutu.delete(kutu.keys().next().value); sayac.tahliye += 1; } }, get girdiSayisi() { return kutu.size; }, isabetOrani: () => sayac.isabet / (sayac.isabet + sayac.iska), baytIsabetOrani: () => sayac.isabetBayt / (sayac.isabetBayt + sayac.iskaBayt), }; }
Tahliye sırası Map’in ekleme sırasından gelir: okunan girdi silinip yeniden konarak sona
taşınır, kapasite aşıldığında baştaki — yani en uzun süredir kullanılmayan — girdi düşer.
Kapasite ile İsabet Oranı
İlk ölçüm tek bir düğmeyi çevirir: önbellek kapasitesi. İş yükü 2000 istektir ve üç işlem türünü kapsar; farklı anahtar sayıları farklı tekrar oranları üretir.
// kapasite.mjs — kapasite arttikca isabet orani ve kaynaga giden sorgu nasil degisiyor import { DatabaseSync } from "node:sqlite"; import { onbellekAc } from "./onbellek.mjs"; const db = new DatabaseSync("kutuphane.db"); const sorgu = { sayi: 0 }; const kaynak = { kitap: (id) => { sorgu.sayi += 1; return db.prepare( "SELECT kitap_id, baslik, yazar, basim_yili FROM kitap WHERE kitap_id = ?").get(id); }, stok: (id) => { sorgu.sayi += 1; return db.prepare( `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k WHERE k.sube_id = ? AND NOT EXISTS (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`).get(id, id); }, liste: (id) => { sorgu.sayi += 1; return db.prepare( "SELECT kitap_id, baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id").all(id); }, }; function isYuku(adet) { // kitap istekleri 60 baslikta yogunlasir let tohum = 20250729; const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648); return Array.from({ length: adet }, () => { const r = rast(); if (r < 0.75) return ["kitap", Math.floor(rast() * 60) + 1]; if (r < 0.9) return ["stok", Math.floor(rast() * 4) + 1]; return ["liste", Math.floor(rast() * 4) + 1]; }); } const istekler = isYuku(2000); console.log(["kapasite", "isabet", "iska", "isabet orani", "tahliye", "kaynak sorgusu", "sure~"] .map((b, i) => (i === 0 ? b.padEnd(10) : b.padStart([0, 8, 8, 14, 9, 16, 9][i]))).join("")); for (const kapasite of [4, 8, 16, 32, 64, 128]) { sorgu.sayi = 0; const onbellek = onbellekAc({ kapasite }); const t = performance.now(); for (const [tur, id] of istekler) { const anahtar = `kutuphane:b1:n1:k${tur === "kitap" ? "genel" : `sube-${id}`}:${tur}:${id}`; let deger = onbellek.al(anahtar); if (deger === undefined) { deger = kaynak[tur](id); onbellek.koy(anahtar, deger); } } const sure = Math.round((performance.now() - t) / 10) * 10; const s = onbellek.sayac; console.log(String(kapasite).padEnd(10) + [String(s.isabet).padStart(8), String(s.iska).padStart(8), (`%` + (onbellek.isabetOrani() * 100).toFixed(1)).padStart(14), String(s.tahliye).padStart(9), String(sorgu.sayi).padStart(16), (sure + " ms").padStart(9)].join("")); } db.close();
kapasite isabet iska isabet orani tahliye kaynak sorgusu sure~ 4 133 1867 %6.7 1863 1867 910 ms 8 289 1711 %14.4 1703 1711 760 ms 16 588 1412 %29.4 1396 1412 460 ms 32 1053 947 %52.6 915 947 200 ms 64 1851 149 %92.5 85 149 10 ms 128 1932 68 %96.6 0 68 10 ms
Süre sütunu on milisaniyeye yuvarlanmıştır ve makineye bağlıdır; ilk beş sütun değildir.
Eğrinin biçimi anlatıcıdır. Kapasite 32’den 64’e çıktığında isabet oranı %52,6’dan %92,5’e sıçrıyor, 64’ten 128’e çıktığında yalnız dört puan artıyor. Sıçramanın yeri iş yükünün çalışma kümesini verir: 60 kitap, 4 stok ve 4 liste anahtarı, toplam 68 farklı anahtar. Kapasite bu sayının altındayken her yeni anahtar bir başkasını atar ve tahliye sayısı ıskaların neredeyse tamamına eşit olur — kapasite 4’te 1867 ıskaya karşılık 1863 tahliye. Kapasite çalışma kümesini geçtiğinde tahliye sıfırlanır ve kalan 68 ıska yalnız ilk yüklemelerdir.
Bu, kapasite kararının doğru sorusunu verir: “ne kadar bellek ayıralım” değil, “çalışma kümesi kaç anahtar”. Tahliye sayacı bu soruyu doğrudan yanıtlar. Tahliye ıska sayısına yakınsa önbellek kendi girdilerini birbirine yedirmektedir; sıfıra yakınsa kapasite yeterlidir.
Süre sütunu formülün doğruladığı şeyi gösteriyor. Kapasite 4’te isabet oranı %6,7 ve iş yükü 910 milisaniye sürdü; %92,5’te 10 milisaniyeye indi. Aradaki fark, ıska cezasının bu iş yükünde büyük olmasından gelir — stok sorgusu diğerlerinden pahalıdır.
Toplam Oranın Gizlediği
Yukarıdaki tabloda tek bir oran vardı çünkü iş yükünün her parçası önbelleğe uygundu. Gerçek uygulamalarda böyle olmaz. Aşağıdaki iş yüküne, iki yüz farklı üyenin ödünç geçmişi eklenmiştir: çok anahtarlı, az tekrarlı bir okuma.
// ayrisik.mjs — tek bir isabet orani neyi gizler: ad alanina ve bayta gore ayrisim import { DatabaseSync } from "node:sqlite"; import { onbellekAc } from "./onbellek.mjs"; const db = new DatabaseSync("kutuphane.db"); const kaynak = { kitap: (id) => db.prepare("SELECT kitap_id, baslik, yazar FROM kitap WHERE kitap_id = ?").get(id), stok: (id) => db.prepare( `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k WHERE k.sube_id = ? AND NOT EXISTS (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`).get(id, id), liste: (id) => db.prepare("SELECT kitap_id, baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id").all(id), gecmis: (id) => db.prepare( "SELECT odunc_id, kitap_id FROM odunc WHERE uye_id = ? ORDER BY odunc_id").all(id), }; let tohum = 20250729; const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648); const istekler = Array.from({ length: 2000 }, () => { const r = rast(); if (r < 0.70) return ["kitap", Math.floor(rast() * 60) + 1]; if (r < 0.80) return ["stok", Math.floor(rast() * 4) + 1]; if (r < 0.85) return ["liste", Math.floor(rast() * 4) + 1]; return ["gecmis", Math.floor(rast() * 200) + 1]; // 200 uye, tekrar orani dusuk }); function kosu(onbelleklenir) { const onbellek = onbellekAc({ kapasite: 64 }); const tur = { kitap: [0, 0], stok: [0, 0], liste: [0, 0], gecmis: [0, 0] }; // [isabet, iska] let kaynakSorgusu = 0; for (const [t, id] of istekler) { if (!onbelleklenir.has(t)) { kaynak[t](id); kaynakSorgusu += 1; tur[t][1] += 1; continue; } const anahtar = `kutuphane:b1:n1:k${t === "kitap" ? "genel" : `sube-${(id % 4) + 1}`}:${t}:${id}`; const oncekiIsabet = onbellek.sayac.isabet; let deger = onbellek.al(anahtar); if (deger === undefined) { deger = kaynak[t](id); kaynakSorgusu += 1; onbellek.koy(anahtar, deger); } tur[t][onbellek.sayac.isabet > oncekiIsabet ? 0 : 1] += 1; } return { onbellek, tur, kaynakSorgusu }; } const { onbellek, tur, kaynakSorgusu } = kosu(new Set(["kitap", "stok", "liste", "gecmis"])); console.log(["ad alani", "istek", "isabet", "iska", "isabet orani"] .map((b, i) => (i === 0 ? b.padEnd(10) : b.padStart([0, 8, 8, 8, 14][i]))).join("")); for (const [ad, [i, k]] of Object.entries(tur)) { console.log(ad.padEnd(10) + [String(i + k).padStart(8), String(i).padStart(8), String(k).padStart(8), ("%" + ((i / (i + k)) * 100).toFixed(1)).padStart(14)].join("")); } const s = onbellek.sayac; console.log(`\ntoplam istek isabet orani : %${(onbellek.isabetOrani() * 100).toFixed(1)}`); console.log(`bayt isabet orani : %${(onbellek.baytIsabetOrani() * 100).toFixed(1)}`); console.log(`isabetten donen bayt : ${s.isabetBayt} kaynaktan gelen bayt: ${s.iskaBayt}`); console.log(`kaynak sorgusu : ${kaynakSorgusu}`); const ikinci = kosu(new Set(["kitap", "stok", "liste"])); // gecmis hic onbelleklenmiyor console.log(`\ngecmis onbelleklenmediginde:`); console.log(` kitap isabet orani : %${((ikinci.tur.kitap[0] / (ikinci.tur.kitap[0] + ikinci.tur.kitap[1])) * 100).toFixed(1)}`); console.log(` tahliye : ${ikinci.onbellek.sayac.tahliye} (onceki: ${s.tahliye})`); console.log(` kaynak sorgusu : ${ikinci.kaynakSorgusu}`); db.close();
ad alani istek isabet iska isabet orani kitap 1380 965 415 %69.9 stok 202 189 13 %93.6 liste 110 84 26 %76.4 gecmis 308 19 289 %6.2 toplam istek isabet orani : %62.8 bayt isabet orani : %51.8 isabetten donen bayt : 303731 kaynaktan gelen bayt: 282500 kaynak sorgusu : 743 gecmis onbelleklenmediginde: kitap isabet orani : %90.7 tahliye : 77 (onceki: 679) kaynak sorgusu : 449
Toplam oran %62,8’dir ve tek başına raporlandığında “orta düzeyde bir önbellek” izlenimi verir. Ayrışım başka bir şey söylüyor: ödünç geçmişi ad alanının isabet oranı %6,2. İki yüz farklı anahtar ve düşük tekrar, bu veriyi önbelleğe uygunsuz kılıyor. Dahası bu ad alanı zararlıdır — koyduğu her girdi kapasiteden yer alır ve tekrar okunmadan tahliye edilir. Kitap ad alanının isabet oranını %69,9’a düşüren şey budur.
Son bölüm bunu kanıtlıyor. Ödünç geçmişi hiç önbelleklenmediğinde kitap ad alanının isabet oranı %69,9’dan %90,7’ye çıktı, tahliye sayısı 679’dan 77’ye indi ve kaynağa giden toplam sorgu 743’ten 449’a düştü. Bir ad alanının önbellekten çıkarılması kaynak yükünü düşürdü. Bu sonuç yalnız ayrışık ölçümle bulunabilirdi; toplam oran her iki koşuda da benzer görünürdü.
Bundan çıkan işletme kuralı şudur: isabet oranı ad alanı başına ölçülür. Anahtar Tasarımı dersinde anahtara konan ad alanı bölümünün ikinci işlevi budur — ölçütü ayrıştırılabilir kılmak.
Bayt İsabet Oranı
Çıktının ikinci satır çifti ayrı bir ayrımı gösteriyor. İstek isabet oranı %62,8, bayt isabet oranı %51,8. Fark, değerlerin boyutlarının eşit olmamasından gelir: liste ve geçmiş yanıtları kitap kaydından çok daha büyüktür.
İki oran farklı soruları yanıtlar. İstek isabet oranı kaynağa kaç çağrı gitmediğini söyler; gidiş-dönüş ve sorgu maliyetini bu sayı belirler. Bayt isabet oranı ağdan geçmeyen veri miktarını söyler; bant genişliği ve aktarım süresini bu sayı belirler. Frontend Kalitesi kursundaki Statik Barındırma dersinde kenar önbelleği için tanımlanan bu iki ölçüt, sunucu önbelleğinde de aynı ayrımı taşır.
Hangisinin izleneceği darboğaza bağlıdır. Kaynak sorgu sayısıyla zorlanıyorsa istek isabet oranı, ağ ile zorlanıyorsa bayt isabet oranı belirleyicidir.
Özet
- İsabet oranı isteklerin önbellekten karşılanan payıdır; ortalama süre biçiminde davranır, bu yüzden %90’dan %95’e çıkmak kaynak yükünü yarıya indirir.
- Kapasite çalışma kümesinin altındayken tahliye sayısı ıska sayısına yaklaşır; 68 anahtarlık kümede kapasite 32’den 64’e çıkınca isabet oranı %52,6’dan %92,5’e sıçradı.
- Tahliye sayacı kapasite kararının doğrudan ölçütüdür: ıskaya yakınsa önbellek kendi girdilerini yiyordur, sıfıra yakınsa kapasite yeterlidir.
- Tek bir toplam oran ad alanı farklarını gizler: %62,8’lik toplamın içinde %6,2’lik bir ad alanı vardı ve o ad alanı önbellekten çıkarıldığında kaynak sorgusu 743’ten 449’a indi.
- İstek isabet oranı kaynağa giden çağrı sayısını, bayt isabet oranı ağdan geçmeyen veri miktarını ölçer; aynı koşuda %62,8 ve %51,8 çıktılar.
Sonraki Adım
Bu konu boyunca yapılan iş tek bir cümleyle özetlenebilir: aynı yanıtın yeniden üretilmesi önlendi. Kapasite ölçümündeki 1867 kaynak sorgusu 68’e indi; okuma yolu gerçekten kısaldı.
Kısalmayan bir yol kaldı. Önbellek yalnız okumaya yarar; yazma her zaman kaynağa gider ve isteğin süresine eklenir. Yazmanın yanına iliştirilmiş işler de öyledir: ödünç verildiğinde gönderilen bildirim, gün sonunda üretilen gecikme raporu, yüklenen kapak görselinin işlenmesi. Bunların hiçbiri kullanıcının yanıtı için gerekli değildir, ama hepsi yanıtın gelmesini geciktirir. İstek yolunu kısaltmanın ikinci yolu işi hızlandırmak değil, işi istekten çıkarmaktır. Sonraki ders bu yolu açar: bir işin eşzamansız yürütülmesinin ne zaman doğru olduğunu, isteğin süresinden neyin düştüğünü ve karşılığında hangi yeni sorumlulukların doğduğunu ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.