İçeriğe geç
academia.sh

Ders 01 / 18

Veri Deposu Seçimi

Depo türünün erişim örüntüsüne bağlanması: üç örüntünün istek ve kayıt hızlarının ayrılması, tek depo ile örüntüye göre ayrılmış düzenin dokunulan tablo ve işlenen kayıt cinsinden gerçek bir motorda ölçülmesi, karar ekseninin veri modelinden değil örüntü ile ölçekten çıkarılması ve ayrımın giriş kursundaki depo yükü ile saklanan veri satırlarına ödettiği bedelin hesaplanması.

İçindekiler

Uygulama Katmanı ve Servis Etkileşimi kursu servisleri durumsuzlaştırdı, adreslerini bulmalarını sağladı, işi kuyruğa aldı ve iş akışını koordine etti. Bütün o tasarım tek bir varsayımın üzerinde durdu: veri bir yerde duruyordu ve o yerin ne olduğu hiç sorulmadı. Kanıtı hesaptadır — K01’in tablosundaki depoya ulasan istek/s (138,89) ve saklanan veri GB (712,48) satırları o kurs boyunca hiç oynamadı.

Bu kurs o satırları ele alır ve ilk soru deponun türüdür. İlişkisel modelin ne verdiği, ilişkisel olmayan ailelerin neyi bıraktığı ve şema esnekliğinin gerçekte nereye taşındığı Veri Modelleme ve İlişkisel Kuram kursunda kuruldu; burada veri modelleme öğretilmez. Buradaki eksen başkadır: erişim örüntüsü ve ölçek. Aynı veri, farklı sorularla okunduğunda farklı depo düzeni ister; bu ders o bağı ölçer.

Üç Erişim Örüntüsü

Bir depo kararı yalnız veriye bakarak savunulamaz; savunma sorunun biçimine dayanır. Gönderi takip ve ücretlendirme servisinde üç örüntü vardır ve üçünün hızları K01’in hesabından gelir.

P1 — takip numarasına göre tekil okuma. Anahtar bellidir, yanıt küçüktür (V5: 480 bayt) ve kenar önbelleğinin arkasında saniyede 41,67 istek kalır. Sorgu önceden bilinir ve değişmez.

P2 — taşıyıcıya göre durum olayı yazma. Taşıyıcı sistemleri gönderi başına yedi olay üretir (V4); tepe hızı 97,22 istek/s’dir. Her olay yeni bir kayıttır, var olan bir kaydın üzerine yazılmaz.

P3 — satıcıya göre dönemsel toplu tarama. Ücretlendirme işi otuz günü tarar (V11) ve dört saatlik pencerede saniyede 833,33 kayıt okur. İstek sayısı olarak sıfıra yakındır, kayıt sayısı olarak en büyüğüdür.

Üç örüntünün birlikte söylediği ilk şey şudur: istek hızı ile kayıt hızı ayrı ölçülerdir. P3 depoya günde bir kez uğrar ama P1’in bütün gününden çok kayıt okur. Bir depoyu istek hızıyla boyutlandırmak, bu yüzden eksik bir hesaptır.

Örüntülerin Depoda Bıraktığı İz

Ölçüm için gerçek bir motor kullanılır. node:sqlite ile bellek içinde iki düzen kurulur: tek depo düzeninde gönderi ve olay ayrı tablolardadır, örüntüye göre ayrılmış düzende P1 için ayrıca bir belge tablosu vardır ve o tablo gönderinin durumu ile son üç rota adımını tek satırda tutar (V5’in tarif ettiği yanıt). Ölçülen şey sorgu dilinin kendisi değil, motorun dokunduğu tablo sayısı ile işlediği kayıt sayısıdır; kayıt sayımı motorun her satır için çağırdığı bir sayaç işleviyle yapılır.

// depo/oruntu.mjs — uc erisim oruntusunun iki duzende dokundugu tablo ve isledigi kayit.
// node:sqlite ile gercek bir motor kosar; olculen sey sorgu dili degil, motorun dokundugu
// tablo sayisi ile isledigi kayit sayisidir. Sorgu dili M17'de kuruldu, burada anlatilmaz.
import { DatabaseSync } from "node:sqlite";

const SATICI = 20, GUN = 30, GUNLUK = 100, OLAY = 7;   // model olcegi: satici basina 3000 gonderi
const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE gonderi(no TEXT PRIMARY KEY, satici INTEGER, gun INTEGER);
CREATE INDEX gonderi_satici ON gonderi(satici, gun);
CREATE TABLE olay(no TEXT, sira INTEGER, durum TEXT);
CREATE INDEX olay_no ON olay(no);
CREATE TABLE belge(no TEXT PRIMARY KEY, govde TEXT);`);

let sayac = 0;
db.function("say", { deterministic: false }, (x) => { sayac += 1; return x; });
const adim = (s) => db.prepare(`EXPLAIN QUERY PLAN ${s}`).all()
  .flatMap((r) => r.detail.match(/(?:SCAN|SEARCH) \w+/g) ?? []);

const g = db.prepare("INSERT INTO gonderi VALUES(?,?,?)"), o = db.prepare("INSERT INTO olay VALUES(?,?,?)");
const bl = db.prepare("INSERT INTO belge VALUES(?,?)");
db.exec("BEGIN");
for (let s = 0; s < SATICI; s += 1) for (let d = 0; d < GUN; d += 1) for (let k = 0; k < GUNLUK; k += 1) {
  const no = `TR-${s}-${d}-${k}`;
  g.run(no, s, d);
  for (let i = 0; i < OLAY; i += 1) o.run(no, i, `durum${i}`);
  bl.run(no, JSON.stringify({ durum: `durum${OLAY - 1}`, adim: [OLAY - 3, OLAY - 2, OLAY - 1] }));
}
db.exec("COMMIT");
console.log(`model: ${SATICI * GUN * GUNLUK} gonderi, ${SATICI * GUN * GUNLUK * OLAY} olay, ` +
  `${SATICI} satici, ${GUN} gun (satici basina ${GUN * GUNLUK} gonderi)`);

const OKUMA = [
  ["P1 takip no ile tekil okuma", "tek depo", "TR-10-15-50",
    "SELECT say(g.no), o.durum FROM gonderi g JOIN olay o ON o.no = g.no WHERE g.no = ? ORDER BY o.sira DESC LIMIT 3"],
  ["P1 takip no ile tekil okuma", "ayrilmis", "TR-10-15-50", "SELECT say(govde) FROM belge WHERE no = ?"],
  ["P3 saticiya gore donem taramasi", "tek depo", 10,
    "SELECT say(o.no) FROM olay o JOIN gonderi g ON g.no = o.no WHERE g.satici = ?"],
  ["P3 saticiya gore donem taramasi", "ayrilmis", 10,
    "SELECT say(no) FROM gonderi WHERE satici = ? AND gun BETWEEN 0 AND 29"],
];
console.log(`\n${"oruntu".padEnd(33)}${"duzen".padEnd(10)}${"tablo".padStart(6)}` +
  `${"islenen".padStart(9)}${"donen".padStart(7)}  motorun adimlari`);
for (const [ad, duzen, arg, sql] of OKUMA) {
  sayac = 0;
  const donen = db.prepare(sql).all(arg).length, a = adim(sql);
  console.log(`${ad.padEnd(33)}${duzen.padEnd(10)}${String(new Set(a).size).padStart(6)}` +
    `${String(sayac).padStart(9)}${String(donen).padStart(7)}  ${a.join(" + ")}`);
}

// P2: yazma yolunun uzunlugu — bir durum olayi kac tabloya dokunuyor
const yaz = (duzen) => {
  db.prepare("INSERT INTO olay VALUES(?,?,?)").run("TR-0-0-0", OLAY, "durumYeni");
  if (duzen === "tek depo") return 1;
  db.prepare("UPDATE belge SET govde = ? WHERE no = ?")
    .run(JSON.stringify({ durum: "durumYeni", adim: [OLAY - 2, OLAY - 1, OLAY] }), "TR-0-0-0");
  return 2;
};
console.log(`\nP2 tasiyicidan gelen durum olayi yazmasi: tek depo ${yaz("tek depo")} tablo, ` +
  `ayrilmis ${yaz("ayrilmis")} tablo`);
model: 60000 gonderi, 420000 olay, 20 satici, 30 gun (satici basina 3000 gonderi)

oruntu                           duzen      tablo  islenen  donen  motorun adimlari
P1 takip no ile tekil okuma      tek depo       2        7      3  SEARCH g + SEARCH o
P1 takip no ile tekil okuma      ayrilmis       1        1      1  SEARCH belge
P3 saticiya gore donem taramasi  tek depo       2    21000  21000  SEARCH g + SEARCH o
P3 saticiya gore donem taramasi  ayrilmis       1     3000   3000  SEARCH gonderi

P2 tasiyicidan gelen durum olayi yazmasi: tek depo 1 tablo, ayrilmis 2 tablo

Bu sayılar ölçüm sınıfındadır: bu makinede koşan bir motordan gelirler ve belirlenimlidirler, ama plan seçimi motorun kendi kararıdır ve sürümüne bağlıdır. Model ölçeği de bir seçimdir; satıcı başına 3000 gönderi, K01’in 100 gönderi/gün oranıyla otuz günün karşılığıdır.

Üç satır okunmalı. P1 iki düzende iki kat farklı iş yapıyor. Tek depoda motor iki tabloya dokunuyor ve yedi olay kaydını işleyip üçünü döndürüyor; ayrılmış düzende tek tabloya dokunuyor ve tek kayıt işliyor. Oran yedi katıdır ve bir dizin kararından gelmiyor — iki düzende de dizin var. Fark, verinin okunduğu biçimde durup durmamasıdır.

P3’ün oranı aynı yedi kat, ama karşıt yönde savunuluyor. Ücretlendirme işi olay ayrıntısını istemez, gönderinin ticari alanlarını ister; tek depoda birleştirme onu 21.000 kayda sürüklüyor, ayrılmış düzende 3000 kayıtta kalıyor. Aynı ayrım P1’i ucuzlatan ayrımdır.

P2 bedeli ödüyor. Tek depoda bir durum olayı tek tabloya yazılır; ayrılmış düzende olay tablosuna yazılır ve belge güncellenir, yani yazma yolu iki tabloya uzar. Okuma yolunun kısalması bedava değildir; bedeli yazma yolunda toplanır.

Karar Ekseni

Ölçüm, seçimi dört soruya indirger. Sorular veri modeliyle değil örüntü ve ölçekle ilgilidir.

Anahtar sabit mi. P1 hep aynı anahtarla gelir ve tek kayıt ister. Bu, bir anahtar–değer deposunun (key–value store) yaptığı işin tamamıdır; bildirimsel sorguya, birleştirmeye ve şemada kısıta ihtiyaç yoktur.

Kayıt tek parça hâlinde mi okunuyor. P1’in istediği alanlar birlikte okunur ve birlikte güncellenir; sınır bir belge deposunun (document store) doğal birimiyle örtüşür. P3’ün istediği alanlar aynı sınırın içinde değildir.

Sorgu önceden biliniyor mu. Ücretlendirme dönem, satıcı, tarife ve sözleşme üzerinden değişken süzme yapar ve soruları zamanla artar. Önceden bilinmeyen soru bildirimsel sorgu ister; ilişkisel model burada kazanır ve kaydın kaynağı olarak kalır.

Ölçek tek motoru aşıyor mu. Yazma 97,22 istek/s, günlük artış 976 MB, saklanan veri 712,48 GB. Üç sayı da bir tasarımın tek motorla taşıyabileceği büyüklüktedir; bu yüzden bu derste verilen karar veriyi bölmek değil, okuma yolunu örüntüye göre ayırmaktır. Bölme kararı ölçek büyüdüğünde gündeme gelir ve bu konunun sonraki dersleri onu ele alır.

Dört sorunun yanıtı tek bir depoya işaret etmeyebilir. İlişkisel olmayan aileler — anahtar–değer, belge, geniş sütun (wide column) ve çizge deposu (graph store) — kayıt sisteminin yerine geçmez, ondan türetilen ikincil depolardır; bu konunun son dersi ailelerin kullanım alanlarını ayrı ele alır. Geniş sütun ailesi Veri Modelleme ve İlişkisel Kuram kursunda “sütun ailesi deposu” adıyla geçmişti; ikisi aynı ailedir. Kayıt sisteminin tek kalması kuralı da o kursta kuruldu ve burada değiştirilmez.

Hesaba Geri Dönüş

Ayrımın bedeli K01’in satırlarında görünür. İkincil depo yalnız yolda olan gönderiler için gereklidir; teslim edilmiş bir gönderinin takip sayfası seyrek açılır.

VD1 — takip belgesinin saklanma süresi 30 gün. Gerekçe: gönderi teslim edildikten sonra takip sorgusu seyrekleşir, oysa kaydın kendisi sözleşme uyuşmazlıkları için 730 gün durur (V12). Bu varsayım K01’in tablosuna eklenmez; duyarlılığı aşağıda 90 günle verilir.

// depo/hesap.mjs — oruntuye gore ayrilmis duzenin K01 hesabindaki satirlara etkisi
const OKUMA = 41.67, YAZMA = 97.22;        // K01: onbellek arkasi okuma/s, tepe yazma istek/s
const DEPO = 138.89, ORAN = 2.33;          // K01: depoya ulasan istek/s, depoda yazma/okuma orani
const TARAMA = 833.33;                     // K01: toplu is tarama kayit/s
const ARTIS = 976, SAKLAMA = 712.48;       // K01: gunluk veri artisi MB, saklanan veri GB
const V3 = 400_000, V5 = 480;              // K01: gunluk gonderi, takip yaniti govdesi (bayt)
const VD1 = 30;                            // bu dersin varsayimi: takip belgesi saklama (gun)

// Olculen duzenek: P1 tek depoda 7 kayit / 2 tablo, ayrilmis duzende 1 kayit / 1 tablo;
// P2 tek depoda 1 tabloya, ayrilmis duzende 2 tabloya yaziyor; P3 21000 / 3000 kayit.
const D = [["tek depo", 7, 1, 21000], ["oruntuye gore ayrilmis", 1, 2, 3000]];
const b = (x) => x.toFixed(2);

console.log(`${"duzen".padEnd(24)}${"okuma kayit/s".padStart(15)}${"depo islem/s".padStart(14)}` +
  `${"yazma/okuma".padStart(13)}${"tarama kayit/s".padStart(16)}`);
for (const [ad, kayit, hedef, tarama] of D) {
  const okumaKayit = OKUMA * kayit, islem = OKUMA + YAZMA * hedef;
  console.log(`${ad.padEnd(24)}${b(okumaKayit).padStart(15)}${b(islem).padStart(14)}` +
    `${b((YAZMA * hedef) / OKUMA).padStart(13)}${b(TARAMA * (tarama / 3000)).padStart(16)}`);
}
console.log(`K01 tabani: depoya ulasan ${DEPO} istek/s, yazma/okuma orani ${ORAN}, ` +
  `toplu tarama ${TARAMA} kayit/s`);

const belgeGunluk = (V3 * V5) / 1e6;                 // MB/gun
const belgeSaklanan = (V3 * V5 * VD1) / 1e9;         // GB
console.log(`\n${"buyume kalemi".padEnd(26)}${"K01".padStart(10)}${"ayrilmis".padStart(11)}${"kat".padStart(8)}`);
console.log(`${"gunluk yazilan MB".padEnd(26)}${b(ARTIS).padStart(10)}${b(ARTIS + belgeGunluk).padStart(11)}` +
  `${b((ARTIS + belgeGunluk) / ARTIS).padStart(8)}`);
console.log(`${"saklanan veri GB".padEnd(26)}${b(SAKLAMA).padStart(10)}${b(SAKLAMA + belgeSaklanan).padStart(11)}` +
  `${b((SAKLAMA + belgeSaklanan) / SAKLAMA).padStart(8)}`);
console.log(`takip belgesi deposu ${b(belgeSaklanan)} GB = saklanan verinin ` +
  `1/${(SAKLAMA / belgeSaklanan).toFixed(0)}'i; VD1 = 90 gun olsaydi ${b((V3 * V5 * 90) / 1e9)} GB`);
duzen                     okuma kayit/s  depo islem/s  yazma/okuma  tarama kayit/s
tek depo                         291.69        138.89         2.33         5833.31
oruntuye gore ayrilmis            41.67        236.11         4.67          833.33
K01 tabani: depoya ulasan 138.89 istek/s, yazma/okuma orani 2.33, toplu tarama 833.33 kayit/s

buyume kalemi                    K01   ayrilmis     kat
gunluk yazilan MB             976.00    1168.00    1.20
saklanan veri GB              712.48     718.24    1.01
takip belgesi deposu 5.76 GB = saklanan verinin 1/124'i; VD1 = 90 gun olsaydi 17.28 GB

Bu satırlar hesap sınıfındadır; girdileri K01’in hesapları, ölçülen kayıt oranları ve VD1’dir.

depoda yazma/okuma orani 2,33’ten 4,67’ye çıkıyor, çünkü her durum olayı iki yere yazılıyor ve okuma yolu kısaldığı için okuma tarafı büyümüyor. depoya ulasan istek/s‘nin yerini alan depo işlem hızı 138,89’dan 236,11’e çıkıyor: 1,70 kat. Karşılığında okuma tarafında işlenen kayıt hızı 291,69’dan 41,67’ye, toplu taramanın kayıt hızı da 5833,31’den 833,33’e iniyor. Yani karar yükü yok etmiyor, okuma yolundan yazma yoluna taşıyor.

İki büyüme kalemi de birbirinden ayrılıyor. Günlük yazılan bayt 976 MB’den 1168 MB’ye çıkıyor (1,20 kat), ama saklanan veri yalnız 712,48 GB’den 718,24 GB’ye gidiyor (1,01 kat), çünkü belge otuz gün sonra düşüyor. Sonucun tasarım karşılığı çarpıcıdır: P1’in bütün okuma yükünü karşılayan depo, saklanan verinin 1/124’üdür. VD1 üç katına çıksa 17,28 GB olurdu ve oran yine kırkta bir mertebesinde kalırdı. Depo türü kararı bu yüzden veri hacmine değil, hacmin hangi payının okunduğuna bakar.

Özet

  • Bir depo kararı erişim örüntüsüyle savunulur; P1 41,67 istek/s tekil okuma, P2 97,22 istek/s ekleme, P3 saniyede 833,33 kayıt tarama olarak ayrılır ve istek hızı ile kayıt hızı ayrı ölçülerdir.
  • Aynı okuma tek depoda 2 tabloya dokunup 7 kayıt işlerken, örüntüye göre ayrılmış düzende 1 tabloya dokunup 1 kayıt işliyor; toplu tarama 21.000 kayıttan 3000 kayda iniyor.
  • Bedel yazma yolundadır: bir durum olayı bir tablo yerine iki tabloya yazılıyor.
  • Seçim dört soruyla verilir — anahtar sabit mi, kayıt tek parça mı okunuyor, sorgu önceden biliniyor mu, ölçek tek motoru aşıyor mu; ilişkisel model önceden bilinmeyen sorular yüzünden kaydın kaynağı olarak kalır.
  • K01’e dönüş: depo işlem hızı 138,89’dan 236,11 işlem/s’ye (1,70 kat) ve yazma/okuma oranı 2,33’ten 4,67’ye çıkıyor; okuma tarafında işlenen kayıt 291,69’dan 41,67’ye iniyor.
  • VD1 = 30 gün ile takip belgesi deposu 5,76 GB, yani saklanan verinin 1/124’ü; günlük yazılan bayt 976’dan 1168 MB’ye çıkarken saklanan veri 712,48’den 718,24 GB’ye çıkıyor.

Sonraki Adım

Depo türü kararı verildi ve okuma yolu kısaldı, ama ortada hâlâ tek bir kopya var. O kopya saniyede 236,11 işlem alıyor, 718,24 GB taşıyor ve düştüğünde takip sorgusunun da ücretlendirmenin de yanıtı yok. Okuma yükünü birden çok makineye dağıtmanın ilk yolu veriyi bölmek değil, çoğaltmaktır: aynı veriyi birden çok yerde tutmak. Sonraki ders bu kararı ele alır — ana–kopya düzeninde okuma kaç kopyaya bölünüyor, yazma neden bölünmüyor, her kopyanın gördüğü yazma/okuma oranı ne oluyor, saklanan veri kaç katına çıkıyor ve yazmayı birden çok yerde kabul etmek hangi sayıyı doğuruyor.

İ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