Ders 04 / 15
Darboğaz Analizi
Bir yük testi sonucunun bir katmana bağlanması: uygulama, veritabanı ve aktarım sürelerinin istek içinde ayrı ölçülmesi, aynı istemci belirtisini veren üç kipin üç ayrı katmana atfedilmesi, atfetme kuralının pay eşiğiyle taranması ve zamanın doğru katmanda geçtiği hâlde nedenin başka bir kararda olduğu durumun sayıyla gösterilmesi.
İçindekiler
Önceki ders raporu şu cümleye indirdi: p99 25,35 milisaniye, eşik 20, kaldı. Cümle bir kusuru bildirir ama nerede olduğunu söylemez. Aynı sayı uygulama katmanındaki bir hesaptan, veritabanındaki bir sorgudan ya da yanıtın büyüklüğünden doğmuş olabilir ve üçünün düzeltmesi birbirinden başkadır. Bu ders sonucu bir katmana bağlamayı ele alır.
Katman Payı
Bir isteğin toplam süresi istemcide ölçülür ve tek bir sayıdır. Darboğaz (bottleneck) o sürenin çoğunu tüketen yerdir; bulunması için sürenin istek içinde bölünmesi gerekir. Sunucu her istekte üç saati okur: sorgu öncesi, sorgu sonrası, hesap sonrası ve yanıt yazıldıktan sonra. Aradaki farklar veritabanı, uygulama ve aktarım katmanlarının süreleridir.
Bu üç sayı tek başına yeterli değildir, çünkü her istekte her katman bir miktar iş yapar. Anlamlı olan farktır: aynı düzenekte hiçbir kusur taşımayan bir taban koşumu alınır ve her katmanın süresi tabana göre farkıyla okunur. Katman payı, bir katmanın farkının istemci farkına bölümüdür.
IO6 (varsayım) — bir sonucu bir katmana atfetmek için o katmanın payı en az 0,50 olmalıdır; değilse sonuç atfedilemez sayılır. Gerekçe: pay 0,50’nin altındayken o katmanın tamamen giderilmesi bile belirtinin yarısından azını götürür ve düzeltme kararı savunulamaz. Eşik aşağıda taranır.
Kararın sahibi bir bütçedir: atıf, düzeltmenin hangi ekibe ve hangi çalışmaya gideceğini belirler. Yanlış atıf yalnız zaman kaybettirmez, kusuru yerinde bırakır. Başarım Karşıt Kalıpları ve İzleme kursu belirtiden nedene giden on tanıyı kurmuştu; burada sorulan soru ondan öncedir: sonuç hangi katmana aittir.
Düzenek
Ödünç ucu beş kiple koşar. taban dizinli sorgu ve küçük yanıt; db aynı sorguyu dizinsiz yapar; uyg uygulama katmanında bir hesap turu döner; ag açılışta bir kez çekilmiş 4.000 satırlık bir sayfayı her yanıtta serileştirip yazar; karma aynı sayfayı her istekte veritabanından yeniden çeker. Dördü aynı belirtiyi vermek üzere ayarlanmıştır.
// kutuphane/sunucu.mjs — odunc ucu, bes kiple. Her istekte uc katmanin suresi ayri olculur: // veritabani (sorgu), uygulama (hesap), aktarim (serilestirme + yaniti yazma). // Argumanlar: <baglanti-noktasi> <kip taban|db|uyg|ag|karma> import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const UYE = 500, BASLANGIC = 55000, TUR = 29000, SATIR = 4000; // katalog boyu, hesap turu, sayfa satiri const port = Number(process.argv[2]), kip = process.argv[3]; if (Number.isInteger(port) === false || ["taban", "db", "uyg", "ag", "karma"].includes(kip) === false) { console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> taban|db|uyg|ag|karma"); } else { const db = new DatabaseSync(":memory:"); db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)"); if (kip !== "db") db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)"); db.exec("BEGIN"); const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)"); for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400); db.exec("COMMIT"); const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?"); const cek = db.prepare("SELECT no, kitap_no, gun FROM odunc LIMIT ?"); const onbellek = kip === "ag" ? cek.all(SATIR) : []; // ag kipinde sayfa acilista bir kez cekilir let istek = 0, sdb = 0, suyg = 0, sag = 0, bayt = 0, tarama = 0, hesap = 0; createServer((gelen, yanit) => { const u = new URL(gelen.url, "http://yerel"); if (u.pathname === "/olcut") { yanit.writeHead(200, { "content-type": "application/json" }); yanit.end(JSON.stringify({ istek, sdb, suyg, sag, bayt, tarama, hesap })); return; } istek += 1; const uye = u.searchParams.get("uye") ?? "U-1"; const t0 = performance.now(); const n = say.get(uye).n; // veritabani katmani const sayfa = kip === "karma" ? cek.all(SATIR) : onbellek; tarama += kip === "db" ? BASLANGIC : Math.ceil(Math.log2(BASLANGIC)); const t1 = performance.now(); let ozet = ""; // uygulama katmani if (kip === "uyg") for (let k = 0; k < TUR; k += 1) { ozet = `${uye}/${(n + k) % 97}`; hesap += 1; } const t2 = performance.now(); const govde = JSON.stringify({ uye, odunc: n, ozet, sayfa }); // aktarim katmani sdb += t1 - t0; suyg += t2 - t1; bayt += govde.length; yanit.writeHead(200, { "content-type": "application/json" }); yanit.on("finish", () => { sag += performance.now() - t2; }); yanit.end(govde); }).listen(port); }
Üreteç ilk saniyeyi ölçüme katmaz ve sunucu sayaçlarını o pencerenin başında bir kez daha okur; böylece hem istemci hem sunucu tarafı aynı aralığı anlatır.
// kutuphane/olc.mjs — acik dongulu uretec; istemci suresini ve sunucunun katman sayaclarini // okur. Argumanlar: <baglanti-noktasi> <kip> <hedef hiz> <sure s> import { Agent, request } from "node:http"; const ISINMA = 1; // ilk saniye olcume katilmaz: JIT ve onbellek isinmasi const [port, kip, hiz, sure] = process.argv.slice(2).map((x, i) => (i === 1 ? x : Number(x))); const agent = new Agent({ keepAlive: true, maxSockets: 8192 }); const yuz = (d, p) => { const t = [...d].sort((a, b) => a - b); return t[Math.min(t.length - 1, Math.floor((p / 100) * t.length))]; }; let sira = 0; function birIstek(kayit, sn) { const bas = performance.now(), i = sira++; return new Promise((coz) => { const r = request({ port, path: `/odunc?uye=U-${i % 500}`, agent }, (y) => { y.resume(); y.on("end", () => { kayit.push({ ms: performance.now() - bas, sn }); coz(); }); }); r.on("error", () => { kayit.push({ ms: performance.now() - bas, sn }); coz(); }); r.end(); }); } const olcut = () => new Promise((coz) => { request({ port, path: "/olcut", agent }, (y) => { let g = ""; y.on("data", (p) => { g += p; }); y.on("end", () => coz(JSON.parse(g))); }).end(); }); if (Number.isInteger(port) === false) { console.log("kullanim: node kutuphane/olc.mjs <baglanti-noktasi> <kip> <hedef hiz> <sure>"); } else { const kayit = [], ucus = []; let borc = 0, sonT = 0, o1 = null; const bas = performance.now(); while (performance.now() - bas < sure * 1000) { const t = (performance.now() - bas) / 1000; if (o1 === null && t >= ISINMA) o1 = await olcut(); // isinma penceresi burada kapanir borc += hiz * (t - sonT); sonT = t; while (borc >= 1) { borc -= 1; ucus.push(birIstek(kayit, t)); } await new Promise((c) => setTimeout(c, 5)); } await Promise.all(ucus); const o2 = await olcut(), n = o2.istek - o1.istek; const d = kayit.filter((r) => r.sn >= ISINMA).map((r) => r.ms); console.log(JSON.stringify({ kip, hiz, ornek: d.length, istemci: d.reduce((a, b) => a + b, 0) / d.length, p95: yuz(d, 95), vt: (o2.sdb - o1.sdb) / n, uyg: (o2.suyg - o1.suyg) / n, ag: (o2.sag - o1.sag) / n, bayt: (o2.bayt - o1.bayt) / n, tarama: (o2.tarama - o1.tarama) / n })); } agent.destroy();
// kutuphane/atif.mjs — katman paylarini tabana gore hesaplar, atfi kurala baglar ve // pay esigini tarar. Arguman: <jsonl> import { readFileSync } from "node:fs"; const PAY = 0.5; // IO6: bir katmana atfetmek icin en az pay const GERCEK = { db: "veritabani", uyg: "uygulama", ag: "aktarim", karma: "aktarim" }; const KAT = ["vt", "uyg", "ag"], AD = { vt: "veritabani", uyg: "uygulama", ag: "aktarim" }; const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x)); const bul = (k) => s.find((r) => r.kip === k); const T = bul("taban"), KIP = ["db", "uyg", "ag", "karma"]; const v = (x, n = 3) => x.toFixed(n).replace(".", ","); const g = (x, n) => String(x).padStart(n); const fark = (r) => ({ istemci: r.istemci - T.istemci, vt: r.vt - T.vt, uyg: r.uyg - T.uyg, ag: r.ag - T.ag }); const atif = (r, esik) => { // en buyuk pay esigi asiyorsa o katman const f = fark(r); const en = KAT.reduce((a, k) => (f[k] > f[a] ? k : a), "vt"); return f[en] / f.istemci >= esik ? AD[en] : "atfedilemez"; }; console.log(`taban: istemci ${v(T.istemci)} ms, vt ${v(T.vt)}, uyg ${v(T.uyg)}, aktarim ${v(T.ag)}, ` + `bayt ${Math.round(T.bayt)}`); console.log("kip istemci p95 fark vt payi uyg payi aktarim payi bayt atif"); for (const k of KIP) { const r = bul(k), f = fark(r); console.log(k.padEnd(7) + g(v(r.istemci, 2), 7) + g(v(r.p95, 2), 6) + g(v(f.istemci, 2), 7) + g(v(f.vt / f.istemci, 2), 9) + g(v(f.uyg / f.istemci, 2), 10) + g(v(f.ag / f.istemci, 2), 14) + g(Math.round(r.bayt), 9) + " " + atif(r, PAY)); } console.log("\npay esigi taramasi: dort kipin atfi (gercek katman: db=veritabani, uyg=uygulama, " + "ag ve karma=aktarim)"); console.log("pay esigi dogru atif /4 yanlis atif /4 atfedilemez /4"); for (const e of [0.3, 0.5, 0.7, 0.8, 1.0]) { const a = KIP.map((k) => atif(bul(k), e)); const dogru = a.filter((x, i) => x === GERCEK[KIP[i]]).length; const yok = a.filter((x) => x === "atfedilemez").length; console.log(v(e, 1).padEnd(11) + g(dogru, 13) + g(4 - dogru - yok, 16) + g(yok, 16)); }
# olc.sh — bes kosum: taban ve dort kip, ayni hedef hizda, her biri yeni bir sunucuyla. rm -f sonuc.jsonl for K in taban db uyg ag karma; do node kutuphane/sunucu.mjs 8854 $K & SUNUCU=$! sleep 1.2 node kutuphane/olc.mjs 8854 $K 250 6 >> sonuc.jsonl kill $SUNUCU wait $SUNUCU 2>/dev/null done node kutuphane/atif.mjs sonuc.jsonl
taban: istemci 1,040 ms, vt 0,043, uyg 0,000, aktarim 0,177, bayt 48 kip istemci p95 fark vt payi uyg payi aktarim payi bayt atif db 2,99 4,95 1,95 1,09 -0,00 -0,08 48 veritabani uyg 2,80 4,90 1,76 0,00 0,78 0,02 55 uygulama ag 2,93 4,87 1,89 -0,01 -0,00 0,84 164730 aktarim karma 3,14 6,80 2,10 0,56 -0,00 0,33 164730 veritabani pay esigi taramasi: dort kipin atfi (gercek katman: db=veritabani, uyg=uygulama, ag ve karma=aktarim) pay esigi dogru atif /4 yanlis atif /4 atfedilemez /4 0,3 3 1 0 0,5 3 1 0 0,7 3 0 1 0,8 2 0 2 1,0 1 0 3
Aynı Belirti, Üç Ayrı Katman
Birinci sütun kararın kendisidir ve dört kipte de aynıdır. İstemci ortalaması bu koşumda 2,80 ile 3,14 milisaniye arasında, yani en yüksek ile en düşük arasında yüzde on ikilik bir fark var; p95 üçünde 4,87–4,95, birinde 6,80. Yalnız istemci sayısına bakan bir rapor bu dört yapıyı birbirinden ayıramaz.
Pay sütunları ayırıyor. db kipinde veritabanı payı 1,09, uyg kipinde uygulama payı
0,78, ag kipinde aktarım payı 0,84. Üç kip aynı belirtiyi veriyor ve üç ayrı katmana
atfediliyor; düzeltmeleri de birbirinden başkadır — dizin eklemek, hesabı seyreltmek, yanıtı
küçültmek.
Bir payın 1’i aşması ölçümün sınırını gösterir. db kipinde veritabanı farkı istemci
farkından büyük çıktı; iki koşumun taban gürültüsü aynı değildir ve fark küçüldükçe bu
gürültü paya oranla büyür. Kural yalnız en büyük paya baktığı için sonuç değişmiyor, ama
pay 1’i aştığında okunacak şey “veritabanı belirtinin tamamını açıklıyor”dur, daha fazlası
değil.
Atfetme Yanlış Olduğunda
Dördüncü satır kuralın kırıldığı yerdir. karma kipinde veritabanı payı 0,56, aktarım payı
0,33; kural veritabanını suçluyor. Ölçüm doğrudur: zaman gerçekten sorguda geçiyor, çünkü
sorgu 4.000 satır okuyor. Sonuç yine de yanlıştır.
Kanıt son sütunda ve koşumdan bağımsızdır: karma ile ag, yanıt başına aynı 164.730
baytı gönderiyor. ag kipinde o sayfa açılışta bir kez çekilmiş ve önbelleğe alınmıştır,
bu yüzden veritabanı payı sıfırdır ve kural aktarımı suçlar. İki kipin farkı sorguda değil,
yanıtın büyüklüğünde alınan karardadır — ve o karar ikisinde de aynıdır. karma kipinde
veritabanına dizin eklemek hiçbir şey değiştirmez; sorgu zaten dizin gerektirmiyor, 4.000
satır okuyor.
Kural şudur: katman payı zamanın nerede geçtiğini söyler, onu hangi kararın ürettiğini
söylemez. İkincisini söyleyen ölçüm koşumdan bağımsız niceliktir — gönderilen bayt,
okunan satır, taranan kayıt. Bu koşumda db kipi istek başına 55.000 satır tarıyor,
ötekiler 16; ag ve karma istek başına 164.730 bayt gönderiyor, ötekiler 48 ve 55. İki
katmanın aynı belirtiyi verdiği her yerde ayırt eden ölçüm budur.
Pay Eşiği Taraması
İkinci tablo IO6’nın 0,50’sini kaydırıyor. Tablonun tek tek sayıları koşuma bağlıdır — pay,
ölçülen süre farklarından çıkar ve bu makinede koşumdan koşuma oynar — ama yönü koşumdan
bağımsızdır ve karar o yönde verilir. Düşük eşikte dört kipin dördü de bir katmana bağlanır
ve karma yanlış atıf alır; eşik yükseldikçe önce yanlış atıf sıfırlanır, sonra doğru
atıflar da eşiğin altında kalıp atfedilemez olur. Yukarıdaki koşumda dönüş 0,70’te
başlıyor; başka bir koşumda bir basamak önce ya da sonra başlayabilir, çünkü eşiği geçen şey
payın kendisidir.
İki hata biçimi arasındaki takas budur. Düşük eşik suçsuz bir katmanı suçlar; yüksek eşik kusuru kimseye bağlayamaz ve bulgu bir kayda dönüşmeden kapanır. IO6’nın 0,50’si yanlış atfı sıfırlamaz — onu sıfırlayan şey eşik değil, koşumdan bağımsız niceliğin okunmasıdır.
Bedel ve Kaçan Sınıf
Bedelin koşuma bağlı yanı ortama bağlıdır: beş koşum bu makinede kırk saniyenin altında sürdü. Koşumdan bağımsız yanı sayılır — beş sunucu süreci, koşum başına altı saniyenin beş saniyesi ölçülüyor (ilk saniye ısınmaya ayrıldı) ve her koşumda istek başına dört saat okuması yapılıyor, yani enstrümantasyon istek başına dört ek ölçüm ekliyor.
Kaçan sınıf da sayılabilir: bu düzenek yalnız sunucu içindeki süreyi bölüyor. İstemci
farkı ile ölçülen katmanların toplamı arasındaki artık — uyg kipinde farkın yüzde
yirmisi — hiçbir katmana yazılmıyor. Ağ turu, olay döngüsünde bekleme ve işletim sisteminin
soket işi orada durur. Doygunluğa yaklaşan bir sistemde bu artık büyür ve pay tabanlı
atfetme kullanılamaz hâle gelir; o durumda okunacak ölçüt katman payı değil, önceki dersin
verim–gecikme çiftidir.
Özet
- Katman payı, bir katmanın taban koşumuna göre farkının istemci farkına bölümüdür; atıf kuralı en büyük payın IO6’nın 0,50’sini aşmasını ister.
- Aynı istemci belirtisi (ortalama 2,80–3,14 ms) üç ayrı katmana atfedildi: veritabanı 1,09, uygulama 0,78, aktarım 0,84 payla.
- Atfetme
karmakipinde yanlış: zaman gerçekten sorguda geçiyor amaagile aynı 164.730 baytı gönderiyor; nedeni sorgu değil, yanıtın büyüklüğü kararıdır. - Pay eşiği taraması takası gösteriyor: eşik yükseldikçe önce yanlış atıf, sonra doğru atıf düşer ve atfedilemez koşum artar; dönüşün hangi basamakta olduğu koşuma bağlıdır.
- Katman payı zamanın nerede geçtiğini söyler; hangi kararın onu ürettiğini koşumdan bağımsız nicelik söyler (55.000 taranan satır, 164.730 gönderilen bayt).
Sonraki Adım
Buraya kadarki her sayı tek bir koşumdan okundu. Bu dersin taban koşumu bir kez alındı ve bütün paylar ona bölündü; bir katman payının 1’i aşması da, aynı düzeneğin iki koşumunun aynı sayıyı vermediğini gösteriyor. Üstelik düzenek ilk saniyeyi ölçüme katmıyor — yani bir ısınma etkisinin varlığı kabul edildi ama büyüklüğü hiç ölçülmedi. Sonraki ders ölçümün kendisini sınar: ısınmanın kaç koşum sürdüğünü, aynı yapının koşumları arasındaki değişkenliği ve iki yapı arasındaki bir farkın anlamlı sayılabilmesi için kaç koşum gerektiğini sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.