İçeriğe geç
academia.sh

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:

h=isabetisabet+ıskah = \frac{\text{isabet}}{\text{isabet} + \text{ıska}}

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 tot_o, kaynak erişimi tkt_k ise:

T=to+(1h)tkT = t_o + (1 - h)\,t_k

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 1h1-h ç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 T=to+(1h)tkT = t_o + (1-h)t_k 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat