Ders 01 / 16
Meşgul Veritabanı
Aynı belirtiyi veren iki karşıt nedenin bir ölçümle ayrılması: depo katmanı doluyken uygulama katmanının boşta kalması, azaltmayan işin depoda bırakılması ile azaltan işin uygulamaya taşınmasının aynı belirtiyi vermesi, sınır oranının düzeltmenin yönünü seçmesi ve tek bir sayının hangi durumda yanlış planı önerdiği.
İçindekiler
Önceki kurs yirmi kalıbı iki ayrı günün sayısıyla savundu, ama hepsi aynı varsayımın üstünde durdu: arıza fark edilmiş olacak. Devre kesici bir eşiğin aşıldığını, devralma bir kopyanın düştüğünü bilecekti. Farkın nasıl edileceği tasarlanmadı. Bu kurs oradan başlar ve arızadan değil, arızasız günün kendisinden: bir sistem hiç bozulmadan da kötüleşir, kötüleşmenin adı konmadan da düzeltilemez.
Bu dersin ve ardından gelen dokuzun yürüttüğü yol üç adımdır. Belirti bir sayıdır; “yavaş” bir belirti değildir. Ayırt etme, aynı belirtiyi veren en az iki nedeni birbirinden ayıran bir ölçümdür; ölçüm yoksa yapılan şey tahmindir ve bu kursta tahmin diye adlandırılır. Düzeltme, kalıbı giderir ve karşılığında neyin büyüdüğünü sayar. Üçü tamamlanmadan bir karşıt kalıp (antipattern) tanınmış olmaz. Ve hiçbir karşıt kalıp her ölçekte yanlış değildir; her ders aynı biçimin doğru olduğu koşulu bir sayıyla sınırlar.
Belirti: Bir Katman Dolu, Öteki Boş
Belirti gün sonu ücretlendirme akışında görünüyor. K01’in kabaca büyüklük hesabı bu işi
otuz günün 12.000.000 kaydını dört saatlik pencerede taramak olarak yazmıştı: saniyede 833,33
kayıt. İş pencereye sığmıyor. Aynı anda iki doluluk okuması var ve ikisi ters yönde: depo
katmanının işlemci doluluğu tepe yapıyor, uygulama düğümlerinin doluluğu düşük kalıyor.
Doluluk M19/K01 temel-nitelikler/01’de tanımlandı, burada yeniden tanımlanmaz.
İlk refleks bir tahmindir ve tahmin olduğu için adı konur: “depo yetmiyor, daha büyük makine gerekiyor.” Bu cümlenin tehlikesi yanlış olması değil, yönsüz olmasıdır. Aşağıdaki iki neden aynı belirtiyi verir ve düzeltmeleri birbirinin tersidir; yön seçilmeden yapılan her müdahalenin yarısı belirtiyi kötüleştirir.
Neden A — azaltmayan iş depoda. Depo, sonuç kayıt sayısını hiç değiştirmeyen bir iş yapıyor: satır başına dize üretimi, biçimlendirme, sunum için kod çözümü. Girdi kaydı ile çıktı kaydı eşit; harcanan işlemci işi sınırdan geçen veriyi bir bayt bile azaltmıyor.
Neden B — azaltan iş uygulamada. Uygulama ham kayıtları çekip toplamayı kendi yapıyor. Depo satır başına hiç hesap yapmıyor, ama on iki milyon kaydı okuyup seri hâle getirip göndermek zorunda. Depo yine dolu, işin cinsi başka.
Meşgul veritabanı (busy database) bu iki nedenin ortak adıdır: hesabın yanlış katmanda yapılması. Adın tek başına bir işe yaramadığı buradan görünüyor — ad iki nedeni de kapsıyor, düzeltme ise nedeni bilmeyi gerektiriyor.
Dört Plan, Tek Düzenek
Ayırt etme için aynı gün sonu işi dört planla koşuluyor. İki istek var: satıcının kalem dökümü (her gönderi bir satır) ve fatura satırı (satıcı-gün başına bir satır, K01’in V13 = 100 kalem oranı). Her istek iki katman seçimiyle yazılıyor.
Ölçüm gerçek bir motorda yapılıyor ve model ölçeği M19/K04’ün ölçeğidir: 20 satıcı, 30 gün, günde 100 gönderi. Sayılan üç şey var — sınırdan geçen kayıt, iki katmanda yapılan satır işi (satır başına çalıştırılan hesap sayısı, motora kayıtlı bir sayaç işleviyle sayılıyor) ve sınırdan geçen bayt. Süre ölçülmüyor: süre bu makineye bağlıdır, sayılan üç nicelik değildir.
// katman/plan.mjs — ayni gun sonu isinin dort plani: sinirdan gecen kayit, sinir orani, // iki katmanda yapilan satir isi ve tasinan bayt. node:sqlite ile gercek bir motor kosar. import { DatabaseSync } from "node:sqlite"; const SATICI = 20, GUN = 30, GUNLUK = 100, TARIFE = 40, BOLGE = 12; // M19/K04 model olcegi const N = SATICI * GUN * GUNLUK, FATURA = SATICI * GUN; // 60000 gonderi, 600 fatura satiri const db = new DatabaseSync(":memory:"); db.exec(`CREATE TABLE gonderi(no TEXT PRIMARY KEY, satici INT, gun INT, durum INT, agirlik REAL, oran REAL, katsayi REAL, indirim REAL);`); db.exec("BEGIN"); const ekle = db.prepare("INSERT INTO gonderi VALUES(?,?,?,?,?,?,?,?)"); 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 ti = (s * 7 + d + k) % TARIFE, bi = (s + k) % BOLGE; ekle.run(`TR-${s}-${d}-${k}`, s, d, k % 5, 1 + (k % 30), 12 + (ti % 7) * 0.5, 1 + (bi % 5) * 0.1, (s % 9) * 0.01); } db.exec("COMMIT"); const TUTAR = "agirlik * oran * katsayi * (1 - indirim)"; const DURUM = "CASE durum WHEN 0 THEN 'kabul' WHEN 1 THEN 'aktarma' WHEN 2 THEN 'dagitimda' " + "WHEN 3 THEN 'teslim' ELSE 'bekliyor' END"; const AD = ["kabul", "aktarma", "dagitimda", "teslim", "bekliyor"]; const SQL = (i) => ({ // i: satir isini sayan sarmalayici D1: `SELECT no, ${i(`printf('%s / %02d. gun', no, gun)`)} AS baslik, ${i(`printf('%.2f', ${i(TUTAR)})`)} AS tutar, ${i(`printf('%.1f kg', agirlik)`)} AS agirlik, ${i(DURUM)} AS durum FROM gonderi`, D2: `SELECT no, gun, ${i(TUTAR)} AS tutar, agirlik, durum FROM gonderi`, F1: `SELECT satici, gun, count(*) AS kalem, sum(${i(TUTAR)}) AS tutar FROM gonderi GROUP BY satici, gun`, F2: `SELECT satici, gun, agirlik, oran, katsayi, indirim FROM gonderi`, }); const UYGULAMA = { // uygulama katmaninda yapilan satir isi D1: (r) => 0, D2: (r) => { let c = 0; for (const x of r) { c += 4; void [`${x.no} / ${String(x.gun).padStart(2, "0")}. gun`, x.tutar.toFixed(2), `${x.agirlik.toFixed(1)} kg`, AD[x.durum]]; } return c; }, F1: (r) => { let c = 0; for (const x of r) { c += 2; void [x.tutar.toFixed(2), `${x.kalem} kalem`]; } return c; }, F2: (r) => { const t = new Map(); let c = 0; for (const x of r) { c += 1; const k = `${x.satici}/${x.gun}`; const v = t.get(k) ?? { kalem: 0, tutar: 0 }; v.kalem += 1; v.tutar += x.agirlik * x.oran * x.katsayi * (1 - x.indirim); t.set(k, v); } for (const v of t.values()) { c += 2; void [v.tutar.toFixed(2), `${v.kalem} kalem`]; } return c; }, }; const GEREKLI = { D1: N, D2: N, F1: FATURA, F2: FATURA }; // istegin sonucundaki kayit sayisi let sayac = 0; db.function("i", (x) => { sayac += 1; return x; }); const olcu = {}; for (const [ad, sorgu] of Object.entries(SQL((e) => `i(${e})`))) { sayac = 0; const satir = db.prepare(sorgu).all(); const depo = sayac; const bayt = Buffer.byteLength(JSON.stringify(satir.map((r) => Object.values(r)))); olcu[ad] = { gecen: satir.length, gerekli: GEREKLI[ad], oran: satir.length / GEREKLI[ad], depo, uygulama: UYGULAMA[ad](satir), bayt }; } const PLAN = ["D1", "D2", "F1", "F2"]; const ETIKET = { D1: "D1 dokum, bicim depoda", D2: "D2 dokum, bicim uygulamada", F1: "F1 fatura, toplama depoda", F2: "F2 fatura, toplama uygulamada" }; const say = (x) => x.toLocaleString("tr-TR"); console.log(`model: ${say(N)} gonderi, ${SATICI} satici, ${GUN} gun; dokum istegi ${say(N)} satir, ` + `fatura istegi ${FATURA} satir dondurur; taranan kayit dort planda da ${say(N)}`); console.log(`\n${"plan".padEnd(28)}${"gecen kayit".padStart(13)}${"gerekli".padStart(10)}` + `${"sinir orani".padStart(13)}${"depoda satir isi".padStart(18)}${"uygulamada".padStart(12)}` + `${"gecen bayt".padStart(12)}${"kayit basina".padStart(14)}`); for (const p of PLAN) { const o = olcu[p]; console.log(ETIKET[p].padEnd(28) + say(o.gecen).padStart(13) + say(o.gerekli).padStart(10) + o.oran.toFixed(2).padStart(13) + say(o.depo).padStart(18) + say(o.uygulama).padStart(12) + say(o.bayt).padStart(12) + (o.bayt / o.gecen).toFixed(1).padStart(14)); } const OLCEK = 12_000_000 / N, PENCERE = 4 * 3600; // K01: 30 gunun 12.000.000 kaydi, 4 saat console.log(`\nK01 olceginde (olcek ${OLCEK}, pencere ${PENCERE} s, tarama ` + `${(12e6 / PENCERE).toFixed(2)} kayit/s); sinir bandi F2 = K01'in 6 Mbit/s'i olarak olceklendi`); console.log(`${"plan".padEnd(28)}${"depoda satir isi/s".padStart(20)}${"uygulamada/s".padStart(14)}` + `${"sinir bandi Mbit/s".padStart(20)}`); for (const p of PLAN) { const o = olcu[p]; console.log(ETIKET[p].padEnd(28) + ((o.depo * OLCEK) / PENCERE).toFixed(2).padStart(20) + ((o.uygulama * OLCEK) / PENCERE).toFixed(2).padStart(14) + ((6 * o.bayt) / olcu.F2.bayt).toFixed(3).padStart(20)); } console.log(`\nKK1 (depoda bir satir isi uygulamadakinin k katina mal olur) ile plan cifti secimi`); console.log(`${"plan cifti".padEnd(14)}${"k=2".padStart(12)}${"k=6".padStart(12)}${"k=12".padStart(12)}` + `${"gecen kayit".padStart(14)}${"sinir bandi Mbit/s".padStart(20)}`); for (const [a, b] of [["D1", "F1"], ["D2", "F1"], ["D1", "F2"], ["D2", "F2"]]) { const depo = olcu[a].depo + olcu[b].depo, uyg = olcu[a].uygulama + olcu[b].uygulama; const bayt = olcu[a].bayt + olcu[b].bayt; console.log(`${a} + ${b}`.padEnd(14) + [2, 6, 12].map((k) => say(depo * k + uyg).padStart(12)).join("") + say(olcu[a].gecen + olcu[b].gecen).padStart(14) + ((6 * bayt) / olcu.F2.bayt).toFixed(3).padStart(20)); }
model: 60.000 gonderi, 20 satici, 30 gun; dokum istegi 60.000 satir, fatura istegi 600 satir dondurur; taranan kayit dort planda da 60.000 plan gecen kayit gerekli sinir orani depoda satir isi uygulamada gecen bayt kayit basina D1 dokum, bicim depoda 60.000 60.000 1.00 300.000 0 3.991.401 66.5 D2 dokum, bicim uygulamada 60.000 60.000 1.00 60.000 240.000 1.988.049 33.1 F1 fatura, toplama depoda 600 600 1.00 60.000 1.200 13.407 22.3 F2 fatura, toplama uygulamada 60.000 600 100.00 0 61.200 1.302.519 21.7 K01 olceginde (olcek 200, pencere 14400 s, tarama 833.33 kayit/s); sinir bandi F2 = K01'in 6 Mbit/s'i olarak olceklendi plan depoda satir isi/s uygulamada/s sinir bandi Mbit/s D1 dokum, bicim depoda 4166.67 0.00 18.386 D2 dokum, bicim uygulamada 833.33 3333.33 9.158 F1 fatura, toplama depoda 833.33 16.67 0.062 F2 fatura, toplama uygulamada 0.00 850.00 6.000 KK1 (depoda bir satir isi uygulamadakinin k katina mal olur) ile plan cifti secimi plan cifti k=2 k=6 k=12 gecen kayit sinir bandi Mbit/s D1 + F1 721.200 2.161.200 4.321.200 60.600 18.448 D2 + F1 481.200 961.200 1.681.200 60.600 9.220 D1 + F2 661.200 1.861.200 3.661.200 120.000 24.386 D2 + F2 421.200 661.200 1.021.200 120.000 15.158
Tablonun sayıları ölçüm sınıfındadır; K01 ölçeğine çevrilmiş satırlar hesap sınıfındadır. Sınır bandı sütununda mutlak bir bayt iddiası yoktur: modelin ham satırı K01’in 900 baytlık gönderi kaydından dardır, bu yüzden band F2 planı K01’in yazdığı 6 Mbit/s’e eşitlenerek ölçeklendi. Taşınabilir olan planların birbirine oranıdır.
Sınır Oranı Yönü Seçiyor
Üçüncü sütun tanının kendisidir. Sınır oranı, bir isteğin sınırdan geçirdiği kayıt sayısının o isteğin sonucunda gerçekten bulunan kayıt sayısına bölümüdür. Dört planın üçünde 1,00; F2’de 100,00.
Oran 1,00 olduğunda depo zaten azaltmıştır ve depo yine de doluysa suçlu azaltmayan iştir. D1 bunu tam olarak gösteriyor: sınır oranı 1,00 ama depoda satır işi 300.000, yani kayıt başına beş. D2 aynı sonucu üretiyor ve depodaki iş 60.000’e, kayıt başına bire iniyor. Fark biçimlendirme işidir ve o iş sınırdan geçen kaydı bir tane bile azaltmıyor — üstelik artırıyor: geçen bayt 3.991.401’den 1.988.049’a, kayıt başına 66,5 bayttan 33,1 bayta iniyor. Depoda üretilen dizeler, üretildikleri sayılardan geniştir. Düzeltmenin yönü yukarıdır.
Oran 1’in çok üstünde olduğunda azaltma yanlış katmandadır. F2’de depoda satır işi sıfır — depo hiç hesap yapmıyor — ama 60.000 kaydı okuyup seri hâle getirip gönderiyor, oysa isteğin sonucu 600 satır. F1 aynı sonucu 600 kayıtla veriyor ve sınır bandını 6,000’den 0,062 Mbit/s’e indiriyor: 96,8 kat. Düzeltmenin yönü aşağıdır.
İki neden de “depo dolu, uygulama boşta” belirtisini verir, ama biri işi yukarı, öteki aşağı taşımayı ister. Sınır oranı bu iki durumu tek bir sayıyla ayırır ve ayırdığı şey düzeltmenin yönüdür.
Tek Sayı Tanı Vermiyor
Yönü bulmak planı seçmeye yetmez. Son tablo bir varsayımla plan çiftlerini karşılaştırıyor.
KK1 — depoda yapılan bir satır işi, uygulamada yapılan bir satır işinin katına mal olur;
temel değer . Gerekçe: uygulama katmanı durumsuzdur ve düğüm eklenerek yatay ölçeklenir
(M19/K01 temel-nitelikler/03), depo katmanında ise yazma tek noktadadır ve çekirdek eklemek
makine değiştirmektir. İki katmandaki bir birim iş aynı fiyata alınmaz. Duyarlılık ve
sütunlarında verildi.
Ağırlıklı satır işi ölçüsü üç değerinde de aynı çifti öneriyor: D2 + F2, 661.200 birimle (k = 6). Bu öneri yanlıştır. Aynı satırda sınırdan geçen kayıt 120.000, sınır bandı 15,158 Mbit/s; D2 + F1 çiftinde ise 60.600 kayıt ve 9,220 Mbit/s. Satır işi ölçüsü F2’yi ucuz sanıyor, çünkü F2’nin depoya yüklediği iş satır işi değil — okuma, seri hâle getirme ve taşımadır ve o sütunda hiç görünmüyor.
Kursun ilk kuralı budur: bir belirti tek bir sayıyla açıklanmaz. Depo iki ayrı para biriminde meşgul olur — satır başına hesap ve sınırdan geçen bayt — ve bir plan yalnız birine bakılarak seçilirse öteki büyür. Doğru seçim D2 + F1’dir: her iki istekte de gereksiz iş katmandan çıkarılmıştır.
Depoda Kalmanın Doğru Olduğu Koşul
Hesabın depoda yapılması bir yasak değildir; F1 tam olarak bunu yapıyor ve dört planın en ucuzudur. Koşul ölçülebilir: depoda yapılan iş, sınırdan geçen kaydı azalttığı ölçüde haklıdır. F1’de 60.000 satır işi, sınırdan geçen kaydı 60.000’den 600’e indiriyor — kayıt başına 100 birim azaltma, K01’in V13 = 100 oranının kendisi. D1’de 240.000 fazladan satır işi sınırdan geçen kaydı hiç azaltmıyor, baytı 2,01 katına çıkarıyor.
Koşul bir ölçekle sınırlıdır. Fatura satırı başına düşen kalem sayısı 100’den 1’e inseydi — yani her gönderi kendi fatura satırını doğursaydı — F1 ile F2 arasındaki 96,8 katlık band farkı kaybolur ve toplamayı depoda yapmanın hiçbir karşılığı kalmazdı. Aynı biçim, aynı sistemde, başka bir kırılım oranında yanlış tarafa geçer.
Düzeltmenin Karşılığında Büyüyen
D1’den D2’ye geçiş depodaki satır işini 300.000’den 60.000’e indirdi; uygulamadaki satır işi 0’dan 240.000’e çıktı. K01 ölçeğinde bu, depoda saniyede 4166,67 satır işinden 833,33’e inmek ve uygulamada 0’dan 3333,33’e çıkmaktır. KK1’in değerinde takas kârlıdır, olsaydı olmazdı — yani düzeltmenin gerekçesi katmanların ölçekleme asimetrisidir, işin kendisi değil.
İkinci karşılık koddadır ve sayılmaz ama görülür: biçimlendirme kuralı depodan çıkınca sorgunun her tüketicisi onu kendi tarafında tekrar yazmak zorunda kalır. Depoda tek bir yerde duran kural, uygulamada tüketici sayısı kadar yerde durur. Bu bedel D2’yi yanlış yapmaz; bedelin adının konmamış olması yanlış yapar.
Özet
- Tanı üç adımdır: belirti bir sayıdır, ayırt etme aynı belirtiyi veren iki nedeni ayıran bir ölçümdür, düzeltme karşılığında büyüyeni sayar. Ölçümsüz yapılan şey tahmindir.
- Meşgul veritabanının iki nedeni aynı belirtiyi verir: azaltmayan iş depoda (D1) ve azaltan iş uygulamada (F2). Düzeltmeleri birbirinin tersidir.
- Sınır oranı yönü seçer: 1,00 ise iş yukarı taşınır (D1 → D2, geçen bayt 3.991.401 → 1.988.049), 1’in çok üstündeyse aşağı indirilir (F2 → F1, band 6,000 → 0,062 Mbit/s, 96,8 kat).
- Tek sayı tanı vermez: ağırlıklı satır işi ölçüsü D2 + F2’yi öneriyor (661.200, k = 6), oysa o çift sınırdan 120.000 kayıt ve 15,158 Mbit/s geçiriyor; doğru seçim D2 + F1’dir.
- Hesabın depoda kalması, sınırdan geçen kaydı azalttığı ölçüde haklıdır: F1’de 60.000 satır işi kaydı 100 kat azaltıyor. Kırılım oranı 1’e inseydi aynı seçim yanlışa dönerdi.
- Düzeltmenin karşılığı uygulama katmanında büyüyor: satır işi 0 → 240.000 (K01 ölçeğinde saniyede 0 → 3333,33) ve biçimlendirme kuralı tek yerden tüketici sayısı kadar yere dağılıyor.
Sonraki Adım
Sınır oranı, işin hangi katmanda yapılacağını iki katman için çözdü: depo ve uygulama. Oysa zincirin bir ucu daha var ve orası hiç ölçülmedi. D2 planı biçimlendirmeyi depodan çıkardı ve uygulamaya koydu; aynı iş bir adım daha ileri gidip istemciye de konabilir — sunucu ham kaydı gönderir, tarayıcı biçimlendirir. Bu, aynı takasın üçüncü katmanıdır ve orada ölçünün adı değişir: uygulama katmanında iş düğüm eklenerek dağıtılır, istemcide dağıtılacak bir düğüm yoktur. Sonraki ders bu katmanı ele alır: sunucunun gönderdiği yükün istemcideki işi nasıl büyüttüğü, aynı yavaşlık belirtisinin sunucu kaynaklı mı istemci kaynaklı mı olduğunu ayıran ölçüm ve yükü küçültmenin karşılığında sunucuda büyüyen iş.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.