İçeriğe geç
academia.sh

Ders 05 / 19

Zaman Serisi Depoları

Zaman damgalı ölçüm verisinin iki yerleşimde saklanmasının bedeli: satır başına tam damga ile bölüt başına taban damga, fark kodlaması ve blok sıkıştırmanın saklanan bayt farkı, aralık sorgusunun taradığı bölüt ve giriş sayısının aralık daraldıkça tersine dönmesi, saklama politikasının bölüt sınırına hizalandığında dokunulan girişi sıfıra indirmesi.

İçindekiler

Buraya kadarki dört ailede kaydın kendisi bir varlıktı — bir kitap, bir üye, bir ödünç — ve en son ele alınan model bağın kendisini birinci sınıf nesne yaptı. Kütüphanenin verisinin bir bölümü ise ne varlıktır ne bağ: raf bölgelerinin sıcaklığı, turnikenin saydığı kişi, ödünç sayacının değeri. Bunlar zaman serisidir (time series) — bir kaynağa ait, zaman damgalı, sırayla gelen ölçümler.

Bu verinin yük profili öteki ailelerinkine benzemez. Yazma yalnız sona eklemedir; geçmiş bir ölçüm neredeyse hiç güncellenmez. Okuma tekil kayda değil aralığa gelir. Ve verinin bir son kullanma tarihi vardır: üç ay önceki dakikalık ölçüm hiçbir soruya girmez. Ders bu üç özelliğin depo tarafındaki karşılığını ölçüyor: yerleşimin saklanan bayta etkisi, aralık sorgusunun taradığı birim, saklama politikasının bedeli.

İki Yerleşim, Aynı Ölçümler

Ölçüm satırı küçüktür: bir kaynak kimliği, bir zaman damgası ve tek bir sayı — damga, yanında taşıdığı değerden büyük olabilir. Zaman serisi deposunun ilk kararı buradan gelir: ölçümler tek tek değil, bölütler (segment) hâlinde saklanır; her bölüt bir kaynağın bir zaman kutusudur, taban damgasını bir kez yazar, geri kalanı fark olarak tutar.

NS25: on iki raf bölgesinde birer sıcaklık ölçeri vardır, her biri 60 saniyede bir ölçüm yazar ve otuz günlük veri incelenir. NS26: satır yerleşiminde bir ölçüm 2 bayt kaynak kimliği, 8 bayt zaman damgası ve 8 bayt değer taşır. NS6: bölüt birimi bir kaynak–gün çiftidir.

// zaman/yerlesim.mjs — ayni olcum dizisi iki yerlesimde seriliyor: (a) satir basina tam
// zaman damgasi + deger, (b) bolut basina taban damga + fark kodlamasi + blok sikistirma.
// Sayilan sey saklanan bayttir; sikistirma gercekten calistirilir, tohum gorunurdur.
import { deflateSync } from "node:zlib";
const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1);
const NOKTA = 86_400_000 / ARALIK;               // bolut basina giris: gunluk olcum sayisi
let tohum = 20240601;                            // gorunur tohum; uretec kendi yazildi
const gurultu = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648 - 0.5);
const olcum = (s, i) =>                          // ondalik derece cinsinden tamsayi: 18,4 -> 184
  Math.round((18 + s * 0.25 + 3 * Math.sin((i / NOKTA) * 2 * Math.PI) + gurultu() * 0.6) * 10);
const varint = (n) => { const b = []; let x = ((n << 1) ^ (n >> 31)) >>> 0;   // zigzag + varint
  do { let y = x & 0x7f; x >>>= 7; if (x) y |= 0x80; b.push(y); } while (x); return b; };

const satir = Buffer.alloc(SERI * GUN * NOKTA * 18);       // 2 bayt seri + 8 damga + 8 deger
let ofset = 0, hamBayt = 0, bolutBayt = 0, bolutSayisi = 0;
for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1) {
  const govde = [];                              // bolut govdesi: farklarin varinti
  let oncekiFark = 0, oncekiDeger = null;
  for (let i = 0; i < NOKTA; i += 1) {
    const t = T0 + g * 86_400_000 + i * ARALIK, d = olcum(s, g * NOKTA + i);
    satir.writeUInt16LE(s, ofset); satir.writeDoubleLE(t, ofset + 2);
    satir.writeDoubleLE(d / 10, ofset + 10); ofset += 18;
    if (oncekiDeger === null) { govde.push(...varint(d)); oncekiDeger = d; continue; }
    govde.push(...varint(ARALIK - oncekiFark)); oncekiFark = ARALIK;   // damga farkinin farki
    govde.push(...varint(d - oncekiDeger)); oncekiDeger = d;           // deger farki
  }
  const bas = Buffer.alloc(14);                  // taban damga + seri + giris sayisi
  bas.writeDoubleLE(T0 + g * 86_400_000, 0); bas.writeUInt16LE(s, 8); bas.writeUInt32LE(NOKTA, 10);
  hamBayt += bas.length + govde.length;
  bolutBayt += bas.length + deflateSync(Buffer.from(govde)).length;
  bolutSayisi += 1;
}

const N = SERI * GUN * NOKTA;
console.log(`${SERI} seri x ${GUN} gun x ${NOKTA} olcum = ${N} olcum; ` +
  `${bolutSayisi} bolut, bolut basina ${NOKTA} giris`);
console.log(`\n${"yerlesim".padEnd(34)}${"bayt".padStart(11)}${"olcum basina".padStart(14)}${"kat".padStart(7)}`);
for (const [ad, b] of [["satir basina tam damga + deger", satir.length],
    ["bolut, yalniz fark kodlamasi", hamBayt], ["bolut, fark + blok sikistirma", bolutBayt]])
  console.log(ad.padEnd(34) + String(b).padStart(11) + (b / N).toFixed(2).padStart(14) +
    (satir.length / b).toFixed(2).padStart(7));
12 seri x 30 gun x 1440 olcum = 518400 olcum; 360 bolut, bolut basina 1440 giris

yerlesim                                 bayt  olcum basina    kat
satir basina tam damga + deger        9331200         18.00   1.00
bolut, yalniz fark kodlamasi          1042560          2.01   8.95
bolut, fark + blok sikistirma          329013          0.63  28.36

Sayılar ölçüm sınıfındadır: iki yerleşim de gerçekten serildi, sıkıştırma gerçekten çalıştırıldı.

Aynı 518.400 ölçüm satır yerleşiminde 9.331.200 bayt, bölütlenmiş yerleşimde 329.013 bayttır: ölçüm başına 18 bayta karşı 0,63 bayt, yirmi sekiz kat. Ara satır kazancın nereden geldiğini ayırıyor. Yalnız fark kodlaması, blok sıkıştırma olmadan, ölçüm başına 2,01 bayta iniyor — dokuz kat. Nedeni verinin biçimidir: ölçüm aralığı sabit olduğu için ardışık damgaların farkı hep aynıdır, farkın farkı sıfırdır ve tek baytla yazılır; değer farkı da küçük bir tamsayıdır. Sekiz baytlık damga ve sekiz baytlık değer, iki bayta düşer. Blok sıkıştırma bunun üstüne üç kat daha ekliyor: tekrarlayan sıfır ve küçük fark dizisi sıkıştırıcının en verimli çalıştığı girdidir.

Bedel aynı kararın içindedir. Bölüt sıkıştırılmış bir bloktur; içindeki tek bir ölçüme bölütün tamamı çözülmeden erişilemez — erişimin birimi ölçüm değil, bölüttür. Kazanç da verinin sıralı ve düzenli aralıklı olmasına bağlıdır: ölçümler gecikmeli ve karışık sırada gelirse damga farkının farkı sıfır olmaz ve bölüt şişer.

Zaman Aralığı Sorgusu

Bu ailenin sorguları hemen her zaman bir zaman aralığı taşır. Satır yerleşimi zamana göre sıralı tek dizidir: aralığın başı ikili aramayla bulunur, ama pencere içindeki bütün kaynakların ölçümleri okunur. Bölüt yerleşiminde üstveri (kaynak kimliği ve taban damga) sorguyla kesişmeyen bölütleri hiç açmadan eler.

// zaman/aralik.mjs — ayni olcum kumesi iki yerlesimde sorgulaniyor. Satir yerlesimi zamana
// gore sirali tek dizidir: aralik ikili aramayla bulunur, pencere icindeki butun seriler
// taranir. Bolut yerlesiminde ustveri (seri, taban damga) eleme yapar, ama acilan bolut
// butunuyle cozulur. Sayilar kosumdan bagimsizdir.
const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1), NOKTA = 1440;
const seri = [], damga = [];                     // satir yerlesimi: zamana gore sirali
for (let g = 0; g < GUN; g += 1) for (let i = 0; i < NOKTA; i += 1)
  for (let s = 0; s < SERI; s += 1) { seri.push(s); damga.push(T0 + g * 86_400_000 + i * ARALIK); }
const bolut = [];                                // bolut yerlesimi: bolut = (seri, gun)
for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1)
  bolut.push({ seri: s, taban: T0 + g * 86_400_000, son: T0 + (g + 1) * 86_400_000, giris: NOKTA });

function satirSorgu(s, alt, ust) {               // ikili arama + pencere taramasi
  let a = 0, b = damga.length;
  while (a < b) { const o = (a + b) >> 1; if (damga[o] < alt) a = o + 1; else b = o; }
  let taranan = 0, sonuc = 0;
  for (let i = a; i < damga.length && damga[i] < ust; i += 1) {
    taranan += 1; if (s === null || seri[i] === s) sonuc += 1; }
  return { birim: 0, taranan, sonuc };
}
function bolutSorgu(s, alt, ust) {               // ustveriyle eleme, sonra bolutu ac
  let acilan = 0, taranan = 0, sonuc = 0;
  for (const b of bolut) {
    if (s !== null && b.seri !== s) continue;
    if (b.son <= alt || b.taban >= ust) continue;
    acilan += 1; taranan += b.giris;             // bolut butunuyle cozulur
    for (let i = 0; i < b.giris; i += 1) { const t = b.taban + i * ARALIK;
      if (t >= alt && t < ust) sonuc += 1; }
  }
  return { birim: acilan, taranan, sonuc };
}

const G12 = T0 + 11 * 86_400_000;
const sorgular = [["3 nolu raf, 12 Haziran (tam gun)", 3, G12, G12 + 86_400_000],
  ["butun raflar, 12-14 Haziran", null, G12, G12 + 3 * 86_400_000],
  ["3 nolu raf, 12 Haziran 09:00-10:00", 3, G12 + 9 * 3_600_000, G12 + 10 * 3_600_000],
  ["3 nolu raf, butun ay", 3, T0, T0 + GUN * 86_400_000]];
console.log(`satir yerlesimi ${damga.length} giris; bolut yerlesimi ${bolut.length} bolut`);
console.log(`\n${"sorgu".padEnd(35)}${"satir: taranan".padStart(15)}${"bolut".padStart(7)}` +
  `${"bolut: taranan".padStart(15)}${"sonuc".padStart(8)}`);
for (const [ad, s, alt, ust] of sorgular) {
  const x = satirSorgu(s, alt, ust), y = bolutSorgu(s, alt, ust);
  if (x.sonuc !== y.sonuc) throw new Error("iki yol ayni sonucu vermedi");
  console.log(ad.padEnd(35) + String(x.taranan).padStart(15) + String(y.birim).padStart(7) +
    String(y.taranan).padStart(15) + String(x.sonuc).padStart(8));
}
satir yerlesimi 518400 giris; bolut yerlesimi 360 bolut

sorgu                               satir: taranan  bolut bolut: taranan   sonuc
3 nolu raf, 12 Haziran (tam gun)             17280      1           1440    1440
butun raflar, 12-14 Haziran                  51840     36          51840   51840
3 nolu raf, 12 Haziran 09:00-10:00             720      1           1440      60
3 nolu raf, butun ay                        518400     30          43200   43200

Dört satırın üçü bölüt yerleşiminin lehinedir; modelin sınırı dördüncüsündedir.

Tek bir rafın bir günü satır yerleşiminde 17.280 giriş taratıyor, bölüt yerleşiminde tek bölüt açılıyor ve 1.440 giriş okunuyor. Oran tam olarak kaynak sayısıdır: sıralı dizide bir rafın ölçümleri on iki rafınkiyle iç içe durur ve aralık daraltılsa da o iç içelik kalır. Aynı rafın bütün ayında katsayı korunuyor: 518.400 giriş taramaya karşı 30 bölüt ve 43.200 giriş.

İkinci satır bölütlemenin hiçbir şey kazandırmadığı durumu gösteriyor: sorgu bütün rafları istediğinde eleyecek üstveri kalmaz, iki yol da 51.840 giriş okur ve bölüt yoluna yalnız 36 bölüt açma işi eklenir.

Üçüncü satır bedeli taşıyor. Bir saatlik pencere satır yerleşiminde 720 giriş taratıyor; bölüt yerleşiminde aynı soru 1.440 giriş çözdürüyor, çünkü açılan bölüt bir günlüktür ve sıkıştırılmış blok kısmen çözülemez. Sonuç iki yolda da 60 ölçümdür. Bölütün süresi, aralık sorgusunun en küçük okuma birimidir: bölüt büyüdükçe sıkıştırma kazancı artar, dar aralık sorgusunun israfı da artar. Dar aralıklar sık soruluyorsa karşılığı ölçümü daha kaba çözünürlükte ikinci kez saklamaktır — bu kararın adı seyreltmedir ve Vaka Çalışmaları kursunda ölçülmüştür.

Saklama Politikası

Zaman serisi verisinin ötekilerden ayrıldığı son nokta silinmesidir. Bir kitap kaydı silinmez; bir dakikalık ölçüm belirli bir yaştan sonra hiçbir soruya girmez ve saklama (retention) politikası onu düşürür. Soru bu düşürmenin neye mal olduğudur. NS27: saklama süresi yedi gündür.

// zaman/saklama.mjs — saklama politikasinin iki yerlesimde uygulanmasi. Satir yerlesiminde
// suresi dolan her kayit tek tek dusurulur. Bolut yerlesiminde tamami suresi dolmus bolut
// birim olarak dusurulur; yalniz siniri kesen bolut yeniden yazilir. Sayilar kosumdan
// bagimsizdir.
const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1), NOKTA = 1440;
const SIMDI = T0 + GUN * 86_400_000;
const damga = [];                                // satir yerlesimi: zamana gore sirali
for (let g = 0; g < GUN; g += 1) for (let i = 0; i < NOKTA; i += 1)
  for (let s = 0; s < SERI; s += 1) damga.push(T0 + g * 86_400_000 + i * ARALIK);
const bolut = [];
for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1)
  bolut.push({ taban: T0 + g * 86_400_000, son: T0 + (g + 1) * 86_400_000, giris: NOKTA });

function satirSaklama(sinir) {                   // her kayit tek tek dusurulur
  let dokunulan = 0, dusen = 0;
  for (const t of damga) { dokunulan += 1; if (t < sinir) dusen += 1; else break; }
  return { birim: "kayit", dokunulan, dusen, kalan: damga.length - dusen };
}
function bolutSaklama(sinir) {                   // ustveri okunur, bolut birim olarak duser
  let dokunulan = 0, dusen = 0, yeniden = 0;
  for (const b of bolut) {
    if (b.son <= sinir) { dusen += b.giris; continue; }          // hicbir giris acilmaz
    if (b.taban >= sinir) continue;
    dokunulan += b.giris;                                        // siniri kesen bolut
    for (let i = 0; i < b.giris; i += 1)
      if (b.taban + i * ARALIK < sinir) dusen += 1; else yeniden += 1;
  }
  return { birim: "bolut", dokunulan, dusen, kalan: SERI * GUN * NOKTA - dusen, yeniden };
}

console.log(`${damga.length} olcum, ${bolut.length} bolut, bolut basina ${NOKTA} giris`);
console.log(`\n${"saklama".padEnd(22)}${"yerlesim".padEnd(9)}${"dokunulan giris".padStart(16)}` +
  `${"dusen kayit".padStart(13)}${"kalan".padStart(8)}`);
for (const [ad, sure] of [["7 gun (bolut siniri)", 7 * 86_400_000],
    ["7 gun 12 saat", 7.5 * 86_400_000]]) {
  const sinir = SIMDI - sure;
  for (const r of [satirSaklama(sinir), bolutSaklama(sinir)])
    console.log(ad.padEnd(22) + r.birim.padEnd(9) + String(r.dokunulan).padStart(16) +
      String(r.dusen).padStart(13) + String(r.kalan).padStart(8));
}
const y = bolutSaklama(SIMDI - 7.5 * 86_400_000);
console.log(`\nsiniri kesen bolutler: ${y.dokunulan / NOKTA} bolut acildi, ` +
  `${y.yeniden} giris yeniden yazildi`);
518400 olcum, 360 bolut, bolut basina 1440 giris

saklama               yerlesim  dokunulan giris  dusen kayit   kalan
7 gun (bolut siniri)  kayit              397441       397440  120960
7 gun (bolut siniri)  bolut                   0       397440  120960
7 gun 12 saat         kayit              388801       388800  129600
7 gun 12 saat         bolut               17280       388800  129600

siniri kesen bolutler: 12 bolut acildi, 8640 giris yeniden yazildi

Birinci çift bölütlemenin en keskin kazancıdır. İki yerleşim de aynı 397.440 kaydı düşürüp arkasında aynı 120.960 kaydı bırakıyor, ama satır yerleşimi bunun için 397.441 girişe dokunuyor: düşen her kayda bir kez, artı sınırı gösteren bir kayda daha. Bölüt yerleşiminde dokunulan giriş sıfırdır — yalnız 360 bölüt üstverisi okunur ve süresi dolmuş 276 bölüt (397.440 ÷ 1.440, hesap) dosya olarak düşürülür. Silme burada veri işlemi değil, dosya işlemidir.

Sıfır, saklama sınırının bölüt sınırına denk gelmesine bağlıdır. İkinci çift bunu bozuyor: yedi buçuk günlük saklamada sınır bölütlerin ortasından geçer, on iki bölüt açılır, 17.280 girişe dokunulur ve bunun 8.640’ı bölütler yeniden yazıldığı için ikinci kez saklanır. Düşen kayıt sayısı gene iki yolda aynıdır; değişen yalnız iş miktarıdır. Kural şudur: saklama süresi bölüt süresinin katı seçildiğinde silme bedeli sıfıra iner; seçilmediğinde bedel, sınırı kesen bölüt sayısı çarpı bölüt boyudur.

Bölüt süresi kararının üçüncü yüzü budur: bölüt büyüdükçe saklanan bayt düşer, dar aralık sorgusunun israfı ve saklama sınırındaki yeniden yazma artar. Üçünü birden en iyileyen bir değer yoktur.

Özet

  • Zaman serisi verisi sona eklemeli yazılır, aralıkla okunur ve yaşlandığında düşürülür; depo bu üç özelliği veri modelinin içine alır.
  • Satır başına tam damga ve değer 18 bayt tutarken, taban damga + fark kodlaması 2,01 bayta, blok sıkıştırma eklendiğinde 0,63 bayta iniyor — 9.331.200 bayta karşı 329.013 bayt, 28,4 kat. Kazanç verinin sıralı ve düzenli aralıklı olmasına bağlıdır; bedeli, erişim biriminin ölçüm değil bölüt olmasıdır.
  • Bölüt üstverisi aralık sorgusunda eleme yapar: bir rafın bir günü 17.280 giriş taraması yerine tek bölüt ve 1.440 giriştir. Sorgu bütün kaynakları istediğinde eleme yoktur, iki yol da 51.840 giriş okur.
  • Bölüt süresi en küçük okuma birimidir: bir saatlik pencere satır yerleşiminde 720 giriş, bölüt yerleşiminde 1.440 giriş çözdürür ve sonuç 60 ölçümdür.
  • Saklama sınırı bölüt sınırına denk geldiğinde 397.440 kayıt hiçbir girişe dokunulmadan, 276 bölüt dosya olarak düşürülerek silinir; denk gelmediğinde 12 bölüt açılır, 17.280 girişe dokunulur, 8.640 giriş yeniden yazılır.

Sonraki Adım

Beş aile de aynı sırayla anlatıldı: modelin ayırt edici mekaniği, ucuzlattığı ve pahalılaştırdığı sorgu sınıfları. Ortaya çıkan tablo bir sıralama değildir — her aile bir erişim örüntüsünü ucuzlatmak için başkasını pahalılaştırdı ve her ölçümde ters yönlü satır da göründü.

Geriye karar kalıyor: eldeki iş için hangi aile. Sonraki ders bu soruyu tercihle değil ölçütle karşılıyor — erişim örüntüsünün hangi özelliği hangi aileye işaret eder, aynı iş yükü iki ailede koşturulduğunda fark hangi eşikte anlam kazanır, ve bir aile yanlış seçildiğinde bedel nerede görünü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