İçeriğe geç
academia.sh

Ders 13 / 25

Parçalama Kalıpları

Bölümlerin ayrı makinelere dağıtılması, parça anahtarının uygulamaya dayattığı sınırlar, çapraz parça sorgusunun kuyruk gecikmesi, parçalar arası birleştirme ve benzersizlik sorunu ile yeniden dengelemede taşınan veri miktarı.

İçindekiler

Önceki ders bir tabloyu bölümlere ayırdı ve bütün bölümler aynı veritabanının içindeydi. Bölümleri ayrı makinelere dağıtmak, aynı fikrin bir adım ötesidir ve o adımda nitelik değişir.

Parçalama (sharding), verinin yatay olarak bölünüp birden çok bağımsız veritabanı örneğine dağıtılmasıdır. Her parça kendi süreçlerini, kendi tampon havuzunu, kendi günlüğünü ve kendi kilit yöneticisini taşır. Motor Mimarisi konusunda kurulan her yapı parça başına bir kez vardır ve parçalar birbirinin varlığından haberli değildir. Kazanç budur: yazma yükü, bellek ve disk gerçekten bölünür. Bedel de budur: bir tablonun tek bir motor içinde bedelsiz sayılan üç yeteneği — birleştirme, benzersizlik ve işlem — parçalar arasında bedelsiz olmaktan çıkar.

Bu ders o üç bedeli sayar. Ölçümler node ile kurulan modellerdir; gerçek bir dağıtık veritabanı değildirler. Modelin sayabildiği şey ilişkilerdir — hangi seçim hangi maliyeti kaç kat büyütür — mutlak değerler değil.

Parça Anahtarı Uygulamaya Ne Dayatır

Parça anahtarı (shard key), bir satırın hangi parçaya düşeceğini belirleyen sütundur. Kütüphane verisinde iki aday vardır ve seçim, hangi sorgunun ucuz olacağını belirler.

Üye kimliğine göre parçalanan bir kurulumda bir üyenin bütün ödünç kayıtları tek bir parçadadır. “Bu üyenin açık ödünçleri” sorusu tek parçaya gider; sorgu, tek makineli bir kurulumdaki sorgudan farksızdır. Aynı kurulumda “bu kitabı kimler ödünç aldı” sorusunun yanıtı bütün parçalara dağılmıştır.

Kitap kimliğine göre parçalanan kurulumda ilişki tersine döner. Hangi seçimin doğru olduğu veriden değil, iş yükünün soru dağılımından çıkar: en sık sorulan ve en düşük gecikme beklenen soru hangi anahtarı süzüyorsa parça anahtarı odur.

Bu karar, tek makineli bir kurulumda geri alınabilir bir dizin kararına benzemez. Dizin kaldırılıp yeniden kurulabilir; parça anahtarı değiştirmek verinin tamamının taşınması demektir. Parçalamanın uygulamaya ilk dayattığı şey budur: sorgu biçimleri veri düzenini belirler ve sonradan değiştirilmesi pahalıdır.

Çapraz Parça Sorgusu

Parça anahtarını süzmeyen bir sorgu bütün parçalara gönderilir, her parçadan kısmi sonuç alınır ve eşgüdümcü bunları birleştirir. Bu kalıba dağıt–topla (scatter–gather) denir ve maliyeti sezgiye aykırıdır: yapılan toplam iş aynı kalsa bile yanıt süresi bozulur, çünkü sorgu en yavaş parçayı beklemek zorundadır.

cat > capraz.mjs <<'JS'
// Model: 8 parcaya dagitilmis odunc kayitlarinda sorgu turlerinin dokundugu parca sayisi
// ve yanit suresi. Gercek bir dagitik veritabani degil; sayilan sey dokunulan parca
// sayisi ile kuyruk gecikmesi arasindaki iliskidir.
const PARCA = 8, SORGU = 20000;
// Yinelenebilir sozde rastgele uretec (dogrusal kongruansiyel).
let durum = 20240728;
const rastgele = () => (durum = (durum * 1103515245 + 12345) % 2147483648) / 2147483648;

// Bir parcanin yanit suresi: 2 ms taban + agirlikli kuyruk. Yuzde bir olasilikla yavas.
function parcaSuresi() {
  const r = rastgele();
  return r < 0.99 ? 2 + 3 * r : 40 + 60 * r;
}
function olc(dokunulanParca) {
  const sureler = [];
  for (let i = 0; i < SORGU; i++) {
    let enYavas = 0;
    for (let p = 0; p < dokunulanParca; p++) enYavas = Math.max(enYavas, parcaSuresi());
    sureler.push(enYavas + 1);            // 1 ms eşgüdüm payı
  }
  sureler.sort((a, b) => a - b);
  const yuzde = (q) => sureler[Math.floor(SORGU * q)];
  return { orta: yuzde(0.5), p95: yuzde(0.95), p99: yuzde(0.99), enKotu: sureler[SORGU - 1] };
}
console.log('dokunulan parça   ortanca      %95      %99   en kötü');
for (const d of [1, 2, 4, 8]) {
  const s = olc(d);
  console.log(String(d).padStart(14),
              s.orta.toFixed(1).padStart(9), s.p95.toFixed(1).padStart(8),
              s.p99.toFixed(1).padStart(8), s.enKotu.toFixed(1).padStart(9));
}
console.log('\n— sorgu karışımı: parça anahtarı var mı —');
const karisim = [
  ['üyenin ödünçleri (parça anahtarı var)', 1],
  ['kitabın ödünç geçmişi (anahtar yok)',   PARCA],
  ['aylık rapor (anahtar yok)',             PARCA],
];
for (const [ad, d] of karisim) {
  const s = olc(d);
  console.log(ad.padEnd(38), 'parça', String(d).padStart(2),
              '| ortanca', s.orta.toFixed(1).padStart(5),
              '| %99', s.p99.toFixed(1).padStart(6), 'ms');
}
JS
node capraz.mjs
dokunulan parça   ortanca      %95      %99   en kötü
             1       4.5      5.9      6.0     101.0
             2       5.1      5.9    100.6     101.0
             4       5.5      6.0    100.8     101.0
             8       5.8    100.5    100.9     101.0

— sorgu karışımı: parça anahtarı var mı —
üyenin ödünçleri (parça anahtarı var)  parça  1 | ortanca   4.5 | %99    6.0 ms
kitabın ödünç geçmişi (anahtar yok)    parça  8 | ortanca   5.8 | %99  100.9 ms
aylık rapor (anahtar yok)              parça  8 | ortanca   5.8 | %99  100.9 ms

Modelde her parçanın yüzde bir olasılıkla yavaş yanıt verdiği varsayılmıştır. Ortanca süre bu varsayımdan neredeyse etkilenmiyor: tek parçada 4,5 ms, sekiz parçada 5,8 ms. Kuyruk ise çöküyor. Tek parçada yüzde 99’luk dilim 6,0 ms iken sekiz parçada 100,9 ms oldu — on yedi kat.

Aritmetiği doğrudandır. Tek bir parçanın yavaş olma olasılığı yüzde bir ise, sekiz parçadan en az birinin yavaş olma olasılığı 10,9980,0771 - 0{,}99^8 \approx 0{,}077, yani yüzde 7,7. Dağıt–topla, her parçanın nadir kusurunu sorgunun sık kusuruna çevirir.

Buradan parçalamanın en çok yanlış anlaşılan yanı çıkar: parçalama çıktıyı (throughput) büyütür, tek bir sorgunun gecikmesini küçültmez. Ortalama sürelere bakan bir ölçüm bu bozulmayı hiç görmez; görülmesi için kuyruk dilimlerine bakmak gerekir.

Birleştirme, Benzersizlik ve İşlem

Üç yetenek parça sınırında niteliğini değiştirir.

Birleştirme. Üye kimliğine göre parçalanmış ödünç kayıtları, aynı anahtara göre parçalanmış üye tablosuyla parça içinde birleştirilebilir; iki tablonun ilgili satırları aynı makinededir. Buna birlikte yerleştirme (co-location) denir ve parçalı bir tasarımda birleştirmenin ucuz kalmasının tek yoludur. Kitap tablosu ise ödünç kayıtlarıyla birlikte yerleştirilemez, çünkü ödünç kaydı iki farklı anahtara birden ait olamaz. Yaygın çözüm, küçük ve seyrek değişen tabloların her parçaya kopyalanmasıdır; kütüphane örneğinde şube listesi böyle bir tablodur.

Benzersizlik. Tek bir motorda benzersizlik kısıtı bir dizinle uygulanır. Parçalar arasında böyle bir dizin yoktur: her parça yalnız kendi satırlarını görür, dolayısıyla yalnız parça içinde benzersizlik verebilir. Genel benzersizlik gerektiren bir alan — üyenin e-posta adresi gibi — ya parça anahtarı olur, ya ayrı bir arama tablosunda tutulur. Otomatik artan kimlikler de aynı nedenle çalışmaz; parçalı kurulumlarda kimlikler merkezden değil, parça numarasını içeren ya da çakışma olasılığı ihmal edilebilir olacak kadar geniş bir üreteçten alınır.

İşlem. ACID Özellikleri dersinde tanımlanan atomiklik tek bir motorun günlüğüne dayanır. İki parçaya birden yazan bir işlem iki ayrı günlüğe yazar ve ikisinin birlikte kesinleşmesi ayrı bir protokol gerektirir: eşgüdümcü önce bütün parçalara hazırlanma buyruğu verir, hepsinden onay alırsa kesinleştirir. Bu protokolün bedeli ek gidiş dönüşler değil, askıda kalma olasılığıdır — eşgüdümcü hazırlık ile kesinleştirme arasında düşerse parçalar kilitlerini tutarak bekler. Parçalı tasarımın altın kuralı bu yüzden şudur: bir işlem tek bir parçanın içinde kalmalıdır. Kalamıyorsa, tasarım ya parça anahtarını ya işlem sınırını yanlış seçmiştir.

Yeniden Dengeleme

Parça sayısı sabit kalmaz. Yeni bir parça eklendiğinde anahtar–parça eşlemesi değişir ve değişen her eşleme, veri taşınması demektir. Taşınan miktar eşlemenin nasıl kurulduğuna bağlıdır ve bu hesaplanabilir.

cat > dengele.mjs <<'JS'
// Model: parca sayisi degistiginde tasinmasi gereken satir orani. Gercek bir yeniden
// dengeleme yordami degil; sayilan sey yalnizca eslemenin tasima maliyetine etkisidir.
const UYE = 120000, KOVA = 4096;
const M = 2654435761n;
const karma = (x) => Number(((BigInt(x) * M) % 4294967296n) >> 8n);

// Uye basina odunc sayisi: az sayida uye cok odunc alir.
const uye = [];
{
  let toplam = 0;
  const agirlik = [];
  for (let i = 1; i <= UYE; i++) { const a = 1 / Math.pow(i, 0.8); agirlik.push(a); toplam += a; }
  for (let i = 0; i < UYE; i++)
    uye.push({ id: i + 1, odunc: Math.max(1, Math.round(agirlik[i] / toplam * 2000000)) });
}
const toplamSatir = uye.reduce((t, u) => t + u.odunc, 0);

function modTasima(eski, yeni) {
  let tasinan = 0;
  for (const u of uye) if (karma(u.id) % eski !== karma(u.id) % yeni) tasinan += u.odunc;
  return tasinan;
}
// Sanal kova: anahtar sabit sayida kovaya duser, kovalar parcalara atanir.
function kovaAtama(P) {
  const atama = new Array(KOVA);
  for (let b = 0; b < KOVA; b++) atama[b] = b % P;
  return atama;
}
function kovaTasima(eski, yeni) {
  const eskiAtama = kovaAtama(eski);
  const hedef = Math.floor(KOVA / yeni);
  const sayac = new Array(yeni).fill(0);
  const yeniAtama = new Array(KOVA).fill(-1);
  for (let b = 0; b < KOVA; b++) {                 // once yerinde kalabilecekler
    const p = eskiAtama[b];
    if (p < yeni && sayac[p] < hedef) { yeniAtama[b] = p; sayac[p]++; }
  }
  let p = 0;
  for (let b = 0; b < KOVA; b++) {                 // bosta kalanlar aza dogru dagitilir
    if (yeniAtama[b] !== -1) continue;
    while (p < yeni - 1 && sayac[p] >= hedef) p++;
    yeniAtama[b] = p; sayac[p]++;
  }
  let tasinan = 0;
  for (const u of uye) {
    const b = karma(u.id) % KOVA;
    if (eskiAtama[b] !== yeniAtama[b]) tasinan += u.odunc;
  }
  return tasinan;
}
console.log('modeldeki toplam ödünç:', toplamSatir);
console.log('geçiş        doğrudan bölme        sanal kova');
for (const [e, y] of [[8, 9], [8, 12], [8, 16]]) {
  const a = modTasima(e, y), b = kovaTasima(e, y);
  const yuzde = (x) => (100 * x / toplamSatir).toFixed(1) + '%';
  console.log(`${e}${y}`.padEnd(12),
              (a.toLocaleString('tr-TR') + ' (' + yuzde(a) + ')').padStart(21),
              (b.toLocaleString('tr-TR') + ' (' + yuzde(b) + ')').padStart(18));
}
JS
node dengele.mjs
modeldeki toplam ödünç: 2001046
geçiş        doğrudan bölme        sanal kova
8 → 9            1.793.865 (89.6%)    228.270 (11.4%)
8 → 12           1.337.553 (66.8%)    675.360 (33.8%)
8 → 16           1.006.912 (50.3%)    982.390 (49.1%)

Doğrudan bölme — anahtarın karmasını parça sayısına bölüp kalanı almak — sekizden dokuza geçişte kayıtların yüzde 89,6’sını taşıttı. Tek bir parça eklemek için verinin neredeyse tamamının yerinden oynaması gerekiyor, çünkü bölen değiştiğinde kalan da neredeyse her anahtar için değişir.

Sanal kova (virtual bucket) yaklaşımı araya bir katman koyar: anahtar önce sabit sayıda kovaya (burada 4.096) düşürülür, kovalar parçalara atanır. Parça sayısı değiştiğinde anahtar–kova eşlemesi hiç değişmez; yalnız kova–parça ataması güncellenir ve yalnız yeni parçaya devredilen kovaların verisi taşınır. Sekizden dokuza geçişte taşınan oran yüzde 11,4 oldu — kuramsal alt sınır olan 1/9 ≈ yüzde 11,1’e çok yakın.

Üçüncü satır bir uyarı taşır. Sekizden on altıya geçişte iki yöntem eşitleniyor (yüzde 50,3’e karşı yüzde 49,1), çünkü parça sayısını ikiye katlamak doğrudan bölmenin en şanslı durumudur: her parça tam ikiye ayrılır. Bu, ikiye katlamayı geçerli bir büyüme stratejisi yapar — ama ancak ikiye katlanabildiği sürece. Kaynak planlaması buna zorlandığında maliyet, gerekmeyen kapasitenin peşinen alınmasıdır.

Taşınan oranın kendisi de son bedel değildir. Taşıma sırasında iki kopya bir süre birlikte yaşar, okumalar iki yerden yanıtlanabilir olmalıdır ve yazmalar taşınmakta olan kovalar için ya durdurulur ya iki yere birden yazılır. Bu yüzden yeniden dengeleme, parça sayısından çok, taşımanın kesintisiz yapılıp yapılamadığıyla ölçülen bir yetenektir.

Parçalamaya Ne Zaman Gerek Yoktur

Parçalama, sayılan bedellerin hepsini uygulama koduna taşır. Bu yüzden son soru, ondan kaçınmanın yollarının tükenip tükenmediğidir. Bu kursta ölçülen üç yol çoğu zaman daha önce gelir: doğru dizinler okuma maliyetini büyüklük sırasıyla düşürdü, bölümleme eski verinin kaldırılmasını sabit maliyete indirdi, okuma yükü ise çoğaltmayla dağıtılabilir.

Parçalamayı gerçekten zorunlu kılan tek etken yazma yüküdür ve o da tek bir motorun günlük ile denetim noktası kapasitesi aşıldığında. Karar bu ölçüyle verilir; “tablo büyük” ölçüsüyle değil.

Özet

  • Parçalama veriyi bağımsız veritabanı örneklerine dağıtır; parça anahtarı hangi sorgunun tek parçada kalacağını belirler ve sonradan değiştirilmesi verinin tamamının taşınması demektir.
  • Dağıt–topla kalıbı ortanca süreyi az etkiler, kuyruğu bozar: modelde tek parçada 6,0 ms olan yüzde 99’luk dilim sekiz parçada 100,9 ms oldu.
  • Birleştirme ancak birlikte yerleştirilmiş tablolarda ucuz kalır; benzersizlik yalnız parça içinde uygulanabilir; iki parçaya yazan işlem ayrı bir protokol ve askıda kalma riski getirir.
  • Doğrudan bölmede sekizden dokuz parçaya geçiş kayıtların yüzde 89,6’sını taşıttı; sanal kova katmanıyla aynı geçiş yüzde 11,4’e indi.
  • Parçalamayı zorunlu kılan etken yazma yükünün tek motorun kapasitesini aşmasıdır; dizin, bölümleme ve çoğaltma bu eşiğe kadar daha ucuz yollardır.

Sonraki Adım

Bu konu verinin nasıl düzenlendiğini ve hangi yollardan okunduğunu tamamladı: dizin türleri, sütun sırası, kapsama, bakım, bölümleme ve parçalama. Motor Mimarisi konusuyla birlikte artık verinin nerede durduğu ve nasıl bulunduğu kurulmuş durumda — sistem çalışır hâldedir. Çalışır olmak ile işletiliyor olmak ayrı şeylerdir. Sonraki konu bu ayrımla açılır ve ilk sorusu en eskisidir: bu veriye kim erişebilir ve ne yapabilir? Ödünç masasındaki uygulamanın ödünç kaydı açması gerekir, üye kaydını silmesi gerekmez; bu ayrımın yetki düzeyinde nasıl kurulacağı sonraki dersin konusudur.

İ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