Tampilan
Kenapa penting
"Aplikasinya cepat kok di laptop saya" tidak berarti apa-apa ketika 2.000 pengguna membuka halaman promo bersamaan. Load test menjawab pertanyaan yang tidak bisa dijawab unit test: berapa beban yang sanggup ditahan sistem sebelum latensi dan error naik, dan komponen mana yang jebol duluan? Di tempat kerja, kamu akan diminta membuktikan bahwa endpoint baru memenuhi target (misalnya p95 < 300 ms pada 200 request/detik), membandingkan performa sebelum dan sesudah optimasi, atau mencari penyebab lambat saat jam sibuk. Bab ini mengajari cara menjalankan test yang jujur dan — yang lebih sering salah — cara membaca hasilnya.
Konsep inti
Jenis test performa
Semua jenis di bawah memakai alat yang sama; yang berbeda adalah bentuk beban dan pertanyaan yang ingin dijawab.
| Jenis | Bentuk beban | Pertanyaan |
|---|---|---|
| Smoke | 1–2 pengguna, sebentar | Apakah script dan endpoint jalan sama sekali? |
| Load | Beban normal–puncak yang diharapkan, stabil | Apakah target (SLO) terpenuhi di beban biasa? |
| Stress | Dinaikkan bertahap melewati puncak | Di titik mana sistem mulai gagal, dan bagaimana gagalnya? |
| Spike | Lonjakan mendadak (mis. flash sale) | Apakah sistem bertahan dan pulih setelah lonjakan? |
| Soak | Beban sedang selama berjam-jam | Ada memory leak, koneksi bocor, atau log yang memenuhi disk? |
Mulailah selalu dengan smoke test. Tidak ada gunanya menembak 500 pengguna virtual bila 1 request pun sudah error karena token salah.
Metrik yang dibaca
- Throughput — request yang selesai per detik (RPS). Naiknya beban seharusnya menaikkan throughput; bila throughput mendatar sementara beban terus naik, sistem sudah jenuh.
- Latensi dalam percentile — p50 (median) adalah pengalaman request "biasa"; p95 dan p99 adalah pengalaman request paling lambat 5% dan 1%. Satu halaman dashboard sering memanggil beberapa endpoint, jadi pengguna jauh lebih sering terkena ekor p99 daripada yang dikira.
- Error rate — persentase request gagal (status 5xx, timeout, koneksi ditolak). Latensi bagus tidak berarti apa-apa bila 10% request error — error sering justru "cepat".
- Saturasi — seberapa penuh sumber daya: CPU, thread pool, connection pool, antrean. Ini yang menjelaskan kenapa latensi naik.
Kenapa rata-rata menyesatkan? Program berikut menghitung statistik dari 100 latensi, dengan 6 request lambat yang tersangkut menunggu koneksi database.
java
import java.util.Arrays;
public class LatencyReport {
// Nearest-rank percentile: urutkan, lalu ambil elemen di peringkat ceil(p/100 * n)
static long percentile(long[] sortedMillis, double p) {
int rank = (int) Math.ceil(p / 100.0 * sortedMillis.length);
return sortedMillis[Math.max(rank - 1, 0)];
}
public static void main(String[] args) {
long[] latencies = new long[100];
for (int i = 0; i < latencies.length; i++) {
latencies[i] = 40 + (i % 10); // mayoritas request cepat: 40–49 ms
}
for (int i = 0; i < 6; i++) {
latencies[i * 15] = 1200 + i * 100; // 6 request lambat (mis. menunggu koneksi DB)
}
long[] sorted = latencies.clone();
Arrays.sort(sorted);
double avg = Arrays.stream(sorted).average().orElse(0);
System.out.printf("avg = %.1f ms%n", avg);
System.out.println("p50 = " + percentile(sorted, 50) + " ms");
System.out.println("p95 = " + percentile(sorted, 95) + " ms");
System.out.println("p99 = " + percentile(sorted, 99) + " ms");
System.out.println("max = " + sorted[sorted.length - 1] + " ms");
}
}text
avg = 129.0 ms
p50 = 45 ms
p95 = 1200 ms
p99 = 1600 ms
max = 1700 msTidak ada satu pun request yang benar-benar butuh 129 ms. Rata-rata menyembunyikan dua kelompok yang sangat berbeda; percentile memperlihatkannya.
Little's Law: menghubungkan angka-angka
Hukum sederhana dari teori antrean: request yang sedang diproses = throughput × latensi. Pada 200 RPS dengan latensi 0,25 detik, rata-rata ada 50 request bersamaan di dalam sistem. Bila tiap request memegang satu koneksi DB selama itu dan pool hanya berisi 10 koneksi, sisanya mengantre — dan antrean itulah yang menaikkan p95. Rumus ini membantu kamu menebak ukuran thread pool dan connection pool sebelum menebak-nebak konfigurasi.
Target dan SLO
Load test tanpa target hanya menghasilkan angka. Tetapkan dulu SLO (Service Level Objective) yang bisa diukur, misalnya: "pada 200 RPS, p95 < 300 ms, p99 < 800 ms, error rate < 1%". Target ini lalu ditulis sebagai thresholds di script, sehingga hasilnya jelas lulus atau gagal.
k6: script sederhana
k6 adalah alat load test open source dari Grafana Labs. Script-nya JavaScript, tetapi engine-nya ditulis dalam Go sehingga ringan. Simpan sebagai load-test.js:
javascript
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 20 }, // warm-up: naik pelan ke 20 VU
{ duration: "2m", target: 100 }, // beban target
{ duration: "30s", target: 0 }, // turun
],
thresholds: {
http_req_duration: ["p(95)<300", "p(99)<800"],
http_req_failed: ["rate<0.01"],
},
};
const BASE_URL = __ENV.BASE_URL || "http://localhost:8080";
export default function () {
const res = http.get(`${BASE_URL}/api/products/SKU-001`);
check(res, { "status 200": (r) => r.status === 200 });
sleep(1); // jeda "berpikir" pengguna
}VU (virtual user) adalah pengguna simulasi yang menjalankan fungsi default berulang-ulang. Tanpa menginstal apa pun, jalankan lewat image Docker resmi grafana/k6:
bash
# Aplikasi berjalan di host port 8080; host.docker.internal dipetakan ke host (perlu di Linux)
docker run --rm -i --add-host=host.docker.internal:host-gateway \
-e BASE_URL=http://host.docker.internal:8080 \
grafana/k6 run - < load-test.js
echo "exit code: $?"Di akhir run, k6 mencetak ringkasan metrik seperti http_reqs (jumlah dan rate per detik), http_req_duration (avg, med, p(90), p(95), max), http_req_failed, dan status tiap threshold (✓ atau ✗). Tata letak ringkasan bisa sedikit berbeda antar versi k6, tetapi nama metriknya tetap. Bila ada threshold yang gagal, exit code bukan nol — cocok sebagai gerbang di CI.
VU vs arrival rate
Dengan stages, k6 memakai model tertutup: VU baru mengirim request setelah request sebelumnya selesai. Bila server melambat, beban ikut turun dan masalah tersamar. Untuk mengukur "apakah kami sanggup 200 RPS", pakai executor constant-arrival-rate atau ramping-arrival-rate (model terbuka) yang tetap mengirim request sesuai jadwal.
Gatling sekilas
Gatling adalah alternatif populer di ekosistem JVM. Skenarionya ditulis dengan DSL Java (juga Scala/Kotlin) dan dijalankan lewat plugin Maven/Gradle, lalu menghasilkan laporan HTML lengkap dengan grafik percentile.
java
// Potongan — Gatling Java DSL, dijalankan dengan plugin Maven Gatling
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
public class ProductSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http.baseUrl("http://localhost:8080");
ScenarioBuilder scn = scenario("Lihat produk")
.exec(http("GET produk").get("/api/products/SKU-001").check(status().is(200)));
{
setUp(scn.injectOpen(rampUsersPerSec(1).to(50).during(60)))
.protocols(httpProtocol)
.assertions(global().failedRequests().percent().lt(1.0));
}
}Pilih salah satu dan konsisten; konsep yang dibaca (throughput, percentile, error) sama persis.
Warm-up JIT dan lingkungan uji
JVM menjalankan kode mula-mula lewat interpreter, lalu JIT mengompilasi method yang sering dipanggil (seperti yang kamu lihat di bab profiling). Ditambah connection pool yang masih kosong dan cache yang dingin, menit pertama setelah start hampir selalu lebih lambat. Karena itu beri tahap warm-up di stages, dan bandingkan hasil hanya dari fase stabil.
Lingkungan juga harus jujur: jalankan generator beban di mesin yang berbeda dari aplikasi bila bisa (k6 juga butuh CPU), pakai data yang ukurannya mirip production (query ke tabel 100 baris dan 10 juta baris berperilaku sangat berbeda), dan ulangi test minimal dua kali sebelum menyimpulkan.
Mencari bottleneck
Hasil load test memberi tahu bahwa sistem lambat; metrik aplikasi (Actuator/Micrometer, JFR) memberi tahu di mana. Pola yang paling sering:
| Gejala saat beban naik | Metrik pendukung | Dugaan |
|---|---|---|
| Latensi naik, CPU rendah | hikaricp.connections.pending > 0 terus-menerus | Connection pool DB habis / query lambat |
| Latensi naik, CPU rendah, pool DB longgar | thread Tomcat sibuk semua, banyak thread WAITING | Thread pool penuh, panggilan eksternal blocking |
| Latensi bergerigi, lonjakan periodik | jvm.gc.pause panjang, heap hampir penuh | Tekanan GC / alokasi berlebihan |
| CPU 100%, throughput mendatar | flame graph JFR menunjukkan satu method dominan | Kode boros (serialisasi, regex, loop) |
| Error 5xx/timeout tiba-tiba | log timeout ke layanan lain | Dependency eksternal jenuh |
Perbaiki satu hal, ukur ulang, baru lanjut. Mengubah lima konfigurasi sekaligus membuat kamu tidak tahu mana yang menolong.
JMH: untuk micro-benchmark
Kadang pertanyaannya sempit: "mana lebih cepat, String.split atau parser manual?" Untuk itu pakai JMH (Java Microbenchmark Harness) dari OpenJDK. JMH mengurus warm-up, menjalankan benchmark di JVM terpisah (fork), dan menyediakan Blackhole agar JIT tidak membuang kode yang hasilnya tidak dipakai — jebakan klasik stopwatch manual.
java
// Potongan — butuh dependency jmh-core + annotation processor
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@Fork(1)
public class SkuParserBenchmark {
String sku = "ELEC-2026-000123";
@Benchmark
public void viaSplit(Blackhole bh) {
bh.consume(sku.split("-"));
}
}JMH melengkapi load test, bukan menggantikannya: hasil nanodetik di satu method jarang berarti bila 95% waktu request habis menunggu database.
Contoh di dunia backend
Tim inventaris akan menghadapi flash sale. SLO yang disepakati: endpoint cek stok sanggup 300 RPS dengan p95 < 200 ms dan error < 0,5%. Script memakai model terbuka dan menandai request dengan tag agar mudah difilter:
javascript
import http from "k6/http";
import { check } from "k6";
export const options = {
scenarios: {
flash_sale: {
executor: "ramping-arrival-rate",
startRate: 20,
timeUnit: "1s",
preAllocatedVUs: 100,
maxVUs: 400,
stages: [
{ duration: "1m", target: 50 }, // warm-up
{ duration: "3m", target: 300 }, // naik ke target
{ duration: "5m", target: 300 }, // tahan di target
],
},
},
thresholds: {
"http_req_duration{endpoint:stock}": ["p(95)<200"],
http_req_failed: ["rate<0.005"],
},
};
const skus = ["SKU-001", "SKU-002", "SKU-003", "SKU-004"];
export default function () {
const sku = skus[Math.floor(Math.random() * skus.length)];
const res = http.get(`${__ENV.BASE_URL}/api/inventory/${sku}`, {
tags: { endpoint: "stock" },
});
check(res, { "status 200": (r) => r.status === 200 });
}Run pertama gagal: p95 melonjak ke 1,4 detik setelah 180 RPS, CPU 30%, dan hikaricp.connections.pending mencapai 40. Pakai Little's Law: 180 RPS × 0,06 detik waktu query ≈ 11 koneksi bersamaan, sedangkan pool default HikariCP hanya 10. JFR memperlihatkan query stok melakukan full scan. Tim menambah index pada kolom sku (waktu query turun ke 5 ms), mengukur ulang, dan lulus di 300 RPS. Perhatikan urutannya: ukur → hipotesis dari metrik → satu perubahan → ukur ulang. Menaikkan ukuran pool tanpa memperbaiki query hanya memindahkan antrean ke database.
Jangan load test production tanpa izin
Load test sengaja menekan sistem sampai batasnya. Di production, itu bisa mematikan layanan untuk pelanggan sungguhan, memicu alert on-call, menghabiskan kuota API pihak ketiga, atau membuat ribuan pesanan palsu. Jalankan di environment staging yang mirip production. Bila memang perlu di production, harus ada izin tertulis, jadwal di luar jam sibuk, koordinasi dengan tim infrastruktur, dan cara menghentikan test seketika. Hal yang sama berlaku untuk sistem milik orang lain — tanpa izin, itu sama saja serangan DoS.
Pertanyaan interview
1. Kenapa p99 lebih penting daripada latensi rata-rata?
Rata-rata mencampur request cepat dan lambat sehingga menyembunyikan ekor distribusi. p99 menunjukkan pengalaman 1% request terburuk — dan karena satu halaman memanggil banyak endpoint serta pengguna aktif mengirim banyak request, sebagian besar pengguna akan terkena ekor itu cepat atau lambat. Ekor lambat juga sering menjadi tanda awal antrean pada pool atau GC.
2. Latensi naik saat beban naik, tetapi CPU aplikasi rendah. Apa yang kamu periksa?
CPU rendah berarti thread sedang menunggu, bukan menghitung. Periksa saturasi sumber daya yang ditunggu: antrean connection pool DB (hikaricp.connections.pending), thread pool server yang penuh, panggilan ke layanan eksternal yang lambat, atau lock contention. Thread dump atau JFR memperlihatkan thread sedang menunggu di mana.
3. Apa beda model beban tertutup dan terbuka di load test?
Pada model tertutup (jumlah VU tetap), request baru dikirim setelah respons sebelumnya diterima, sehingga saat server melambat, laju request ikut turun dan masalah tersamar. Model terbuka (arrival rate) mengirim request sesuai jadwal terlepas dari respons, mirip pengguna nyata di internet. Untuk memvalidasi target throughput, model terbuka lebih jujur.
Jebakan umum
- Melaporkan rata-rata saja. Selalu sertakan p95/p99 dan error rate; rata-rata 100 ms bisa menyembunyikan p99 3 detik.
- Tidak ada warm-up. Menit pertama setelah JVM start mengukur interpreter dan pool kosong, bukan performa sebenarnya.
- Generator beban jadi bottleneck. Menjalankan k6 dengan ribuan VU di mesin yang sama dengan aplikasi membuat keduanya berebut CPU — hasilnya mengukur laptopmu.
- Data uji tidak realistis. Tabel kosong dan satu SKU yang sama terus-menerus membuat semua data ada di cache; production tidak sebaik itu.
- Mengubah banyak hal sekaligus. Ubah satu konfigurasi per percobaan agar tahu persis apa yang berpengaruh.
- Menyimpulkan dari satu run. Hasil bervariasi; ulangi dan bandingkan dengan baseline yang disimpan.
Ringkasan
- Pilih jenis test sesuai pertanyaan: smoke, load, stress, spike, atau soak — selalu mulai dari smoke.
- Baca throughput, percentile (p50/p95/p99), error rate, dan saturasi bersama-sama; rata-rata saja menyesatkan.
- Tetapkan SLO lebih dulu dan tulis sebagai
thresholdsk6 agar hasilnya lulus/gagal dan bisa dipakai di CI. - Beri warm-up, pakai data realistis, dan cocokkan hasil dengan metrik aplikasi untuk menemukan bottleneck (pool DB, thread pool, GC).
- JMH untuk micro-benchmark satu method; load test untuk sistem utuh — dan jangan pernah menekan production tanpa izin.