Lưu ý: Trong tài liệu này mình sử dụng k6 (một số người đôi khi viết nhầm thành “k8”). Nếu bạn muốn dùng công cụ khác, hãy nói rõ.
Mục tiêu:
- Cung cấp kịch bản test cơ bản và nâng cao cho API/Web.
- Cách cài đặt k6 trên Ubuntu 22.04, cách chạy, và cách đọc kết quả.
- Mẫu code k6 sẵn dùng, có thể tùy biến nhanh.
Nội dung:
- Cài đặt và chuẩn bị môi trường
- Quy ước chung sử dụng trong mã k6
- Kịch bản cơ bản
- Kịch bản nâng cao
- Cách chạy test và đọc kết quả
- Gợi ý theo dõi/giám sát
- Mẹo tối ưu và tránh sai lầm phổ biến
-
Yêu cầu:
- Ubuntu Server 22.04
- Quyền sudo
- Cổng mạng/Firewall cho phép outbound HTTP/HTTPS để client test gọi tới hệ thống mục tiêu
-
Cài đặt k6 (qua apt):
bash scripts/install-k6-ubuntu-22.04.sh
Kiểm tra:
k6 version
-
Cấu trúc thư mục mẫu
docs/ load-testing-k6-ubuntu-22.04.md loadtests/ basic-smoke.js closed-model-vus.js constant-arrival-rate.js spike.js stress.js soak.js multi-scenario.js login-and-use-token.js data/ users.csv scripts/ install-k6-ubuntu-22.04.sh
- Biến môi trường:
- BASE_URL: URL gốc API, ví dụ https://api.example.com
- AUTH_PATH: Đường dẫn login (nếu có), ví dụ /auth/login
- Thresholds (ngưỡng SLO mẫu):
- p(95) < 300ms, tỉ lệ lỗi < 0.1%
- Mô hình tải:
- Closed model: theo số VU (virtual users), dùng executor ramping-vus hoặc constant-vus
- Open model: theo RPS (arrival rate), dùng executor constant-arrival-rate hoặc ramping-arrival-rate
- Think-time (sleep) chỉ có ý nghĩa trong closed model; với open model, tốc độ đến được điều khiển bởi executor.
- Smoke Test: kiểm tra nhanh tính sẵn sàng (1-2 VU, thời gian ngắn)
- Closed model (ramping-vus): mô phỏng số user tăng/giảm
- Open model (constant-arrival-rate): cố định RPS
- Spike test: tải tăng đột biến rồi giảm nhanh
Các file tương ứng:
- loadtests/basic-smoke.js
- loadtests/closed-model-vus.js
- loadtests/constant-arrival-rate.js
- loadtests/spike.js
Xem nội dung trong các file ở phần “Mẫu mã” bên dưới.
- Stress test: tăng tải theo bậc đến khi hệ thống vỡ ngưỡng
- Soak/Endurance: tải vừa phải kéo dài (hàng giờ) để phát hiện rò rỉ tài nguyên
- Multi-scenario: nhiều kịch bản song song với tỉ lệ khác nhau (read-heavy, write-light, v.v.)
- Đăng nhập + tương quan (correlation): login lấy token rồi dùng cho các request tiếp theo (có tham số từ file CSV)
Các file tương ứng:
- loadtests/stress.js
- loadtests/soak.js
- loadtests/multi-scenario.js
- loadtests/login-and-use-token.js
- loadtests/data/users.csv
-
Thiết lập biến môi trường:
export BASE_URL="https://api.example.com" export AUTH_PATH="/auth/login"
-
Chạy từng kịch bản:
# Smoke k6 run -e BASE_URL="$BASE_URL" loadtests/basic-smoke.js # Closed model (VU ramping) k6 run -e BASE_URL="$BASE_URL" loadtests/closed-model-vus.js # Constant RPS (Open model) k6 run -e BASE_URL="$BASE_URL" loadtests/constant-arrival-rate.js # Spike k6 run -e BASE_URL="$BASE_URL" loadtests/spike.js # Stress k6 run -e BASE_URL="$BASE_URL" loadtests/stress.js # Soak (chạy dài, cân nhắc screen/tmux) k6 run -e BASE_URL="$BASE_URL" loadtests/soak.js # Login + Token (cần users.csv) k6 run -e BASE_URL="$BASE_URL" -e AUTH_PATH="$AUTH_PATH" loadtests/login-and-use-token.js
-
Ghi log kết quả ra file JSON:
k6 run -e BASE_URL="$BASE_URL" --summary-export=summary.json loadtests/constant-arrival-rate.js
-
Cách đọc kết quả:
- http_req_duration: xem p(50), p(90), p(95), p(99)
- http_req_failed: tỉ lệ lỗi
- iterations, data_received/sent: thông tin phụ
- Khi p95/p99 tăng mạnh khi tăng tải nhẹ => có thể đụng hàng đợi hoặc nghẽn tài nguyên (DB/CPU/Pool)
- Tích hợp output:
- InfluxDB + Grafana, hoặc Prometheus Remote Write (xem tài liệu k6 output)
- Kết hợp APM/Observability:
- Prometheus (exporters), Grafana, OpenTelemetry, Jaeger để theo dõi DB, cache, downstream
- Gắn nhãn/note test: để phân tích dễ hơn sau này
- Tách generator ra máy riêng nếu server test yếu (tránh nghẽn ở client)
- Dataset đa dạng, tránh ID lặp lại gây cache hit giả tạo
- Test cả “cold cache” và “warm cache” nếu cần
- Sử dụng open model để giảm “coordinated omission” khi đo latency
- Theo dõi tài nguyên: CPU, RAM, IO, connection pool, thread/worker, DB locks
- Đặt ngưỡng (thresholds) rõ ràng để CI/CD có thể fail sớm
Có thể điều chỉnh các kịch bản và tham số cho sát thực tế hơn (ví dụ mix đọc/ghi, payload kích thước, think-time…).
Script example:
loadtests/basic-smoke.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 1,
duration: '30s',
thresholds: {
http_req_failed: ['rate<0.01'], // <1% lỗi
http_req_duration: ['p(95)<500'], // p95 < 500ms
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/health`); // thay endpoint phù hợp
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
loadtests/closed-model-vus.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
ramping_users: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 50 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '30s',
},
},
thresholds: {
http_req_failed: ['rate<0.001'],
http_req_duration: ['p(95)<300', 'p(99)<800'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/items?limit=20`);
check(res, { '200 OK': (r) => r.status === 200 });
// Think-time có ý nghĩa trong closed model
sleep(1 + Math.random() * 2);
}
loadtests/constant-arrival-rate.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
steady_rps: {
executor: 'constant-arrival-rate',
rate: 200, // 200 requests/giây
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 50, // VU sơ bộ
maxVUs: 200, // trần VU khi hệ thống chậm
},
},
thresholds: {
http_req_failed: ['rate<0.001'],
http_req_duration: ['p(95)<300'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/search?q=test`);
check(res, { '200 OK': (r) => r.status === 200 });
}
loadtests/spike.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
spike_rps: {
executor: 'ramping-arrival-rate',
startRate: 0,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: 500,
stages: [
{ target: 50, duration: '30s' }, // tăng nhẹ
{ target: 500, duration: '15s' }, // spike
{ target: 50, duration: '1m' }, // giảm về mức cũ
{ target: 0, duration: '30s' },
],
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<600'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/hot-items`);
check(res, { '200 OK': (r) => r.status === 200 });
}
loadtests/stress.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
stress_steps: {
executor: 'ramping-arrival-rate',
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 1000,
stages: [
{ target: 100, duration: '1m' },
{ target: 200, duration: '2m' },
{ target: 300, duration: '2m' },
{ target: 400, duration: '2m' },
{ target: 500, duration: '2m' },
{ target: 0, duration: '1m' },
],
},
},
thresholds: {
http_req_failed: ['rate<0.02'], // cho phép lỗi nhiều hơn khi stress
http_req_duration: ['p(95)<800'], // p95 có thể tăng
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/items?limit=50`);
check(res, { '200 OK': (r) => r.status === 200 });
}
loadtests/soak.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
soak_test: {
executor: 'constant-arrival-rate',
rate: 50, // tải vừa phải
timeUnit: '1s',
duration: '2h', // endurance
preAllocatedVUs: 20,
maxVUs: 100,
},
},
thresholds: {
http_req_failed: ['rate<0.005'],
http_req_duration: ['p(95)<400'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function () {
const res = http.get(`${BASE_URL}/profile?id=${Math.floor(Math.random()*100000)}`);
check(res, { '200 OK': (r) => r.status === 200 });
// Think-time không ảnh hưởng đến arrival-rate nhưng vẫn hợp lý mô phỏng hành vi
sleep(Math.random());
}
loadtests/multi-scenario.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
// 70% đọc danh sách
list_items: {
executor: 'constant-arrival-rate',
rate: 140,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 40,
maxVUs: 200,
exec: 'listItems',
},
// 25% xem chi tiết
view_detail: {
executor: 'constant-arrival-rate',
rate: 50,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 20,
maxVUs: 100,
exec: 'viewDetail',
},
// 5% thao tác ghi
create_item: {
executor: 'constant-arrival-rate',
rate: 10,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 10,
maxVUs: 50,
exec: 'createItem',
},
},
thresholds: {
'http_req_failed{scenario:list_items}': ['rate<0.001'],
'http_req_failed{scenario:view_detail}': ['rate<0.001'],
'http_req_failed{scenario:create_item}': ['rate<0.005'],
'http_req_duration{scenario:list_items}': ['p(95)<300'],
'http_req_duration{scenario:view_detail}': ['p(95)<350'],
'http_req_duration{scenario:create_item}': ['p(95)<600'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export function listItems() {
const res = http.get(`${BASE_URL}/items?limit=20`);
check(res, { '200 OK': (r) => r.status === 200 });
}
export function viewDetail() {
const id = Math.floor(Math.random() * 100000) + 1;
const res = http.get(`${BASE_URL}/items/${id}`);
check(res, { '200 OK': (r) => r.status === 200 });
sleep(Math.random() * 0.2);
}
export function createItem() {
const payload = JSON.stringify({
name: `item-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
price: Math.floor(Math.random() * 10000) + 1000,
});
const headers = { 'Content-Type': 'application/json' };
const res = http.post(`${BASE_URL}/items`, payload, { headers });
check(res, {
'201 Created or 200': (r) => r.status === 201 || r.status === 200,
});
}
loadtests/login-and-use-token.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
export const options = {
scenarios: {
auth_and_use: {
executor: 'constant-arrival-rate',
rate: 50,
timeUnit: '1s',
duration: '15m',
preAllocatedVUs: 20,
maxVUs: 200,
},
},
thresholds: {
http_req_failed: ['rate<0.005'],
http_req_duration: ['p(95)<400'],
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
const AUTH_PATH = __ENV.AUTH_PATH || '/auth/login';
// Tải danh sách user từ CSV (header: username,password)
const users = new SharedArray('users', () => {
const raw = open('./data/users.csv'); // relative to script
const lines = raw.trim().split('\n');
const header = lines.shift();
return lines
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [username, password] = line.split(',');
return { username, password };
});
});
function pickRandomUser() {
return users[Math.floor(Math.random() * users.length)];
}
export default function () {
// 1) Đăng nhập
const { username, password } = pickRandomUser();
const loginRes = http.post(
`${BASE_URL}${AUTH_PATH}`,
JSON.stringify({ username, password }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(loginRes, {
'login success (200/201)': (r) => r.status === 200 || r.status === 201,
});
if (loginRes.status >= 400) {
// Fail fast nếu login lỗi
return;
}
let token;
try {
token = loginRes.json('token') || loginRes.json('access_token');
} catch (_e) {
token = null;
}
// 2) Gọi các API yêu cầu xác thực
const authHeaders = token
? { Authorization: `Bearer ${token}` }
: {};
const listRes = http.get(`${BASE_URL}/me/orders?limit=10`, { headers: authHeaders });
check(listRes, { 'orders 200': (r) => r.status === 200 });
const detailRes = http.get(`${BASE_URL}/me/profile`, { headers: authHeaders });
check(detailRes, { 'profile 200': (r) => r.status === 200 });
// 3) Hành vi viết có kiểm soát
if (Math.random() < 0.2) {
const createRes = http.post(
`${BASE_URL}/me/notes`,
JSON.stringify({ body: `note ${Date.now()}` }),
{ headers: { ...authHeaders, 'Content-Type': 'application/json' } }
);
check(createRes, {
'create note 2xx': (r) => r.status === 201 || r.status === 200,
});
}
// Think-time nhẹ nhàng
sleep(0.5 + Math.random() * 1.5);
}
loadtests/data/users.csv
username,password
user001@example.com,pass001
user002@example.com,pass002
user003@example.com,pass003
user004@example.com,pass004
user005@example.com,pass005