İçeriğe geç
academia.sh

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 karma kipinde yanlış: zaman gerçekten sorguda geçiyor ama ag ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat