Skip to main content

loom-performance-testing

Performance and load testing with k6, locust, JMeter, Gatling, and artillery.

ソース情報

リポジトリ
cosmix/loom
ソースの最終更新活動
2026年8月29日 21:19
検出された SKILL.md の言語
英語
スター
61
フォーク
4

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
loom-performance-testing
description
Performance and load testing with k6, locust, JMeter, Gatling, and artillery.
triggers
["performance testing","load testing","stress testing","stress test","load test","performance test","k6","locust","JMeter","Gatling","artillery","benchmark","benchmarking","profiling","latency","throughput","RPS","requests per second","concurrent users","virtual users","percentile","p95","p99","p50","median latency","saturation","bottleneck","coordinated omission","performance budget","API load testing","database performance","query optimization","[Truncated]"]
# Performance Testing ## Overview Validate that a system meets latency, throughput, and stability targets under load, and locate the bottleneck when it doesn't. This file assumes you can write the test code; it focuses on the measurement traps that make load-test numbers lie and the decision criteria for tool/shape selection. ## Core Concepts (read first — these are where numbers go wrong) ### Open vs. closed workload models The single most consequential choice. It determines what your numbers mean. | Model | Load driver | Throughput is… | Overload behavior | Use for | | ---------- | ------------------------------------ | ------------------------ | ------------------------------------------------ | -------------------------------- | | **Closed** | Fixed VUs, each loops request→wait | *Emergent* (backpressure) | Self-throttles: slow server → fewer requests sent | Modeling a fixed client pool | | **Open** | Fixed *arrival rate* (req/s) | *Controlled* (you set it) | Queue grows unbounded; latency explodes | Web traffic, finding breaking pt | ⚠ **Closed models hide overload.** With fixed VUs, when the server slows down each VU sends *fewer* requests, so offered load silently drops. You can't overwhelm the server past what its own latency allows — you measure a moving target, not capacity. Real internet traffic is open (users arrive independently of server health), so **use an arrival-rate executor to find true breaking points.** - k6: closed = `constant-vus`/`ramping-vus`; open = `constant-arrival-rate`/`ramping-arrival-rate`. - Locust is closed-model (users); JMeter thread groups are closed; Gatling `injectOpen`/`injectClosed`; artillery `arrivalRate` is open. - Open executors need `preAllocatedVUs`/`maxVUs` headroom; if k6 warns "insufficient VUs," it *dropped* iterations and your rate was never achieved. ### Coordinated omission — why naive latency numbers lie A closed-model generator that waits for each response before sending the next **stops the clock during a stall.** One 10s stall that should have blocked 10,000 scheduled 1ms requests gets recorded as *one* 10s sample instead of 10,000 samples ≥ their overdue time. Result: p99 looks great while users are timing out. - **Detect:** suspiciously clean tail (p99 ≈ p50) under load that clearly stuttered; throughput dips that don't show in latency percentiles. - **Fix:** measure against an *intended schedule*, not send-time. Use tools that correct it: - `wrk2 -R<rate> --latency` (constant throughput + HdrHistogram correction) — the reference implementation. - k6 arrival-rate executors record each iteration against its scheduled start, so `iteration_duration` is corrected; still prefer open model for tail truth. - HdrHistogram `recordValueWithExpectedInterval(v, interval)` back-fills omitted samples. - **Never** report tail latency from a closed-loop `ab`/single-thread `wrk` run at saturation. ### Percentiles, not averages - Report **p50 / p95 / p99 / max** — never mean alone. Averages hide the tail; the mean can be 40ms while p99 is 3s. - Latency distributions are right-skewed and multi-modal (cache hit vs miss, GC pause). Mean sits in an empty valley between modes. - **p99 is a per-request user probability, not a rare edge case:** a page issuing 100 requests hits its p99 latency on ~63% of loads (1−0.99¹⁰⁰). Tail latency *is* the typical experience for fan-out. - Percentiles **don't average across shards** — never mean the p99s of two load-gen workers. Merge the raw histograms (HdrHistogram) or you get garbage. ### Little's Law — sanity-check every run `L = λ × W` → concurrency = throughput × latency. - Required VUs ≈ target_RPS × avg_response_time_s. Wanting 1000 RPS at 200ms needs ≥200 concurrent in flight. - If achieved `RPS ≈ VUs / (latency + think_time)` doesn't hold, your generator is the limit, not the server. ### Saturation: knee vs. collapse As load rises: throughput climbs, then **plateaus (the knee)** while latency turns hockey-stick. Past the knee, more load buys only more latency and eventually errors/collapse. - **Report the knee** (max sustainable throughput at acceptable latency), not the collapse point. - USE method for the SUT: **U**tilization, **S**aturation (queue depth/run-queue), **E**rrors — per resource (CPU, mem, disk, net, connection pool). ### Warm-up, JIT, GC — discard the transient - **Always discard a warm-up window.** Cold caches, empty connection pools, unfilled DB buffer pool, and JIT (JVM/V8) compilation make the first seconds meaningless. In k6 use a ramp stage; in JMH warm-up iterations; in Gatling a `nothingFor`/ramp. - **GC/JIT cause tail latency**, not throughput loss — stop-the-world pauses show up as p99/max spikes. A soak test surfaces them; a 30s run won't. - JVM needs thousands of iterations to reach steady state (C2 compiler). Benchmark JVM services *after* warm-up or numbers are 2–10× pessimistic. ### Measure the server, not the client - **Run the generator on a separate host** from the SUT — co-located, they fight for CPU/net and you measure interference. - **Load-generator-is-the-bottleneck** is the most common false result. Detect: latency rises but *server* CPU is low while *generator* CPU pegs, or you hit ephemeral-port/FD exhaustion. Symptoms: - Single-threaded generators (`ab`, artillery/Node, locust single-worker) cap ~1 core. - Ephemeral port exhaustion (`netstat` full of TIME_WAIT) → looks like server refusing connections. - FD limit (`ulimit -n`) caps concurrent connections. - **Fix:** multi-core generators (k6, Gatling, wrk, JMeter), distributed mode (locust master/worker, k6 cloud/operator), raise `ulimit -n`, reuse connections. **Always correlate client numbers with server-side metrics** (APM/Prometheus) — divergence means you're measuring the wrong thing. ### Connection reuse & think time - **Keep-alive dominates:** new-connection-per-request adds TCP + TLS handshake (often > the request itself). Most tools reuse per VU by default (k6 does). Test *both* if clients differ (mobile cold-start vs. pooled service). k6: `noConnectionReuse: true` / `noVUConnectionReuse`. - **Think time** models real users (pauses between actions) and, in closed models, sets effective concurrency (Little's Law). Zero think time in a closed model = an unrealistic hammer that mostly measures your keep-alive path. Use randomized think time (not fixed — fixed sleeps create synchronized request waves). ### Realistic data & cardinality - **Hitting one key lies:** repeating `GET /users/1` measures a cache/DB-buffer hit path nobody experiences. Use realistic key distribution — **Zipfian/Pareto** (80/20), not uniform, not constant. - Dataset must exceed cache size, or you benchmark RAM. - Unique-per-request params defeat query-plan/result caches (good for worst case); constant params inflate results. - Match production **payload sizes, cardinality, and content** (unicode, large records). --- ## Load Shapes One canonical k6 config per shape. Swap `constant-vus`→`constant-arrival-rate` (and `ramping-vus`→`ramping-arrival-rate`, `target` = req/s) for the open model when finding limits. | Shape | Goal | Pattern | | -------------- | -------------------------- | ------------------------------- | | **Smoke** | Validate script works | 1–5 VUs, 1 min | | **Load** | Baseline at expected load | Ramp → hold steady → ramp down | | **Stress** | Find breaking point | Step up past expected until fail | | **Spike** | Sudden burst recovery | Jump to N×, hold, drop, recover | | **Soak** | Leaks/degradation | Moderate load for hours | | **Breakpoint** | Max capacity | Linear ramp with no cap | ```javascript // Load: warm-up ramp is mandatory; thresholds abort/mark-fail the run. export const options = { stages: [ { duration: "2m", target: 100 }, // ramp (discard for analysis) { duration: "10m", target: 100 }, // steady state = the measurement { duration: "2m", target: 0 }, // ramp down ], thresholds: { http_req_duration: ["p(95)<500", "p(99)<1000"], http_req_failed: ["rate<0.01"], }, }; ``` ```javascript // Stress (open model — reveals true limit): step arrival rate until errors/latency break. export const options = { scenarios: { stress: { executor: "ramping-arrival-rate", startRate: 50, timeUnit: "1s", preAllocatedVUs: 100, maxVUs: 2000, // headroom or iterations drop silently stages: [ { duration: "2m", target: 200 }, { duration: "2m", target: 400 }, { duration: "2m", target: 800 }, // watch for the knee here { duration: "2m", target: 1600 }, ], }, }, }; ``` ```javascript // Spike: validate autoscaling/cache/recovery, not steady throughput. export const options = { stages: [ { duration: "1m", target: 50 }, // baseline { duration: "10s", target: 1000 }, // spike { duration: "3m", target: 1000 }, // hold — does it shed load or fall over? { duration: "10s", target: 50 }, // drop { duration: "3m", target: 50 }, // recovery — latency must return to baseline ], }; ``` ```javascript // Soak: surfaces leaks, FD/connection exhaustion, GC creep, disk fill. Hours, not minutes. export const options = { vus: 100, duration: "4h", thresholds: { http_req_duration: ["p(99)<1000"], http_req_failed: ["rate<0.01"] }, }; // Watch: RSS growth, GC frequency, pool saturation, p99 drift over time (not the average). ``` --- ## Tools ### k6 (default for API/protocol load testing) Go-based, multi-core, JS scripting, open+closed executors, built-in thresholds. Best default. ```javascript import http from "k6/http"; import { check, sleep } from "k6"; import { Rate, Trend } from "k6/metrics"; const errorRate = new Rate("errors"); export const options = { scenarios: { // open model: request rate is what you control, independent of server health api: { executor: "ramping-arrival-rate", startRate: 100, timeUnit: "1s", preAllocatedVUs: 200, maxVUs: 1000, stages: [{ duration: "2m", target: 500 }, { duration: "5m", target: 500 }], }, }, thresholds: { "http_req_duration{expected_response:true}": ["p(95)<500", "p(99)<1000"], http_req_failed: ["rate<0.01"], }, }; export function setup() { const r = http.post(`${__ENV.API_URL}/auth/login`, JSON.stringify({ email: "load@test.com", password: "x" }), { headers: { "Content-Type": "application/json" } }); return { token: r.json("token") }; // runs once; returned data passed to default fn } export default function (data) { const res = http.get(`${__ENV.API_URL}/api/users/${__VU}`, { headers: { Authorization: `Bearer ${data.token}` }, tags: { name: "GetUser" }, // aggregate by name, NOT by dynamic URL (avoids metric cardinality blowup) }); errorRate.add(res.status !== 200); check(res, { "200": (r) => r.status === 200 }); sleep(1); } ``` k6 gotchas: - **Tag dynamic URLs** with a static `tags.name`, or per-URL metrics explode and dashboards choke. - `discardResponseBodies: true` cuts generator memory/CPU when you don't inspect bodies — raises max achievable load. - `check()` failures do **not** fail the run; only `thresholds` do (and `--fail-on-check` / `abortOnFail` on a threshold aborts early). - CLI: `k6 run -e API_URL=... script.js`; `--out experimental-prometheus-rw` or `--out influxdb=...` for Grafana; `k6 run --vus 50 --duration 30s` overrides options. - Extensions (xk6) add gRPC, SQL, browser, Kafka — not in the core binary. ### Locust (Python, closed-model, distributed) Use when scenarios need Python logic/libraries or complex stateful user journeys. ```python from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time = between(1, 3) # think time → sets effective concurrency (closed model) def on_start(self): r = self.client.post("/login", json={"username": "u", "password": "p"}) self.token = r.json()["token"] @task(3) # weight: runs 3× as often as weight-1 tasks def browse(self): # name= groups metrics for parameterized URLs (same role as k6 tags.name) self.client.get(f"/api/products/{randint(1,1000)}", name="/api/products/[id]", headers={"Authorization": f"Bearer {self.token}"}) ``` ```bash locust -f lf.py --host=https://api.example.com # web UI locust -f lf.py --headless -u 500 -r 50 -t 5m --host=... # -u users, -r spawn/s locust -f lf.py --master & locust -f lf.py --worker --master-host=IP # distributed: 1 worker/core ```
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る