İçeriğe geç
academia.sh

Ders 16 / 18

Olay Kaynaklı Tasarım

Değişimin kayıt olarak saklanmasının ölçekleme bedeli: eklemeli kütüğün yazma yolundan oku-değiştir-yaz okumasını kaldırması, istek başına depo işleminin 4,50'den 3,10'a inmesi, kaynak olayın ek alanlarının günlük artışı 976'dan 1168 MB'a ve saklanan veriyi 712,48'den 852,64 GB'a çıkarması, dönemsel taramanın 10,80'den 35,04 GB'a tırmanması.

İçindekiler

Önceki ders izdüşümü komutla aynı anda güncelledi ve ayrımın amacını yarısından etti: aynı tutarlılık sınırında duran iki depo ayrı ölçeklenemez. İzdüşüm kendi hızıyla güncellenecekse neyden türetileceği belirlenmelidir ve gönderi kaydı buna elverişli değildir — yalnız son durumu tutar, kaydın o hâle hangi değişimle geldiğini söylemez. İzdüşümü tazelemek için değişimin kendisi gerekir.

Olay kaynaklı kalıcılık (event sourcing) tam olarak bunu yapar: saklanan asıl şey durum değil, durumu üreten olayların dizisidir. Kalıp Alan Odaklı Tasarım kursunda kuruldu ve orada yanıtlanabilirlik ölçüsüyle savunuldu — durum tablosunun şemasında olmayan soruların olay dizisinden çıkması. Bu dersin sorusu başkadır: kalıp veri katmanının ölçekleme sayılarına ne yapıyor. Kütük yazma yolunu ucuzlatıyor mu, saklanan veriyi ne kadar büyütüyor ve hangi erişim örüntüsünü pahalıya düşürüyor.

Yazma Yolunda Gizlenen Okuma

K01 depoya ulaşan istek hızını 138,89 bulmuştu: saniyede 41,67 okuma isteği ve 97,22 yazma isteği, oran 2,33. Bu sayı istek sayar. Bir yazma isteğinin depoda kaç işleme dönüştüğü sorulmamıştı.

Durumu yerinde güncelleyen bir kalıcılıkta bir durum olayı üç işlem eder: olay denetim için yazılır, gönderi kaydı okunur, güncellenmiş kayıt geri yazılır. Ortadaki okuma bir istek değildir, bu yüzden K01’in tablosunda görünmez; ama depoya gerçekten ulaşır. Eklemeli bir kütükte o okuma yoktur: yazma yalnız dizinin sonuna ekler ve önceki durumu bilmek zorunda değildir.

Kaynak kayıt olmanın bir bedeli var. K01’in olay kaydı (V6, 220 bayt) bir denetim satırıdır: takip numarası, durum, rota adımı, zaman damgası. Aynı kayıt tek doğruluk kaynağı olduğunda fazladan alan taşır — olay türü, dizideki sıra numarası, şema sürümü ve tekillik anahtarı. Sıra numarası olmadan katlama sırası belirsizdir, şema sürümü olmadan eski biçimli bir olay okunamaz, tekillik anahtarı olmadan yeniden gönderilen bir olay ikinci kez uygulanır.

OY2 — kaynak olay kaydının ek yükü: 60 bayt. Bu konunun varsayımıdır, K01’in tablosuna eklenmez. Gerekçesi yukarıdaki dört alandır. Duyarlılığı ölçümün son satırlarında veriliyor: ek yük 0 alınırsa K01’in sayıları aynen çıkar, 120 bayt alınırsa saklanan veri bir kez daha büyür.

Düzenek

İki kalıcılık aynı arayüzü sunar ve aynı olayları görür. Süreç içi bir modeldir; disk, ağ ve süre yoktur. Sayılan iki şey vardır: her erişimin ürettiği depo işlemi ve her kaydın eklediği saklanan bayt. Bayt sayısı JSON boyundan değil, K01’in V6 ve V7 varsayımlarından gelir; amaç modelin kendi biçimini değil, K01’in hesabını sürdürmektir.

// olay/kalicilik.mjs — iki kalicilik biciminin surec ici modeli. Ayni arayuz, ayni olaylar;
// her erisim depo islemi sayar, her kayit K01'in bayt varsayimlarindan saklanan bayt ekler.
export const V4 = 7, V6 = 220, V7 = 900;   // K01 varsayimlari
export const OY2 = 60;                     // bu konunun varsayimi: kaynak olayin ek alanlari

function sayacKur() {
  return { okuma: 0, yazma: 0, bayt: 0 };
}

// A: durumu yerinde guncelleyen kalicilik; olaylar ayrica denetim icin yazilir.
export function durumGunceller() {
  const kayit = new Map(), denetim = new Map();
  const s = sayacKur();
  return {
    ad: "durum gunceller", sayac: s,
    olustur(id, bolge) {
      kayit.set(id, { id, bolge, durum: "kabul-edildi", sonAdim: [] });
      denetim.set(id, []);
      s.yazma += 1; s.bayt += V7;
    },
    yaz(id, nokta) {
      denetim.get(id).push({ id, nokta });
      s.yazma += 1; s.bayt += V6;
      const k = kayit.get(id);
      s.okuma += 1;                       // oku-degistir-yaz: mevcut satir once okunur
      k.durum = "aktarmada";
      s.yazma += 1;                       // guncellenmis satir geri yazilir
    },
    oku(id) {                             // takip yaniti: satir + son uc rota adimi icin olaylar
      const k = kayit.get(id);
      s.okuma += 1;
      const d = denetim.get(id);
      s.okuma += d.length;
      return { id: k.id, bolge: k.bolge, durum: k.durum, sonAdim: d.slice(-3).map((o) => o.nokta) };
    },
    tara(idler) {                         // donemsel toplu tarama: yalniz gonderi satiri okunur
      let bayt = 0;
      for (const id of idler) { kayit.get(id); s.okuma += 1; bayt += V7; }
      return bayt;
    },
  };
}

// B: yalniz eklemeli olay kutugu; durum katlamayla turetilir, yerinde guncelleme yoktur.
export function olayKutugu() {
  const kutuk = new Map();
  const s = sayacKur();
  const katla = (dizi) => {
    const bas = dizi[0];
    let durum = "kabul-edildi";
    const adim = [];
    for (const o of dizi.slice(1)) { durum = "aktarmada"; adim.push(o.nokta); }
    return { id: bas.id, bolge: bas.bolge, durum, sonAdim: adim.slice(-3) };
  };
  return {
    ad: "olay kutugu", sayac: s,
    olustur(id, bolge) {
      kutuk.set(id, [{ tur: "gonderi-olusturuldu", id, bolge }]);
      s.yazma += 1; s.bayt += V7 + OY2;
    },
    yaz(id, nokta) {
      kutuk.get(id).push({ tur: "aktarmaya-girdi", id, nokta });
      s.yazma += 1; s.bayt += V6 + OY2;   // yazma yolunda okuma yok
    },
    oku(id) {
      const dizi = kutuk.get(id);
      s.okuma += dizi.length;
      return katla(dizi);
    },
    tara(idler) {
      let bayt = 0;
      for (const id of idler) {
        const dizi = kutuk.get(id);
        s.okuma += dizi.length;
        bayt += V7 + OY2 + (dizi.length - 1) * (V6 + OY2);
      }
      return bayt;
    },
  };
}

Katlama işlevi son üç rota adımını dizinin kendisinden çıkarır; durumu güncelleyen kalıcılık ise aynı üç adımı denetim olaylarından okur. İki yolun aynı yanıtı ürettiği ölçümde sınanıyor.

Ölçüm

// olay/olc.mjs — ayni olaylar iki kaliciliga da yazilir; istek basina depo islemi ve gonderi
// basina saklanan bayt olculur, sonra K01'in hizlarina ve hacimlerine uygulanir.
import { durumGunceller, olayKutugu, V4, V6, V7, OY2 } from "./kalicilik.mjs";

const NOKTA = ["34", "06", "35", "01", "16"];
const N = 200;

function kosum(depo) {
  let x = 7;
  const sonraki = () => (x = (x * 48271 + 11) % 2147483647);
  const idler = Array.from({ length: N }, (_, i) => `G${i + 1}`);
  for (const id of idler) {
    depo.olustur(id, NOKTA[sonraki() % NOKTA.length]);
    for (let k = 0; k < V4; k += 1) depo.yaz(id, NOKTA[sonraki() % NOKTA.length]);
  }
  const kurulus = { ...depo.sayac };
  depo.sayac.okuma = 0; depo.sayac.yazma = 0;
  const yanit = idler.map((id) => depo.oku(id));
  const sorgu = depo.sayac.okuma / N;
  depo.sayac.okuma = 0;
  const taramaBayt = depo.tara(idler) / N;
  const tarama = { islem: depo.sayac.okuma / N, bayt: taramaBayt };
  depo.sayac.okuma = 0; depo.sayac.yazma = 0;
  for (const id of idler) depo.yaz(id, "16");
  const komut = { okuma: depo.sayac.okuma / N, yazma: depo.sayac.yazma / N };
  return { ad: depo.ad, komut, sorgu, tarama, gonderiBayt: kurulus.bayt / N, yanit };
}

const A = kosum(durumGunceller()), B = kosum(olayKutugu());
const b = (x, n = 2) => x.toFixed(n);
const p = (x, n) => String(x).padStart(n);

console.log(`model: ${N} gonderi, gonderi basina ${V4} durum olayi (K01 V4)`);
console.log(`iki kaliciligin urettigi takip yaniti ayni mi = ${JSON.stringify(A.yanit) === JSON.stringify(B.yanit)}\n`);
console.log(`${"kalicilik".padEnd(16)}${"komut islemi".padStart(14)}${"sorgu islemi".padStart(14)}` +
  `${"tarama islemi".padStart(15)}${"gonderi bayt".padStart(14)}`);
for (const r of [A, B])
  console.log(`${r.ad.padEnd(16)}${`${r.komut.okuma}o + ${r.komut.yazma}y`.padStart(14)}` +
    `${p(r.sorgu, 14)}${p(r.tarama.islem, 15)}${p(r.gonderiBayt, 14)}`);

// K01 hesaplari ve varsayimlari
const OKUMA = 41.67, YAZMA = 97.22, G_GONDERI = 400_000, SAKLAMA = 730, TOPLU_GUN = 30, PENCERE = 4 * 3600;
console.log(`\nK01: depoya ulasan istek/s = ${b(OKUMA + YAZMA)} (${OKUMA} okuma + ${YAZMA} yazma)` +
  `, oran ${b(YAZMA / OKUMA)}`);
console.log(`${"kalicilik".padEnd(16)}${"okuma islemi/s".padStart(16)}${"yazma islemi/s".padStart(16)}` +
  `${"toplam islem/s".padStart(16)}${"islem/istek".padStart(13)}`);
for (const r of [A, B]) {
  const o = OKUMA * r.sorgu + YAZMA * r.komut.okuma, y = YAZMA * r.komut.yazma;
  console.log(`${r.ad.padEnd(16)}${b(o).padStart(16)}${b(y).padStart(16)}${b(o + y).padStart(16)}` +
    `${b((o + y) / (OKUMA + YAZMA)).padStart(13)}`);
}

console.log(`\n${"kalicilik".padEnd(16)}${"gunluk artis MB".padStart(17)}${"saklanan GB".padStart(13)}` +
  `${"toplu okunan GB".padStart(17)}${"toplu Mbit/s".padStart(14)}`);
for (const r of [A, B]) {
  const gunluk = (G_GONDERI * r.gonderiBayt) / 1e6;
  const toplu = (TOPLU_GUN * G_GONDERI * r.tarama.bayt) / 1e9;
  console.log(`${r.ad.padEnd(16)}${b(gunluk).padStart(17)}${b((gunluk * SAKLAMA) / 1000).padStart(13)}` +
    `${b(toplu).padStart(17)}${b((toplu * 1e9 * 8) / (PENCERE * 1e6)).padStart(14)}`);
}

const kat = (f) => f(B) / f(A);
console.log(`\nkutugun katsayilari: gunluk artis x${b(kat((r) => r.gonderiBayt))}` +
  `, toplu okuma x${b(kat((r) => r.tarama.bayt))}, komut islemi x${b(kat((r) => r.komut.okuma + r.komut.yazma))}`);
for (const ek of [0, OY2, 2 * OY2]) {
  const gunluk = (G_GONDERI * (V7 + ek + V4 * (V6 + ek))) / 1e6;
  console.log(`OY2 = ${p(ek, 3)} bayt -> gunluk artis ${b(gunluk)} MB` +
    `, saklanan ${b((gunluk * SAKLAMA) / 1000)} GB`);
}
model: 200 gonderi, gonderi basina 7 durum olayi (K01 V4)
iki kaliciligin urettigi takip yaniti ayni mi = true

kalicilik         komut islemi  sorgu islemi  tarama islemi  gonderi bayt
durum gunceller        1o + 2y             8              1          2440
olay kutugu            0o + 1y             8              8          2920

K01: depoya ulasan istek/s = 138.89 (41.67 okuma + 97.22 yazma), oran 2.33
kalicilik         okuma islemi/s  yazma islemi/s  toplam islem/s  islem/istek
durum gunceller           430.58          194.44          625.02         4.50
olay kutugu               333.36           97.22          430.58         3.10

kalicilik         gunluk artis MB  saklanan GB  toplu okunan GB  toplu Mbit/s
durum gunceller            976.00       712.48            10.80          6.00
olay kutugu               1168.00       852.64            35.04         19.47

kutugun katsayilari: gunluk artis x1.20, toplu okuma x3.24, komut islemi x0.33
OY2 =   0 bayt -> gunluk artis 976.00 MB, saklanan 712.48 GB
OY2 =  60 bayt -> gunluk artis 1168.00 MB, saklanan 852.64 GB
OY2 = 120 bayt -> gunluk artis 1360.00 MB, saklanan 992.80 GB

Sayıların Okunması

İlk denetim durum gunceller satırının K01’i birebir yeniden ürettiğidir: gönderi başına 2440 bayt, günde 976 MB, 730 günde 712,48 GB, dönemsel taramada 10,80 GB ve 6,00 Mbit/s. K01’in hesabı bu kalıcılık biçimini varsaymıştı; model o varsayımı yeniden üretmeseydi karşılaştırma geçersiz olurdu. İkinci denetim iki kalıcılığın aynı takip yanıtını vermesidir.

Yazma yolu kütüğün kazandığı yerdir. Bir durum olayı durum gunceller yerleşiminde 3 işlem eder (1 okuma, 2 yazma), kütükte 1 işlem eder. Kütük komut maliyetini 0,33 katına indiriyor. K01’in hızlarında bu, saniyede 194,44 yazma işleminin 97,22’ye ve tablonun hiç saymadığı 97,22 gizli okumanın sıfıra inmesi demektir. İstek başına depo işlemi 4,50’den 3,10’a düşüyor: K01’in 138,89 isteği aslında 625,02 işlem üretiyordu, kütükte 430,58 üretiyor.

Saklama yolu kütüğün kaybettiği yerdir. Gönderi başına 2440 bayt 2920 bayta çıkıyor; günlük artış 976 MB’dan 1168 MB‘a, saklanan veri 712,48 GB’dan 852,64 GB‘a. Artış 1,20 kat ve tamamı OY2’den geliyor — duyarlılık satırları bunu doğruluyor: ek yük sıfır alındığında kütüğün saklama sayıları K01’inkilerle aynı, 120 bayt alındığında saklanan veri 992,80 GB. Buradaki asıl bulgu şudur: olay dizisi K01’in tablosunda zaten vardı. Günlük 976 MB’ın 616 MB’ı olay kaydıdır; kütük saklanan veriyi 5,7 kat büyütmüyor, yalnız kaynak olmanın ek alanlarını ekliyor.

Tarama yolu kütüğün kırıldığı yerdir. Dönemsel toplu tarama satıcının otuz gününü gezer ve durum gunceller yerleşiminde gönderi başına tek satır okur. Kütükte aynı bilgi için sekiz olay okunması ve katlanması gerekir: taranan bayt 10,80 GB’dan 35,04 GB‘a, dört saatlik pencerenin bant genişliği 6,00 Mbit/s’den 19,47 Mbit/s’e çıkıyor. K01 bu satırı zaten en büyük kaynak kalemi olarak işaretlemişti — tepe okuma çıkışının 3,75 katıydı; kütükle birlikte 12,17 katına çıkıyor. Bir erişim örüntüsü için verilen karar başka bir örüntüyü üç kattan fazla pahalıya düşürebilir.

Anlık Görüntünün İşe Yaramadığı Yer

Alan Odaklı Tasarım kursu kütüğün okuma maliyetine bir çare koymuştu: anlık görüntü, yeniden kurmayı dizinin başından değil bir noktadan başlatır. O ders kuralı da yazmıştı — görüntü aralığı ortalama geçmiş uzunluğunun altına inmedikçe okunan olay sayısı düşmez.

Bu sistemde ortalama geçmiş uzunluğu 8’dir (bir oluşturma olayı ve V4 durum olayı). Kural uygulandığında görüntü aralığının 8’in altında olması gerekir; aralık 1 olduğunda ise saklanan şey her olaydan sonra güncellenen bir durum satırıdır, yani durum gunceller yerleşiminin kendisi. Kısa geçmişli bir alanda anlık görüntü ayrı bir seçenek üretmez; iki uç arasında yalnız birkaç adım vardır. Bu kursun sorusu bu yüzden görüntüde değil izdüşümdedir: kütüğü okuyan sorgu değil, kütükten türetilmiş ayrı bir okuma modeli.

Kütüğün kendi sınırı da burada söylenmeli. Yazılmış bir olay değişmediği için bir kaydın tamamen silinmesi gerektiğinde dizinin eklemeli olması bir engeldir; K01’in 730 günlük saklama varsayımı (V12) bu yüzden bir sözleşme kararıdır, teknik bir varsayım değil.

Özet

  • Kütük yazma yolundaki gizli okumayı kaldırır: durum olayı 3 işlemden 1 işleme, istek başına depo işlemi 4,50’den 3,10’a iner; K01’in 138,89 isteğinin işlem karşılığı 625,02’den 430,58’e.
  • Kaynak olmanın bedeli OY2 = 60 bayttır: günlük artış 976 → 1168 MB, saklanan veri 712,48 → 852,64 GB, yani 1,20 kat. Ek yük 0 alındığında K01’in sayıları aynen çıkıyor.
  • Olay dizisi K01’in tablosunda zaten vardı — günlük 976 MB’ın 616 MB’ı olay kaydıdır; kütük saklamayı katlamaz, yalnız sıra, tür, şema sürümü ve tekillik alanlarını ekler.
  • Kırılan örüntü dönemsel taramadır: gönderi başına 1 okuma yerine 8 olay okunur, taranan veri 10,80 → 35,04 GB, pencere bant genişliği 6,00 → 19,47 Mbit/s.
  • Ortalama geçmiş uzunluğu 8 olduğu için anlık görüntü ayrı bir seçenek üretmiyor; aralık 1 olduğunda görüntü zaten durum satırının kendisidir.

Sonraki Adım

Kütük yazma yolunu ucuzlattı ve okuma yolunu pahalıya düşürdü; iki sayı arasındaki fark ancak okuma yolu kütüğün dışına çıkarılırsa kapanır. Önceki dersin izdüşümü bunu yapıyordu ama komutla aynı anda güncelleniyordu ve bu yüzden ayrı ölçeklenemiyordu. Kütük artık türetmenin girdisini sağlıyor: izdüşüm kütüğü kendi hızıyla okuyabilir, geride kalabilir, sıfırdan yeniden kurulabilir. Sonraki ders bu türetmeyi ele alır — izdüşümün sakladığı ek veriyi, yeniden üretim için okunan olay sayısını, yeniden üretim sırasında akan yeni olayların yakalanma koşulunu ve şema değiştiğinde yeniden üretimin ne kadar sürdüğünü ölçer.

İ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