結論先講
k6 腳本就是 JavaScript,但它不是跑在 Node.js 上。 k6 有自己的 Go runtime,所以你不能 require('axios') 也不能 import fs。但它內建的 http、check、group 已經夠用了。搞懂這幾個 API,你就能寫出涵蓋 90% 場景的壓測腳本。
最小可用腳本
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('http://localhost:3000/health');
check(res, {
'status is 200': (r) => r.status === 200,
});
}這就是一個完整的壓測腳本。10 個 VU,跑 30 秒,每個 VU 不斷打 /health。
幾個重要的事:
export default function是每個 VU 的「主迴圈」。k6 會不斷重複執行這個函式options控制 VU 數量和時間check不會讓測試失敗——它只是記錄「這個 assertion 通過了幾次」。你在報告裡看到checks: 98% ✓就知道有 2% 的 request 沒回 200
Stages:模擬真實流量的升降
固定 10 VU 跑 30 秒太假了。真實流量是慢慢上來、維持一段時間、然後慢慢下去。
export const options = {
stages: [
{ duration: '1m', target: 50 }, // 1 分鐘內從 0 爬到 50 VU
{ duration: '3m', target: 50 }, // 維持 50 VU 跑 3 分鐘
{ duration: '1m', target: 100 }, // 1 分鐘內爬到 100 VU
{ duration: '3m', target: 100 }, // 維持 100 VU 跑 3 分鐘
{ duration: '1m', target: 0 }, // 1 分鐘內降到 0(收尾)
],
};我們壓測平台用的標準 stages:每個 VU 等級維持 3 分鐘。為什麼?
- 太短(30 秒): 系統還沒穩定就結束了,數據不準
- 太長(10 分鐘): 9 框架 × 7 VU 等級 = 63 次,每次 10 分鐘要跑 10 小時
- 3 分鐘: 足夠讓系統穩定,也不會跑太久
Group:給你的操作取名字
如果腳本裡有多個操作,你需要知道「哪個操作慢」。用 group 包起來:
import { group } from 'k6';
export default function () {
group('Register', function () {
http.post('http://localhost:3000/api/users/register', JSON.stringify({
email: `user_${__VU}_${__ITER}@test.com`,
password: 'Test1234!',
name: 'Test User',
}), { headers: { 'Content-Type': 'application/json' } });
});
group('Login', function () {
const loginRes = http.post('http://localhost:3000/api/users/login', JSON.stringify({
email: `user_${__VU}_${__ITER}@test.com`,
password: 'Test1234!',
}), { headers: { 'Content-Type': 'application/json' } });
const token = JSON.parse(loginRes.body).token;
group('Get Profile', function () {
http.get('http://localhost:3000/api/users/me', {
headers: { Authorization: `Bearer ${token}` },
});
});
});
}__VU 是當前 VU 的編號,__ITER 是當前迭代次數。用它們組合出唯一的 email,避免 VU 之間的資料衝突。
在報告裡你會看到每個 group 的獨立指標——Register 平均多少 ms、Login 平均多少 ms。這比「整體平均 500ms」有用太多了。
混合場景腳本:加權隨機
我們的混合場景是 70% Read + 20% Write + 10% Auth。怎麼實作?
export default function () {
const rand = Math.random();
if (rand < 0.7) {
// 70% Read
if (Math.random() < 0.5) {
group('Post List', () => http.get(`${BASE_URL}/api/posts`));
} else {
group('Post Detail', () => http.get(`${BASE_URL}/api/posts/1`));
}
} else if (rand < 0.9) {
// 20% Write
group('Create Post', () => {
http.post(`${BASE_URL}/api/posts`, JSON.stringify({
title: `Post ${Date.now()}`,
content: 'Benchmark test content',
}), { headers: authHeaders });
});
} else {
// 10% Auth
group('Register + Login', () => {
// register and login flow
});
}
}每個 VU 每次迭代隨機決定做什麼操作。跑夠多次之後,自然會趨近 70/20/10 的比例。
Check:不只是 status code
很多人的 check 只寫 status === 200。但壓測時你應該驗更多東西:
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 2s': (r) => r.timings.duration < 2000,
'body has token': (r) => JSON.parse(r.body).token !== undefined,
'body is not empty': (r) => r.body.length > 0,
});為什麼?因為有些框架在高併發時會回 200 但 body 是空的——server 沒處理完就回了。只檢查 status code 會漏掉這種問題。
Tag:讓指標可以切片
如果你想在 Grafana 裡按操作類型篩選,用 tag:
http.get(`${BASE_URL}/api/posts`, {
tags: { operation: 'read', endpoint: 'post_list' },
});
http.post(`${BASE_URL}/api/posts`, body, {
tags: { operation: 'write', endpoint: 'create_post' },
});這樣在 InfluxDB/Grafana 裡你可以查「只看 read 操作的 P95」或「只看 write 操作的 error rate」。
檔案上傳腳本
k6 的檔案上傳不太直覺。你不能直接讀本地檔案(記得嗎,k6 不跑在 Node.js 上)。要用 open() 在 init 階段讀檔,然後在 VU 階段用 http.file() 包裝:
import http from 'k6/http';
// init 階段:讀檔(只執行一次)
const testFile = open('./test-file-1mb.bin', 'b');
export default function () {
const data = {
file: http.file(testFile, 'test-file.bin', 'application/octet-stream'),
};
http.post(`${BASE_URL}/api/files/upload`, data);
}open() 是 k6 的特殊函式,只能在 init 階段(export default function 外面)使用。它把檔案讀進記憶體,然後所有 VU 共享這份資料。
常見的坑
1. 不要在 VU 函式裡做 setup
// 錯的 ❌ — 每個 VU 每次迭代都在 register
export default function () {
http.post('/register', ...);
http.post('/login', ...);
http.get('/profile', ...);
}
// 對的 ✅ — setup 階段 register,VU 階段只做要測的操作
export function setup() {
const res = http.post('/register', ...);
return { token: JSON.parse(res.body).token };
}
export default function (data) {
http.get('/profile', {
headers: { Authorization: `Bearer ${data.token}` },
});
}setup() 只跑一次,結果傳給所有 VU。這樣你測的是「Get Profile 的效能」而不是「Register + Login + Get Profile 的效能」。
2. sleep 不是可選的
export default function () {
http.get('/api/posts');
sleep(1); // 模擬用戶的思考時間
}沒有 sleep,每個 VU 會不停送 request——這不是壓力測試,是 DDoS。加 sleep(1) 模擬用戶看完頁面後才點下一個。
不過我們的跨框架比較刻意不加 sleep,因為我們要測的是「框架的極限」而非「真實用戶場景」。這是有意的選擇,在結果解讀時要記得。
3. 動態資料要小心
// 每個 VU 用同一組帳號 → 會衝突
const email = 'test@test.com';
// 用 VU 編號 + 迭代次數組合 → 唯一
const email = `user_${__VU}_${__ITER}@test.com`;下一篇
用 Grafana 讀懂壓測數據 — 腳本寫好、壓測跑完,然後呢?打開 Grafana 看到一堆線和數字,哪些要看、哪些可以忽略、什麼圖形代表「系統正在崩潰」。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:控制變因的藝術:跨框架壓測怎麼做到公平 → 下一篇:用 Grafana 讀懂壓測數據:哪些數字要看、哪些可以忽略