Chapter-chapter sebelumnya mengukur operasi satu per satu: sekali query, sekali komputasi, sekali permintaan. Kenyataannya, server jarang menerima permintaan satu per satu — pengguna datang bersamaan, dan pertanyaan yang sesungguhnya adalah bagaimana program kita berperilaku saat itu terjadi. Load testing menjawabnya dengan sengaja mengirim beban dalam jumlah besar sebelum pengguna sungguhan yang melakukannya. Chapter ini membahas metrik yang perlu dibaca, membangun generator beban kecil buatan sendiri, lalu memperkenalkan alat beban yang dipakai di dunia nyata.

Apa yang Berubah saat Beban Naik #

Pada beban rendah, setiap permintaan datang, diproses, dijawab — antrean nyaris tak terlihat. Saat laju permintaan melampaui kemampuan server memproses, antrean mulai tumbuh: permintaan menunggu giliran di socket, callback menumpuk di event loop, dan latency merambat naik jauh sebelum server benar-benar menolak permintaan. Gejala inilah yang ingin kita amati lebih awal, karena pola kegagalannya jarang terlihat pada uji manual satu permintaan.

Tiga angka menjadi bahasa bersama load testing. Throughput, biasanya dinyatakan sebagai permintaan per detik (RPS), menyatakan berapa banyak pekerjaan yang selesai. Latency menyatakan berapa lama satu permintaan dituntaskan — dan seperti akan kita lihat, angka tunggal untuk latency hampir selalu menyesatkan. Error rate menyatakan proporsi permintaan yang gagal; beban yang sehat menaikkan latency lebih dulu, error menyusul ketika server kehabisan cara menahan.

Rata-rata Mudah Menipu, Persentil Lebih Jujur #

Katakanlah kita mengukur 20 permintaan dan mendapat angka-angka berikut — sampel dari hasil pengukuran yang sudah lewat, dipakai tetap agar perhitungannya bisa dibahas dengan tenang:

js
// Menghitung persentil dari sampel latency.
// Angka sampel diambil dari hasil pengukuran yang sudah lewat,
// sehingga hasil perhitungannya stabil untuk dipelajari.
function persentil(sampel, p) {
  const urut = [...sampel].sort((a, b) => a - b);
  const indeks = Math.ceil((p / 100) * urut.length) - 1;
  return urut[Math.max(0, Math.min(indeks, urut.length - 1))];
}

function rataRata(sampel) {
  const total = sampel.reduce((jumlah, nilai) => jumlah + nilai, 0);
  return total / sampel.length;
}

const latensi = [
  42, 38, 51, 47, 39, 55, 61, 44, 40, 58,
  46, 43, 52, 49, 41, 57, 63, 45, 48, 250,
];

console.log(`jumlah sampel : ${latensi.length}`);
console.log(`rata-rata     : ${rataRata(latensi).toFixed(2)} ms`);
for (const p of [50, 90, 99]) {
  console.log(`p${String(p).padStart(2)}           : ${persentil(latensi, p)} ms`);
}

Output yang Dihasilkan:

text
jumlah sampel : 20
rata-rata     : 58.45 ms
p50           : 47 ms
p90           : 61 ms
p99           : 250 ms

Mari kita bedah perhitungannya:

  1. persentil(sampel, p): sampel disalin lalu diurutkan menaik; indeks dihitung dengan Math.ceil((p/100) * panjang) - 1 lalu dijepit ke rentang valid agar tidak keluar batas.
  2. p50 = 47 ms: separuh permintaan lebih cepat dari angka ini — angka yang dilihat mayoritas pengguna.
  3. p90 = 61 ms: sembilan dari sepuluh permintaan lebih cepat; satu sisanya lebih lambat.
  4. p99 = 250 ms: inilah baris paling penting. Ada satu sampel yang melonjak jauh — khas tail latency — dan justru persentil tinggi yang menangkapnya.
  5. Rata-rata 58,45 ms vs p50 47 ms: satu nilai ekstrem menarik rata-rata menjauh dari pengalaman umum. Laporan yang hanya menunjukkan rata-rata akan menyembunyikan persis masalah yang paling terasa.

Persentil inilah yang dibandingkan antar uji: “p99 naik dari 60 ke 250 ms setelah perubahan kemarin” adalah kalimat yang bisa ditindaklanjuti, sedangkan “rata-rata agak naik” tidak.

Generator Beban Buatan Sendiri #

Sebelum memakai alat pihak ketiga, membangun generator kecil membantu memahami apa yang sesungguhnya dilakukan alat beban: mengirim banyak permintaan — sebagian serentak — lalu mengumpulkan hasilnya. Server uji kita menyediakan rute /tugas/:id yang menjawab 200 untuk id yang ada dan 404 untuk yang tidak:

js
// Generator beban buatan sendiri: menghujani server dengan
// permintaan serentak, lalu melaporkan jumlah per status code.
// Durasi sengaja tidak dicetak: angkanya berubah tiap eksekusi.
import http from "node:http";

const tugas = new Map([
  [1, "Mengukur performance"],
  [2, "Profiling CPU dan heap"],
  [3, "Memory management"],
  [4, "Caching"],
  [5, "Clustering"],
  [6, "Pola worker pool"],
  [7, "Load testing"],
  [8, "Selesai"],
]);

const server = http.createServer((req, res) => {
  const cocok = req.url.match(/^\/tugas\/(\d+)$/);
  if (!cocok) {
    res.writeHead(404, { "Content-Type": "application/json" });
    return res.end(JSON.stringify({ error: "rute tidak dikenal" }));
  }
  const nama = tugas.get(Number(cocok[1]));
  if (!nama) {
    res.writeHead(404, { "Content-Type": "application/json" });
    return res.end(JSON.stringify({ error: "tugas tidak ada" }));
  }
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ id: Number(cocok[1]), nama }));
});

async function gelombangBeban(base, daftarId, lebarSerentak = 4) {
  const hitungStatus = {};
  for (let mulai = 0; mulai < daftarId.length; mulai += lebarSerentak) {
    const potongan = daftarId.slice(mulai, mulai + lebarSerentak);
    const kirim = potongan.map((id) =>
      fetch(`${base}/tugas/${id}`).then((r) => r.status)
    );
    for (const status of await Promise.all(kirim)) {
      hitungStatus[status] = (hitungStatus[status] ?? 0) + 1;
    }
  }
  return hitungStatus;
}

function cetakRingkasan(judul, hitungStatus, jumlahPermintaan) {
  const baris = Object.keys(hitungStatus)
    .sort()
    .map((status) => `${status}: ${hitungStatus[status]}`)
    .join(", ");
  console.log(`${judul} (${jumlahPermintaan} permintaan) -> ${baris}`);
}

server.listen(0, "127.0.0.1", async () => {
  const base = `http://127.0.0.1:${server.address().port}`;

  // Gelombang 1: 12 permintaan, 8 id valid dan 4 id kosong.
  const daftarA = [];
  for (let id = 1; id <= 12; id++) daftarA.push(id);
  const hasilA = await gelombangBeban(base, daftarA);
  cetakRingkasan("gelombang 1", hasilA, daftarA.length);

  // Gelombang 2: 8 permintaan, semuanya id valid.
  const daftarB = [];
  for (let id = 1; id <= 8; id++) daftarB.push(id);
  const hasilB = await gelombangBeban(base, daftarB);
  cetakRingkasan("gelombang 2", hasilB, daftarB.length);

  const total = { ...hasilA };
  for (const [status, n] of Object.entries(hasilB)) {
    total[status] = (total[status] ?? 0) + n;
  }
  cetakRingkasan("total    ", total, daftarA.length + daftarB.length);

  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
gelombang 1 (12 permintaan) -> 200: 8, 404: 4
gelombang 2 (8 permintaan) -> 200: 8
total     (20 permintaan) -> 200: 16, 404: 4

Bagian-bagian pentingnya:

  1. Server uji: handler memilah tiga kemungkinan — rute tidak dikenal, id tidak ada, dan id valid — sehingga beban kita menghasilkan campuran status code yang bisa diverifikasi.
  2. gelombangBeban(base, daftarId, lebarSerentak = 4): permintaan dipotong kelompok berisi empat; setiap kelompok dikirim serentak lewat Promise.all — inilah inti “beban”, bukan urutan for yang menunggu satu per satu.
  3. hitungStatus: satu-satunya yang dilaporkan. Hitungan status code bersifat deterministik — 8 id valid dan 4 id kosong selalu menghasilkan 200: 8, 404: 4 — berbeda dengan durasi yang berubah pada tiap eksekusi, maka itu sengaja tidak dicetak.
  4. Dua gelombang lalu total: pola ringkasan bertingkat meniru cara membaca laporan alat beban sungguhan — per skenario, lalu gabungan.
  5. Pembersihan: close() dan closeAllConnections() memastikan proses keluar rapi — kebiasaan lama dari seluruh demo server di Course ini.

Generator ini sengaja sederhana. Ia tidak mengatur laju per detik, tidak menghitung latency, dan tidak memvariasikan ukuran permintaan — tiga hal yang justru pekerjaan utama alat beban sungguhan.

Alat Beban Sungguhan #

Di dunia nyata, autocannon adalah pilihan populer di ekosistem Node.js: ditulis sepenuhnya di atas Node, dijalankan tanpa instalasi permanen lewat npx, dan langsung mencetak ringkasan latency per persentil beserta throughput-nya. Perintah tipikalnya: npx autocannon -c 50 -d 10 http://127.0.0.1:3000/tugas. Dua flag di situ mengatur dua tombol utama setiap alat beban: -c 50 membuka 50 koneksi serentak, -d 10 menjalankan selama 10 detik. Alat lain memakai istilah serupa — jumlah pengguna maya, durasi, atau jumlah total permintaan — namun ketiganya selalu memetakan ke pertanyaan yang sama: seberapa ramai, berapa lama, dan ke rute mana. Dokumentasi node:http, tempat server uji kita dibangun, tersedia di referensi API Node.js .

Membaca Hasil dan Menindaklanjuti #

Angka load testing paling berarti sebagai perbandingan, bukan kebenaran mutlak. Ukur versi saat ini, simpan ringkasannya, lalu ukur lagi setelah setiap perubahan yang berpotensi memengaruhi performance: naiknya p99 dari 60 ke 250 ms setelah sebuah fitur ditambahkan adalah temuan yang jauh lebih tajam daripada angka tunggal tanpa pembanding. Pola yang sering muncul juga membantu menentukan arah perbaikan — p50 yang tinggi menunjuk ke pekerjaan per permintaan yang mahal, sementara p50 sehat dengan p99 melonjak menunjuk ke antrean atau puncak beban yang perlu diratakan.

Dengan ini pengembaraan performance kita lengkap: mengukur, memprofil, menjaga memori, men-cache, membagi beban lewat cluster dan worker pool, dan akhirnya menguji semuanya di bawah tekanan. Section berikutnya berpindah dari pertanyaan “seberapa cepat” ke “bagaimana menjalankannya di dunia nyata”: Deployment dan Operations, dimulai dari persiapan deployment.