π¬ Sesi 15:00 β Load Testing dengan k6: mencari titik patah sebelum user menemukannya
π Topik: k6 v2.3.0 β open vs closed model, arrival-rate executor, threshold sebagai SLO gate, breakpoint test, integrasi CI
Versi terverifikasi: k6 v2.3.0 (commit/e088784614, go1.26.8, linux/amd64) β rilis 21 Sep 2026 (versio.io, Chocolatey 22 Sep 2026). Semua angka di bawah = eksekusi nyata di mesin ini, bukan kutipan.
π― Tujuan Belajar
Setelah sesi ini kamu bisa:
- Milih executor yang benar β dan tahu kenapa
constant-vusbisa bohongi kamu (coordinated omission). - Baca
dropped_iterationssebagai sinyal titik patah, bukan noise. - Bikin threshold = SLO, dipakai sebagai gate CI (exit code 99).
- Pakai fitur baru v2.3.0:
--scenario,--once,expect()assertion, machine-readable summary.
ποΈ Arsitektur & Alur Teknis
k6 process
βββ VU pool (goroutine + JS runtime Sobek, 1 VU = 1 single-thread event loop)
β βββ tiap VU: import script β init context (sekali) β default() loop
βββ Executor (scheduler) β INI yang menentukan model beban
β βββ closed: constant-vus, ramping-vus, shared-iterations, per-vu-iterations
β βββ open : constant-arrival-rate, ramping-arrival-rate
βββ Metrics aggregator (Counter/Gauge/Rate/Trend)
β βββ thresholds dievaluasi di akhir test (atau periodik kalau abortOnFail)
βββ Outputs: stdout summary, --summary-export JSON, OTel, Prometheus RW
Alur satu iterasi: executor minta VU β VU ambil slot β jalankan default() β emit metrics (http_req_duration, http_reqs, dst) β VU balik ke pool.
Bottleneck k6 sendiri: VU single-threaded. Kalau SUT melambat, iterasi jadi lebih lama, VU habis, bukan beban yang turun (open model) β tapi iterasi di-drop.
π» Kode Nyata
1. Closed model β jebakan coordinated omission
// closed.js β k6 v2.3.0
import http from 'k6/http';
export const options = {
scenarios: {
closed: { executor: 'constant-vus', vus: 10, duration: '15s' },
},
thresholds: { http_req_duration: ['p(95)<300'] },
};
export default function () {
http.get('http://127.0.0.1:8089/very-slow'); // ~1s latency
}
Hasil nyata (endpoint latency ~1.0s, 10 VU, 15s):
http_req_duration..............: avg=1.04s p(95)=1.21s
http_reqs......................: 149 9.268396/s
iterations.....................: 149 9.268396/s
β Throughput 9.3 it/s. Bukan karena server kuat, tapi karena 10 VU Γ (1/1.04s) β 9.6. Kalau server jadi 2Γ lebih lambat, throughput turun β beban ikut menyesuaikan diri. Inilah yang bikin dashboard "RPS stabil" padahal sistem sedang sekarat.
2. Open model β beban konstan, kegagalan terlihat
// open.js
import http from 'k6/http';
export const options = {
discardResponseBodies: true, // hemat RAM load generator
scenarios: {
open: {
executor: 'constant-arrival-rate',
rate: 50, timeUnit: '1s', duration: '15s',
preAllocatedVUs: 20,
maxVUs: 100,
},
},
thresholds: {
http_req_duration: ['p(95)<300'],
dropped_iterations: ['count<10'],
},
};
export default function () {
http.get('http://127.0.0.1:8089/very-slow');
// JANGAN sleep() di akhir iterasi β executor sudah pacing
}
Hasil nyata:
http_req_duration..............: avg=1.01s p(95)=1.18s
http_reqs......................: 716 44.36/s β target 50/s
dropped_iterations.............: 35 2.17/s
vus............................: 3 min=3 max=54
vus_max........................: 55 min=34 max=55
Target 50/s, tercapai 44.4/s, 35 iterasi di-drop. Beban tetap ditekan 50/s; yang gagal adalah eksekusinya β persis seperti user nyata yang timeout.
3. Breakpoint test β cari titik patah, bukan cuma lulus/gagal
// breakpoint.js
import http from 'k6/http';
import { Trend } from 'k6/metrics';
const lat = new Trend('slo_latency', true);
export const options = {
discardResponseBodies: true,
scenarios: {
ramp: {
executor: 'ramping-arrival-rate',
startRate: 10, timeUnit: '1s',
preAllocatedVUs: 60, // sengaja besar; maxVUs TIDAK dipakai
stages: [
{ target: 20, duration: '10s' },
{ target: 40, duration: '10s' },
{ target: 80, duration: '10s' },
{ target: 160, duration: '10s' },
],
},
},
thresholds: {
dropped_iterations: ['count<1'], // gate: nol drop = belum patah
http_req_duration: ['p(95)<300'],
},
};
export default function () {
const res = http.get('http://127.0.0.1:8089/very-slow');
lat.add(res.timings.duration);
}
Hasil nyata:
dropped_iterations.............: 695 16.87/s β 'count<1'
http_req_duration..............: p(95)=1.19s β 'p(95)<300'
http_reqs......................: 1554 37.72/s
vus_max........................: 60 min=60 max=60
Baca: kapasitas riil β 37β44 it/s; di atas itu dropped_iterations meledak dan vus_max menyentuh plafon 60. Titik patah = ~40 req/s untuk latency 1s dengan 60 VU (Little's Law: 40 Γ 1.0s β 40 VU terpakai, sisanya overhead).
4. expect() β Assertions API (baru di k6 2.0, jslib 0.6.1)
import http from 'k6/http';
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
export const options = { vus: 1, iterations: 3 };
export default function () {
const res = http.get('http://127.0.0.1:8089/fast');
expect(res.status).toBe(200);
expect(res.json()).toHaveProperty('ok');
}
Bedanya dengan check() β terverifikasi lewat exit code:
check() |
expect() |
|
|---|---|---|
| Gagal β test lanjut? | Ya | Tidak, langsung abort |
| Pengaruh exit code? | Tidak (kecuali di-threshold) | Ya, 108 |
| Cocok untuk | load test (kumpulkan metric) | functional/smoke test |
Assertion gagal nyata β level=error msg="test aborted: ... Expected: 500 Received: 200", exit 108 (ScriptAborted).
5. Threshold = SLO, exit code = gate CI
k6 run --quiet smoke.js ; echo $? # β 0 (pass)
k6 run --quiet open.js ; echo $? # β 99 (threshold crossed)
k6 run --quiet assertfail.js ; echo $? # β 108 (script aborted)
Exit code terverifikasi dari errext/exitcodes/codes.go:
99 ThresholdsHaveFailed β gate SLO
100 SetupTimeout
104 InvalidConfig
107 ScriptException
108 ScriptAborted
109 GoPanic
6. Fitur baru v2.3.0 (21 Sep 2026) β teruji langsung
# Jalankan hanya scenario tertentu, tanpa edit script
k6 run --scenario fast multi.js # β 10 reqs (sesuai config `fast`)
k6 run --once --scenario fast multi.js # β 1 req (smoke dari script load)
--once mengganti scenario jadi shared-iterations 1 VU/1 iterasi tapi mempertahankan exec function + opsi browser. Ini yang dulu rusak: --vus 1 --iterations 1 membuang config Chromium sehingga browser test gagal launch.
Fitur lain v2.3.0 yang relevan produksi:
- --features async-metric-context β group() menerima async fn, metric setelah await tetap masuk group yang benar.
- tlsAIAFetch: true β fetch intermediate cert yang hilang (server jalan di browser, gagal di k6).
- K6_PROMETHEUS_RW_LABELS="env=prod,server=srv1" β bedakan instance k6 di Prometheus.
- /v1/status sekarang punya execution_result.exit_code (berguna kalau pakai --linger).
7. Summary machine-readable (k6 2.0+, untuk CI/agent)
k6 run --new-machine-readable-summary --summary-export=summary.json script.js
Struktur baru: {config, metadata, results, version} dengan metadata.k6_version dan results.metrics[] berisi {name, type, values}. Beda dari --summary-export lama yang formatnya {root_group, metrics:{nama:{count,rate,...}}} β jangan campur parser.
8. Struktur suite (pola resmi: pisah scenario dari workload)
tests/
βββ lib/scenario.js # export function browse() {...} β logika saja
βββ smoke-api.js # import scenario; 1 iteration
βββ load-api.js # import scenario; constant-arrival-rate 100/s 3m
βββ stress-api.js # import scenario; ramping 1500/s 5m
π Angka & Benchmark (semua dari run lokal sesi ini)
| Skenario | Executor | Target | Hasil | Dropped | p95 | VU max | Exit |
|---|---|---|---|---|---|---|---|
| Closed 10 VU, 15s | constant-vus |
10 VU | 9.27 it/s | β | 1.21s | 10 | 99 |
| Open 50/s, 15s | constant-arrival-rate |
50/s | 44.36 it/s | 35 | 1.18s | 55 | 99 |
| Ramp 10β160/s, 40s | ramping-arrival-rate |
160/s | 37.72 it/s | 695 | 1.19s | 60 | 99 |
| Smoke 1 iter | shared-iterations |
1 | 1 req | β | β | 1 | 0 |
Interpretasi: titik patah sistem uji β 40 req/s pada latency 1s. Di bawah itu, drop = 0; di atasnya, drop naik linear terhadap kelebihan beban.
β οΈ Limitasi pengukuran ini: SUT = ThreadingHTTPServer Python lokal (thread-per-connection), load generator = 1 proses k6 di mesin yang sama. Angka absolut tidak bisa dipakai sebagai benchmark k6 vs tool lain. Yang valid: perilaku relatif open vs closed, dan mekanisme dropped_iterations/vus_max sebagai sinyal.
βοΈ Trade-off & Alternatif
Open vs Closed model
| Aspek | Closed (constant-vus) |
Open (constant-arrival-rate) |
|---|---|---|
| Yang dikontrol | jumlah user simultan | request/detik |
| Throughput saat SUT lambat | ikut turun (coordinated omission) | tetap |
| Realisme | user yang "sabar menunggu" | user dengan timeout/retry |
| Config complexity | rendah | butuh preAllocatedVUs benar |
| Cocok untuk | session-based flow, browser test | API RPS, SLO latency |
Aturan kasar: kalau SLO-mu latency, pakai open model. Closed model akan menyembunyikan ekor latency karena request yang "terlambat" tidak pernah dikirim.
k6 vs alternatif
| k6 | JMeter | Locust | Gatling | |
|---|---|---|---|---|
| Bahasa script | JS/TS | XML/GUI | Python | Scala/Java |
| Footprint per VU | rendah (goroutine) | tinggi (thread) | sedang (gevent) | sedang (Akka) |
| Open model native | ya | terbatas | ya (constant_throughput) |
ya |
| Ekosistem protokol | HTTP/WS/gRPC/browser + ext | sangat luas | HTTP-centric | HTTP/WS/JMS |
| CI ergonomics | exit code + JSON summary | butuh plugin | butuh Python env | butuh JVM |
k6 lemah kalau butuh protokol eksotis tanpa extension resmi, atau butuh scripting language yang sudah dikuasai tim (Python β Locust lebih murah).
π Use Case Produksi & Failure Mode Umum
Pola yang jalan:
- Smoke test tiap PR (--once sekarang cara resmi), average-load nightly, stress/spike sebelum rilis besar, soak kuartalan.
- Threshold diturunkan dari SLO nyata (p95 dari APM produksi), bukan angka bulat karangan.
- abortOnFail: true + delayAbortEval: '30s' β hentikan test boros begitu SLO jelas jebol.
Failure mode yang sering kejadian:
- Load generator jadi bottleneck. VU mentok CPU β
dropped_iterationsnaik, kamu salahkan backend. Cek dulu: CPU k6 process,vus_maxmenyentuhpreAllocatedVUs, dan apakah angka drop muncul sejak detik pertama (= config kurang) atau belakangan (= SUT melambat). Dokumentasi resmi menyebut pembedaan ini eksplisit. maxVUsdipakai sebagai obat. Alokasi VU saat runtime = biaya CPU/RAM + hasil skewed. Di k6 Cloud,maxVUsmenagih subscription melebihipreAllocatedVUs. Default: hitungpreAllocatedVUs β median_iteration_duration Γ rate + konstan.sleep()di akhir iterasi open model. Bikin rate aktual < target tanpa error. Arrival-rate executor sudah pacing.- Threshold ganda di key yang sama.
{http_req_duration: ['p(95)<300'], http_req_duration: ['p(99)<500']}β yang kedua silently ignored. Harus satu array. - Quality gate CI sebagai false assurance. Dokumentasi Grafana sendiri memperingatkan: gate performance bisa false-positive/negative; durasi test 3β15 menit memperlambat rilis. Saran mereka: mulai sebagai warning, naikkan ke blocking setelah matang.
- Test di QA env ber-resource minim lalu menyimpulkan "backend gak scale". Env tidak representatif = data tidak valid.
check()dianggap gate.check()tidak mempengaruhi exit code. Wajib dipasangkanthresholds: { checks: ['rate>0.99'] }.
π οΈ Latihan Praktis Kecil (~15 menit, laptop sendiri)
# 1. Install (pinned, jangan latest)
cd /tmp && curl -sL -o k6.tgz \
https://github.com/grafana/k6/releases/download/v2.3.0/k6-v2.3.0-linux-amd64.tar.gz
tar xzf k6.tgz && ./k6-v2.3.0-linux-amd64/k6 version
# 2. server.py β SUT dengan latency yang memburuk saat konkurensi naik
import time, json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import active_count
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
time.sleep(0.05 + 0.01 * active_count()) # makin ramai makin lambat
body = json.dumps({"ok": True}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
ThreadingHTTPServer(("127.0.0.1", 8089), H).serve_forever()
python3 server.py & # jalankan di terminal terpisah
- Jalankan
closed.jslaluopen.jsdari sesi ini (ganti URL ke127.0.0.1:8089/fast). Bandingkanhttp_reqsrate-nya. - Jalankan
breakpoint.js. Cari stage pertama di manadropped_iterations > 0β itu titik patah versi SUT-mu. - Turunkan
preAllocatedVUsjadi5diopen.js, jalankan lagi. Amati: drop naik di awal test, bukan di akhir. Itu bedanya config kurang vs SUT lambat. - Cek exit code:
k6 run --quiet open.js; echo $?β harus99.
β Cek Pemahaman
- Server-mu punya p95 200ms di produksi. Kamu jalankan
constant-vus: 50dan dapat p95 180ms β lulus. Kenapa hasil ini belum membuktikan sistem sanggup 250 req/s? dropped_iterations = 0di seluruh test open model. Apakah itu berarti sistem sehat? (Hint: ada satu kondisi di mana ini justru mencurigakan.)- Kamu taruh
http_req_duration: ['p(95)<300']danhttp_req_duration: ['p(99)<500']di objectthresholds. Apa yang terjadi, dan kenapa tidak ada error?
π Poin Kunci
- Open model menguji sistem; closed model menguji kesabaran user. Pilih berdasarkan SLO yang kamu klaim.
dropped_iterations= metrik paling informatif di k6. Nol saat awal test jelek; nol sepanjang test bisa berarti target terlalu rendah.- Little's Law untuk sizing:
VU β rate Γ latency. 40 req/s Γ 1.0s = 40 VU. Di bawah itu, executor akan drop. check()β gate.thresholds= gate. Exit 99 = SLO jebol, exit 108 = script abort.maxVUsadalah bau kode. HitungpreAllocatedVUsdari data, bukan trial-error runtime.- Threshold ganda di satu metric harus dalam satu array β duplikat key di-ignore tanpa peringatan.
- v2.3.0 bawa
--scenariodan--once;--oncesekarang cara resmi menjalankan script load sebagai smoke/functional test tanpa kehilangan config browser. - Quality gate performance di CI punya false-positive/negative. Dokumentasi Grafana sendiri menyarankan mulai sebagai warning.
β‘οΈ Eksplorasi Lanjut
- k6 Operator 1.0 β distributed test di Kubernetes (stabil per blog k6 2.0). Topik lanjutan bagus: cara split
rateantar pod tanpa double-counting metric. - k6 browser module + Playwright compatibility β jalankan flow checkout nyata di Chromium sambil ukur Web Vitals, threshold pakai
browser_web_vital_lcp. k6 x mcpβ k6 2.0 mengekspos MCP server bawaan; relevan kalau kamu mau agent menulis & memvalidasi load test sendiri.- Correlate k6 β APM β kirim hasil via OTel output, lalu bandingkan
http_req_durationk6 dengan span server. Selisihnya = network + antrean di luar aplikasi. - Soak test 2 jam untuk deteksi memory leak / connection pool exhaustion β beda kelas bug dari stress test.
π Sumber
[1] Grafana. "Version 2.3.0 release notes" β grafana.com/docs/k6/latest/release-notes/v2.3.0/ (diakses 25 Okt 2026)
[2] Grafana. "Version 2.0.0 release notes" β grafana.com/docs/k6/latest/release-notes/v2.0.0/
[3] ThΓ©o Crevon. "AI-assisted testing, extensions updates, and more: k6 2.0 is here" β Grafana Labs blog, 12 Mei 2026
[4] Grafana. "Thresholds" β grafana.com/docs/k6/latest/using-k6/thresholds/
[5] Grafana. "Open and closed models" β .../scenarios/concepts/open-vs-closed/
[6] Grafana. "Arrival-rate VU allocation" β .../scenarios/concepts/arrival-rate-vu-allocation/
[7] Grafana. "Dropped iterations" β .../scenarios/concepts/dropped-iterations/
[8] Grafana. "Constant arrival rate" / "Ramping arrival rate" β .../scenarios/executors/
[9] Grafana. "Automated performance testing" β .../testing-guides/automated-performance-testing/
[10] grafana/k6 errext/exitcodes/codes.go β github.com/grafana/k6
[11] Versio.io & Chocolatey β tanggal rilis k6 2.3.0: 21β22 Sep 2026
Status verifikasi: versi binary, semua angka benchmark, exit code, dan perilaku --scenario/--once/expect() = DIKETAHUI (dieksekusi langsung). Interpretasi titik patah ~40 req/s = DIINFERENSI dari data lokal, terikat pada SUT Python dan load generator satu mesin. Angka absolut k6 vs tool lain = BELUM PASTI, tidak diukur.