Ders 02 / 25
Önbellek Stratejileri
Yanında okuma, içinden yazma ve arkadan yazma stratejilerinin aynı iş yükü üzerinde karşılaştırılması: kaynağa giden okuma ve yazma sayıları, isabet oranı, arkadan yazmanın dayanıklılık ödünü ve yanında okumanın ıska ile yazma arasındaki yarışı.
İçindekiler
Önceki ders önbelleği okuma yolunun içine elle yerleştirdi: bak, yoksa kaynaktan getir, koy. Bu yazımın bir adı vardır ve tek seçenek değildir. Önbellek okuma yolunun yanında durabilir ya da bütün okumaların üzerinden geçtiği bir katman olabilir. Yazma yolunda ise ayrı bir soru vardır: kaynak güncellendiğinde önbellekteki kopyaya ne olur — aynı anda mı güncellenir, silinir mi, yoksa yazma önce önbelleğe mi gider?
Bu ders üç stratejiyi aynı iş yükü üzerinde koşturur: yanında okuma (cache-aside), içinden yazma (write-through) ve arkadan yazma (write-behind). Ölçülen şey kaynağa giden okuma ve yazma sayıları ile kaynağın gerçekten geride kaldığı kayıt sayısıdır.
Ölçüm Düzeneği
Ölçüm, raftaki kopya sayısı üzerinden yapılır: ödünç verme bu sayıyı düşürür, iade artırır.
Bu, kütüphane verisinin en sık yazılan alanıdır ve yazma stratejilerini ayırt etmeye
elverişlidir. Önceki dersteki kitap tablosuna bu sayıyı tutan bir sütun eklenir.
# kur.sh — dersin veritabani: kitap tablosuna raftaki kopya sayisi eklenir rm -f kutuphane.db sqlite3 kutuphane.db <<'SQL' CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL); CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL, sube_id INTEGER REFERENCES sube(sube_id), rafta INTEGER NOT NULL); INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),(3,'Kadıköy','İstanbul'); INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 10 + 1), n % 3 + 1, 3 FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 60) SELECT n FROM s); SQL sqlite3 kutuphane.db "SELECT 'kitap=' || count(*) || ' toplam_rafta=' || sum(rafta) FROM kitap;"
kitap=60 toplam_rafta=180
İş yükü 1200 işlemdir: %85 okuma, %15 yazma, on iki popüler kitap üzerinde yoğunlaşmış. Her strateji kendi veritabanı kopyasında koşar, böylece karşılaştırma aynı başlangıç durumundan yapılır.
Üç Strateji
Üç kurulum aynı arayüzü verir: oku ve yaz. Aralarındaki fark yalnız bu iki işlevin
gövdesindedir.
// stratejiler.mjs — ayni is yuku uc stratejiyle: kaynak erisimi ve tutarsizlik olculur import { DatabaseSync } from "node:sqlite"; import { copyFileSync, rmSync } from "node:fs"; // Ayni is yuku: %85 okuma, %15 yazma; istekler ilk 12 kitapta yogunlasir. function isYuku(adet) { let tohum = 20250729; const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648); return Array.from({ length: adet }, () => { const yaz = rast() < 0.15; return [yaz ? "yaz" : "oku", Math.floor(rast() * 12) + 1, yaz ? (rast() < 0.5 ? -1 : 1) : 0]; }); } function kaynakAc(dosya) { rmSync(dosya, { force: true }); copyFileSync("kutuphane.db", dosya); const db = new DatabaseSync(dosya); const s = { okuma: 0, yazma: 0 }; return { s, oku: (id) => { s.okuma += 1; return db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta; }, yaz: (id, d) => { s.yazma += 1; db.prepare("UPDATE kitap SET rafta = ? WHERE kitap_id = ?").run(d, id); }, denetim: (id) => db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta, kapat: () => db.close(), }; } function kosu(ad, kur) { const kaynak = kaynakAc(`${ad}.db`); const onbellek = new Map(); const gercek = new Map(); // is yukunun mantiksal dogrusu const o = { isabet: 0, iska: 0, bayat: 0 }; const { oku, yaz, bitir } = kur(kaynak, onbellek, o); for (const [tur, id, delta] of isYuku(1200)) { if (tur === "oku") { const d = oku(id); if (!gercek.has(id)) gercek.set(id, kaynak.denetim(id)); if (d !== gercek.get(id)) o.bayat += 1; } else { if (!gercek.has(id)) gercek.set(id, kaynak.denetim(id)); const yeni = Math.max(0, gercek.get(id) + delta); gercek.set(id, yeni); yaz(id, yeni); } } const bekleyen = bitir ? bitir() : 0; const sapan = [...gercek.keys()].filter((id) => kaynak.denetim(id) !== gercek.get(id)).length; kaynak.kapat(); rmSync(`${ad}.db`, { force: true }); return { ad, ...kaynak.s, ...o, bekleyen, sapan }; } const yanindaOkuma = (k, c, o) => ({ oku(id) { if (c.has(id)) { o.isabet += 1; return c.get(id); } o.iska += 1; const d = k.oku(id); c.set(id, d); return d; }, yaz(id, d) { k.yaz(id, d); c.delete(id); }, // yazma sonrasi girdi silinir }); const icindenYazma = (k, c, o) => ({ oku(id) { if (c.has(id)) { o.isabet += 1; return c.get(id); } o.iska += 1; const d = k.oku(id); c.set(id, d); return d; }, yaz(id, d) { k.yaz(id, d); c.set(id, d); }, // kaynak ve onbellek birlikte guncellenir }); const arkadanYazma = (k, c, o) => { const bekleyen = new Map(); let sayim = 0; return { oku(id) { if (c.has(id)) { o.isabet += 1; return c.get(id); } o.iska += 1; const d = k.oku(id); c.set(id, d); return d; }, yaz(id, d) { // yazma once onbellege, kaynak sonra c.set(id, d); bekleyen.set(id, d); if ((sayim += 1) % 20 === 0) { for (const [i, v] of bekleyen) k.yaz(i, v); bekleyen.clear(); } }, bitir: () => bekleyen.size, // surec burada dursaydi kaybolacak yazma sayisi }; }; const basliklar = ["strateji", "kay.okuma", "kay.yazma", "isabet", "iska", "bayat", "bekleyen", "sapan"]; console.log(basliklar.map((b, i) => (i === 0 ? b.padEnd(16) : b.padStart(10))).join("")); for (const [ad, kur] of [["yaninda okuma", yanindaOkuma], ["icinden yazma", icindenYazma], ["arkadan yazma", arkadanYazma]]) { const r = kosu(ad.replace(/ /g, "-"), kur); console.log(ad.padEnd(16) + [r.okuma, r.yazma, r.isabet, r.iska, r.bayat, r.bekleyen, r.sapan] .map((n) => String(n).padStart(10)).join("")); }
strateji kay.okuma kay.yazma isabet iska bayat bekleyen sapan yaninda okuma 156 175 869 156 0 0 0 icinden yazma 7 175 1018 7 0 0 0 arkadan yazma 7 74 1018 7 0 10 5
Sayıların Söyledikleri
Yanında okuma 156 kaynak okuması yaptı. On iki farklı anahtar var; kalan 144 ıska, her yazmadan sonra girdinin silinmesinden doğdu. Silinen her girdi bir sonraki okumada yeniden yüklenir. Bu, stratejinin bedelidir: yazma sıklığı arttıkça ıska sayısı da artar.
İçinden yazma 7 kaynak okuması yaptı — yalnız ilk kez görülen her anahtar için bir tane. Yazma önbellekteki girdiyi silmek yerine yeni değerle güncellediği için girdi hiç düşmedi. İsabet sayısı 869’dan 1018’e çıktı. Karşılığında önbellek, hiç okunmayacak değerleri de taşımaya başlar: nadiren okunan bir kitabın raf sayısı her yazmada önbelleğe girer ve yer kaplar.
Arkadan yazma kaynak yazma sayısını 175’ten 74’e düşürdü. Yazmalar önce önbelleğe gitti, her yirmi yazmada bir toplu olarak kaynağa aktarıldı ve aynı kitaba yapılan ardışık yazmalar tek bir güncellemede birleşti. Bu, kaynak üzerindeki yazma yükünü yarıdan aşağı indiren tek stratejidir.
Son iki sütun bu kazancın bedelini gösteriyor. İş yükü bittiğinde 10 yazma hâlâ önbellekteydi ve kaynaktaki 5 kayıt gerçek değerden sapmış durumdaydı. Süreç o anda dursaydı bu yazmalar kaybolurdu — önbellek belleğe dayanır, kaynak diske. Arkadan yazma, yazma yükünü düşürmek için dayanıklılığı öder. Kütüphane örneğinde raf sayısının bir kopyası kaybolabilir; aynı strateji ödünç kaydının kendisi için kullanılamaz.
bayat sütununun her satırda sıfır olması dikkat çekicidir. Üç strateji de okuyucuya yanlış
değer vermedi, çünkü hepsi yazmayı önbelleğe yansıttı. Tutarsızlık okuyucuda değil, kaynak
ile önbellek arasında oluştu ve sapan sütununda göründü.
Yanında Okumanın Yarışı
Yukarıdaki koşuda işlemler sırayla çalıştı. Gerçek bir sunucuda iki istek aynı anda ilerler ve yanında okumanın bilinen bir açığı ortaya çıkar: bir ıska yüklemesi ile bir yazma çakışabilir. Aşağıdaki blok bu sıralamayı adım adım kurar.
// yaninda-yarisma.mjs — yaninda okumanin klasik yarisi: iska yuklemesi ile yazma cakisir import { DatabaseSync } from "node:sqlite"; import { copyFileSync, rmSync } from "node:fs"; rmSync("yaris.db", { force: true }); copyFileSync("kutuphane.db", "yaris.db"); const db = new DatabaseSync("yaris.db"); const kaynakOku = (id) => db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta; const kaynakYaz = (id, d) => db.prepare("UPDATE kitap SET rafta = ? WHERE kitap_id = ?").run(d, id); const onbellek = new Map(); const yaz = (adim, ne) => console.log(`${String(adim).padStart(2)}. ${ne.padEnd(46)} ` + `onbellek=${onbellek.has(7) ? onbellek.get(7) : "-"} kaynak=${kaynakOku(7)}`); // A istegi: okuma yapiyor, iska aldi ve kaynagi okudu; henuz onbellege koymadi. const aOkudu = kaynakOku(7); yaz(1, "A: iska, kaynaktan okudu (deger elinde)"); // B istegi arada tamamlaniyor: bir kopya odunc verildi. kaynakYaz(7, aOkudu - 1); onbellek.delete(7); yaz(2, "B: kaynagi guncelledi, onbellek girdisini sildi"); // A istegi simdi elindeki eski degeri onbellege koyuyor. onbellek.set(7, aOkudu); yaz(3, "A: elindeki eski degeri onbellege koydu"); let bayat = 0; for (let i = 0; i < 50; i++) { const d = onbellek.has(7) ? onbellek.get(7) : kaynakOku(7); if (d !== kaynakOku(7)) bayat += 1; } yaz(4, `sonraki 50 okumadan bayat olan: ${bayat}`); db.close(); rmSync("yaris.db", { force: true });
1. A: iska, kaynaktan okudu (deger elinde) onbellek=- kaynak=3 2. B: kaynagi guncelledi, onbellek girdisini sildi onbellek=- kaynak=2 3. A: elindeki eski degeri onbellege koydu onbellek=3 kaynak=2 4. sonraki 50 okumadan bayat olan: 50 onbellek=3 kaynak=2
Yazma kuralına harfiyen uyuldu — kaynak güncellendi, girdi silindi — ve önbellek yine de bayat kaldı. Kusur sıradadır: silme, konmamış bir girdiyi silemez. Bayatlık kendiliğinden düzelmez; girdi bir daha yazılana ya da süresi dolana kadar yanlış değer sunulur.
Bu, yanında okumanın hiçbir zaman tek başına yeterli olmadığının kanıtıdır. Yanına en az bir zaman sınırı konur; sıralamayı kesin olarak çözmek gerektiğinde girdiye bir sürüm damgası eklenir ya da aynı anahtarın yüklenmesi tek bir çağrıya kısılır. Üçü de sonraki derslerin konusudur.
Seçim Ölçütü
Üç strateji birbirinin yerine geçen seçenekler değildir; farklı okuma–yazma oranlarına uyarlar.
Okuma ağırlıklı ve seyrek değişen veride yanında okuma yeterlidir; kodun yalnız okuma yolunu bilmesi gerekir ve önbellek düştüğünde uygulama çalışmayı sürdürür. Aynı anahtarın hem sık okunduğu hem sık yazıldığı yerde içinden yazma ıskaları önler, karşılığında önbelleği okunmayan değerlerle doldurur. Yazma hızının kaynağın dayanabileceğinin üstüne çıktığı ve tek tek yazmaların kaybının katlanılabilir olduğu yerde arkadan yazma yükü düşürür; kayıp katlanılamazsa bu strateji elenir.
Yazmanın önbelleğe hiç uğramadığı bir dördüncü seçenek de vardır: yanından yazma (write-around). Kaynak güncellenir, önbellek girdisi ne silinir ne yazılır; girdi süresi dolana kadar bayat kalır. Bir kerelik yazılıp uzun süre okunmayan verilerde önbelleği kirletmemek için kullanılır, ama bayatlık penceresi açıkça kabul edilmiş olur.
Özet
- Yanında okuma yazmada girdiyi siler; 1200 işlemlik iş yükünde 156 kaynak okuması üretti, bunun 144’ü silinen girdilerin yeniden yüklenmesidir.
- İçinden yazma girdiyi silmek yerine güncellediği için kaynak okumasını 7’ye indirdi ve isabeti 1018’e çıkardı; karşılığında okunmayan değerleri de önbellekte tutar.
- Arkadan yazma kaynak yazmasını 175’ten 74’e düşürdü, çünkü aynı anahtara ardışık yazmalar toplu aktarımda birleşti; bedeli, süreç durduğunda kaybolacak 10 bekleyen yazmadır.
- Üç stratejide de okuyucu bayat değer görmedi; tutarsızlık kaynak ile önbellek arasında oluştu. Dayanıklılık gerektiren veride arkadan yazma kullanılamaz.
- Yanında okumada ıska yüklemesi ile yazma çakıştığında silme işe yaramaz ve bayat girdi kendiliğinden düzelmez; strateji tek başına yeterli değildir.
Sonraki Adım
Son ölçüm bir soruyu açıkta bıraktı: önbellekteki bayat girdi ne zaman gider? Bu dersteki stratejilerin hepsi yazma anını yakaladıklarını varsaydı. Yazma başka bir süreçten geldiğinde, veri doğrudan veritabanında değiştirildiğinde ya da yukarıdaki gibi bir sıralama kusuru oluştuğunda o varsayım düşer. Girdinin ne kadar yaşayacağı, hangi olayın onu düşüreceği ve anahtarın kendisinin değişmesiyle eski girdinin nasıl erişilemez kılınacağı ayrı bir karar kümesidir. Sonraki ders süre, olay ve sürüm tabanlı geçersizleştirmeyi aynı iş yükünde karşılaştırır ve her birinin ürettiği bayat okuma sayısını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.