Ders 03 / 14
Sürümleme ve Yaşam Döngüsü Kuralları
Otomatik geçişin ve silmenin 36 aylık bir nesne yaşamı üzerinde koşturulması: kural olmadan biriken bayt, kuralla geçen ve silinen nesne sayısı, geçiş ücretinin küçük nesnede kazancı yutması, erken geçişin geri getirme bedeli ve sürümleme açıldığında silinen nesnenin gerçekten silinmemesi.
İçindekiler
Önceki dersin sınıf dağıtımı bir varsayıma dayanıyordu: her nesnenin yıllık okuma sayısı bilinir ve doğru sınıfa elle konur. Milyonlarca nesnede bu ne bilinir ne de elle yapılır. Nesne deposu bunun için yaşam döngüsü kuralı (lifecycle rule) sunar: bir önek altındaki nesneler belirli bir yaşa geldiğinde sınıf değiştirir ya da silinir. Kural erişime değil yaşa bakar ve bu ayrım ölçülebilir bir bedel taşır.
Aynı ders sürümlemeyi de açar. Sürümleme (versioning) kova düzeyinde bir anahtardır ve açıldıktan sonra geriye dönüşü yoktur. İkisi birlikte ölçülür: sürümleme baytı büyütür, yaşam döngüsü kuralı küçültür.
- DT13 — 36 aylık nesne yaşamı modeldir: aylık üretim, ortalama boyut ve okumanın yaşla yarılanma süresi modelde yazılıdır. Rastgelelik yoktur, her sayı kohorttan çıkar.
- DT14 — Geçiş bir istektir, nesne başına ücretlidir; en az saklama süresi sınıfa girişte başlar.
- DT15 — Kural yalnız yaşa bakar; bir nesnenin gerçekte okunup okunmadığını görmez.
// yasam/model.mjs — 36 aylik nesne yasami MODELDIR: rastgelelik yok, her sayi kohorttan cikar. // Ucretler kurgu birimdir; siniflar yalnizca erisim sikligiyla anilir. export const AY = 36; export const SINIF = { // saklama B/GiB/ay, istek ve gecis B/istek, getirme B/GiB sik: { saklama: 0.0230, istek: 0.00040 / 1000, getirme: 0.000, enAz: 0, gecis: 0.0050 / 1000 }, seyrek: { saklama: 0.0125, istek: 0.00100 / 1000, getirme: 0.010, enAz: 1, gecis: 0.0100 / 1000 }, arsiv: { saklama: 0.0010, istek: 0.00500 / 1000, getirme: 0.030, enAz: 6, gecis: 0.0500 / 1000 }, }; // aylikUretim: her ay yaratilan nesne; taban: yasi sifirken aylik okuma; yariOmur: okumanin yarilanma ayi export const AILE = [ { ad: "sayac-okumalari", uretim: 120_000, ort: 2400, taban: 3.0, yari: 1.5 }, { ad: "fatura-belgeleri", uretim: 7_200, ort: 380 * 1024, taban: 2.2, yari: 4 }, { ad: "saha-fotograflari", uretim: 3_500, ort: Math.round(3.6 * 1024 * 1024), taban: 0.9, yari: 2 }, { ad: "is-emri-kayitlari", uretim: 26_000, ort: 11 * 1024, taban: 4.0, yari: 3 }, ]; export const GIB = (bayt) => bayt / 1024 ** 3; export const okuma = (f, yas) => f.taban * Math.pow(0.5, yas / f.yari); // Kural: yas (ay) esiginde sinif degistir; sil esiginde nesneyi kaldir. export const KURAL = { "kural yok": { gecis: [], sil: null }, ilimli: { gecis: [[3, "seyrek"], [12, "arsiv"]], sil: 30 }, agresif: { gecis: [[1, "seyrek"], [3, "arsiv"]], sil: 7 }, "erken-arsiv": { gecis: [[1, "arsiv"]], sil: null }, }; export const sinifi = (k, yas) => { // yasa gore icinde bulunulan sinif ve o sinifa girilen yas let ad = "sik", giris = 0; for (const [esik, s] of k.gecis) if (yas >= esik) { ad = s; giris = esik; } return { s: SINIF[ad], ad, giris }; };
Kural Yaşa Bakar, Erişime Değil
// yasam/kural.mjs — yasam dongusu kurallari 36 ay boyunca kohortlar uzerinde kosturulur import { AY, SINIF, AILE, KURAL, GIB, okuma, sinifi } from "./model.mjs"; const g = (x, n, d = 2) => x.toFixed(d).padStart(n); const s = (x, n) => String(x).padStart(n); const bos = () => ({ saklama: 0, istek: 0, getirme: 0, gecisUcret: 0, gecisAdet: 0, borc: 0, silinen: 0, kalanB: 0 }); const kosu = (k, aile) => { // tek ailenin 36 aylik yasami const r = bos(); for (let dogum = 0; dogum < AY; dogum++) // her ay dogan kohort, ay ay yasatilir for (let ay = dogum; ay < AY; ay++) { const yas = ay - dogum; if (k.sil !== null && yas >= k.sil) { // silme aninda en az saklama borcu cikar if (yas === k.sil) { const c = sinifi(k, yas - 1); r.borc += aile.uretim * Math.max(0, c.s.enAz - (yas - c.giris)) * c.s.saklama * GIB(aile.ort); r.silinen += aile.uretim; } continue; } const c = sinifi(k, yas), oku = okuma(aile, yas); if (k.gecis.some(([e]) => e === yas)) { r.gecisAdet += aile.uretim; r.gecisUcret += aile.uretim * c.s.gecis; } r.saklama += aile.uretim * GIB(aile.ort) * c.s.saklama; r.istek += aile.uretim * oku * c.s.istek; r.getirme += aile.uretim * oku * GIB(aile.ort) * c.s.getirme; if (ay === AY - 1) r.kalanB += aile.uretim * aile.ort; } r.toplam = r.saklama + r.istek + r.getirme + r.gecisUcret + r.borc; return r; }; const T = {}; // T[kural][aile] ve T[kural].hepsi for (const [ad, k] of Object.entries(KURAL)) { T[ad] = { hepsi: bos() }; for (const f of AILE) { const r = T[ad][f.ad] = kosu(k, f); for (const a of ["saklama", "istek", "getirme", "gecisUcret", "gecisAdet", "borc", "silinen", "kalanB", "toplam"]) T[ad].hepsi[a] = (T[ad].hepsi[a] ?? 0) + r[a]; } } console.log(`${AILE.reduce((a, f) => a + f.uretim, 0).toLocaleString("tr")} nesne/ay, ${AY} ay; ` + `okuma yasla yariliyor, rastgelelik yok. Ucretler kurgu birim.`); console.log("\nkural | GiB | gecis istegi | saklama | getirme | gecis ucreti | toplam"); console.log("------------|--------|--------------|---------|---------|--------------|-------"); for (const [ad, r] of Object.entries(T).map(([a, x]) => [a, x.hepsi])) console.log(`${ad.padEnd(11)} | ${g(GIB(r.kalanB), 6, 1)} | ${s(r.gecisAdet.toLocaleString("tr"), 12)} | ` + `${g(r.saklama, 7)} | ${g(r.getirme, 7)} | ${g(r.gecisUcret, 12)} | ${g(r.toplam, 6)}`); console.log("\n36 aylik toplam, aile aile (kurgu birim):"); console.log("aile |" + Object.keys(KURAL).map((a) => a.padStart(12)).join(" |") + " | en ucuz kural"); console.log("-------------------|" + Object.keys(KURAL).map(() => "-".repeat(13)).join("|") + "|--------------"); for (const f of AILE) { const v = Object.keys(KURAL).map((a) => T[a][f.ad].toplam); console.log(`${f.ad.padEnd(18)} |` + v.map((x) => g(x, 12)).join(" |") + ` | ${Object.keys(KURAL)[v.indexOf(Math.min(...v))]}`); } const [yok, ili, agr, erk] = ["kural yok", "ilimli", "agresif", "erken-arsiv"].map((a) => T[a]); console.log(`\nkural yokken 36. ayda ${GIB(yok.hepsi.kalanB).toFixed(1)} GiB birikiyor, hicbir nesne silinmiyor. Ilimli kural ` + `${ili.hepsi.silinen.toLocaleString("tr")} nesne silip saklamayi ` + `${(100 * (1 - ili.hepsi.saklama / yok.hepsi.saklama)).toFixed(1)} puan indiriyor, ama gecis ucreti toplami ` + `${(ili.hepsi.toplam / yok.hepsi.toplam).toFixed(2)} katina cikariyor.`); const karma = AILE.reduce((a, f) => a + Math.min(...Object.keys(KURAL).map((k) => T[k][f.ad].toplam)), 0); console.log(`aileye gore secilen karma kural ${karma.toFixed(2)} birim: tek kuralin en iyisinin ` + `(${Math.min(...Object.values(T).map((x) => x.hepsi.toplam)).toFixed(2)}) altinda. Kural nesne turune gore yazilir.`); console.log(`erken gecisin bedeli getirme sutununda: ilimli ${ili.hepsi.getirme.toFixed(2)}, erken-arsiv ` + `${erk.hepsi.getirme.toFixed(2)} birim (${(erk.hepsi.getirme / ili.hepsi.getirme).toFixed(1)} kat); erken-arsiv saklamada ` + `${(ili.hepsi.saklama - erk.hepsi.saklama).toFixed(2)} kazanip getirme ve istekte ` + `${(erk.hepsi.getirme + erk.hepsi.istek - ili.hepsi.getirme - ili.hepsi.istek).toFixed(2)} birim kaybediyor.`); console.log(`agresif kural 7. ayda siliyor, arsivin en az ${SINIF.arsiv.enAz} aylik borcu var: ` + `${agr.hepsi.borc.toFixed(2)} birim artik var olmayan nesneler icin odeniyor.`);
156.700 nesne/ay, 36 ay; okuma yasla yariliyor, rastgelelik yok. Ucretler kurgu birim. kural | GiB | gecis istegi | saklama | getirme | gecis ucreti | toplam ------------|--------|--------------|---------|---------|--------------|------- kural yok | 556.4 | 0 | 236.74 | 0.00 | 0.00 | 257.94 ilimli | 463.6 | 8.931.900 | 92.06 | 13.05 | 239.75 | 379.90 agresif | 108.2 | 10.655.600 | 28.07 | 30.28 | 313.40 | 460.64 erken-arsiv | 556.4 | 5.484.500 | 22.53 | 58.48 | 274.22 | 540.24 36 aylik toplam, aile aile (kurgu birim): aile | kural yok | ilimli | agresif | erken-arsiv | en ucuz kural -------------------|-------------|-------------|-------------|-------------|-------------- sayac-okumalari | 17.46 | 203.62 | 292.04 | 318.98 | kural yok fatura-belgeleri | 41.19 | 37.32 | 38.33 | 56.13 | ilimli saha-fotograflari | 188.63 | 83.22 | 44.92 | 51.49 | agresif is-emri-kayitlari | 10.66 | 55.74 | 85.34 | 113.64 | kural yok kural yokken 36. ayda 556.4 GiB birikiyor, hicbir nesne silinmiyor. Ilimli kural 940.200 nesne silip saklamayi 61.1 puan indiriyor, ama gecis ucreti toplami 1.47 katina cikariyor. aileye gore secilen karma kural 110.36 birim: tek kuralin en iyisinin (257.94) altinda. Kural nesne turune gore yazilir. erken gecisin bedeli getirme sutununda: ilimli 13.05, erken-arsiv 58.48 birim (4.5 kat); erken-arsiv saklamada 69.52 kazanip getirme ve istekte 195.39 birim kaybediyor. agresif kural 7. ayda siliyor, arsivin en az 6 aylik borcu var: 0.90 birim artik var olmayan nesneler icin odeniyor.
Kural olmadan otuz altı ayda 556,4 GiB birikiyor ve hiçbir nesne silinmiyor; saklama kalemi toplamın neredeyse tamamıdır. Ilımlı kural bu kalemi yüzde 61,1 indirir, 940.200 nesne siler, baytı 463,6 GiB’e çeker — ve toplamı artırır. Nedeni geçiş ücreti sütunundadır: 8.931.900 geçiş isteği 239,75 birim tutar, saklamada kazanılanın üstündedir.
Aile tablosu kuralın nerede kazandığını gösteriyor. Saha fotoğraflarında agresif kural toplamı 188,63’ten 44,92 birime iner: nesne büyüktür, geçiş isteğinin taşıdığı bayt çoktur. Sayaç okumalarında aynı kural 17,46’dan 292,04 birime çıkar; 2,3 KiB’lik bir nesne için geçiş isteği o nesnenin yıllarca saklanmasından pahalıdır. Kural bu yüzden önek düzeyinde yazılır: karma kural 110,36 birimle tek kuralın en iyisinin yarısından azdır.
Erken geçiş üçüncü kalemi açıyor. Erken-arşiv kuralı saklamada 69,52 birim kazanır, getirme ve istekte 195,39 birim kaybeder: birinci ayında hâlâ okunan bir nesne arşive gönderildiğinde her okuma geri getirme ücreti doğurur. Agresif kuralın yedinci ayda sildiği nesneler ise arşivin altı aylık süresini doldurmaz; 0,90 birim artık var olmayan nesneler için ödenir.
Sürümleme: Silmenin Silmediği Yer
- DT16 — Güncelleme oranı modeldir: nesne yaşlandıkça üzerine yazma seyrekleşir ve her üzerine yazma bir eski sürüm bırakır.
- DT17 — Sürümleme kova düzeyindedir; açıkken silme bir silme işareti koyar ve geçerli sürümü eski sürüme çevirir.
- DT18 — Eski sürümler ancak ayrı bir yaşam döngüsü kuralıyla temizlenir.
// yasam/surumleme.mjs — surumleme acikken silmenin ve kapatmanin ne yaptigi sayilir (MODEL) import { AY, SINIF, AILE, GIB } from "./model.mjs"; // Guncelleme MODELI: nesne yaslandikca uzerine yazma seyreliyor. Her uzerine yazma bir eski surum birakir. const GUNCELLEME = { "is-emri-kayitlari": { taban: 0.85, yari: 2 }, "fatura-belgeleri": { taban: 0.30, yari: 3 } }; const SIL_AYI = 30, SURUM_OMRU = 3, KAPATMA_AYI = 24; // eski surum kurali: 3 ay sonra temizle const secili = AILE.filter((f) => GUNCELLEME[f.ad]); const g = (x, n, d = 2) => x.toFixed(d).padStart(n); const s = (x, n) => String(x).padStart(n); // Her ay: gecerli nesne bayti, uretilen eski surum bayti (uretildigi aya gore) toplanir. const kosu = ({ surumleme, surumKurali, kapatma }) => { const r = { gecerli: 0, gecerliB: 0, surum: 0, surumB: 0, saklama: 0, silinen: 0, bosalan: 0 }; for (const f of secili) { const u = GUNCELLEME[f.ad], uretilen = []; // uretilen[m] = o ay dogan eski surum sayisi for (let m = 0; m < AY; m++) uretilen.push(0); for (let m = 0; m < AY; m++) { let gecerli = 0; for (let dogum = 0; dogum <= m; dogum++) { const yas = m - dogum; if (yas >= SIL_AYI) { // silme: surumleme kapaliysa bayt gider if (yas === SIL_AYI) { r.silinen += f.uretim; if (surumleme && !(kapatma && m >= KAPATMA_AYI)) uretilen[m] += f.uretim; else r.bosalan += f.uretim * f.ort; } continue; } gecerli += f.uretim; const yazma = f.uretim * u.taban * Math.pow(0.5, yas / u.yari); if (surumleme && !(kapatma && m >= KAPATMA_AYI)) uretilen[m] += yazma; } let surum = 0; // o ay ayakta olan eski surumler for (let k = 0; k <= m; k++) if (!surumKurali || m - k < SURUM_OMRU) surum += uretilen[k]; r.saklama += GIB(f.ort) * (gecerli + surum) * SINIF.sik.saklama; if (m === AY - 1) { r.gecerli += gecerli; r.gecerliB += gecerli * f.ort; r.surum += surum; r.surumB += surum * f.ort; } } } return r; }; const D = { "surumleme kapali": kosu({ surumleme: false }), "surumleme acik": kosu({ surumleme: true }), "acik + surum kurali": kosu({ surumleme: true, surumKurali: true }), "24. ayda kapatildi": kosu({ surumleme: true, kapatma: true }), }; console.log(`${secili.map((f) => f.ad).join(" + ")}; ${AY} ay, ${SIL_AYI}. ayda silme, ` + `eski surum kurali ${SURUM_OMRU} ay. Guncelleme orani modelde.`); console.log("\ndurum | gecerli nesne | eski surum | eski surum GiB | 36 aylik saklama"); console.log("--------------------|---------------|------------|----------------|-----------------"); for (const [ad, r] of Object.entries(D)) console.log(`${ad.padEnd(19)} | ${s(Math.round(r.gecerli).toLocaleString("tr"), 13)} | ` + `${s(Math.round(r.surum).toLocaleString("tr"), 10)} | ${g(GIB(r.surumB), 14, 1)} | ${g(r.saklama, 16)}`); const [kap, acik, kural, kapatma] = Object.values(D); console.log(`\nsilme deneyi: ${Math.round(kap.silinen).toLocaleString("tr")} nesne silindi. Bosalan bayt — ` + `surumleme kapali ${GIB(kap.bosalan).toFixed(1)} GiB, acik ${GIB(acik.bosalan).toFixed(1)} GiB.`); console.log(` eski surum / gecerli veri orani ${(acik.surumB / acik.gecerliB).toFixed(2)}; surum kurali ` + `${GIB(acik.surumB - kural.surumB).toFixed(1)} GiB geri getiriyor.`); console.log(`kapatma deneyi: ${KAPATMA_AYI}. ayda kapatildi. Yeni surum uretilmiyor, birikmis ` + `${GIB(kapatma.surumB).toFixed(1)} GiB duruyor ve ${AY - KAPATMA_AYI} ay daha ucretlendiriliyor;`); console.log(` kapatmadan sonraki silmeler ${GIB(kapatma.bosalan).toFixed(1)} GiB bosaltiyor, oncekiler icin ` + `yapilabilecek tek sey ayri bir surum kurali yazmaktir.`);
fatura-belgeleri + is-emri-kayitlari; 36 ay, 30. ayda silme, eski surum kurali 3 ay. Guncelleme orani modelde. durum | gecerli nesne | eski surum | eski surum GiB | 36 aylik saklama --------------------|---------------|------------|----------------|----------------- surumleme kapali | 996.000 | 0 | 0.0 | 42.75 surumleme acik | 996.000 | 3.070.001 | 165.9 | 102.12 acik + surum kurali | 996.000 | 357.335 | 22.4 | 53.68 24. ayda kapatildi | 996.000 | 1.839.941 | 93.6 | 92.51 silme deneyi: 199.200 nesne silindi. Bosalan bayt — surumleme kapali 17.3 GiB, acik 0.0 GiB. eski surum / gecerli veri orani 1.92; surum kurali 143.5 GiB geri getiriyor. kapatma deneyi: 24. ayda kapatildi. Yeni surum uretilmiyor, birikmis 93.6 GiB duruyor ve 12 ay daha ucretlendiriliyor; kapatmadan sonraki silmeler 17.3 GiB bosaltiyor, oncekiler icin yapilabilecek tek sey ayri bir surum kurali yazmaktir.
Geçerli nesne sayısı dört satırda da aynı: 996.000. Uygulamanın gördüğü veri değişmez, değişen faturadır. Sürümleme açıldığında 3.070.001 eski sürüm birikir, 165,9 GiB tutar — geçerli verinin 1,92 katı, üzerine yazmanın kendi ürettiği bayt.
Silme deneyi kavramı yerine oturtuyor. 199.200 nesne siliniyor: sürümleme kapalıyken 17,3 GiB boşalır, açıkken 0,0 GiB. Silme başarılıdır, nesne listede görünmez — ve bayt yerinde durur, çünkü silinen nesne bir eski sürüme dönüşmüştür. Eski sürümler için ayrı bir kural yazıldığında 143,5 GiB geri gelir, saklama 102,12’den 53,68 birime iner; o kural yazılmadıkça sürümleme açmak silmeyi sessizce anlamsızlaştırır.
Kapatma deneyi geri alınamazlığı gösteriyor. Yirmi dördüncü ayda sürümleme kapatılır: yeni sürüm üretilmez, sonraki silmeler yeniden bayt boşaltır, ama birikmiş 93,6 GiB durur ve on iki ay daha ücretlendirilir. Sürümleme bu kursun ikinci geri alınamayan kalemidir: açmak bir anahtardır, kapatmak aynı anahtar değildir. Sağlayıcı yalnız kuralı işletir; eşikler, silme ve eski sürüm ömrü elde kalır.
Özet
- Kural yaşa bakar, erişime bakmaz: erken-arşiv saklamada 69,52 birim kazanıp getirme ve istekte 195,39 birim kaybeder, geri getirme kalemi 4,5 katına çıkar.
- Geçiş ücreti nesne başınadır: ılımlı kural saklamayı yüzde 61,1 indirse de toplamı 1,47 katına çıkarır. Kural kova düzeyinde değil önek düzeyinde yazılır.
- Karma kural 110,36 birimdir, tek kuralın en iyisi 257,94: saha fotoğraflarında agresif kural kazandırır, sayaç okumalarında aynı kural on yedi kat zarar ettirir.
- Erken silme en az saklama borcu doğurur: yedinci ayda silinen nesneler için 0,90 birim ödenir, nesne artık yoktur.
- Sürümleme açıkken 199.200 nesne silindiğinde boşalan 0,0 GiB’dir, 165,9 GiB eski sürüm birikir. Kapatmak birikmiş sürümleri temizlemez, yalnız yenisini üretmez.
Sonraki Adım
Bu üç ders tek bir varsayımı paylaşıyor: veri bütün hâlinde yazılıp bütün hâlinde okunuyor. Kurgu ölçüm ağının bir bölümü böyle çalışmaz — toplayıcı süreç bir veritabanı dosyasına saniyede yüzlerce küçük yazma yapar ve her yazma dosyanın ortasındaki birkaç kilobaytı değiştirir. Birinci derste ölçülen 190 kat yazma büyütmesi bu iş yükünü nesne deposunda tek başına imkânsız kılar. Sonraki ders veriyi bayt bloğu olarak sunan türe geçer. Ölçülecek üç şey var — birim türü ve boyutunun verdiği giriş çıkış bütçesi, iş yükü profillerinin bu bütçeyi doyurduğu nokta, ve boyutla başarımın birbirine bağlı olmasının açtığı tasarım tuzağı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.