Mở đầu — vì sao bài này quan trọng
Suốt hơn 40 bài trước, bạn đã học rời rạc từng mảnh: cách viết Thread Group trong JMeter, cách khai báo options và stages trong k6, cách đặt Threshold làm cổng SLA, cách parameterize dữ liệu test, cách đọc Latency và Throughput. Từng mảnh đó, khi đứng riêng, giống như những viên gạch nằm rải rác. Bài 51 là lúc bạn xây căn nhà đầu tiên: một bộ load test hoàn chỉnh cho một sàn thương mại điện tử Việt Nam thực tế, chạy được từ đầu tới cuối, cho ra một con số bạn dám mang đi báo cáo.
Đây là project đầu tiên trong chuỗi 4 project của khóa học, và nó cố tình được đặt ở đây vì thương mại điện tử là "bài toán kinh điển" của performance testing. Một sàn e-commerce có đủ mọi loại tình huống: người dùng đăng nhập, tìm kiếm, xem sản phẩm, thêm giỏ hàng, thanh toán — mỗi bước có đặc tính tải khác nhau. Nếu bạn làm chủ được project này, bạn có bộ khung áp dụng được cho gần như mọi hệ thống web sau này.
Điều quan trọng cần nhớ: mục tiêu của Bài 51 không phải là học thêm tính năng mới của JMeter hay k6 (những thứ đó bạn đã học rồi), mà là ghép chúng lại thành một quy trình dự án thật: xác định scope, xây workload model đúng, chạy, và rút ra kết luận có ý nghĩa kinh doanh. Chúng ta sẽ dùng k6 làm công cụ chính vì code gọn, dễ đọc trong khuôn khổ một bài, nhưng tư duy hoàn toàn áp dụng được cho JMeter.
Khái niệm cốt lõi
Scope: 5 endpoint quan trọng của luồng mua hàng
Một sàn e-commerce có hàng trăm endpoint, nhưng bạn không load test tất cả. Bạn chọn critical path — con đường tạo ra doanh thu. Với dự án này, scope gồm 5 endpoint:
- Login (
POST /api/auth/login) — cổng vào, tạo session/token. - Search (
GET /api/search?q=...) — thường là endpoint tốn tài nguyên nhất vì đụng vào search engine và database. - Product detail (
GET /api/products/{id}) — endpoint có lượng gọi lớn nhất, thường được cache. - Add to cart (
POST /api/cart) — ghi dữ liệu, đụng session. - Checkout (
POST /api/checkout) — endpoint "vàng", ít gọi nhất nhưng quan trọng nhất, thường đụng payment và transaction.
Workload model: không phải mọi endpoint đều được gọi như nhau
Đây là khái niệm cốt lõi và cũng là chỗ người mới hay sai nhất. Trong thực tế, tỷ lệ người dùng đi qua từng bước tạo thành một phễu (funnel) thu hẹp dần. Nếu bạn để 5 endpoint được gọi với tỷ lệ 20-20-20-20-20 thì bạn đang test một hệ thống không tồn tại trên đời.
Một mô hình phễu thực tế cho e-commerce VN trông giống thế này:
| Bước | Tỷ lệ user thực hiện | Ghi chú |
|---|---|---|
| Search / Browse | 100% | Ai vào cũng tìm/xem |
| Product detail | 80% | Đa số click vào ít nhất 1 sản phẩm |
| Login | 30% | Nhiều người xem mà chưa đăng nhập |
| Add to cart | 25% | Một phần thêm giỏ |
| Checkout | 8% | Tỷ lệ chuyển đổi thật thường 2–10% |
Concurrency (VU) vs. Request rate (RPS)
Bạn cần phân biệt hai cách mô tả tải, cả hai đã học ở bài Scenarios/executor:
- VU (Virtual Users) — số người dùng ảo đồng thời. Dùng khi bạn nghĩ theo kiểu "hệ thống chịu được bao nhiêu người online cùng lúc?".
- RPS (Requests per second) — số request mỗi giây. Dùng khi bạn có số liệu từ Analytics kiểu "giờ cao điểm hệ thống nhận 500 req/s".
Định nghĩa tiêu chí Pass/Fail trước khi chạy
Nguyên tắc bất di bất dịch: định nghĩa "đạt" trước khi chạy test, không phải nhìn kết quả rồi mới nghĩ. Đây chính là Threshold trong k6. Ví dụ cho project này:
- p95 latency của toàn hệ thống < 800ms.
- p95 latency của checkout < 1500ms (được phép chậm hơn vì nặng).
- Tỷ lệ lỗi (error rate) < 1%.
- Checkout không được lỗi quá 0.5%.
Tình huống thực tế
Ví dụ 1 — Sàn thời trang "Sen Đỏ Fashion" và cái bẫy tỷ lệ đều
Một team QA của sàn thời trang giả định "Sen Đỏ Fashion" (khoảng 200.000 khách/ngày) được giao load test trước mùa sale. Bạn QA junior viết script k6 gọi 5 endpoint theo tỷ lệ đều 20% mỗi cái, chạy 500 VU. Kết quả: hệ thống "gãy" ở checkout, p95 checkout lên 6 giây, team hoảng loạn báo cáo "hệ thống không chịu nổi sale".
Khi mentor xem lại, vấn đề lộ ra: với tỷ lệ đều, script tạo ra 100 checkout đồng thời liên tục — tương đương 20% của 500 VU. Nhưng trong thực tế, tỷ lệ checkout của sàn này chỉ 6%. Tức là script đã ép hệ thống chịu lượng giao dịch payment cao gấp hơn 3 lần so với đời thực. Con số "6 giây" là kết quả của một kịch bản không bao giờ xảy ra.
Bài học: workload model sai làm toàn bộ kết luận vô nghĩa — thường theo hướng bi quan giả tạo, khiến team tốn tiền scale hạ tầng cho một tải không có thật. Luôn lấy tỷ lệ phễu từ dữ liệu thật.
Ví dụ 2 — Tiki-style search và endpoint bị bỏ quên
Một sàn điện máy (kiểu mini-Tiki) load test 4 endpoint và bỏ qua search vì "nó chỉ là GET, nhẹ mà". Test pass đẹp. Đến ngày sale 12.12, hệ thống sập — không phải checkout, mà là search. Lý do: search đụng thẳng vào Elasticsearch, và ở mùa sale, người dùng tìm kiếm điên cuồng ("máy lạnh giảm giá", "tủ lạnh trả góp"). Search chiếm 100% lượng user nhưng lại là endpoint bị loại khỏi scope.
Sau sự cố, team đo lại: search đóng góp tới 45% tổng tài nguyên CPU database trong giờ cao điểm, dù mỗi request "chỉ là GET".
Bài học: endpoint "nhẹ về mặt HTTP" không có nghĩa là "nhẹ về mặt tài nguyên". Search, filter, autocomplete là những kẻ ngốn CPU/DB thầm lặng và phải nằm trong scope với đúng trọng số cao.
Ví dụ 3 — Login token và số liệu error rate bị "làm đẹp" oan
Một team thương mại điện tử ở TP.HCM chạy test 1000 VU, thấy error rate 22% và định báo cáo "hệ thống yếu". Mentor yêu cầu bóc tách lỗi theo endpoint: 21% lỗi nằm ở add-to-cart và checkout, tất cả trả về HTTP 401. Nguyên nhân: script login lấy token nhưng không truyền token vào các request sau — một lỗi parameterization, không phải lỗi hệ thống.
Khi sửa để chuyền Authorization: Bearer <token> đúng cách qua từng bước, error rate rớt xuống 0.3%.
Bài học: rất nhiều "phát hiện performance" thực ra là bug trong chính script test. Trước khi kết tội hệ thống, hãy kiểm tra script của bạn có mô phỏng đúng luồng xác thực và trạng thái session không.
Hướng dẫn từng bước
Ta xây dựng project bằng k6. Giả sử base URL là https://shop.example.vn.
Bước 1 — Chuẩn bị test data. Login cần tài khoản thật. Tạo file users.csv (đã học ở bài parameterization) với các cặp email/password test hợp lệ do team dev cung cấp trên môi trường staging. Tuyệt đối không test bằng tài khoản thật của khách trên production.
email,password
loadtest01@example.vn,Test@123
loadtest02@example.vn,Test@123
Bước 2 — Dựng bộ khung script với workload model đúng. Ta dùng scenarios để tách hành vi, và dùng xác suất để mô phỏng phễu.
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { SharedArray } from 'k6/data';
import { Trend, Rate } from 'k6/metrics';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';const users = new SharedArray('users', () =>
papaparse.parse(open('./users.csv'), { header: true }).data
);
const checkoutDur = new Trend('checkout_duration', true);
const checkoutErr = new Rate('checkout_errors');
export const options = {
scenarios: {
ecommerce: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 200 }, // ramp-up
{ duration: '5m', target: 200 }, // giữ tải ổn định
{ duration: '2m', target: 0 }, // ramp-down
],
},
},
thresholds: {
http_req_duration: ['p(95)<800'],
http_req_failed: ['rate<0.01'],
checkout_duration: ['p(95)<1500'],
checkout_errors: ['rate<0.005'],
},
};
const BASE = 'https://shop.example.vn';
Bước 3 — Viết user journey theo phễu. Mỗi VU chạy hàm default, và ta dùng Math.random() để quyết định user đó đi sâu tới đâu trong phễu.
export default function () {
let token = null; group('search', () => {
const kw = ['ao thun', 'giay the thao', 'balo', 'dong ho'];
const res = http.get(${BASE}/api/search?q=${kw[Math.floor(Math.random()*kw.length)]});
check(res, { 'search 200': (r) => r.status === 200 });
});
sleep(Math.random() * 3 + 1); // think time 1-4s
// 80% xem chi tiet san pham
if (Math.random() < 0.8) {
group('product_detail', () => {
const id = Math.floor(Math.random() * 5000) + 1;
const res = http.get(${BASE}/api/products/${id});
check(res, { 'product 200': (r) => r.status === 200 });
});
sleep(Math.random() * 3 + 2);
}
// 30% dang nhap
if (Math.random() < 0.3) {
group('login', () => {
const u = users[Math.floor(Math.random() * users.length)];
const res = http.post(${BASE}/api/auth/login, JSON.stringify(u),
{ headers: { 'Content-Type': 'application/json' } });
check(res, { 'login 200': (r) => r.status === 200 });
token = res.json('token');
});
}
// Chi user da login moi add cart + checkout
if (token) {
const authHeaders = { headers: { 'Authorization': Bearer ${token}, 'Content-Type': 'application/json' } };
if (Math.random() < 0.83) { // ~25% tong the
group('add_to_cart', () => {
const res = http.post(${BASE}/api/cart,
JSON.stringify({ productId: Math.floor(Math.random()*5000)+1, qty: 1 }), authHeaders);
check(res, { 'cart 200': (r) => r.status === 200 });
});
sleep(2);
if (Math.random() < 0.32) { // ~8% tong the
group('checkout', () => {
const res = http.post(${BASE}/api/checkout, JSON.stringify({ method: 'cod' }), authHeaders);
const ok = check(res, { 'checkout 200': (r) => r.status === 200 });
checkoutDur.add(res.timings.duration);
checkoutErr.add(!ok);
});
}
}
}
}
Chú ý ba điểm: (1) think time bằng sleep() mô phỏng người thật đọc trang, không bắn liên tục; (2) token login được truyền vào add-cart/checkout — tránh đúng lỗi ở Ví dụ 3; (3) custom metric checkout_duration và checkout_errors để soi riêng endpoint quan trọng nhất.
Bước 4 — Chạy smoke test trước. Đừng nhảy thẳng vào 200 VU. Chạy 1 VU trong 1 phút để chắc script không có bug:
k6 run --vus 1 --duration 1m ecommerce.js
Bước 5 — Chạy full test và ghi kết quả. Xuất ra file để phân tích sau (đã học ở bài CLI/output):
k6 run --out json=result.json ecommerce.js
Bước 6 — Đọc kết quả theo tiêu chí Pass/Fail. Nhìn phần summary: các threshold có dấu ✓ hay ✗? Nếu checkout_duration p(95) vượt 1500ms, đó là phát hiện đầu tiên cần đào sâu (sẽ làm ở bài Bottleneck Analysis). Ghi lại: VU tối đa đạt SLA, RPS đạt được, endpoint chậm nhất.
Lỗi thường gặp & mẹo
Lỗi 1 — Test thẳng lên production. Load test có thể làm sập hệ thống thật, gây thiệt hại doanh thu và ảnh hưởng khách hàng. Luôn test trên staging có cấu hình tương đương production, và nếu buộc phải chạm production thì làm ngoài giờ cao điểm với tải nhỏ, có sự đồng ý của cả team vận hành.
Lỗi 2 — Bỏ qua think time. Không có sleep(), mỗi VU bắn request nhanh gấp hàng chục lần người thật. 200 VU không think time có thể tạo tải tương đương 5000 người dùng thật — con số bạn đo được vô nghĩa. Luôn thêm think time 1–5 giây giữa các bước.
Lỗi 3 — Hardcode một sản phẩm/một user. Nếu 200 VU cùng gọi GET /api/products/123, cache sẽ trả lời tức thì và bạn đo được... sức mạnh của cache, không phải của hệ thống. Ngẫu nhiên hóa product ID và xoay vòng user để mô phỏng tải phân tán thật.
Lỗi 4 — Không tách metric cho endpoint quan trọng. Con số http_req_duration tổng thể bị "pha loãng" bởi hàng nghìn request product-detail nhẹ, che mất việc checkout đang chậm. Luôn tạo custom Trend/Rate riêng cho các endpoint sống còn.
Mẹo — Bắt đầu nhỏ, tăng dần. Chạy 50 → 100 → 200 → 400 VU thành nhiều lần, ghi lại điểm mà SLA bắt đầu vỡ. Đó chính là dữ liệu vàng cho việc capacity planning ở các bài sau.
Mẹo — Dùng tags để lọc. Gắn tag cho request (đã học ở bài Group/Tag) giúp bạn sau này lọc kết quả theo endpoint dễ dàng khi phân tích.
Bài tập thực hành
- Xây dựng lại project cho một sàn bạn chọn. Chọn một e-commerce VN quen thuộc (hoặc tự dựng một API giả bằng công cụ như
httpbin/mock server). Định nghĩa 5 endpoint và viết ra bảng workload model với tỷ lệ phễu của riêng bạn — kèm lý do cho mỗi con số.
- Viết tiêu chí Pass/Fail bằng văn bản trước, rồi mới code Threshold. Ghi ra 4–5 câu SLA (ví dụ: "p95 toàn hệ thống dưới 800ms"). Sau đó dịch mỗi câu thành một dòng trong
options.thresholds.
- Chạy leo thang tải. Chạy script ở 50, 100, 200 VU. Lập bảng: mỗi mức có bao nhiêu RPS, p95 là bao nhiêu, threshold nào vỡ đầu tiên. Xác định "điểm gãy" gần đúng của hệ thống.
- Tự tìm bug trong script. Cố tình bỏ dòng truyền token vào checkout, chạy lại, quan sát error rate tăng vọt. Điều này khắc sâu bài học từ Ví dụ 3: luôn nghi ngờ script trước khi kết tội hệ thống.
Tóm tắt
Bài 51 là project ghép mọi mảnh kiến thức rời rạc thành một bộ load test hoàn chỉnh cho e-commerce Việt Nam. Điểm mấu chốt không nằm ở việc học thêm tính năng, mà ở quy trình dự án đúng:
- Scope đúng — chọn 5 endpoint trên critical path tạo ra doanh thu, không quên những endpoint "nhẹ HTTP nhưng nặng tài nguyên" như search.
- Workload model đúng — mô phỏng phễu chuyển đổi thật (100% search → 8% checkout), không dùng tỷ lệ đều, lấy số liệu từ Analytics/log khi có thể.
- Mô phỏng người thật — think time, ngẫu nhiên hóa data, truyền token đúng qua từng bước.
- Định nghĩa Pass/Fail trước khi chạy — bằng Threshold, và tách metric riêng cho endpoint sống còn như checkout.