Mở đầu — vì sao bài này quan trọng
Ở bài trước bạn đã làm quen với JMeter — một công cụ performance testing kỳ cựu, mạnh mẽ, nhưng nặng nề: giao diện GUI phức tạp, file .jmx dạng XML khó đọc, khó đưa vào Git một cách sạch sẽ, và tốn khá nhiều RAM khi giả lập lượng lớn người dùng ảo. Nếu bạn từng thử review một pull request chứa file .jmx, bạn sẽ hiểu nỗi đau đó: hàng nghìn dòng XML, không ai biết ai đã đổi cái gì.
k6 (đọc là "kay-six"), do Grafana Labs phát triển, ra đời để giải quyết đúng những nỗi đau này. Triết lý của nó là "test as code" — bạn viết kịch bản kiểm thử tải bằng JavaScript, chạy bằng dòng lệnh, kết quả in ra terminal đẹp và rõ ràng, và toàn bộ script chỉ là file .js nằm gọn trong repo cùng với code của dev. Đây là lý do k6 được xếp vào nhóm "developer-friendly": nó nói cùng ngôn ngữ với lập trình viên, hợp với văn hóa DevOps và shift-left mà chúng ta đã bàn xuyên suốt khóa học.
Tại sao một QA/SDET ở Việt Nam nên học k6 ngay bây giờ? Vì thị trường đang dịch chuyển. Các startup fintech, các sàn thương mại điện tử, các ví điện tử — nơi mà một sự cố quá tải trong ngày sale có thể thiệt hại hàng tỷ đồng — đều cần người biết đo lường sức chịu tải của hệ thống một cách tự động và lặp lại được trong CI/CD. k6 nhẹ, chạy được trên máy laptop, tích hợp thẳng vào GitHub Actions hay Jenkins, và không đòi hỏi bạn phải là chuyên gia Java. Đó là combo mà rất nhiều đội QA trẻ đang tìm.
Bài này tập trung riêng vào k6 như một lựa chọn hiện đại thay thế JMeter. Chúng ta không lặp lại lý thuyết performance testing tổng quát (đã có ở bài JMeter), mà đi sâu vào tư duy, cú pháp và cách vận hành k6 trong thực tế.
Khái niệm cốt lõi
k6 là gì và chạy ra sao
k6 là một công cụ mã nguồn mở, viết bằng ngôn ngữ Go (nên rất nhanh và tiết kiệm tài nguyên), nhưng bạn viết kịch bản bằng JavaScript (chuẩn ES6). Nghe hơi lạ: script JS nhưng lõi Go? Đúng vậy — k6 nhúng một engine JavaScript vào trong binary Go. Điều này có một hệ quả quan trọng bạn phải nhớ: k6 KHÔNG phải là Node.js. Bạn không dùng được npm package tùy tiện, không có require('fs'), không có DOM. Nó chỉ hiểu các module mà k6 cung cấp (như k6/http, k6/check) cộng với JavaScript thuần.
VU và Iteration — hai khái niệm nền tảng
Hai từ bạn sẽ gặp liên tục:
- VU (Virtual User) — người dùng ảo. Mỗi VU là một "luồng" chạy lặp đi lặp lại kịch bản của bạn. Nếu bạn cấu hình 50 VU, tức là 50 người dùng ảo đang đồng thời gọi vào hệ thống.
- Iteration — một vòng chạy trọn vẹn hàm kịch bản của một VU. Nếu hàm của bạn gọi 3 API rồi kết thúc, thì đó là một iteration gồm 3 request.
Cấu trúc một script k6
Một file k6 tối giản luôn có hai phần:
import http from 'k6/http';
import { check, sleep } from 'k6';// options: cấu hình tải — bao nhiêu VU, chạy bao lâu
export const options = {
vus: 10,
duration: '30s',
};
// default function: kịch bản mỗi VU lặp lại
export default function () {
const res = http.get('https://test-api.example.vn/products');
check(res, {
'status là 200': (r) => r.status === 200,
'phản hồi dưới 500ms': (r) => r.timings.duration < 500,
});
sleep(1); // giả lập "thời gian suy nghĩ" của người dùng
}
Thresholds — tiêu chí Pass/Fail tự động
Đây là tính năng khiến k6 cực kỳ hợp với CI/CD. Thresholds cho phép bạn định nghĩa ngưỡng chấp nhận, và nếu vượt ngưỡng, k6 sẽ trả về exit code khác 0 → pipeline tự động fail. Ví dụ:
export const options = {
vus: 50,
duration: '2m',
thresholds: {
http_req_duration: ['p(95)<800'], // 95% request phải dưới 800ms
http_req_failed: ['rate<0.01'], // tỷ lệ lỗi dưới 1%
},
};
p(95) nghĩa là percentile thứ 95 — 95% người dùng có trải nghiệm nhanh hơn con số này. Đây là chỉ số vàng trong performance testing, quan trọng hơn nhiều so với "trung bình", vì trung bình che giấu những người dùng xui xẻo ở đuôi phân phối.
Các loại test load
k6 hỗ trợ nhiều "hình dạng" tải khác nhau thông qua stages hoặc scenarios:
- Smoke test — vài VU, chạy ngắn, chỉ để kiểm tra script chạy đúng.
- Load test — mô phỏng lượng người dùng bình thường/cao điểm dự kiến.
- Stress test — đẩy dần lên quá mức bình thường để tìm điểm gãy.
- Spike test — tăng vọt đột ngột (giống lúc mở bán vé concert).
- Soak test — tải vừa phải nhưng kéo dài nhiều giờ để phát hiện rò rỉ bộ nhớ.
stages:export const options = {
stages: [
{ duration: '1m', target: 100 }, // tăng dần lên 100 VU trong 1 phút
{ duration: '3m', target: 100 }, // giữ 100 VU trong 3 phút
{ duration: '1m', target: 0 }, // hạ dần về 0
],
};
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử chuẩn bị cho ngày 12/12
Một công ty thương mại điện tử tầm trung ở TP.HCM (giả định tên là ShopViet) chuẩn bị cho đợt sale 12/12. Năm ngoái, đúng 0h ngày sale, trang danh sách sản phẩm sập trong 8 phút vì database quá tải, ước tính mất khoảng 400 triệu đồng doanh thu. Năm nay đội QA quyết định không để chuyện đó lặp lại.
Họ dùng k6 viết một stress test cho endpoint nóng nhất — API tìm kiếm sản phẩm. Kịch bản tăng dần từ 200 lên 2.000 VU. Ở mốc khoảng 1.400 VU, họ thấy p(95) của http_req_duration nhảy vọt từ 600ms lên 4.200ms và http_req_failed bắt đầu tăng — đó chính là điểm gãy (breaking point). Điều tra sâu hơn qua log, họ phát hiện query tìm kiếm không có index phù hợp và connection pool của database chỉ đặt 100.
Sau khi thêm index và nâng pool lên 300, cùng với cache Redis cho các truy vấn phổ biến, họ chạy lại k6: hệ thống trụ vững tới 2.500 VU với p(95) chỉ 850ms. Bài học: performance test không chỉ để "biết hệ thống chịu được bao nhiêu", mà để tìm ra nút thắt cụ thể và chứng minh việc sửa chữa thực sự có hiệu quả bằng con số trước–sau.
Ví dụ 2 — Ví điện tử và bài học spike test
Một đội làm ví điện tử (giả định tên VietPay) gặp sự cố lạ: hệ thống chạy êm cả ngày, nhưng cứ đúng 12h trưa lại chậm bất thường trong vài phút. Nguyên nhân: đó là giờ nhiều người thanh toán tiền ăn trưa, tạo ra một cú "spike" ngắn.
Đội QA mô phỏng lại bằng k6 spike test: giữ 50 VU nền, rồi đột ngột bơm lên 800 VU trong 20 giây, rồi hạ về 50. Họ phát hiện dịch vụ auth (xác thực) không kịp co giãn (auto-scale) đủ nhanh — mỗi lần scale-up mất tới 90 giây, trong khi cú spike chỉ kéo dài 30 giây, nên khi container mới sẵn sàng thì spike đã qua và người dùng đã nhận lỗi timeout.
Giải pháp không nằm ở tăng server mãi mãi, mà là warm-up trước giờ cao điểm và tối ưu thời gian khởi động container. Bài học: load test đều đặn không phát hiện được vấn đề này; chỉ có spike test mô phỏng đúng hình dạng tải thực tế mới lộ ra được điểm yếu về khả năng co giãn.
Ví dụ 3 — Đưa k6 vào CI để chặn hồi quy hiệu năng
Một startup SaaS B2B ở Hà Nội (giả định tên là Basework) từng gặp tình trạng: mỗi vài sprint, một dev vô tình thêm một truy vấn N+1 (gọi database trong vòng lặp), khiến một API tụt hiệu năng dần dần mà không ai nhận ra cho tới khi khách hàng phàn nàn.
Họ thêm một smoke performance test bằng k6 vào GitHub Actions, chạy trên mỗi pull request với thresholds: { http_req_duration: ['p(95)<600'] }. Chỉ 20 VU trong 1 phút, đủ nhẹ để không làm chậm pipeline. Ba tuần sau, một PR bị k6 báo fail: p(95) lên 1.100ms. Đúng là một truy vấn N+1 mới lọt vào. Nó bị chặn trước khi lên production.
Bài học: giá trị lớn nhất của k6 với đội nhỏ không phải là stress test hoành tráng, mà là biến performance thành một hàng rào tự động — rẻ, nhanh, chạy mỗi PR, bắt lỗi ngay khi nó vừa xuất hiện.
Hướng dẫn từng bước
Bước 1 — Cài đặt k6. k6 là một binary độc lập, không cần Node hay Java.
macOS
brew install k6Ubuntu/Debian
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
| sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6Windows: winget install k6 --source winget
Kiểm tra: k6 version.
Bước 2 — Viết script đầu tiên. Tạo file test.js:
import http from 'k6/http';
import { check, sleep } from 'k6';export const options = {
vus: 10,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://test.k6.io');
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
Bước 3 — Chạy và đọc kết quả.
k6 run test.js
Nhìn vào output, ba dòng quan trọng nhất:
http_req_duration— kèm avg, min, med, p(90), p(95). Nhìn p(95), đừng nhìn avg.http_req_failed— tỷ lệ request lỗi. Muốn càng gần 0 càng tốt.iterationsvàhttp_reqs— tổng lượng việc đã làm và throughput (req/s).
Bước 4 — Thêm dữ liệu động và giai đoạn tải. Test đăng nhập với nhiều tài khoản khác nhau, và dùng stages thay cho duration cố định:
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';const users = new SharedArray('users', () =>
JSON.parse(open('./users.json')) // [{email, pass}, ...]
);
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 50 },
{ duration: '30s', target: 0 },
],
};
export default function () {
const u = users[Math.floor(Math.random() * users.length)];
const res = http.post('https://api.example.vn/login', JSON.stringify({
email: u.email, password: u.pass,
}), { headers: { 'Content-Type': 'application/json' } });
check(res, { 'đăng nhập thành công': (r) => r.status === 200 });
}
SharedArray rất quan trọng: nó nạp dữ liệu một lần và chia sẻ cho mọi VU, thay vì mỗi VU nạp một bản sao ngốn RAM.
Bước 5 — Đưa vào CI (GitHub Actions). Tạo .github/workflows/perf.yml:
name: k6 Performance
on: [pull_request]
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/setup-k6-action@v1
- run: k6 run tests/smoke.js
Nếu threshold fail, exit code khác 0 → job đỏ → PR bị chặn. Đó chính là hàng rào tự động ở Ví dụ 3.
Bước 6 — Trực quan hóa (tùy chọn). k6 có thể đẩy metric ra Grafana Cloud (k6 run --out cloud) hoặc ra InfluxDB/Prometheus để vẽ dashboard theo thời gian thực. Với đội nhỏ, output terminal thường đã đủ; dashboard hữu ích khi bạn cần theo dõi xu hướng qua nhiều lần chạy.
Lỗi thường gặp & mẹo
Tưởng k6 là Node.js. Đây là lỗi số một của người mới. Bạn không thể require('axios') hay import fs from 'fs'. Chỉ dùng module của k6 (k6/http, k6/check, k6/data...) và hàm open() để đọc file. Cần thư viện ngoài thì phải bundle qua webpack/rollup trước — nhưng phần lớn trường hợp bạn không cần.
Nhìn vào giá trị trung bình (avg) thay vì percentile. Trung bình 300ms nghe đẹp, nhưng nếu p(95) là 3.000ms thì cứ 20 người dùng có 1 người khổ sở. Luôn đặt threshold trên p(95) hoặc p(99), không phải avg.
Chạy tải lớn từ một laptop rồi kết luận sai. Nếu bạn thấy hệ thống "chậm", hãy chắc chắn không phải chính máy chạy k6 bị nghẽn CPU/mạng. Với tải rất lớn, cần chạy k6 phân tán (Grafana Cloud hoặc nhiều máy). Máy phát tải yếu sẽ cho số liệu sai lệch.
Quên sleep() giữa các request. Không có sleep, mỗi VU bắn request liên tục hết tốc lực — không giống người dùng thật. Thêm sleep(1) hoặc dùng sleep(Math.random() * 3) để mô phỏng "think time" thực tế hơn.
Test thẳng vào production trong giờ cao điểm. Đây là cách nhanh nhất để tự gây sự cố và bị sếp gọi. Luôn test trên môi trường staging giống production, hoặc nếu buộc phải test production thì làm vào giờ thấp điểm, có cảnh báo trước cho đội vận hành, và giới hạn tải cẩn thận.
Mẹo — dùng check cho tính đúng đắn, threshold cho tiêu chí đỗ/rớt. check chỉ ghi nhận đúng/sai và không làm test fail; threshold mới quyết định exit code. Nhiều người nhầm hai cái này rồi ngạc nhiên sao pipeline vẫn xanh dù nhiều check đỏ.
Mẹo — bắt đầu bằng smoke test. Trước khi chạy 2.000 VU, hãy chạy 1 VU × 1 iteration để chắc script không sai logic. Không có gì phí thời gian hơn việc chạy stress test 10 phút rồi phát hiện URL gõ sai.
Bài tập thực hành
- Smoke test cơ bản: Cài k6, viết script gọi
GET https://test.k6.io, chạy với 1 VU trong 10 giây. Đọc output và ghi lại giá trịp(95)củahttp_req_duration.
- Thêm threshold: Sửa script trên, thêm
thresholdsyêu cầup(95)<300vàhttp_req_failed rate<0.01. Chạy lại, quan sát exit code bằng lệnhecho $?ngay sau khi chạy. Thử hạ ngưỡng xuốngp(95)<50để cố tình làm nó fail và xác nhận exit code khác 0.
- Kịch bản tải nhiều giai đoạn: Viết một load test dùng
stagestăng từ 0 → 30 VU trong 30 giây, giữ 30 VU trong 1 phút, rồi hạ về 0. Chọn một API công khai bất kỳ (hoặc mock server của bạn) để thử.
- Dữ liệu động: Tạo file
users.jsonvới 5 tài khoản giả, dùngSharedArrayvàopen()để mỗi VU chọn ngẫu nhiên một tài khoản khi gọi API đăng nhập.
- Thử thách CI: Viết file workflow GitHub Actions chạy smoke test k6 trên mỗi pull request. Cố tình đặt một threshold quá chặt để thấy PR bị chặn, sau đó nới ngưỡng cho hợp lý và xác nhận nó xanh trở lại.
Tóm tắt
k6 là công cụ performance testing hiện đại của Grafana Labs, sinh ra để khắc phục sự nặng nề của các công cụ GUI truyền thống. Điểm mạnh cốt lõi: bạn viết kịch bản bằng JavaScript ("test as code"), chạy bằng dòng lệnh, và nó nhẹ nhờ lõi Go. Ba khái niệm bạn phải nắm chắc là VU (người dùng ảo), iteration (một vòng chạy kịch bản), và thresholds (ngưỡng đỗ/rớt tự động khiến k6 trả exit code khác 0 khi vi phạm — chìa khóa để tích hợp CI/CD).
Hãy luôn nhìn percentile p(95)/p(99) thay vì giá trị trung bình, dùng đúng loại test cho đúng câu hỏi (smoke, load, stress, spike, soak), và nhớ rằng k6 không phải Node.js. Giá trị thực tế lớn nhất, như ba tình huống ở trên cho thấy, không chỉ là biết hệ thống chịu tải bao nhiêu, mà là tìm ra nút thắt cụ thể, mô phỏng đúng hình dạng tải thật, và biến performance thành hàng rào tự động chặn hồi quy ngay trong pipeline. Đó là tư duy của một SDET hiện đại — và k6 là một trong những công cụ gọn gàng nhất để hiện thực hóa nó.