Ders 01 / 15
Başarım Testi Türleri
Yük altındaki davranışın dört ayrı soruya bölünmesi: yük, stres, dayanıklılık ve ani yük testlerinin aynı ölçütü farklı bir karar penceresinde okuması, aynı iki yapı üzerinde dördünün dört ayrı karar vermesi, eşiğin bir bütçe paylaştırmasından türetilmesi ve eşik taramasıyla hangi türün hangi eşik aralığında yanlış geçme ürettiğinin sayılması.
İçindekiler
Tümleşim, Sözleşme ve Uçtan Uca Test kursunun kapanışı üç soru bıraktı: sistemin yük altında nasıl davrandığı, saldırı altında ne verdiği ve arıza altında nasıl bozulduğu hiçbir derste sınanmadı; oradaki her testin sorusu işlev doğru mu idi. Bu kurs birinciden başlar ve ilk kararı sorar: hangi test koşacak. Yük altında sınamanın dört biçimi vardır ve aralarındaki fark süre ya da şiddet değil, sordukları sorudur.
Dört Soru, Dört Karar Penceresi
Yük testi (load test) hedeflenen yükte eşiğin karşılanıp karşılanmadığını, dayanıklılık testi (soak test) aynı yükün uzun sürede bozulma üretip üretmediğini, ani yük testi (spike test) sıçramadan sonra toparlanmanın ne kadar sürdüğünü sorar. Üçü Başarım Karşıt Kalıpları ve İzleme kursunda kurulmuş ve ölçülmüştü; orada sonuç bir kapasite kararına çevrildi. Burada testin kendisi tasarlanır ve dördüncü tür eklenir: stres testi (stress test) yükü eşik kırılana kadar artırır ve kırılma noktasını (breaking point) arar.
Dördü de tek bir ölçüt okur — ödünç isteğinin p95 gecikmesi — ama her biri onu başka bir karar penceresinde okur: yük testi bütün koşumdan, stres testi hedef hızın iki katı basamağından, dayanıklılık testi son çeyrekten, ani yük testi sıçramadan sonraki ilk saniyeden.
Eşik ve Eşiğin Kaynağı
Bir başarım testinin çıktısı bir dağılımdır; “geçti” demek bir eşik seçmektir. Bu kursun kuralı: eşik kaynağıyla yazılır.
IO1 (varsayım + hesap) — gişe akışının sunucu tarafı bütçesi 60 ms’tir; akış üç servis çağrısı yapar ve bütçe eşit paylaştırılır. Ödünç servisinin payı: p95 ≤ 20 ms. Bütçe bir varsayımdır, pay bir hesaptır; eşik hiçbir ölçümden okunmadı. IO2 (varsayım) — tepe saatte gişe işlemi hızı 1.000 istek/s.
Kararın sahibi de yazılır: kırmızı sonuç yayımı durdurur; stres testinin verdiği kırılma noktası durdurmaz, kapasite planına girdi olur.
Düzenek
Sınanan sistem kütüphane ödünç servisidir: üyenin açık ödünç sayısı doğrulanır, sonra kayıtlar yazılır. Bir gişe işlemi üyeye en çok beş kitap verir — Birim Testi ve Test Odaklı Geliştirme kursundan gelen ödünç sınırı — ve her kitap bir satır ekler. Üç yapı koşturulur: dizinsiz katalog, dizinli katalog ve dizinli olduğu hâlde ödünç sayısını yanlış hesaplayan bir üçüncü yapı.
// kutuphane/sunucu.mjs — odunc servisi: uyenin acik odunc sayisini node:sqlite katalogunda // dogrular ve yeni kayitlari yazar. Argumanlar: <baglanti-noktasi> <yapi 0|1|2> // 0 = dizinsiz (tam tarama), 1 = dizinli, 2 = dizinli ama sayim yanlis. import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const UYE = 500, BASLANGIC = 18000, KITAP = 5; // uye basina 36 kayit; gise islemi 5 kitap yazar const port = Number(process.argv[2]), yapi = process.argv[3]; if (Number.isInteger(port) === false || ["0", "1", "2"].includes(yapi) === false) { console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <yapi 0|1|2>"); } else { const db = new DatabaseSync(":memory:"); db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)"); if (yapi !== "0") 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(yapi === "2" ? "SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ? AND gun = 0" // kusurlu sayim : "SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?"); const ekle = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)"); let istek = 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({ istek, kayit, tarama })); return; } istek += 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-${(istek + j) % 9000}`, istek % 400); kayit += KITAP; yanit.end(JSON.stringify({ uye, odunc: n + 1 })); }).listen(port); }
Yük üreteci açık döngülüdür: istekleri hedef hızda gönderir, sunucunun yetişmesini beklemez. Yük testi koşumunda bir de test kâhini çalışır — Kalite ve Test Temelleri kursunun terimiyle beklenen sonucun kaynağı. Kuralı şudur: her istek üyeye beş kayıt yazdığı için dönen ödünç sayısı bir önceki yanıttan büyük olmalıdır.
// kutuphane/turler.mjs — acik dongulu uretec ve dort test turu; dordu de ayni olcutu (p95) // baska bir pencerede okur. Argumanlar: <baglanti-noktasi> <yapi 0|1|2> <tur> import { Agent, request } from "node:http"; const TABAN = 1000, ESIK = 20; // IO2: hedef hiz (istek/s), IO1: p95 esigi (ms) const BASAMAK = [0.5, 1, 2, 3]; // stres merdiveni: hedef hizin katlari, basamak 1 s const YAPI = { 0: "dizinsiz", 1: "dizinli", 2: "hizli-yanlis" }; const [port, yapi, tur] = [Number(process.argv[2]), process.argv[3], process.argv[4]]; const agent = new Agent({ keepAlive: true, maxSockets: 8192 }); const son = new Map(); let bozuk = 0, denetim = 0; 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))]; }; const pencere = (k, a, b) => k.filter((r) => r.sn >= a && r.sn < b).map((r) => r.ms); const vir = (x) => x.toFixed(1).replace(".", ","); 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(); }); function birIstek(i, kayit, sn, kahin) { const bas = performance.now(), uye = `U-${i % 500}`; return new Promise((coz) => { const r = request({ port, path: `/odunc?uye=${uye}&kitap=K-${i % 900}`, agent }, (y) => { let g = ""; y.on("data", (p) => { g += p; }); y.on("end", () => { if (kahin) { // her istek uyeye bes kayit yazar: sayi artmali const n = JSON.parse(g).odunc; if (son.has(uye)) { denetim += 1; if (n <= son.get(uye)) bozuk += 1; } son.set(uye, n); } kayit.push({ ms: performance.now() - bas, sn }); coz(); }); }); r.on("error", () => { kayit.push({ ms: performance.now() - bas, sn, hata: 1 }); coz(); }); r.end(); }); } async function kosum(sure, hiz, kahin = false) { // hiz(t): t saniyesindeki hedef hiz const kayit = [], ucus = []; let borc = 0, i = 0, sonT = 0; const bas = performance.now(); while (performance.now() - bas < sure * 1000) { const t = (performance.now() - bas) / 1000; borc += hiz(t) * (t - sonT); // gecen gercek sureye gore: zamanlayici sapmasini yutar sonT = t; while (borc >= 1) { borc -= 1; ucus.push(birIstek(i++, kayit, t, kahin)); } await new Promise((c) => setTimeout(c, 5)); } await Promise.all(ucus); return { kayit, gecen: (performance.now() - bas) / 1000 }; } if (Number.isInteger(port) === false || ["yuk", "stres", "dayaniklilik", "ani"].includes(tur) === false) { console.log("kullanim: node kutuphane/turler.mjs <nokta> <yapi 0|1|2> yuk|stres|dayaniklilik|ani"); } else { await birIstek(0, [], 0, false); // isinma istegi let r, karar, ek; if (tur === "yuk") { r = await kosum(2, () => TABAN, true); // kahin yalniz doymamis kosumda anlamli karar = yuz(r.kayit.map((k) => k.ms), 95); ek = `kahin ${bozuk}/${denetim} bozuk`; } else if (tur === "stres") { r = await kosum(4, (t) => TABAN * BASAMAK[Math.min(3, Math.floor(t))]); const p = BASAMAK.map((_, i) => yuz(pencere(r.kayit, i, i + 1), 95)); const k = p.findIndex((x) => x > ESIK); karar = p[2]; // 2 kat basamaginin p95'i ek = `kirilma ${k < 0 ? "> 3000" : TABAN * BASAMAK[k]} istek/s`; } else if (tur === "dayaniklilik") { r = await kosum(10, () => TABAN); const c = [0, 1, 2, 3].map((i) => yuz(pencere(r.kayit, i * 2.5, i * 2.5 + 2.5), 95)); karar = c[3]; // son ceyregin p95'i ek = `ceyrek p95 ${c.map(vir).join(" / ")}`; } else { r = await kosum(5, (t) => (t >= 1.5 && t < 3 ? TABAN * 3 : TABAN)); karar = yuz(pencere(r.kayit, 3, 4), 95); // sicramadan sonraki ilk saniye const d = [3, 4].find((sn) => yuz(pencere(r.kayit, sn, sn + 1), 95) <= ESIK); ek = `donus ${d === undefined ? "2,0+" : vir(d - 2)} s`; } const o = await olcut(); console.log(JSON.stringify({ yapi: YAPI[yapi], tur, karar, ek, hiz: r.kayit.length / r.gecen, tarama: o.tarama })); } agent.destroy();
Rapor dokuz koşumu okur; tarama eşiği IO1’in verdiği 20 milisaniyeden uzaklaştırır ve her değerde kaç türün yanlış karar verdiğini sayar.
// kutuphane/rapor.mjs — dokuz kosumu tabloya cevirir ve p95 esigini tarar. Arguman: <jsonl> import { readFileSync } from "node:fs"; const ESIK = 20, TUR = ["yuk", "stres", "dayaniklilik", "ani"]; const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x)); const bul = (y, t) => s.find((r) => r.yapi === y && r.tur === t); 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)`); console.log("tur yapi ulasilan/s karar p95 karar tarama adimi tur ozel sayi"); for (const t of TUR) for (const y of ["dizinsiz", "dizinli", "hizli-yanlis"]) { const r = bul(y, t); if (r !== undefined) console.log(t.padEnd(13) + y.padEnd(14) + g(v(r.hiz), 10) + g(v(r.karar), 11) + g(kar(r.karar), 7) + g(r.tarama.toLocaleString("tr-TR"), 15) + " " + r.ek); } console.log("\nesik taramasi: dort turun kaci yanlis karar veriyor (dizinsiz kusurlu, dizinli saglam)"); console.log("p95 esigi (ms) yanlis gecme /4 yanlis kalma /4 ayiran tur /4 ayiran turler"); for (const e of [1, 3, 5, 20, 50]) { const gec = TUR.filter((t) => bul("dizinsiz", t).karar <= e); const kal = TUR.filter((t) => bul("dizinli", t).karar > e); const ayr = TUR.filter((t) => bul("dizinli", t).karar <= e && bul("dizinsiz", t).karar > e); console.log(String(e).padEnd(16) + g(gec.length, 15) + g(kal.length, 17) + g(ayr.length, 15) + " " + (ayr.join(", ") || "-")); }
# olc.sh — dokuz kosum: dort tur x iki yapi, arti ucuncu yapiya bir yuk testi. # Her kosum yeni baslatilmis bir sunucuyla calisir; katalog hep ayni boydan baslar. rm -f sonuc.jsonl for Y in 0 1 2; do for T in yuk stres dayaniklilik ani; do if [ "$Y" = 2 ] && [ "$T" != yuk ]; then continue; fi node kutuphane/sunucu.mjs 8851 $Y & SUNUCU=$! sleep 0.7 node kutuphane/turler.mjs 8851 $Y $T >> sonuc.jsonl kill $SUNUCU wait $SUNUCU 2>/dev/null done done node kutuphane/rapor.mjs sonuc.jsonl
esik p95 <= 20 ms (IO1), hedef hiz 1000 istek/s (IO2) tur yapi ulasilan/s karar p95 karar tarama adimi tur ozel sayi yuk dizinsiz 997,12 3,99 gecti 45.911.030 kahin 0/1496 bozuk yuk dizinli 996,94 2,75 gecti 29.970 kahin 0/1497 bozuk yuk hizli-yanlis 997,22 3,22 gecti 29.985 kahin 1495/1498 bozuk stres dizinsiz 1113,76 1430,86 kaldi 222.558.255 kirilma 2000 istek/s stres dizinli 1622,37 3,37 gecti 100.934 kirilma > 3000 istek/s dayaniklilik dizinsiz 942,36 1229,72 kaldi 429.703.050 ceyrek p95 4,1 / 4,2 / 6,9 / 1229,7 dayaniklilik dizinli 999,23 2,28 gecti 157.487 ceyrek p95 3,0 / 2,3 / 2,4 / 2,3 ani dizinsiz 901,25 40,29 kaldi 303.342.330 donus 2,0+ s ani dizinli 1596,89 2,26 gecti 124.934 donus 1,0 s esik taramasi: dort turun kaci yanlis karar veriyor (dizinsiz kusurlu, dizinli saglam) p95 esigi (ms) yanlis gecme /4 yanlis kalma /4 ayiran tur /4 ayiran turler 1 0 4 0 - 3 0 1 3 yuk, dayaniklilik, ani 5 1 0 3 stres, dayaniklilik, ani 20 1 0 3 stres, dayaniklilik, ani 50 2 0 2 stres, dayaniklilik
Süre sütunları bu makineye bağlıdır; tarama adımı değildir: tam taramanın adımı o anki kayıt sayısı, dizinli aramanınki onun ikili logaritmasıdır. Gönderilen istek de hesaptır — hedef hız ile sürenin çarpımı, dokuz koşumda 55.000. Savlar bu kümeye dayanır; tek koşumdan okunan değerler bu koşumda diye işaretlenir.
Dört Tür, Dört Karar
Dizinsiz yapı dört türden birinde geçti, üçünde kaldı. Yük testi hedef hızda p95’i bu koşumda 3,99 milisaniyede buldu — eşiğin beşte biri. Aynı yapı stres testinde hedefin iki katında kırıldı, dayanıklılık testinde son çeyrekte 1.229,72 milisaniyeye çıktı ve ani yük testinde sıçramadan sonra iki saniye içinde eşiğin altına dönemedi. Aynı ölçüt, aynı eşik, dört ayrı karar; fark penceredir.
Dayanıklılık testinin çeyrek dizisi nedeni de veriyor. Yük sabit, kod aynı; değişen tek şey katalogdaki kayıt sayısıdır. Tam tarama her istekte tablonun tamamına dokunur, tablo her istekte beş satır büyür, hizmet süresi artar ve doluluk bir yerde tavana dayanır. Tarama adımı bunu koşumdan bağımsız gösterir: dizinsiz yapı 429.703.050 satır adımı harcarken dizinli yapı aynı işi 157.487 adımla yaptı — 2.729 kat. Yakalanan kusur sınıfının adı veri büyümesine bağlı bozulmadır; Başarım Karşıt Kalıpları ve İzleme kursundaki karşıt kalıp tanılarının girdisi budur.
Stres testinin verdiği sayı başka türdendir. Merdiven kısa tutulur; uzununda veri büyümesi hıza karışır. Dizinsiz yapı 2.000 istek/s’te kırıldı, dizinli yapı 3.000 istek/s’te hâlâ eşiğin altındaydı. Bu bir geçti-kaldı değil bir mesafedir: hedef hızın kaç katına kadar dayanıldığı.
Eşik Taraması
Bir eşik ancak iki sayıyla savunulabilir: kaç gerçek kusuru kaçırdığı ve kaç sağlam koşumu kırmızıya döndürdüğü. Tarama bunu eşik değeri boyunca sayar.
IO1’in verdiği 20 milisaniyede yanlış geçme 1, yanlış kalma 0. Kaçan tür yük testidir: iki saniyelik bir koşumda tablo yalnız 10.000 satır büyüdüğü için kusurlu yapı eşiğin çok altında kalıyor. Eşiği 5 milisaniyeye indirmek bunu değiştirmiyor; 3 milisaniye yük testini ayırt eder hâle getiriyor ama sağlam yapı bu kez stres testinde kalıyor. 50 milisaniyede yanlış geçme ikiye çıkıyor.
İki sonuç çıkar. Yük testi bu kusur sınıfını IO1’in eşiğinde göremez, çünkü penceresi kusurun büyümesine zaman tanımıyor. Ve 3 milisaniyelik eşik hiçbir bütçeden türetilmemiştir, bu koşumun verisine uydurulmuştur — uydurulduğu hâlde yanlış kalmayı sıfırda tutamıyor. Eşiği kaynağından koparmak, taramayı düzeltmeden kararı boşaltır.
Yakalanmayan Sınıf ve Bedel
Üçüncü yapı dizinlidir, hızlıdır ve yanlıştır: ödünç sayısını yalnız tek bir günün kayıtları üzerinden hesaplar. Yük testinin p95’i bu koşumda 3,22 milisaniye — sağlam yapının 2,75’inden ayırt edilemez ve dört tür de bu yapıyı geçirir. Kâhin çalıştırıldığında 1.498 denetimin 1.495’i bozuk çıkıyor. Kaçan kusur sınıfının adı yanıt doğruluğu kusurudur: başarım testleri süre okur, gövde okumaz. Kör kalmalarının nedeni pencere değil ölçüttür.
Bedelin koşuma bağlı yanı ortama bağlıdır: dokuz koşum bu makinede bir dakikanın altında sürdü. Koşumdan bağımsız yanı sayılır — dokuz sunucu süreci, 55.000 istek, dizinsiz yapıda 1,00 milyar satır adımı. En pahalı tür dayanıklılık testidir: yük testinin beş katı istek, 9,4 katı tarama adımı.
Özet
- Dört tür aynı ölçütü (p95) farklı bir karar penceresinde okur: bütün koşum, iki kat basamağı, son çeyrek, sıçrama sonrası ilk saniye.
- Eşik kaynağıyla yazılır: IO1’in 20 milisaniyesi 60 milisaniyelik bir bütçe varsayımının üç servise eşit paylaştırılmasından gelir, hiçbir ölçümden okunmadı.
- Aynı kusur dört ayrı karar aldı: yük testi geçti, stres testi 2.000 istek/s’te kırıldı, dayanıklılık testi son çeyrekte 1.229,72 milisaniye buldu, ani yük testi toparlanamadı.
- Eşik taraması IO1’de yanlış geçme 1, yanlış kalma 0 veriyor; 3 milisaniye yük testini ayırır ama yanlış kalmayı bire çıkarır ve eşiği kaynağından koparır.
- Yakalanan sınıf veri büyümesine bağlı bozulmadır (tarama adımı 2.729 kat); yakalanmayan sınıf yanıt doğruluğu kusurudur: hızlı ve yanlış bir yapı dört türü de geçer.
Sonraki Adım
Bu 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 arama yapar, sonuca bakar, ödünç verir ve iki işlem arasında düşünür. Sonraki ders senaryoyu ele alır — duraklarla işlem oranlarının aynı hedef hızda nasıl başka bir sonuç ürettiğini, gerçekçi olmayan bir senaryonun gerçek bir kusuru nasıl geçirdiğini ve karışımın hangi payında kusurun görünür olduğunu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.