İçeriğe geç
academia.sh

Ders 14 / 16

Yük Testi

Yükün beklenmek yerine üretilmesi: yük, dayanıklılık ve ani yük testlerinin üç ayrı soru sorduğunun ve üç ayrı belirtiyi ortaya çıkardığının ölçülmesi, açık döngülü bir yük üretecinin yazılması, aynı yükte uzun koşumun ortancada görünmeyen bozulmayı kuyruk gecikmesinde göstermesi ve bir sıçramanın bedelinin sıçramanın kendisinden büyük olduğunun sayılması.

İçindekiler

Önceki üç ders sistemin kendiliğinden ürettiği sayıları topladı, biçimlendirdi ve eşiğe bağladı. Toplanan her sayı olmuş olanı anlatır. Bugünkü yükte ne olduğunu söyler; yarınki yükte ne olacağını, trafik üç katına çıktığında hangi ölçütün önce eşiği aşacağını ve sistem saatlerce aynı yükte çalıştığında bugünkü sayının aynı kalıp kalmayacağını söylemez. Bu üç soru hiç yaşanmadıkları için ölçülmemiştir.

Bu ders yükü beklemek yerine üretir. Üç ayrı test kurulur ve dersin savı şudur: üçü aynı testin uzun ve kısa hâlleri değildir, üç ayrı soru sorar ve üç ayrı belirtiyi ortaya çıkarır.

Üç Soru

Yük testi, hedeflenen yükte eşiklerin karşılanıp karşılanmadığını sorar. Süresi kısadır, yükü sabittir ve yanıtı iki değerlidir: karşılandı ya da karşılanmadı.

Dayanıklılık testi aynı yükü uzun süre uygular ve zamanla bozulma olup olmadığını sorar. Yükü sabit tutması özellikle önemlidir; değişen tek şey süredir, böylece bulunan her bozulma yükün değil, sistemde biriken bir şeyin sonucudur — tutulup bırakılmayan bellek, büyüyen bir dizi, temizlenmeyen bir defter.

Ani yük testi yükü kısa süreliğine katlar ve iki şey sorar: sıçrama sırasında ne oluyor ve sıçrama bittikten sonra sistem ne kadar sürede kendine geliyor. İkinci soru birincisinden önemlidir, çünkü bir sıçrama geçicidir ama toparlanma süresi boyunca sıçramayla hiç ilgisi olmayan istekler de bedel öder.

Aşağıdaki düzenek yük üretecini kendi yazar. Üreteç açık döngülüdür: istekleri hedef hızda gönderir ve sunucunun yetişmesini beklemez. Kapalı döngülü bir üreteç, yani her yanıtı bekleyip sonra göndereni, sunucu yavaşladığında yükü kendiliğinden azaltır ve sıçramayı hiç üretemez. Sunucu tarafında takip ucu, her yüzüncü isteğin biriken kayıtları taradığı bir bakım geçişi içerir; bu geçiş dayanıklılık testinin arayacağı birikmenin kaynağıdır. Ölçülen süreler bu makineye bağlıdır; dersin savları oranlara ve sayaçlara dayanır.

// yuk/sunucu.mjs — takip ucu: her istek bir bekleme ve bir islemci payi harcar, her yuzuncu
// istek birikmis kayitlari tarayan bir bakim gecisi yapar. /olcut ucu sunucunun sayaclarini verir.
import { createServer } from "node:http";

const BEKLEME = 3;        // duzenek parametresi: kayit aramasinin yerine gecen bekleme (ms)
const ISLEMCI = 0.85;     // duzenek parametresi: istek basina islemci payi (ms)
const BAKIM_ARALIK = 100; // duzenek parametresi: kac istekte bir bakim gecisi
const BAKIM_KAT = 600;    // duzenek parametresi: bakim gecisinin birikmis kaydi tarama sayisi

const kayitlar = [];
let istek = 0, bakim = 0, bakimAdim = 0;

function isle(ms) {
  const bit = performance.now() + ms;
  let t = 0;
  while (performance.now() < bit) t += 1;
  return t;
}

const port = Number(process.argv[2]);
if (Number.isInteger(port) === false) console.log("kullanim: node yuk/sunucu.mjs <baglanti-noktasi>");
else createServer(async (gelen, yanit) => {
  const yol = new URL(gelen.url, "http://yerel");
  if (yol.pathname === "/olcut") {
    yanit.writeHead(200, { "content-type": "application/json" });
    yanit.end(JSON.stringify({ istek, kayit: kayitlar.length, bakim, bakimAdim, kat: BAKIM_KAT }));
    return;
  }
  istek += 1;
  const sira = istek;                        // sirasi girerken saptanir, cikarken degil
  kayitlar.push({ no: yol.searchParams.get("no") ?? "-", adim: sira % 7 });
  await new Promise((c) => setTimeout(c, BEKLEME));
  isle(ISLEMCI);
  if (sira % BAKIM_ARALIK === 0) {           // bakim gecisi: isi birikmis kayitla buyur
    bakim += 1;
    let t = 0;
    for (let k = 0; k < BAKIM_KAT; k += 1) for (const kayit of kayitlar) t += kayit.adim;
    bakimAdim += BAKIM_KAT * kayitlar.length;
  }
  yanit.writeHead(200, { "content-type": "application/json" });
  yanit.end(JSON.stringify({ durum: "aktarma merkezinde", adim: sira % 7 }));
}).listen(port);
// yuk/uretec.mjs — acik dongulu yuk ureteci: istekleri hedef hizda gonderir, sunucunun
// yetismesini beklemez. Uc test ayni uretecle, her biri yeni baslatilmis bir sunucuyla kosar.
import { Agent, request } from "node:http";

const port = Number(process.argv[2]);
const test = process.argv[3];
const TABAN = 514;        // hedef hiz (istek/s) — K01: tepe ucta 513,89 istek/s
const SICRAMA = 3;        // K01: tepe carpani 3
const ESIK = 25;          // YT1 (varsayim): takip yanitinin basarim esigi (ms)
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))];
};
const b2 = (x) => x.toFixed(2);
const say = (x, n) => String(x).padStart(n);
const dilim = (k, a, b) => k.filter((r) => r.sn >= a && r.sn < b).map((r) => r.ms);

function birIstek(no, kayit, sn) {
  const bas = performance.now();
  return new Promise((coz) => {
    const r = request({ port, path: `/takip?no=${no}`, 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, hata: 1 }); 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();
});

async function kosum(sure, hiz) {           // hiz(t): saniye cinsinden zamana gore 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(`G${i++}`, kayit, Math.floor(t))); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  return { kayit, gecen: (performance.now() - bas) / 1000 };
}

if (Number.isInteger(port) === false || ["yuk", "dayaniklilik", "ani"].includes(test) === false) {
  console.log("kullanim: node yuk/uretec.mjs <baglanti-noktasi> yuk|dayaniklilik|ani");
} else if (test === "yuk") {
  await birIstek("isinma", [], 0);
  const r = await kosum(3, () => TABAN);
  const m = r.kayit.map((k) => k.ms);
  const asan = m.filter((x) => x > ESIK).length;
  console.log(`YUK TESTI: hedef ${TABAN} istek/s, 3 s, esik ${ESIK} ms`);
  console.log(`  tamamlanan ${m.length}, ulasilan hiz ${b2(m.length / r.gecen)} istek/s ` +
    `(hedefin %${b2((100 * m.length) / (TABAN * r.gecen))}'i)`);
  console.log(`  ortanca ${b2(yuz(m, 50))} ms, p95 ${b2(yuz(m, 95))} ms, p99 ${b2(yuz(m, 99))} ms`);
  console.log(`  esigi asan ${asan} istek (%${b2((100 * asan) / m.length)})`);
} else if (test === "dayaniklilik") {
  await birIstek("isinma", [], 0);
  const o1 = await olcut();
  const r = await kosum(12, () => TABAN);
  const o2 = await olcut();
  console.log(`DAYANIKLILIK TESTI: ayni ${TABAN} istek/s, 12 s`);
  console.log(`  ceyrek   istek   ortanca(ms)   p95(ms)   p99(ms)   esigi asan`);
  const c = [];
  for (let i = 0; i < 4; i += 1) {
    const d = dilim(r.kayit, i * 3, (i + 1) * 3);
    c.push(d);
    console.log(`  ${say(i + 1, 6)}${say(d.length, 8)}${say(b2(yuz(d, 50)), 14)}` +
      `${say(b2(yuz(d, 95)), 10)}${say(b2(yuz(d, 99)), 10)}${say(d.filter((x) => x > ESIK).length, 13)}`);
  }
  console.log(`  son/ilk ceyrek: ortanca ${b2(yuz(c[3], 50) / yuz(c[0], 50))} kat, ` +
    `p95 ${b2(yuz(c[3], 95) / yuz(c[0], 95))} kat, p99 ${b2(yuz(c[3], 99) / yuz(c[0], 99))} kat`);
  const gecis = o2.bakim - o1.bakim;
  console.log(`  sunucu sayaci: biriken kayit ${o1.kayit} -> ${o2.kayit}, bakim gecisi ${gecis}`);
  console.log(`  bakim gecisi basina adim: ilk ${(o1.kat * 100).toLocaleString("tr-TR")}, ` +
    `son ${(o2.kat * 100 * gecis).toLocaleString("tr-TR")} (${gecis} kat)`);
} else {
  await birIstek("isinma", [], 0);
  const r = await kosum(9, (t) => (t >= 3 && t < 4.5 ? TABAN * SICRAMA : TABAN));
  console.log(`ANI YUK TESTI: taban ${TABAN} istek/s, 3,0-4,5 s arasi ${TABAN * SICRAMA} istek/s, 9 s`);
  console.log(`  saniye   tamamlanan   ortanca(ms)   p95(ms)   esigi asan`);
  const taban = [];
  for (let sn = 0; sn < 9; sn += 1) {
    const d = dilim(r.kayit, sn, sn + 1);
    if (sn < 3) taban.push(yuz(d, 95));
    console.log(`  ${say(sn, 6)}${say(d.length, 13)}${say(b2(yuz(d, 50)), 14)}` +
      `${say(b2(yuz(d, 95)), 10)}${say(d.filter((x) => x > ESIK).length, 13)}`);
  }
  let donus = null;
  for (let sn = 5; sn < 9; sn += 1)
    if (donus === null && yuz(dilim(r.kayit, sn, sn + 1), 95) <= ESIK) donus = sn - 4.5;
  const fazla = Math.round(TABAN * (SICRAMA - 1) * 1.5);
  const asan = r.kayit.filter((k) => k.ms > ESIK).length;
  console.log(`  taban p95 ${b2(yuz(taban, 100))} ms; sicramada gonderilen fazla istek ${fazla}, ` +
    `hata ile biten ${r.kayit.filter((k) => k.hata).length}`);
  console.log(`  p95 esigin altina donusu: sicrama bittikten ${donus === null ? "sonra donmedi" : b2(donus) + " s sonra"}`);
  console.log(`  esigi asan toplam istek ${asan} = fazla istegin ${b2(asan / fazla)} kati`);
}
agent.destroy();
# olc.sh — uc test, her biri yeni baslatilmis bir sunucuyla kosar
for T in yuk dayaniklilik ani; do
  node yuk/sunucu.mjs 8851 & SUNUCU=$!
  sleep 1
  node yuk/uretec.mjs 8851 $T
  kill $SUNUCU
  wait $SUNUCU 2>/dev/null
  echo
done
YUK TESTI: hedef 514 istek/s, 3 s, esik 25 ms
  tamamlanan 1539, ulasilan hiz 512.72 istek/s (hedefin %99.75'i)
  ortanca 7.24 ms, p95 9.92 ms, p99 13.05 ms
  esigi asan 0 istek (%0.00)

DAYANIKLILIK TESTI: ayni 514 istek/s, 12 s
  ceyrek   istek   ortanca(ms)   p95(ms)   p99(ms)   esigi asan
       1    1538          7.06      9.04      9.65            0
       2    1544          6.97      9.00     10.12            0
       3    1542          7.30     15.95     22.37            4
       4    1541          7.33     20.04     25.99           22
  son/ilk ceyrek: ortanca 1.04 kat, p95 2.22 kat, p99 2.69 kat
  sunucu sayaci: biriken kayit 1 -> 6166, bakim gecisi 61
  bakim gecisi basina adim: ilk 60.000, son 3.660.000 (61 kat)

ANI YUK TESTI: taban 514 istek/s, 3,0-4,5 s arasi 1542 istek/s, 9 s
  saniye   tamamlanan   ortanca(ms)   p95(ms)   esigi asan
       0          512          7.22     10.01            0
       1          514          7.35     12.71            0
       2          514          7.21     11.43            0
       3         1545        101.74   1508.92         1400
       4         1024        111.74   1028.35          976
       5          516         14.07    108.66          224
       6          514          7.31     16.52            4
       7          512          7.61     20.21           12
       8          516          7.32     22.93           15
  taban p95 12.71 ms; sicramada gonderilen fazla istek 1542, hata ile biten 0
  p95 esigin altina donusu: sicrama bittikten 1.50 s sonra
  esigi asan toplam istek 2631 = fazla istegin 1.71 kati

Çıktının süre sütunları — ortanca, p95, p99 ve bunlardan çıkan katlar — bu makineye ve o andaki yüküne bağlıdır; başka bir makinede başka, aynı makinede tekrarlandığında yine başka çıkar. Sayaç sütunları ise makinenin hızına bağlı değildir: gönderilen istek sayısı, biriken kayıt, bakım geçişi sayısı ve bakım geçişi başına adım, hedef hız ile sürenin çarpımından belirlenir. Aşağıdaki üç bölümün savları bu ikinci kümeye ve süre sayılarının yönüne dayanır; tek bir koşumdan okunan değerler bu koşumda diye işaretlenir.

Yük Testi Geçti

Birinci test hedefe ulaştı: üreteç istenen hızın yüzde birlik bandında kaldı (bu koşumda 512,72 istek/s, hedefin yüzde 99,75’i). Ulaşılan hızın raporlanması gereksiz görünür ve değildir — üreteç hedefine ulaşamadıysa ölçülen gecikme başka bir yükün gecikmesidir ve tablo okunamaz. Yüzdelikler eşiğin belirgin biçimde altında kaldı ve bu koşumda 25 milisaniyelik eşiği aşan istek olmadı. Yanıt iki değerlidir ve olumludur.

Bu testin tek başına verdiği güvence dardır. Söylediği şey şudur: bu yükte, bu süre boyunca, bu durumda olan bir sistem eşiği karşılar. Üç koşuldan ikisini kaldıran iki test daha vardır.

Dayanıklılık Testi: Ortanca Yalan Söyler

İkinci test aynı yükü dört katı süre uyguladı ve tablo ortada ikiye ayrılıyor. Ortanca dört çeyrekte de yerinde duruyor (bu koşumda son çeyrek ilkin 1,04 katı). Bu sayıya bakan biri sistemin değişmediğini söyler. Oysa aynı çeyreklerde p95 ile p99 iki katına yakın büyümüş (bu koşumda 2,22 ve 2,69 kat) ve eşiği aşan istek ilk çeyrekte sıfırken son çeyrekte onlarca isteğe çıkmış (bu koşumda 22). Aynı yükte, aynı kodda, yalnız süre uzadığı için bir eşik ihlali doğdu.

Katların tam değeri koşumdan koşuma oynar, yönü oynamaz; iddiayı taşıyan sayı da onlar değil, sunucunun kendi sayaçlarıdır. Bozulmanın nedeni tahmin değil, orada yazılı: biriken kayıt 6.166’ya çıktı, altmış bir bakım geçişi yapıldı ve bir bakım geçişinin adım sayısı 60.000’den 3.660.000’e, yani tam 61 katına çıktı. Bu oran koşumdan bağımsızdır — makine ne kadar hızlı olursa olsun, altmış birinci geçiş birincinin altmış bir katı iş yapar, çünkü taranan dizi her yüz istekte yüz kayıt büyüyor. Ortancanın kıpırdamaması da aynı sayıdan çıkıyor: bakım geçişi yüz istekten yalnız birini yavaşlatır, yani isteklerin yüzde birini. Ortanca yüzde ellinci istektir ve orada hiçbir şey olmaz. Birikme belirtisi ortancada değil, kuyruk gecikmesinde görünür.

Ani Yük Testi: Bedel Sıçramadan Büyüktür

Üçüncü test yükü bir buçuk saniye boyunca üç katına çıkardı — Sistem Tasarımına Giriş kursunun tepe çarpanı da üçtü. Sıçrama saniyesinde p95 iki mertebe büyüdü (bu koşumda 12,71 milisaniyeden 1.508,92 milisaniyeye). Sıçrama bittikten sonra p95’in eşiğin altına dönmesi bir buçuk saniye sürdü; ölçü saniye çözünürlüğündedir ve bu sürede biriken iş eritildi.

Testin asıl sayısı son satırdadır. Sıçrama penceresinde fazladan 1.542 istek gönderildi — bu sayı hesaptır, üretecin parametrelerinden çıkar ve koşuma bağlı değildir. Eşiği aşan istek sayısı ise ondan büyüktür (bu koşumda 2.631, yani 1,71 katı). Oranın tam değeri koşumdan koşuma oynar ama birden büyük olması oynamaz, çünkü nedeni yapısaldır: kuyruğa giren fazla iş arkadan gelen olağan istekleri de bekletir, dolayısıyla gecikmesi bozulan istek kümesi sıçrayan istek kümesinden geniştir. Bir sıçramanın maliyeti sıçrayan istek sayısıyla değil, gecikmesi bozulan toplam istek sayısıyla ölçülür.

Bir sütun da boş kalıyor: hata ile biten istek sayısı sıfır. Sistem fazla yükü reddetmedi, kuyruğa aldı. Dayanıklılık ve Güvenilirlik kursunda kurulan geri basınç ve yük boşaltma kalıpları tam olarak bu davranışı değiştirir; burada ikisi de yok, bu yüzden kapasite fazlası gecikmeyle ödendi. Ani yük testinin ortaya çıkardığı belirti budur ve yük testi ile dayanıklılık testi onu göremez.

Özet

  • Üç test üç ayrı soru sorar: yük testi bugünün eşiğini, dayanıklılık testi zamanla birikmeyi, ani yük testi sıçrama sırasındaki bozulmayı ve sonrasındaki toparlanmayı ölçer.
  • Yük üreteci açık döngülü olmalıdır: her yanıtı bekleyen bir üreteç sunucu yavaşladığında yükü kendiliğinden azaltır ve sıçramayı hiç üretemez. Ulaşılan hız her koşumda raporlanır (bu koşumda hedefin yüzde 99,75’i).
  • Yük testi eşiği karşıladı: hedef hızın yüzde birlik bandında p99 eşiğin çok altında kaldı ve eşiği aşan istek olmadı (bu koşumda 13,05 ms ve 0 istek).
  • Dayanıklılık testi aynı yükte bozulma buldu: ortanca yerinde kalırken p95 ile p99 iki katına yakın büyüdü ve eşiği aşan istek sıfırdan onlarca isteğe çıktı (bu koşumda 1,04 / 2,22 / 2,69 kat, 22 istek).
  • Bozulmanın kaynağı sunucunun sayaçlarında: bir bakım geçişinin adım sayısı 61 katına çıktı; geçiş yüz istekten birini yavaşlattığı için belirti ortancada değil kuyruk gecikmesinde görünür.
  • Ani yük testinde 1.542 fazla istek, kendisinden daha çok isteğin eşiği aşmasına yol açtı (bu koşumda 2.631, 1,71 kat); oranın birden büyük olması yapısaldır. p95’in eşiğin altına dönmesi bir buçuk saniye sürdü ve hiçbir istek reddedilmedi, fazlası gecikmeyle ödendi.

Sonraki Adım

Üç test bir sistemin bugünkü tek süreçle nereye kadar gittiğini gösterdi, ama hiçbiri “kaç tane gerekiyor” sorusunu sormadı. Yük testinin verdiği sayı bir sınır değil bir doğrulamadır: 514 istek/s karşılanıyor. Sınırın kendisi Sistem Tasarımına Giriş kursunda ölçülmüştü — bir sürecin doyum verimi — ve orada doluluk arttıkça gecikmenin doğrusal büyümediği de gösterilmişti. Sonraki ders bu iki girdiyi alır ve tahmine çevirir: ölçülen doluluktan kaç düğüm gerektiğini çıkarır, bir düğüm kaybında kalan kapasiteyi sayar, dayanıklılık testinin gösterdiği birikmenin düğüm sayısını nasıl değiştirdiğini ve büyüyen bir talebin bu sayıyı kaç ayda bir artırdığını hesaplar. Aradaki tek karar bir orandır ve adı vardır: kapasite payı.

İ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