← semua materi
15:00 Teknis Β· 25 Oct 2026 Β· 15:08

πŸ”¬ 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:

  1. Milih executor yang benar β€” dan tahu kenapa constant-vus bisa bohongi kamu (coordinated omission).
  2. Baca dropped_iterations sebagai sinyal titik patah, bukan noise.
  3. Bikin threshold = SLO, dipakai sebagai gate CI (exit code 99).
  4. 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:

  1. Load generator jadi bottleneck. VU mentok CPU β†’ dropped_iterations naik, kamu salahkan backend. Cek dulu: CPU k6 process, vus_max menyentuh preAllocatedVUs, dan apakah angka drop muncul sejak detik pertama (= config kurang) atau belakangan (= SUT melambat). Dokumentasi resmi menyebut pembedaan ini eksplisit.
  2. maxVUs dipakai sebagai obat. Alokasi VU saat runtime = biaya CPU/RAM + hasil skewed. Di k6 Cloud, maxVUs menagih subscription melebihi preAllocatedVUs. Default: hitung preAllocatedVUs β‰ˆ median_iteration_duration Γ— rate + konstan.
  3. sleep() di akhir iterasi open model. Bikin rate aktual < target tanpa error. Arrival-rate executor sudah pacing.
  4. 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.
  5. 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.
  6. Test di QA env ber-resource minim lalu menyimpulkan "backend gak scale". Env tidak representatif = data tidak valid.
  7. check() dianggap gate. check() tidak mempengaruhi exit code. Wajib dipasangkan thresholds: { 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
  1. Jalankan closed.js lalu open.js dari sesi ini (ganti URL ke 127.0.0.1:8089/fast). Bandingkan http_reqs rate-nya.
  2. Jalankan breakpoint.js. Cari stage pertama di mana dropped_iterations > 0 β€” itu titik patah versi SUT-mu.
  3. Turunkan preAllocatedVUs jadi 5 di open.js, jalankan lagi. Amati: drop naik di awal test, bukan di akhir. Itu bedanya config kurang vs SUT lambat.
  4. Cek exit code: k6 run --quiet open.js; echo $? β†’ harus 99.

βœ… Cek Pemahaman

  1. Server-mu punya p95 200ms di produksi. Kamu jalankan constant-vus: 50 dan dapat p95 180ms β€” lulus. Kenapa hasil ini belum membuktikan sistem sanggup 250 req/s?
  2. dropped_iterations = 0 di seluruh test open model. Apakah itu berarti sistem sehat? (Hint: ada satu kondisi di mana ini justru mencurigakan.)
  3. Kamu taruh http_req_duration: ['p(95)<300'] dan http_req_duration: ['p(99)<500'] di object thresholds. Apa yang terjadi, dan kenapa tidak ada error?
Jawaban ringkas: (1) Closed model β€” throughput = 50/0.18 β‰ˆ 277/s hanya kalau tiap VU selalu sibuk; yang tidak teruji adalah request yang *datang* saat sistem sudah antre. (2) Bisa berarti target rate terlalu rendah, atau `rate` salah satuan (`timeUnit: '1m'` padahal maksud per detik). (3) Key duplikat di object literal JS β†’ yang kedua menimpa/menyilently-ignore; solusi: satu key, array dua elemen.

πŸ”‘ Poin Kunci


➑️ Eksplorasi Lanjut

  1. k6 Operator 1.0 β€” distributed test di Kubernetes (stabil per blog k6 2.0). Topik lanjutan bagus: cara split rate antar pod tanpa double-counting metric.
  2. k6 browser module + Playwright compatibility β€” jalankan flow checkout nyata di Chromium sambil ukur Web Vitals, threshold pakai browser_web_vital_lcp.
  3. k6 x mcp β€” k6 2.0 mengekspos MCP server bawaan; relevan kalau kamu mau agent menulis & memvalidasi load test sendiri.
  4. Correlate k6 ↔ APM β€” kirim hasil via OTel output, lalu bandingkan http_req_duration k6 dengan span server. Selisihnya = network + antrean di luar aplikasi.
  5. 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.