İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat