Ders 09 / 18
Önbellek Yerleşimi
Kopyanın hangi katmanda duracağının bir tasarım kararı olarak ele alınması: istemci, kenar, web, uygulama ve veritabanı katmanlarının kapsam ve kapasiteyle ayrılması, dört katmanlı bir zincirin katman başına isabetinin ve ıskalama cezasının ölçülmesi, giriş kursundaki 0,90 önbellek isabeti varsayımının tasarımın sonucu hâline getirilmesi ve tek bir doğru boyutlu önbelleğin aynı oranı daha az dokunuşla verdiğinin gösterilmesi.
İçindekiler
Önceki konu verinin nerede ve hangi biçimde durduğunu tasarladı: depo türü erişim örüntüsüne göre seçildi, veri çoğaltıldı, parçalandı ve okuma yolu denormalizasyonla kısaltıldı. Bütün bunların sonunda okuma yolu gerçekten kısaldı, ama bir şey değişmedi: her okuma hâlâ bir depoya gidiyor. Sıradaki soru okumanın deponun önünde karşılanmasıdır ve ilki şudur: kopya hangi katmanda durmalı.
Bu ders o soruyu bir yerleşim kararına çevirir. Önbellekleme, Kuyruklar ve Eşzamansız İşleme
kursu önbellek katmanlarını, stratejileri ve geçersizleştirmeyi kurdu ve ölçtü; burada hiçbiri
yeniden anlatılmaz. Değişen şey ölçeğin kendisidir. Sistem Tasarımına Giriş kursunun kabaca
büyüklük hesabında önbellek bir strateji değil tek bir sayıydı: V9, takip sorgusunun
önbellekten karşılanma oranı, 0,90 alınmıştı ve onbellek arkasi okuma/s satırını 41,67’ye,
depoya ulasan istek/s satırını 138,89’a indiriyordu. O sayı bir varsayımdı. Bu dersin işi
onu bir sonuç hâline getirmektir: hangi yerleşim 0,90’ı üretir, üretirken ne öder.
Katmanı Ne Ayırır
Bir önbellek katmanını üç şey tanımlar.
Kapsam, aynı kopyanın kaç isteği karşılayabileceğidir. İstemci önbelleği tek bir kullanıcıya hizmet eder; kenar önbelleği bir bölgedeki bütün kullanıcılara; web katmanındaki paylaşılan yanıt önbelleği bütün isteklere; uygulama içindeki süreç içi önbellek yalnız kendi örneğine düşen isteklere. Kapsam büyüdükçe bir kopyanın ısıtılması daha çok isteğe yarar.
Kapasite, katmanın kaç girdi tutabildiğidir. Kapasite çalışma kümesinin altındaysa katman kendi girdilerini tahliye eder ve artığı bir sonraki katmana sızdırır.
Geri alınabilirlik, konan bir kopyanın kaldırılıp kaldırılamayacağıdır ve kapsamla ters gider. Uygulama içindeki girdi kodla anında silinir. Kenar katmanındaki için ayrı bir işlem gerekir. İstemcideki kopya için hiçbir yol yoktur: konarken verilen süre dolana kadar orada kalır. Bu üçüncü ölçüt bu dersin sayılarında görünmez ama son bölümde kararı belirler.
Beşinci bir katman daha vardır ve ötekilerden farklıdır: veritabanı önbelleği, kaynağın kendi içindedir. Bir sorgunun maliyetini düşürür, sorgu sayısını düşürmez; hesabın hiçbir satırı ona bağlı değildir. Bu ayrım Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunda ölçülerek kuruldu ve burada tekrarlanmaz. Aşağıdaki modelde bu yüzden dört katman vardır.
Zincir Modeli
Model süreç içidir: katmanlar birer nesnedir, sayaçlar açıktır, kapasite ile erişim örüntüsü parametredir. Gerçek bir önbellek ürünü ya da bulut ortamı kurulmaz; ölçülen şey süre değil, sayımdır ve bu yüzden koşumdan koşuma değişmez.
Erişim örüntüsü kursun takip sorgusudur: takip numarasına göre tekil okuma. Bir gönderinin sorguları bir arada gelir — kullanıcı sayfayı açar, yeniler, bir süre sonra tekrar bakar. Bu öbeklenme bu dersin varsayımıdır ve K01’in tablosuna eklenmez.
B1 (bu dersin varsayımı): bir gönderinin sorguları 10 uzunluğunda öbekler hâlinde gelir ve aynı anda 1000 gönderi sorgulanmaktadır. Gerekçe: Trafik Katmanı kursunun İçerik Dağıtım Ağları dersi aynı öbek uzunluğunu kullanmış ve kenar isabetini 0,8998 ölçmüştü; buradaki ek, eşzamanlı gönderi sayısının bir çalışma kümesi tanımlaması ve kapasiteyi anlamlı kılmasıdır. Duyarlılık: öbek 5’e inerse tekil anahtar sayısı iki katına çıkar ve bütün katmanların isabeti düşer; çalışma kümesi 2000’e çıkarsa kapasitesi sabit kalan katmanlar artıklarını bir sonrakine sızdırır.
// onbellek/katman.mjs — katman zincirinin surec ici modeli. Katmanlar birer nesnedir, // sayaclar aciktir. Kapasite, obek uzunlugu ve calisma kumesi model parametreleridir. export function onbellek(kapasite) { const kutu = new Map(); const o = { tahliye: 0, al(a) { const d = kutu.get(a); if (d !== undefined) { kutu.delete(a); kutu.set(a, d); } return d; }, koy(a, d) { kutu.delete(a); kutu.set(a, d); if (kutu.size > kapasite) { kutu.delete(kutu.keys().next().value); o.tahliye += 1; } }, }; return o; } // Erisim oruntusu: takip numarasina gore tekil okuma. Bir gonderinin sorgulari `obek` // uzunlugunda gelir; ayni anda `calismaKumesi` kadar gonderi sorgulanir. Sorgularin // `sahipPayi` kadari gonderiyi baslatan kullanicidan, kalani baska bir kullanicidan gelir. export function akis({ istek, calismaKumesi, obek, kullanici, sahipPayi = 0.7 }) { let tohum = 20260730; const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648); const yeni = (no) => ({ no, kalan: obek, sahip: Math.floor(rast() * kullanici) + 1 }); let sonraki = 1; const etkin = Array.from({ length: calismaKumesi }, () => yeni(sonraki++)); const cikti = []; for (let i = 0; i < istek; i++) { const j = Math.floor(rast() * etkin.length); const g = etkin[j]; const k = rast() < sahipPayi ? g.sahip : Math.floor(rast() * kullanici) + 1; cikti.push([k, `takip:${g.no}`]); if ((g.kalan -= 1) === 0) etkin[j] = yeni(sonraki++); } return cikti; } // Zincir: her istek katmanlari sirayla yoklar. Iska her katmanda bir yoklama, depoda bir // erisim, geri donuste her katmana bir yazma dokunusu sayar. export function zincir(istekler, katmanlar) { const kutular = katmanlar.map(() => new Map()); const sayac = katmanlar.map(() => ({ isabet: 0, tahliye: 0 })); const s = { depo: 0, dokunus: 0, iska: 0 }; const kutuAl = (i, kullanici, anahtar) => { const kapsam = katmanlar[i].kapsam; const kimlik = kapsam === "kullanici" ? kullanici : kapsam === "ornek" ? (anahtar.length + Number(anahtar.slice(6))) % katmanlar[i].ornek : 0; let c = kutular[i].get(kimlik); if (c === undefined) { c = onbellek(katmanlar[i].kapasite); kutular[i].set(kimlik, c); } return c; }; for (const [kullanici, anahtar] of istekler) { let bulundu = -1; for (let i = 0; i < katmanlar.length; i++) { s.dokunus += 1; if (kutuAl(i, kullanici, anahtar).al(anahtar) !== undefined) { bulundu = i; break; } } if (bulundu === -1) { s.depo += 1; s.iska += 1; s.dokunus += 1; } else sayac[bulundu].isabet += 1; const son = bulundu === -1 ? katmanlar.length - 1 : bulundu - 1; for (let i = 0; i <= son; i++) { s.dokunus += 1; kutuAl(i, kullanici, anahtar).koy(anahtar, anahtar); } } for (let i = 0; i < katmanlar.length; i++) for (const c of kutular[i].values()) sayac[i].tahliye += c.tahliye; return { sayac, ...s }; } // M19/K01 kabaca buyukluk hesabinin girdileri (V1-V4, V8): tepe okuma ve tepe yazma. export const K01 = { tepeOkuma: (2_000_000 * 6 / 86_400) * 3, tepeYazma: (400_000 * 7 / 86_400) * 3, };
Pencere 480 saniyedir; K01’in tepe okuma hızı 416,67 istek/s olduğuna göre bu pencereye 200.000 takip sorgusu düşer. Dokunuş, modelin tek maliyet birimidir: bir katman yoklaması, bir depo erişimi ya da bir geri yazma. Dokunuşlar ağırlıklandırılmaz — bir depo erişimiyle bir bellek yoklaması aynı sayılır. Ağırlığı veren şey zaten yerleşim kararının kendisidir; sayım yalnız kaç iş yapıldığını söyler.
Ölçüm
// onbellek/yerlesim.mjs — dort katmanli zincirin olculmesi ve K01 hesabina donus import { akis, zincir, K01 } from "./katman.mjs"; const ISTEK = 200_000, OBEK = 10, CALISMA = 1000, KULLANICI = 40_000; const istekler = akis({ istek: ISTEK, calismaKumesi: CALISMA, obek: OBEK, kullanici: KULLANICI }); const KATMAN = [ { ad: "istemci", kapsam: "kullanici", kapasite: 4 }, { ad: "kenar", kapsam: "paylasilan", kapasite: 250 }, { ad: "web", kapsam: "paylasilan", kapasite: 600 }, { ad: "uygulama", kapsam: "ornek", kapasite: 400, ornek: 4 }, ]; const tekil = new Set(istekler.map(([, a]) => a)).size; const r = zincir(istekler, KATMAN); console.log(`istek ${ISTEK} (480 s pencere), tekil takip numarasi ${tekil}, obek ${OBEK}, es zamanli gonderi ${CALISMA}`); console.log(); console.log("katman kapsam kapasite isabet isabet payi tahliye"); console.log("---------- ----------- --------- -------- ------------ --------"); for (let i = 0; i < KATMAN.length; i++) { const k = KATMAN[i], c = r.sayac[i]; const kap = k.kapsam === "ornek" ? `${k.kapasite}x${k.ornek}` : String(k.kapasite); console.log(`${k.ad.padEnd(10)} ${k.kapsam.padEnd(11)} ${kap.padStart(9)} ` + `${String(c.isabet).padStart(8)} ${(c.isabet / ISTEK).toFixed(4).padStart(12)} ${String(c.tahliye).padStart(8)}`); } console.log(`${"depo".padEnd(10)} ${"-".padEnd(11)} ${"-".padStart(9)} ${String(r.depo).padStart(8)} ` + `${(r.depo / ISTEK).toFixed(4).padStart(12)} ${"-".padStart(8)}`); const h = 1 - r.depo / ISTEK; const arka = K01.tepeOkuma * (1 - h); console.log(); console.log(`birlesik isabet = ${h.toFixed(4)} (V9 varsayimi 0,90)`); console.log(`bir iskanin dokunusu = ${2 * KATMAN.length + 1} (${KATMAN.length} yoklama + 1 depo + ${KATMAN.length} geri yazma)`); console.log(`dokunus / istek = ${(r.dokunus / ISTEK).toFixed(3)}`); console.log(); console.log(`K01'e donus (tepe okuma ${K01.tepeOkuma.toFixed(2)}, tepe yazma ${K01.tepeYazma.toFixed(2)} istek/s)`); console.log(`aga hic cikmayan okuma/s = ${(K01.tepeOkuma * r.sayac[0].isabet / ISTEK).toFixed(2)}`); console.log(`uygulamaya varan okuma/s = ${(K01.tepeOkuma * (r.sayac[3].isabet + r.depo) / ISTEK).toFixed(2)}`); console.log(`onbellek arkasi okuma/s = ${arka.toFixed(2)} (V9 ile 41,67)`); console.log(`depoya ulasan istek/s = ${(arka + K01.tepeYazma).toFixed(2)} (V9 ile 138,89)`); console.log(`depoda yazma/okuma orani = ${(K01.tepeYazma / arka).toFixed(2)} (V9 ile 2,33)`);
istek 200000 (480 s pencere), tekil takip numarasi 20454, obek 10, es zamanli gonderi 1000 katman kapsam kapasite isabet isabet payi tahliye ---------- ----------- --------- -------- ------------ -------- istemci kullanici 4 140045 0.7002 34810 kenar paylasilan 250 16520 0.0826 43185 web paylasilan 600 14390 0.0720 28445 uygulama ornek 400x4 8394 0.0420 19051 depo - - 20651 0.1033 - birlesik isabet = 0.8967 (V9 varsayimi 0,90) bir iskanin dokunusu = 9 (4 yoklama + 1 depo + 4 geri yazma) dokunus / istek = 2.531 K01'e donus (tepe okuma 416.67, tepe yazma 97.22 istek/s) aga hic cikmayan okuma/s = 291.76 uygulamaya varan okuma/s = 60.51 onbellek arkasi okuma/s = 43.02 (V9 ile 41,67) depoya ulasan istek/s = 140.25 (V9 ile 138,89) depoda yazma/okuma orani = 2.26 (V9 ile 2,33)
Bu sayıların hepsi hesap sınıfındadır: belirlenimli bir akış üzerinde sayılmışlardır, süre ölçülmemiştir, koşumdan koşuma değişmezler.
Birinci sonuç V9 hakkındadır. Dört katmanın birleşik isabeti 0,8967 çıktı; varsayılan 0,90’ın
biraz altında. Fark küçük görünüyor ama hesapta görünür: onbellek arkasi okuma/s 41,67 yerine
43,02, depoya ulasan istek/s 138,89 yerine 140,25, depoda yazma/okuma orani 2,33 yerine
2,26. Varsayım artık savunulabilir bir sayıdır çünkü onu üreten yerleşim yazılmıştır. Kapasite
ya da çalışma kümesi değiştiğinde bu satırlar yeniden hesaplanır.
İkinci sonuç isabetin dağılımı hakkındadır. İsteklerin 0,7002’si istemci katmanında, yalnız dört girdilik bir önbellekte durdu — çünkü öbek içindeki sorguların çoğu aynı kullanıcıdan geliyor. Bu, saniyede 291,76 okumanın ağa hiç çıkmaması demektir. Kalan üç katman toplam 0,1966 durdurdu ve uygulamaya varan okuma 416,67’den 60,51 istek/s’ye indi. Trafik Katmanı kursunun İçerik Dağıtım Ağları dersi aynı satırı 41,76 olarak ölçmüştü; oradaki uygulama önbelleği pencerenin bütün anahtarlarını tutabiliyordu, buradaki katmanların kapasitesi çalışma kümesinin altındadır ve tahliye sütunu bunu gösteriyor.
Üçüncü sonuç bedelin nerede olduğudur. Bir ıska dokuz dokunuş yapar: dört yoklama, bir depo erişimi ve dönüşte dört geri yazma. Ortalama 2,531 dokunuş/istek. Zincir uzadıkça isabet artar ama her ıska daha uzun bir yol yürür.
Katman Eklemenin Sınırı
Bir katmanın değeri, durdurduğu istek sayısı ile eklediği dokunuşun oranıdır. Aşağıdaki koşum zinciri baştan kurarak her katmanın kendi payını ayırır ve aynı isabeti tek bir önbellekle elde etmenin ne istediğini gösterir.
// onbellek/kac-katman.mjs — zincire katman eklendikce ne kazanilir, ne odenir import { akis, zincir, K01 } from "./katman.mjs"; const ISTEK = 200_000; const istekler = akis({ istek: ISTEK, calismaKumesi: 1000, obek: 10, kullanici: 40_000 }); const TAM = [ { ad: "istemci", kapsam: "kullanici", kapasite: 4 }, { ad: "kenar", kapsam: "paylasilan", kapasite: 250 }, { ad: "web", kapsam: "paylasilan", kapasite: 600 }, { ad: "uygulama", kapsam: "ornek", kapasite: 400, ornek: 4 }, ]; console.log("zincir isabet depo/s dokunus/istek durdurulan eklenen dokunus dokunus/durdurulan"); console.log("---------------------------- ------ ------- -------------- ----------- ---------------- ------------------"); let onceki = { dokunus: ISTEK, depo: ISTEK }; // onbelleksiz: istek basina bir depo erisimi for (let n = 0; n <= TAM.length; n++) { const r = n === 0 ? { depo: ISTEK, dokunus: ISTEK } : zincir(istekler, TAM.slice(0, n)); const h = 1 - r.depo / ISTEK; const depoS = K01.tepeOkuma * (1 - h) + K01.tepeYazma; const durdurulan = onceki.depo - r.depo; const eklenen = r.dokunus - onceki.dokunus; const ad = n === 0 ? "(onbelleksiz)" : "+" + TAM[n - 1].ad; console.log(`${ad.padEnd(28)} ${h.toFixed(4).padStart(6)} ${depoS.toFixed(2).padStart(7)} ` + `${(r.dokunus / ISTEK).toFixed(3).padStart(14)} ${(n === 0 ? "-" : String(durdurulan)).padStart(11)} ` + `${(n === 0 ? "-" : String(eklenen)).padStart(16)} ${(n === 0 ? "-" : (eklenen / durdurulan).toFixed(2)).padStart(18)}`); onceki = r; } const tam = zincir(istekler, TAM); const h = 1 - tam.depo / ISTEK; console.log(); console.log(`dort katmanin birlesik isabeti = ${h.toFixed(4)}`); console.log(`ayni isabet tek katmanla istenirse gereken kapasite:`); for (const kap of [250, 600, 1000, 1200, 2000]) { const t = zincir(istekler, [{ ad: "tek", kapsam: "paylasilan", kapasite: kap }]); const th = 1 - t.depo / ISTEK; console.log(` kapasite ${String(kap).padStart(4)} -> isabet ${th.toFixed(4)} depo/s ${(K01.tepeOkuma * (1 - th) + K01.tepeYazma).toFixed(2).padStart(6)} dokunus/istek ${(t.dokunus / ISTEK).toFixed(3)}`); }
zincir isabet depo/s dokunus/istek durdurulan eklenen dokunus dokunus/durdurulan ---------------------------- ------ ------- -------------- ----------- ---------------- ------------------ (onbelleksiz) 0.0000 513.89 1.000 - - - +istemci 0.7002 222.13 1.600 140045 119910 0.86 +kenar 0.7828 187.71 2.034 16520 86870 5.26 +web 0.8548 157.73 2.324 14390 58090 4.04 +uygulama 0.8967 140.25 2.531 8394 41302 4.92 dort katmanin birlesik isabeti = 0.8967 ayni isabet tek katmanla istenirse gereken kapasite: kapasite 250 -> isabet 0.2321 depo/s 417.20 dokunus/istek 2.536 kapasite 600 -> isabet 0.5236 depo/s 295.71 dokunus/istek 1.953 kapasite 1000 -> isabet 0.7915 depo/s 184.11 dokunus/istek 1.417 kapasite 1200 -> isabet 0.8646 depo/s 153.63 dokunus/istek 1.271 kapasite 2000 -> isabet 0.8975 depo/s 139.95 dokunus/istek 1.205
Birinci tablo azalan getiriyi sayıyla veriyor. Önbelleksiz tasarımda depoya ulaşan istek 513,89 istek/s’dir — K01’in uçtaki tepe yükünün tamamı. İlk katman bunu 222,13’e indiriyor ve durdurduğu her istek başına yalnız 0,86 dokunuş ekliyor, çünkü durdurduğu istek zaten bir depo erişimini iptal ediyor. Sonraki üç katmanın oranı 5,26, 4,04 ve 4,92: her biri, kurtardığı her istek için beşe yakın ek dokunuş ödetiyor. Dördüncü katman 8394 isteği durdurup 41.302 dokunuş ekliyor ve depoya ulaşan yükü yalnız 17,49 istek/s düşürüyor.
İkinci tablo daha rahatsız edici. Kapasitesi 2000 olan tek bir paylaşılan önbellek 0,8975 isabet veriyor — dört katmanlı zincirin 0,8967’sinin üstünde — ve bunu istek başına 1,205 dokunuşla yapıyor, 2,531 yerine. Zincirin gerekçesi birleşik isabet oranı değildir: doğru boyutlandırılmış tek bir katman aynı oranı daha ucuza verir.
Zincirin gerekçesi isabetin nerede karşılandığıdır. Tek paylaşılan önbellekte isabetlerin tamamı bir ağ turuna mal olur ve hepsi tek bir bileşenden geçer; dört katmanlı yerleşimde saniyede 291,76 okuma hiç ağa çıkmaz. Buna karşılık o 291,76 okuma, geri alınabilirliği en düşük katmanda karşılanır: istemcideki kopya konduktan sonra silinemez. Yerleşim kararı bu iki cümlenin arasında verilir ve ikisi de aynı isabet oranıyla uyumludur.
Özet
- Bir önbellek katmanını kapsam, kapasite ve geri alınabilirlik tanımlar; veritabanı önbelleği bu sıralamanın dışındadır çünkü sorgunun maliyetini düşürür, sayısını düşürmez.
- Dört katmanlı zincirin birleşik isabeti 0,8967 ölçüldü. K01’in V9 = 0,90 varsayımı böylece
tasarımın sonucu oldu:
onbellek arkasi okuma/s41,67 yerine 43,02,depoya ulasan istek/s138,89 yerine 140,25,depoda yazma/okuma orani2,33 yerine 2,26. - İsabetin 0,7002’si dört girdilik istemci önbelleğinde durdu; saniyede 291,76 okuma ağa hiç çıkmadı ve uygulamaya varan okuma 416,67’den 60,51 istek/s’ye indi.
- Zincir uzadıkça her ıska daha uzun yol yürür: dört katmanda bir ıska dokuz dokunuş, ortalama 2,531 dokunuş/istek. Katmanların durdurdukları istek başına ek dokunuşu sırasıyla 0,86, 5,26, 4,04 ve 4,92.
- Kapasitesi 2000 olan tek bir paylaşılan önbellek 0,8975 isabeti 1,205 dokunuşla veriyor; zincirin gerekçesi isabet oranı değil, isabetin hangi katmanda karşılandığıdır.
Sonraki Adım
Bu derste her katman aynı biçimde çalıştı: bak, yoksa bir alttakine sor, dönüşte koy. Bu davranış kodda tek bir döngüdür ve bir adı vardır; üstelik tek seçenek de değildir. Katmanın isabeti yalnız kapasiteye değil, girdinin ne zaman düştüğüne de bağlıdır ve girdiyi düşüren şey çoğunlukla bir yazmadır. K01’in hesabı deponun gördüğü yerde yazma ağırlıklı bir sistem tarif ediyordu: yazma/okuma oranı 2,33. Sıradaki ders uygulamanın kendi denetlediği bu yerleştirmeyi adlandırır ve isabet oranını sabit bir sayı olmaktan çıkarıp yazma sıklığının bir işlevi hâline getirir; o işlev 0,90’ın nerede tutulabildiğini ve nerede tutulamadığını söyler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.