Ders 02 / 15
Test Senaryosu Modelleme
Yükün sayısı değil biçimi: iş karışımının hangi payında kusurun görünür olduğunun ölçülmesi, arama ağırlıklı gerçekçi olmayan bir senaryonun aynı hedef hızda yanlış geçme üretmesi, kapalı döngülü üretecin sanal görevli sayısı ile düşünme süresine göre hedef hıza hiç ulaşamaması ve aynı nominal hızın üç ayrı p95 değeri vermesi.
İçindekiler
Önceki dersin bütün koşumları tek bir uca, sabit hızda ve aralıksız istek gönderdi. Gerçek bir gişe günü böyle değildir: görevli önce arar, sonra ödünç verir ve iki işlem arasında durur. Bu ders yükün sayısını değil biçimini ele alır ve şunu ölçer: aynı hedef hız, senaryo değiştiğinde aynı sistemde başka bir karar üretir.
Senaryonun İki Parametresi
İş karışımı (workload mix), toplam yükün istek türlerine dağılımıdır. Bir sistemin uçları aynı maliyette değildir; karışım, yükün hangi yola düşeceğini belirler ve böylece hangi kusur sınıfının doyuma yaklaşacağını da belirler.
Düşünme süresi (think time), sanal bir kullanıcının iki işlemi arasındaki duraktır ve yalnız kapalı döngülü üreteçte anlam taşır. Kapalı döngüde sanal görevli vardır, her biri yanıtı bekler, kadar durur ve yeni istek gönderir; ulaşılan hız olur. Buradaki sistemin kendi yanıt süresidir — yani yük, sınanan sistemin bir işlevidir. Açık döngülü üreteç bu geri beslemeyi taşımaz.
Eşik ve Varsayımlar
Eşik önceki dersten aynen gelir. IO1 (varsayım + hesap) — gişe akışının 60 ms’lik sunucu bütçesinin üç servise eşit paylaştırılması: p95 ≤ 20 ms. IO2 (varsayım) — tepe saat hedefi 1.000 istek/s. Bu derste iki varsayım daha yazılır.
IO3 (varsayım) — tepe saatte gişe iş karışımı: isteklerin yarısı ödünç, yarısı arama. Gerekçe: gişe tezgâhına gelen kişi ödünç almak için gelir; katalog araması ayrı uçlardan da yapılır ama tezgâh trafiğinin yarısını aşmaz. IO4 (varsayım) — kapalı döngü modelinde sanal görevli sayısı hedef hızdan türetilir: , ms. İki çift sınanır: 22 görevli / 20 ms ve 202 görevli / 200 ms. İkisinin de nominal hızı 1.000 istek/s’tir.
Kararın sahibi bu derste yayım değil, testin kendisidir: senaryo seçimi hangi kusur sınıfının ölçüleceğini belirler ve yanlış seçilmiş bir senaryo yeşil rapor eder.
Düzenek
Servisin iki ucu var. /ara katalogda kitap numarasına göre arar ve her iki yapıda da
dizinlidir; /odunc üyenin açık ödünç sayısını doğrulayıp beş kayıt yazar ve dizin yalnız
sağlam yapıda vardır. Katalog 110.000 kayıtla başlar, yani tam tarama önceki derstekinden
altı kat pahalıdır.
// kutuphane/sunucu.mjs — gise servisi: /ara katalogda kitap arar, /odunc uyenin acik odunc // sayisini dogrulayip bes kayit yazar. Argumanlar: <baglanti-noktasi> <yapi 0|1> // 0 = odunc yolunda uye dizini yok (tam tarama), 1 = dizinli. import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const UYE = 500, BASLANGIC = 110000, KITAP = 5; // uye basina 220 kayit; gise islemi 5 kitap const port = Number(process.argv[2]), yapi = process.argv[3]; if (Number.isInteger(port) === false || ["0", "1"].includes(yapi) === false) { console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <yapi 0|1>"); } else { const db = new DatabaseSync(":memory:"); db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)"); db.exec("CREATE INDEX odunc_kitap ON odunc(kitap_no)"); // arama yolu her iki yapida dizinli if (yapi === "1") 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 ara = db.prepare("SELECT no, uye_no, gun FROM odunc WHERE kitap_no = ? LIMIT 5"); const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?"); const ekle = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)"); let arama = 0, odunc = 0, kayit = BASLANGIC, tarama = 0; createServer((gelen, yanit) => { const u = new URL(gelen.url, "http://yerel"); yanit.writeHead(200, { "content-type": "application/json" }); if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ arama, odunc, kayit, tarama })); return; } if (u.pathname === "/ara") { arama += 1; yanit.end(JSON.stringify({ bulunan: ara.all(u.searchParams.get("kitap") ?? "K-1").length })); return; } odunc += 1; tarama += yapi === "0" ? kayit : Math.ceil(Math.log2(kayit)); // dizinsiz yapida tam tarama const uye = u.searchParams.get("uye") ?? "U-1"; const n = say.get(uye).n; for (let j = 0; j < KITAP; j += 1) ekle.run(uye, `K-${(odunc + j) % 9000}`, odunc % 400); kayit += KITAP; yanit.end(JSON.stringify({ uye, odunc: n + 1 })); }).listen(port); }
Üreteç iki döngü biçimini de taşır. Senaryo yalnız üç sayıdan oluşur: ödünç payı, sanal görevli sayısı ve düşünme süresi.
// kutuphane/senaryo.mjs — acik ve kapali dongulu uretec; senaryo istek karisimini ve // dusunme suresini belirler. Argumanlar: <baglanti-noktasi> <yapi 0|1> <senaryo> import { Agent, request } from "node:http"; const HEDEF = 1000, ESIK = 20, SURE = 3; // IO2: hedef hiz, IO1: p95 esigi (ms), kosum suresi const SENARYO = { // pay: odunc istegi orani, n/z: kapali dongu kolay: { pay: 0.10 }, "pay-25": { pay: 0.25 }, gercekci: { pay: 0.50 }, "pay-100": { pay: 1.00 }, "kapali-22": { pay: 0.50, n: 22, z: 20 }, "kapali-202": { pay: 0.50, n: 202, z: 200 }, }; const [port, yapi, ad] = [Number(process.argv[2]), process.argv[3], process.argv[4]]; const agent = new Agent({ keepAlive: true, maxSockets: 8192 }); const yuz = (d, p) => { const s = [...d].sort((a, b) => a - b); return s.length === 0 ? 0 : s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))]; }; let sira = 0; function birIstek(kayit, pay) { // pay orani kadar istek odunc yoluna gider const i = sira++; const yol = (i * 9973) % 1000 < pay * 1000 ? `/odunc?uye=U-${i % 500}` : `/ara?kitap=K-${i % 9000}`; const bas = performance.now(); return new Promise((coz) => { const r = request({ port, path: yol, agent }, (y) => { y.resume(); y.on("end", () => { kayit.push(performance.now() - bas); coz(); }); }); r.on("error", () => { kayit.push(performance.now() - bas); coz(); }); r.end(); }); } async function acik(sure, hiz, pay) { // hedef hizda gonderir, yanit beklemez const kayit = [], ucus = []; let borc = 0, sonT = 0; const bas = performance.now(); while (performance.now() - bas < sure * 1000) { const t = (performance.now() - bas) / 1000; borc += hiz * (t - sonT); sonT = t; while (borc >= 1) { borc -= 1; ucus.push(birIstek(kayit, pay)); } await new Promise((c) => setTimeout(c, 5)); } await Promise.all(ucus); return { kayit, gecen: (performance.now() - bas) / 1000 }; } async function kapali(sure, n, z, pay) { // n sanal gorevli; her istekten sonra z ms durur const kayit = []; const bas = performance.now(); const gorevli = async () => { while (performance.now() - bas < sure * 1000) { await birIstek(kayit, pay); await new Promise((c) => setTimeout(c, z)); } }; await Promise.all(Array.from({ length: n }, gorevli)); return { kayit, gecen: (performance.now() - bas) / 1000 }; } 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(); }); const s = SENARYO[ad]; if (Number.isInteger(port) === false || s === undefined) { console.log(`kullanim: node kutuphane/senaryo.mjs <nokta> <yapi 0|1> ${Object.keys(SENARYO).join("|")}`); } else { await birIstek([], 1); // isinma istegi const r = s.n === undefined ? await acik(SURE, HEDEF, s.pay) : await kapali(SURE, s.n, s.z, s.pay); const o = await olcut(); console.log(JSON.stringify({ ad, yapi: yapi === "0" ? "dizinsiz" : "dizinli", pay: s.pay, dongu: s.n === undefined ? "acik" : `kapali N=${s.n} Z=${s.z}`, p95: yuz(r.kayit, 95), hiz: r.kayit.length / r.gecen, odunc: o.odunc, tarama: o.tarama })); } agent.destroy();
// kutuphane/rapor.mjs — on kosumu tabloya cevirir ve p95 esigini tarar. Arguman: <jsonl> import { readFileSync } from "node:fs"; const ESIK = 20, SEN = ["kolay", "gercekci", "kapali-22", "kapali-202"]; const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x)); const bul = (y, a) => s.find((r) => r.yapi === y && r.ad === a); const v = (x) => x.toFixed(2).replace(".", ","); const g = (x, n) => String(x).padStart(n); const kar = (x) => (x <= ESIK ? "gecti" : "kaldi"); console.log(`esik p95 <= ${ESIK} ms (IO1), hedef hiz 1000 istek/s (IO2), kosum 3 s`); console.log("senaryo dongu odunc payi yapi ulasilan/s p95(ms) karar"); for (const a of SEN) for (const y of ["dizinsiz", "dizinli"]) { const r = bul(y, a); console.log(a.padEnd(13) + r.dongu.padEnd(19) + g(r.pay.toFixed(2).replace(".", ","), 10) + " " + y.padEnd(10) + g(v(r.hiz), 10) + g(v(r.p95), 9) + g(kar(r.p95), 7)); } console.log("\nodunc payi taramasi (dizinsiz yapi, acik dongu, ayni 1000 istek/s hedefi)"); console.log("odunc payi odunc istegi/s p95(ms) karar tarama adimi"); for (const a of ["kolay", "pay-25", "gercekci", "pay-100"]) { const r = bul("dizinsiz", a); console.log(g(r.pay.toFixed(2).replace(".", ","), 10) + g(v(r.odunc / 3), 16) + g(v(r.p95), 9) + g(kar(r.p95), 7) + g(r.tarama.toLocaleString("tr-TR"), 15)); } console.log("\nesik taramasi: dort senaryonun kaci yanlis karar veriyor"); console.log("p95 esigi (ms) yanlis gecme /4 yanlis kalma /4 ayiran senaryo /4 ayiranlar"); for (const e of [2, 5, 15, 20, 30, 100]) { const gec = SEN.filter((a) => bul("dizinsiz", a).p95 <= e); const kal = SEN.filter((a) => bul("dizinli", a).p95 > e); const ayr = SEN.filter((a) => bul("dizinli", a).p95 <= e && bul("dizinsiz", a).p95 > e); console.log(String(e).padEnd(16) + g(gec.length, 15) + g(kal.length, 17) + g(ayr.length, 19) + " " + (ayr.join(", ") || "-")); }
# olc.sh — on kosum: dort senaryo x iki yapi, arti dizinsiz yapiya iki ek odunc payi. # Her kosum yeni baslatilmis bir sunucuyla calisir; katalog hep 110.000 kayitla baslar. rm -f sonuc.jsonl for Y in 0 1; do for S in kolay gercekci kapali-22 kapali-202; do node kutuphane/sunucu.mjs 8852 $Y & SUNUCU=$! sleep 1.2 node kutuphane/senaryo.mjs 8852 $Y $S >> sonuc.jsonl kill $SUNUCU wait $SUNUCU 2>/dev/null done done for S in pay-25 pay-100; do node kutuphane/sunucu.mjs 8852 0 & SUNUCU=$! sleep 1.2 node kutuphane/senaryo.mjs 8852 0 $S >> sonuc.jsonl kill $SUNUCU wait $SUNUCU 2>/dev/null done node kutuphane/rapor.mjs sonuc.jsonl
esik p95 <= 20 ms (IO1), hedef hiz 1000 istek/s (IO2), kosum 3 s
senaryo dongu odunc payi yapi ulasilan/s p95(ms) karar
kolay acik 0,10 dizinsiz 997,90 8,24 gecti
kolay acik 0,10 dizinli 997,83 2,84 gecti
gercekci acik 0,50 dizinsiz 924,03 568,66 kaldi
gercekci acik 0,50 dizinli 997,86 2,85 gecti
kapali-22 kapali N=22 Z=20 0,50 dizinsiz 693,17 28,90 kaldi
kapali-22 kapali N=22 Z=20 0,50 dizinli 977,58 4,17 gecti
kapali-202 kapali N=202 Z=200 0,50 dizinsiz 857,23 77,31 kaldi
kapali-202 kapali N=202 Z=200 0,50 dizinli 969,12 15,92 gecti
odunc payi taramasi (dizinsiz yapi, acik dongu, ayni 1000 istek/s hedefi)
odunc payi odunc istegi/s p95(ms) karar tarama adimi
0,10 99,00 8,24 gecti 32.889.780
0,25 249,67 13,02 gecti 83.790.630
0,50 498,33 568,66 kaldi 170.033.825
1,00 999,67 6104,57 kaldi 352.367.505
esik taramasi: dort senaryonun kaci yanlis karar veriyor
p95 esigi (ms) yanlis gecme /4 yanlis kalma /4 ayiran senaryo /4 ayiranlar
2 0 4 0 -
5 0 1 3 kolay, gercekci, kapali-22
15 1 1 2 gercekci, kapali-22
20 1 0 3 gercekci, kapali-22, kapali-202
30 2 0 2 gercekci, kapali-202
100 3 0 1 gercekci
Karışım Kararı Değiştiriyor
Hedef hız dört koşumda da 1.000 istek/s. Değişen tek şey bu hızın uçlara dağılımı. Ödünç payı 0,10 iken kusurlu yapı 8,24 milisaniyede kalıyor ve geçiyor; 0,25’te 13,02 ile hâlâ geçiyor; 0,50’de 568,66 milisaniyeye fırlıyor. Aradaki fark sistemde değil senaryodadır.
Nedeni ikinci sütunda: ödünç isteği hızı 99’dan 498’e çıkıyor. Kusurlu yapıda bu uç tam tarama yapar ve kendi tavanı yaklaşık 400 istek/s’tir; pay 0,25’te tavanın altında, 0,50’de üstündedir. Tarama adımı bunu koşumdan bağımsız gösterir: 32.889.780’den 170.033.825’e, 5,2 kat. Yakalanan kusur sınıfının adı tek yolun doyumudur: sistem bütün olarak hedef hızı karşılarken tek bir uç tavana dayanır ve bütün isteklerin gecikmesini bozar.
Kolay senaryo bunun kaçtığı yerdir. Arama isteği yazması en ucuz olan istektir, bu yüzden senaryolar arama ağırlıklı yazılmaya eğilimlidir. IO3 gerçekçi karışımı 0,50 diye yazıyorsa, 0,10 ile koşulan bir test yanlış geçme üretir ve kaçırdığı sınıfın adı bellidir.
Kapalı Döngü Kendi Yükünü Kısıyor
Alt iki senaryo aynı karışımı (0,50) kapalı döngüyle koşuyor ve ikisinin de nominal hızı 1.000 istek/s. Kusurlu yapıda ulaşılan hız 693,17 ve 857,23 istek/s — yani hedefin yüzde 69,3’ü ve yüzde 85,7’si. Üreteç, sınadığı sistem yavaşladığı için yükü kendiliğinden azalttı. IO2’nin istediği yük hiç uygulanmadı; ölçülen p95 başka bir hızın p95’idir.
Bunun sonucu bir küçültmedir. Açık döngü aynı karışımda 568,66 milisaniye raporladı, kapalı döngü 28,90 ve 77,31. Aynı sistem, aynı kusur, yirmi kat ve yedi kat küçük bir sayı. Bu koşumda üçü de 20 milisaniyelik eşiği aştığı için karar aynı kaldı; eşik 30 milisaniye olsaydı kapalı-22 geçer, açık döngü kalırdı.
Sağlam yapı sütunu düşünme süresinin kendi etkisini ayrı gösteriyor. Kusur yokken bile aynı nominal hız üç ayrı p95 veriyor: açık döngüde 2,85, 22 görevli / 20 ms ile 4,17, 202 görevli / 200 ms ile 15,92 milisaniye — 5,6 kat yayılım. Sayı sistemin değil, üreteç parametrelerinin sayısıdır.
Eşik Taraması ve Bedel
Tarama iki yönlü bir sonuç veriyor. IO1’in 20 milisaniyesinde yanlış geçme 1 (kolay senaryo), yanlış kalma 0. Eşiği 15 milisaniyeye çekmek yanlış kalmayı bire çıkarıyor ve kırmızıya dönen koşum sağlam yapının kapali-202 senaryosudur — yani eşiği sıkmak, kusuru değil senaryonun kendi gürültüsünü yakalıyor. 100 milisaniyede yanlış geçme üçe çıkıyor; yalnız açık döngülü gerçekçi senaryo ayırt ediyor.
Kaçan sınıf da sayılabilir: kapalı döngülü iki senaryo IO2’nin hızına hiç ulaşmadığı için hedef hızda doğan doyum onlarda ölçülmemiştir; kusurlu yapıda eksik kalan yük saniyede 307 ve 143 istektir. Bedelin koşuma bağlı yanı ortama bağlıdır — on koşum bu makinede bir dakikanın altında sürdü. Koşumdan bağımsız yanı: açık döngülü her koşum 3.000 istek gönderir ve ödünç isteği sayısı doğrudan paydan gelir (300, 750, 1.500, 3.000); dizinsiz yapıdaki tarama adımı 32.889.780 ile 352.367.505 arasında, 10,7 kat.
Özet
- Senaryonun iki parametresi vardır: iş karışımı yükün hangi uca düşeceğini, düşünme süresi kapalı döngüde ulaşılan hızı belirler.
- Aynı 1.000 istek/s hedefi ödünç payı 0,10’da geçti (8,24 ms), 0,25’te geçti (13,02 ms), 0,50’de kaldı (568,66 ms); kusur sistemde değil, senaryoda görünür oldu.
- Yakalanan sınıf tek yolun doyumudur: ödünç isteği hızı 99’dan 498’e çıkarken uç kendi tavanını aşıyor, tarama adımı 5,2 kat artıyor.
- Kapalı döngü hedef hıza ulaşamadı (693,17 ve 857,23 istek/s) ve bozulmayı 20 ile 7 kat küçük raporladı; ölçülen p95 istenen yükün p95’i değildir.
- Sağlam yapıda bile üreteç parametreleri p95’i 2,85’ten 15,92 milisaniyeye taşıyor; eşik 15 milisaniyeye çekilirse yanlış kalma bu gürültüden doğar.
Sonraki Adım
Buraya kadar her karar tek bir sayıya bakılarak verildi: p95. Bu seçim tartışılmadı. Ortalama alınsaydı 568,66 milisaniyelik koşum bambaşka görünürdü; p99 seçilseydi karar yine değişirdi ve üç saniyelik bir koşumda p99’un kaç örneğe dayandığı hiç sorulmadı. Verim ile gecikmenin birlikte okunması gerektiği de yalnız ima edildi. Sonraki ders ölçüt seçimini ele alır: ortalamanın neyi gizlediğini, bir yüzdeliğin anlamlı olması için kaç örnek gerektiğini ve gecikmenin verimden ayrı okunduğunda niçin yanıltıcı olduğunu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.