Load Testing API Node.js dengan k6 dan Dashboard Grafana

Foto oleh Luke Chesser on Unsplash
k6 adalah tool load-testing open-source dari Grafana Labs. Anda menulis skrip tes dalam JavaScript yang mensimulasikan virtual user memukul sebuah API, menjalankannya dari command line, dan mengukur latensi, throughput, serta tingkat error di bawah beban. Ia umum dipakai untuk menjadikan pipeline CI sebagai gate dan menemukan berapa banyak lalu lintas yang bisa ditangani API sebelum merosot.
Threshold adalah ekspresi atas sebuah metrik, seperti http_req_duration p(95) di bawah 300ms atau http_req_failed rate di bawah 0.01, yang harus terpenuhi di akhir tes. Jika bernilai salah, k6 keluar dengan kode non-nol, yang diperlakukan runner CI mana pun sebagai langkah gagal. Menambahkan abortOnFail menghentikan run begitu threshold dilanggar.
Smoke test memakai VU minimal untuk memastikan skrip dan sistem berfungsi. Average-load test menjalankan lalu lintas produksi yang diharapkan. Stress test mendorong di atas puncak yang diharapkan. Soak test menahan beban rata-rata selama berjam-jam untuk mengungkap kebocoran. Spike test melempar lonjakan mendadak yang masif untuk menguji kelangsungan dan pemulihan.
Jalankan k6 dengan output eksperimental Prometheus remote-write: k6 run -o experimental-prometheus-rw script.js. Arahkan ke endpoint Prometheus lewat K6_PROMETHEUS_RW_SERVER_URL (default http://localhost:9090/api/v1/write), setel K6_PROMETHEUS_RW_TREND_STATS untuk persentil yang diinginkan, lalu impor dashboard k6 Prometheus siap pakai dari Grafana Labs untuk memantau metrik secara langsung.
API Node.js melayani query melalui connection pool berukuran tetap. Di bawah ukuran pool latensi tetap datar, tetapi begitu query serentak yang berjalan melebihinya, request mengantre menunggu koneksi bebas. Waktu antrean itu muncul sebagai lonjakan tajam pada latensi p(95) dan p(99) sementara tingkat error terlihat baik. Menaikkan VU secara bertahap mengungkap titik tekuk itu sebagai batas pool Anda.

Foto oleh Luke Chesser on Unsplash
Ringkasan Utama
k6 adalah tool load-testing berbasis JavaScript yang membuat skrip virtual user memukul sebuah API, meningkatkannya melalui stages, dan menegakkan threshold sebagai gate lolos atau gagal di CI. Streaming metriknya ke Prometheus lewat remote write dan menggambarnya di Grafana mengubah satu run menjadi dashboard langsung, memperlihatkan batas connection pool database tempat latensi tiba-tiba melonjak.
Setiap API yang saya rilis pada akhirnya menghadapi pertanyaan yang sama saat tinjauan insiden: berapa banyak pengguna serentak yang sebenarnya bisa ditanggung sebelum tumbang? Menebak bukanlah jawaban. Load testing mengubah tebakan itu menjadi angka yang bisa Anda pertanggungjawabkan, dan k6 adalah tool andalan saya karena tesnya JavaScript murni, berjalan dari command line, dan menggagalkan pipeline CI begitu performa merosot.
k6 adalah tool load-testing open-source yang dikelola oleh Grafana Labs. Anda menulis skrip yang menjelaskan apa yang dilakukan satu virtual user, memberi tahu k6 berapa banyak virtual user yang dijalankan dan selama berapa lama, lalu k6 memutar ulang perilaku itu di bawah beban sambil mencatat latensi, throughput, dan tingkat error. Di tulisan ini saya membahas cara menulis tes, membuat gate pipeline pada threshold, memilih profil tes yang tepat, streaming hasil ke Grafana, dan memakai semua itu untuk menemukan titik ketika connection pool database menjadi bottleneck.
Skrip k6 punya dua bagian: sebuah objek options yang diekspor untuk mengonfigurasi run, dan sebuah default function yang berisi kode yang dijalankan tiap virtual user dalam sebuah loop. Virtual user, atau VU, adalah satu klien serentak yang disimulasikan. Objek options adalah tempat Anda menentukan berapa banyak VU berjalan dan bagaimana jumlahnya berubah dari waktu ke waktu.
Array stages adalah cara paling berguna untuk membentuk sebuah run. Tiap entri adalah target jumlah VU dan sebuah durasi, dan k6 meningkatkan jumlah VU aktif secara linear menuju target itu selama periode tersebut. Ini adalah pintasan untuk executor ramping-VUs. Skrip di bawah melakukan pemanasan ke 50 VU, menahan, mendorong ke 100, menahan lagi, lalu menurun kembali.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
// A ramping-VUs profile: warm up, hold, push higher, ramp down.
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
// Thresholds are the pass/fail contract. Any breach = non-zero exit.
thresholds: {
http_req_failed: ['rate<0.01'], // < 1% of requests may error
http_req_duration: ['p(95)<300', 'p(99)<800'], // ms
checks: ['rate>0.99'], // > 99% of checks must pass
},
};
export default function () {
const res = http.get('https://api.example.com/orders');
check(res, {
'status is 200': (r) => r.status === 200,
'body is not empty': (r) => r.body.length > 0,
});
sleep(1);
}Di dalam default function, http.get menembakkan request dan check menjalankan asersi terhadap respons. Check tidak pernah menggagalkan tes dengan sendirinya; ia hanya mencatat tingkat lolos atau gagal. Panggilan sleep memodelkan think time antar-request agar VU berperilaku sedikit lebih menyerupai pengguna nyata alih-alih loop pemukul yang rapat.
Jaga default function tetap kecil dan fokus pada satu perjalanan pengguna. Jika Anda perlu memodelkan beberapa perjalanan dengan bobot berbeda, gunakan named scenarios dan executors daripada menjejalkan logika bercabang ke dalam satu fungsi. Satu scenario per perjalanan menjaga metrik tetap bersih dan hasil mudah dibaca.
Threshold adalah yang membuat k6 berguna di dalam pipeline. Tiap threshold adalah ekspresi atas sebuah metrik yang harus tetap benar di akhir run; jika ia bernilai salah, k6 keluar dengan kode non-nol dan langkah pipeline gagal. Ekspresi itu mengikuti bentuk agregasi, operator, nilai — misalnya latensi persentil ke-95 di bawah suatu batas, atau tingkat error di bawah suatu plafon. Metrik yang paling sering saya jadikan gate ada tiga berikut.
Secara default sebuah threshold hanya dievaluasi sekali, di akhir tes. Tambahkan abortOnFail ke sebuah threshold dan k6 menghentikan run seketika saat ia dilanggar, yang menghemat menit pada build yang jelas gagal; delayAbortEval memberi run masa tenggang untuk mengumpulkan data sebelum penegakan mulai. Karena threshold yang gagal adalah kode keluar non-nol, tidak perlu perekat tambahan — GitHub Actions, GitLab CI, atau Jenkins sudah memperlakukannya sebagai langkah yang gagal.
Skrip yang sama menjadi tes yang berbeda tergantung bagaimana Anda menyetel VU dan durasi. Grafana mendokumentasikan sekeluarga kecil tipe tes, masing-masing menjawab pertanyaan yang berbeda. Saya menyimpan sebuah smoke test di setiap pipeline dan menjalankan profil yang lebih berat secara terjadwal atau sebelum rilis besar.
| Profil | Beban dan durasi | Apa yang terungkap |
|---|---|---|
| Smoke | VU minimal, detik hingga menit | Skrip berfungsi dan sistem sehat di bawah beban sepele — gate kewarasan murah untuk CI. |
| Average-load | VU produksi yang diharapkan, 5 hingga 60 menit | Apakah sistem menjaga performa normal di bawah lalu lintas sehari-hari yang khas. |
| Stress | VU di atas rata-rata, 5 hingga 60 menit | Bagaimana sistem berperilaku melewati puncak yang diharapkan, dan seberapa anggun ia merosot. |
| Soak | VU rata-rata, beberapa jam | Keandalan sepanjang waktu — kebocoran memori, kehabisan koneksi, dan pergeseran sumber daya yang lambat. |
| Spike | VU sangat tinggi, beberapa menit | Kelangsungan dan pemulihan ketika lalu lintas melonjak mendadak lalu turun kembali. |
k6 mencetak ringkasan saat sebuah run berakhir, tetapi cerita menariknya adalah apa yang terjadi selama run berlangsung. Output eksperimental Prometheus remote-write men-streaming setiap metrik ke sebuah instance Prometheus secara real time, sehingga Anda bisa menonton latensi naik langsung di dashboard Grafana. Anda mengarahkan k6 ke sebuah endpoint remote-write dengan variabel lingkungan dan menambahkan flag output.
# Point k6 at a Prometheus endpoint that accepts remote write.
# Default target is http://localhost:9090/api/v1/write
export K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write
# Ship percentiles, not just the p(99) default, so dashboards are useful.
export K6_PROMETHEUS_RW_TREND_STATS="p(95),p(99),min,max"
# Stream every metric to Prometheus in real time as the test runs.
k6 run -o experimental-prometheus-rw load-test.jsSecara default k6 hanya mengirim p(99) untuk metrik trend; menyetel K6_PROMETHEUS_RW_TREND_STATS ke daftar seperti p(95),p(99),min,max mengirim persentil yang dibutuhkan dashboard. Grafana Labs menerbitkan dashboard k6 Prometheus siap pakai yang bisa Anda impor, sehingga Anda mendapat panel untuk request rate, error rate, dan persentil latensi tanpa membangunnya sendiri. Untuk fidelitas tertinggi Anda bisa mengaktifkan native histograms, yang membutuhkan Prometheus versi baru dengan fitur itu dihidupkan.
Output Prometheus remote-write masih ditandai eksperimental, dan nama flag-nya mencerminkan itu: k6 run -o experimental-prometheus-rw. Kunci versi k6 Anda di CI agar sebuah upgrade tidak diam-diam mengganti nama flag atau mengubah default di tengah proyek, dan pastikan Prometheus Anda benar-benar mengaktifkan penerima remote-write sebelum menyalahkan k6 atas data yang hilang.
Di sinilah load testing membuktikan nilainya. Sebuah API Node.js biasanya berbicara ke databasenya melalui connection pool berukuran tetap. Di bawah ukuran pool, konkurensi tambahan sebagian besar hanya memakai koneksi menganggur dan latensi tetap datar. Begitu query yang sedang berjalan serentak melebihi pool, request baru mengantre menunggu koneksi bebas, dan waktu antrean itu muncul sebagai lonjakan tajam dan mendadak pada p(95) dan p(99) — meski CPU dan tingkat error masih terlihat baik.
Profil stress atau breakpoint membuat batas itu terlihat. Naikkan VU secara mantap dan amati persentil latensi di dashboard Grafana: titik tekuk pada kurva, tempat latensi berubah dari datar menjadi garis curam sementara throughput berhenti naik, adalah batas pool Anda. Dari sana Anda bisa menaikkan ukuran pool, menambah read replica, atau memperpendek query — tetapi kini keputusan didorong oleh titik putus yang terukur, bukan firasat. Databasenya, bukan k6 atau Node, biasanya adalah batas yang sesungguhnya.
Aturan praktis saya: sebuah smoke test di setiap pull request sebagai gate cepat, sebuah load test dengan threshold nyata sebelum tiap rilis, dan sesekali run soak atau stress yang di-streaming ke Grafana ketika saya perlu mengukur infrastruktur. Skripnya nyaris tidak berubah di antara mereka — hanya VU, durasi, dan threshold yang berubah. Mulai dengan satu endpoint, jadikan gate pada sebuah threshold p(95), dan biarkan pipeline yang merah mengajari Anda di mana API Anda sebenarnya patah.