Performance Testing 入門 — k6 vs JMeter vs Locust 該選哪個
「網站要扛得住 5000 人同時上線」是業務常常開的單。Performance test 不是壓爆它,是知道它什麼時候會爆。這篇給你 QA 角度的完整入門。
4 種效能測試一次看懂
flowchart TB
A[Load Test<br>負載測試] --> A1[正常使用量<br>持續一段時間]
B[Stress Test<br>壓力測試] --> B1[超過正常 2-5 倍<br>找崩潰臨界點]
C[Spike Test<br>尖峰測試] --> C1[突然爆量<br>例如限時搶購]
D[Soak Test<br>耐久測試] --> D1[正常量但跑 8+ 小時<br>找 memory leak]
style A fill:#06b6d4,color:#fff
style B fill:#ef4444,color:#fff
style C fill:#f59e0b,color:#fff
style D fill:#a855f7,color:#fff
| 類型 | 模擬什麼 | 抓什麼 bug |
|---|---|---|
| Load | 正常流量 + 些許 buffer | 平均回應時間、throughput |
| Stress | 超量 2-5 倍 | 崩潰點、graceful degradation |
| Spike | 突發爆量 | autoscaling、queue 機制 |
| Soak | 正常量 8 小時+ | memory leak、connection leak |
多數團隊只做 Load,這是 80% 痛點來源。
為什麼選 k6
flowchart LR
Choose{選效能<br>測試工具} --> JMeter[JMeter]
Choose --> Gatling[Gatling]
Choose --> Locust[Locust]
Choose --> k6[k6]
Choose --> Artillery[Artillery]
JMeter --> JM["Java + GUI<br>老牌、強但重"]
Gatling --> GA["Scala<br>強但學習曲線"]
Locust --> LO["Python<br>容易但效能弱"]
k6 --> K6["JavaScript<br>輕量、CI 友善"]
Artillery --> AR["YAML<br>簡單但功能少"]
style k6 fill:#10b981,color:#fff
style K6 fill:#10b981,color:#fff
| 工具 | 語言 | 學習曲線 | CI 友善 | 1 機可模擬人數 |
|---|---|---|---|---|
| k6 | JS | 低 | ⭐⭐⭐ | 30K+ VU |
| Locust | Python | 低 | ⭐⭐ | 5K VU |
| JMeter | XML / GUI | 中 | ⭐ | 10K VU |
| Gatling | Scala | 高 | ⭐⭐ | 50K+ VU |
| Artillery | YAML | 低 | ⭐⭐ | 5K VU |
新團隊建議 k6:JS 寫、CI 一行裝、效能最好。
30 分鐘從 0 到 1:k6 第一個 test
1. 安裝(macOS)
brew install k6
或 Docker:
docker pull grafana/k6
2. 寫第一支腳本
// test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50, // 50 個 virtual users
duration: '30s', // 跑 30 秒
};
export default function () {
const res = http.get('https://staging.example.com/api/products');
check(res, {
'status is 200': r => r.status === 200,
'response time < 500ms': r => r.timings.duration < 500,
'has data': r => JSON.parse(r.body).length > 0,
});
sleep(1);
}
3. 跑
k6 run test.js
輸出長這樣:
✓ status is 200
✓ response time < 500ms
✓ has data
checks.........................: 100.00%
http_req_duration..............: avg=187.3ms p(95)=423ms p(99)=789ms
http_reqs......................: 1487 49.5/s
vus............................: 50
data_received..................: 2.3 MB
關鍵指標:
- p(95) = 423ms — 95% 的 request 在 423ms 內回完
- p(99) = 789ms — 99% 在 789ms 內
- 49.5/s — 每秒處理 49.5 個 request
寫好 k6 腳本的 5 個原則
1. Stages — 階梯式加壓
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 2 分鐘漸增到 100 VU
{ duration: '5m', target: 100 }, // 維持 5 分鐘
{ duration: '2m', target: 200 }, // 再增到 200
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 }, // 漸減回 0
],
};
突然開 1000 個 user 是 stress test 的玩法,load test 要漸進。
2. Thresholds — 自動 fail
export const options = {
vus: 50,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<500'], // p95 必須 < 500ms
http_req_failed: ['rate<0.01'], // 錯誤率 < 1%
'checks{group:checkout}': ['rate>0.95'],
},
};
CI 不過自動退 PR。
3. Scenarios — 真實場景
export const options = {
scenarios: {
browse_users: {
executor: 'ramping-vus',
stages: [{ duration: '5m', target: 80 }],
exec: 'browse',
},
checkout_users: {
executor: 'constant-vus',
vus: 20,
duration: '5m',
exec: 'checkout',
},
},
};
export function browse() { /* 瀏覽 */ }
export function checkout() { /* 結帳 */ }
80% 在瀏覽、20% 在結帳,比所有人都做同件事真實。
4. Setup / Teardown
export function setup() {
// 跑測試前:建測試帳號、塞測試資料
const res = http.post('/api/test-users');
return { token: res.json().token };
}
export default function (data) {
http.get('/api/profile', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
export function teardown(data) {
// 清理
http.del(`/api/test-users/${data.userId}`);
}
5. Custom Metrics
import { Trend } from 'k6/metrics';
const checkoutTime = new Trend('checkout_time');
export default function () {
const start = Date.now();
// 跑 checkout 流程
checkoutTime.add(Date.now() - start);
}
業務指標自定義,不只看 HTTP timing。
完整實戰範例:模擬電商高峰
import http from 'k6/http';
import { check, group, sleep } from 'k6';
export const options = {
scenarios: {
normal_browsing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
exec: 'browse',
},
checkout_flow: {
executor: 'constant-vus',
vus: 50,
duration: '7m',
exec: 'checkout',
},
},
thresholds: {
'http_req_duration{name:product_list}': ['p(95)<300'],
'http_req_duration{name:add_to_cart}': ['p(95)<500'],
'http_req_duration{name:checkout}': ['p(95)<1000'],
'http_req_failed': ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com';
export function browse() {
group('Browse', () => {
http.get(`${BASE_URL}/api/products`, { tags: { name: 'product_list' } });
sleep(2);
http.get(`${BASE_URL}/api/products/123`, { tags: { name: 'product_detail' } });
sleep(3);
});
}
export function checkout() {
group('Checkout', () => {
const r1 = http.post(`${BASE_URL}/api/cart`,
JSON.stringify({ sku: '123', qty: 1 }),
{ tags: { name: 'add_to_cart' }, headers: { 'Content-Type': 'application/json' } }
);
check(r1, { 'cart created': r => r.status === 201 });
sleep(1);
const r2 = http.post(`${BASE_URL}/api/orders`,
JSON.stringify({ cart_id: r1.json().id }),
{ tags: { name: 'checkout' }, headers: { 'Content-Type': 'application/json' } }
);
check(r2, { 'order placed': r => r.status === 201 });
sleep(2);
});
}
跑:
BASE_URL=https://staging.example.com k6 run --out json=results.json scripts/ecommerce.js
CI 整合(GitHub Actions)
name: Performance Test
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 點跑
workflow_dispatch:
jobs:
perf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/[email protected]
with:
filename: perf/ecommerce.js
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- if: failure()
uses: actions/upload-artifact@v4
with:
name: perf-results
path: results.json
不要每個 PR 都跑 — 太慢、太貴。跑 nightly 或 weekly 即可。
結果該怎麼讀(指標解讀)
flowchart TD
Result[k6 結果] --> Q1{p95 達標?}
Q1 -->|否| F1[效能不夠<br>找 bottleneck]
Q1 -->|是| Q2{錯誤率 < 1%?}
Q2 -->|否| F2[系統不穩<br>看 server log]
Q2 -->|是| Q3{throughput<br>能持續?}
Q3 -->|否| F3[降速、queue 滿]
Q3 -->|是| OK[通過]
style OK fill:#10b981,color:#fff
style F1 fill:#ef4444,color:#fff
style F2 fill:#ef4444,color:#fff
style F3 fill:#f59e0b,color:#fff
我看效能報告的順序
- Error rate — 有錯誤就先看這個
- p99 latency — 看最慘的 1%(這才是使用者罵的)
- p95 — 主要 SLA 指標
- Throughput trend — 看是不是穩定(崩盤前會先降速)
- Server-side metrics(搭配 Datadog / Grafana)— CPU / RAM / DB query
只看 average 是新手。Average 200ms 可能包含 p99 = 5 秒。
反模式
- 本機跑大流量 — 1 機網路打不出 1000 RPS,要用雲端
- 沒 ramp-up — 突然 1000 VU 是 stress test 不是 load
- 打 prod — 除非你想被 fire
- 不清理 test data — 一個月後 staging DB 變垃圾場
- 沒設 threshold — 結果出來大家看一下、沒人 own
- 效能規格未談 — 沒 SLA 不知道過或不過
- 只跑一次 — 應該 nightly 跑、看趨勢
跟其他工具的比較
k6 vs JMeter
- JMeter 強在 GUI 設計複雜流程、適合非工程師
- k6 強在 code 易維護、CI 整合、效能好
- 新案首選 k6
k6 vs Locust
- Locust Python 寫、容易上手
- k6 JS 寫、效能強 5-10 倍
- 重視效能選 k6、重視團隊 Python 熟悉度選 Locust
k6 vs Gatling
- Gatling Scala 寫、效能最強
- k6 JS 寫、夠用且容易
- 電商 / 金融大規模選 Gatling、中小團隊 k6 即可
進階主題(之後可以深入)
- Distributed k6 — 雲端跑、模擬全球流量
- Browser-based perf — k6/browser 加 Chromium 跑(真實前端 perf)
- gRPC / WebSocket 測試 — k6 原生支援
- 整合 APM — Datadog / New Relic 看 server-side
- Continuous performance testing — 整合 SLO 監控
給 QA 的關鍵 takeaway
效能測試不是「測一次過了就算」。它是:
- 建立 baseline(系統正常時的 p95)
- 每次大改動跑、看有沒有 regression
- 收集 trend、跟 PM 約 SLO
- 上線後配合 APM 監控
從一支 k6 腳本開始,3 個月後你會發現自己變不可或缺。