Mở đầu — vì sao bài này quan trọng
Ở bài trước, bạn đã làm quen với options và đặc biệt là stages — cách khai báo một kịch bản tăng-giảm số lượng người dùng ảo (VU) theo thời gian. stages rất tiện, nhưng nó chỉ là "lối tắt" cho đúng một kiểu chạy tải duy nhất. Khi bạn bắt đầu mô phỏng những hệ thống thật — nơi có người dùng đăng nhập liên tục, có API webhook bắn về theo tần suất cố định, có luồng flash sale đổ ập vào trong 30 giây — bạn sẽ nhận ra stages không đủ diễn đạt.
Đây chính là lúc Scenarios và executor types bước vào. Nếu ví một bài test hiệu năng như một vở kịch, thì scenario là từng "màn diễn" với dàn diễn viên riêng, còn executor là "đạo diễn" quyết định các diễn viên đó bước ra sân khấu theo nhịp nào: lần lượt từng người, ồ ạt cùng lúc, hay theo một lịch trình tăng dần được tính toán kỹ.
Hiểu executor types là ranh giới giữa một người "biết chạy k6" và một người "thiết kế được bài test hiệu năng phản ánh đúng thực tế". Rất nhiều kết luận sai về hiệu năng hệ thống bắt nguồn từ việc chọn nhầm executor — ví dụ dùng mô hình "cứ giữ 100 VU chạy vòng lặp" trong khi thực tế lưu lượng của bạn là "cứ mỗi giây có 50 request mới đến bất kể server nhanh hay chậm". Hai mô hình này cho ra kết quả khác nhau hoàn toàn. Bài này sẽ giúp bạn phân biệt và chọn đúng.
Khái niệm cốt lõi
Scenario là gì?
Trong k6, một scenario là một khối cấu hình mô tả: ai chạy (hàm nào trong file test), chạy như thế nào (executor), và chạy với tham số ra sao (số VU, thời lượng, tốc độ...). Bạn khai báo scenarios bên trong options.scenarios, và mỗi scenario có một cái tên do bạn tự đặt.
Điểm mạnh lớn nhất: nhiều scenario chạy song song hoặc lệch giờ trong cùng một lần test. Bạn có thể mô phỏng cùng lúc luồng "khách hàng duyệt sản phẩm", luồng "admin đồng bộ tồn kho", và luồng "webhook thanh toán bắn về" — mỗi luồng một executor riêng, một hàm riêng.
export const options = {
scenarios: {
khach_duyet_hang: {
executor: 'ramping-vus',
exec: 'browse', // gọi hàm export function browse()
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
},
webhook_thanh_toan: {
executor: 'constant-arrival-rate',
exec: 'paymentWebhook',
rate: 30,
timeUnit: '1s',
duration: '9m',
preAllocatedVUs: 50,
},
},
};export function browse() { / ... / }
export function paymentWebhook() { / ... / }
stages mà bạn học ở bài trước thực chất chỉ là đường tắt cho scenario mặc định dùng executor ramping-vus. Khi bạn khai báo scenarios một cách tường minh, bạn mở khóa toàn bộ sức mạnh của k6.
Hai họ executor: điều khiển VU vs điều khiển tốc độ đến
Đây là ý niệm quan trọng nhất của cả bài. Các executor chia làm hai nhóm tư duy khác nhau:
Nhóm 1 — Điều khiển theo số VU (open/closed theo VU): Bạn nói với k6 "có bao nhiêu người dùng ảo". Mỗi VU chạy hàm test lặp đi lặp lại; xong vòng này thì bắt đầu vòng khác ngay. Hệ quả: nếu server chậm, mỗi vòng lặp mất lâu hơn, nên số request thực tế mỗi giây sẽ giảm. Đây gọi là mô hình closed model — số lượng "người trong hệ thống" cố định, tốc độ đến phụ thuộc vào tốc độ server phản hồi.
Thuộc nhóm này: shared-iterations, per-vu-iterations, constant-vus, ramping-vus.
Nhóm 2 — Điều khiển theo tốc độ đến (arrival rate): Bạn nói với k6 "mỗi giây phải có bao nhiêu vòng lặp bắt đầu", bất kể server nhanh hay chậm. k6 tự cấp VU để giữ đúng nhịp đó. Đây là open model — giống lưu lượng thật ngoài đời: người dùng cứ bấm vào web theo nhịp của họ, không đợi server rảnh mới bấm.
Thuộc nhóm này: constant-arrival-rate, ramping-arrival-rate.
Ghi nhớ câu này: nếu bạn muốn kiểm soát "throughput mục tiêu" (ví dụ 500 req/s), hãy dùng arrival-rate. Nếu bạn muốn kiểm soát "số người dùng đồng thời" (ví dụ 200 người đang online), hãy dùng VU-based.
Điểm qua từng executor
| Executor | Mục đích | Tham số chính |
|---|---|---|
shared-iterations | Chia N vòng lặp cho một nhóm VU cùng "gặm" cho đến hết | vus, iterations |
per-vu-iterations | Mỗi VU chạy đúng N vòng lặp của riêng nó | vus, iterations |
constant-vus | Giữ cố định X VU chạy trong khoảng thời gian D | vus, duration |
ramping-vus | Tăng/giảm số VU theo các stages (chính là stages cũ) | startVUs, stages |
constant-arrival-rate | Giữ cố định R vòng lặp/giây bắt đầu | rate, timeUnit, duration, preAllocatedVUs |
ramping-arrival-rate | Tăng/giảm tốc độ đến theo stages | startRate, stages, preAllocatedVUs |
preAllocatedVUs và maxVUs — điểm dễ sai nhất
Với hai executor arrival-rate, k6 cần một "kho VU" để phân phối. preAllocatedVUs là số VU khởi tạo sẵn ngay từ đầu (khởi tạo VU tốn tài nguyên, nên k6 làm trước để không trễ nhịp). maxVUs là trần tối đa k6 được phép cấp thêm nếu server chậm khiến cần nhiều VU hơn để giữ rate.
Nếu bạn đặt preAllocatedVUs quá thấp và không đặt maxVUs, khi server chậm lại k6 sẽ không đủ VU để giữ đúng rate và bạn sẽ thấy cảnh báo "insufficient VUs" — kết quả test không còn phản ánh đúng tải mục tiêu nữa.
Tình huống thực tế
Ví dụ 1 — Tiki và cái bẫy "closed model" khi đo API tìm kiếm
Một đội QA giả định làm cho sàn thương mại điện tử kiểu Tiki cần đo API tìm kiếm sản phẩm. SLA đặt ra: hệ thống phải chịu được 1.000 lượt tìm kiếm mỗi giây. Bạn QA viết bài test đầu tiên với constant-vus, đặt 1.000 VU, mỗi VU gọi API tìm kiếm rồi lặp lại.
Kết quả báo cáo: chỉ đạt khoảng 340 req/s. Đội hạ tầng hoảng hốt tưởng hệ thống yếu. Nhưng vấn đề nằm ở mô hình test. Với constant-vus, mỗi VU phải đợi response xong mới gọi lượt tiếp theo. Khi p95 latency là ~800ms, mỗi VU chỉ tạo được khoảng 1 request/giây, nên 1.000 VU chỉ ra ~340–1.000 req/s tùy độ trễ — throughput bị khóa bởi chính độ chậm của server.
Đội chuyển sang constant-arrival-rate với rate: 1000, timeUnit: '1s', preAllocatedVUs: 1500, maxVUs: 4000. Bây giờ k6 ép đúng 1.000 request khởi phát mỗi giây bất kể server phản hồi ra sao. Kết quả mới trung thực hơn hẳn: throughput giữ đúng 1.000 req/s ở giai đoạn đầu, và khi server bắt đầu quá tải, error rate tăng vọt và số VU active leo lên gần maxVUs — đúng chân dung của một hệ thống chạm giới hạn.
Bài học: Khi SLA phát biểu theo "số request/giây", bắt buộc dùng arrival-rate. VU-based sẽ che giấu sự thật vì nó tự động giảm tải khi server chậm.
Ví dụ 2 — Sàn giao dịch VNPAY và luồng webhook nhịp cố định
Một hệ thống cổng thanh toán kiểu VNPAY cần xử lý webhook báo kết quả giao dịch. Đặc điểm nghiệp vụ: dù có bao nhiêu giao dịch, hệ thống upstream bắn webhook về theo nhịp khá đều, giả sử trung bình 80 webhook/giây vào giờ cao điểm. Đồng thời, đội cũng muốn mô phỏng luồng khách hàng thao tác trên cổng thanh toán, dâng dần từ 0 lên 500 người trong 5 phút.
Họ dùng hai scenario song song:
scenarios: {
webhook: {
executor: 'constant-arrival-rate',
exec: 'handleWebhook',
rate: 80, timeUnit: '1s', duration: '10m',
preAllocatedVUs: 100, maxVUs: 300,
},
nguoi_dung: {
executor: 'ramping-vus',
exec: 'checkoutFlow',
startVUs: 0,
stages: [
{ duration: '5m', target: 500 },
{ duration: '5m', target: 500 },
],
},
}
Kết quả rất giá trị: họ phát hiện khi luồng người dùng leo lên 500 VU, độ trễ xử lý webhook cũng tăng theo (do dùng chung pool kết nối database). Nhờ tách hai scenario với hai executor phù hợp bản chất từng luồng, đội có thể chỉ ra chính xác rằng webhook bị ảnh hưởng bởi tải giao diện — điều mà một bài test gộp chung một hàm sẽ không bao giờ lộ ra.
Bài học: Kết hợp nhiều scenario với executor khác nhau cho phép bạn mô phỏng đúng bản chất của từng luồng lưu lượng, và tách bạch nguyên nhân khi có vấn đề.
Ví dụ 3 — Grab và bài test smoke bằng per-vu-iterations
Trước mỗi lần deploy, một đội kiểu Grab muốn chạy nhanh một bài "smoke test" hiệu năng: 20 tài xế ảo, mỗi tài xế thực hiện đúng 5 vòng nghiệp vụ (nhận chuyến → cập nhật vị trí → hoàn thành), không hơn không kém, để đảm bảo dữ liệu test sạch và dễ đối chiếu log.
Họ dùng per-vu-iterations với vus: 20, iterations: 5. Tổng cộng đúng 100 vòng lặp, mỗi VU độc lập chạy đủ phần của mình. Nếu họ lỡ dùng shared-iterations với iterations: 100, thì có thể một VU nhanh sẽ "gặm" 12 vòng trong khi VU chậm chỉ chạy 3 vòng — phân bố không đều, khó tái lập, không phù hợp cho mục tiêu kiểm tra hành vi từng-người-dùng-hoàn-chỉnh.
Bài học: per-vu-iterations lý tưởng cho các bài test cần tính đều và tính xác định (mỗi VU làm đúng một khối lượng công việc cố định), như smoke test, kiểm thử tính đúng đắn, hoặc chạy một tập dữ liệu CSV cố định mỗi VU một phần.
Hướng dẫn từng bước
Hãy cùng xây một bài test có hai scenario từ đầu.
Bước 1 — Viết các hàm nghiệp vụ và export chúng. Mỗi scenario trỏ tới một hàm qua thuộc tính exec. Tên hàm phải là hàm được export.
import http from 'k6/http';
import { sleep } from 'k6';export function browse() {
http.get('https://test.k6.io/');
sleep(1);
}
export function apiPing() {
http.get('https://test.k6.io/contacts.php');
}
Bước 2 — Khai báo scenarios trong options. Đặt tên có ý nghĩa cho từng scenario.
export const options = {
scenarios: {
duyet_web: {
executor: 'ramping-vus',
exec: 'browse',
startVUs: 0,
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 50 },
{ duration: '30s', target: 0 },
],
},
goi_api: {
executor: 'constant-arrival-rate',
exec: 'apiPing',
rate: 20,
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 30,
maxVUs: 100,
},
},
};
Bước 3 — Dùng startTime nếu muốn scenario chạy lệch giờ. Mặc định mọi scenario khởi động ở giây 0. Nếu bạn muốn scenario goi_api chỉ bắt đầu sau khi luồng duyệt web đã ổn định, thêm startTime: '30s'.
Bước 4 — Chạy và đọc output. Chạy k6 run test.js. Trong bảng tổng kết, k6 gắn nhãn scenario vào các metric, và bạn có thể lọc theo tag scenario để so sánh từng luồng.
Bước 5 — Kiểm tra cảnh báo cấp phát VU. Nếu thấy dòng Insufficient VUs, reached X active VUs and cannot initialize more, nghĩa là arrival-rate scenario của bạn thiếu VU. Tăng maxVUs hoặc preAllocatedVUs.
Bước 6 — Gắn threshold theo từng scenario (nếu cần). Bạn có thể đặt threshold có điều kiện theo tag scenario để mỗi luồng có tiêu chuẩn SLA riêng — nội dung này sẽ đi sâu ở bài về Checks & Thresholds.
Lỗi thường gặp & mẹo
1. Chọn nhầm họ executor. Đây là lỗi phổ biến và nguy hiểm nhất. Nhớ lại: SLA nói theo "req/s" → arrival-rate; SLA nói theo "số người đồng thời" → VU-based. Chọn sai khiến báo cáo hiệu năng sai bản chất.
2. Quên preAllocatedVUs hoặc đặt quá thấp cho arrival-rate. k6 sẽ không giữ đúng rate và bạn nhận cảnh báo insufficient VUs. Quy tắc thô: ước lượng VUs ≈ rate × latency_giây. Nếu rate 100/s và latency ~0.5s, bạn cần ít nhất ~50 VU chỉ để chạy suôn; đặt preAllocatedVUs cao hơn con số đó và maxVUs gấp 2–3 lần để có biên an toàn.
3. Nhầm shared-iterations với per-vu-iterations. shared chia tổng số vòng lặp cho cả nhóm (phân bố không đều, nhanh gặm nhiều); per-vu cố định số vòng mỗi VU (đều và xác định). Chọn theo mục tiêu: cần đều thì per-vu.
4. Đặt exec trỏ tới hàm không tồn tại hoặc không export. k6 sẽ báo lỗi ngay khi khởi động. Luôn export function cho mọi hàm được scenario tham chiếu.
5. Tưởng default function biến mất. Nếu bạn dùng scenarios tường minh mà vẫn có export default function, hàm default chỉ chạy khi có scenario nào trỏ exec: 'default' hoặc khi bạn không khai báo scenarios. Đừng để hàm default "mồ côi" gây hiểu lầm.
Mẹo: Bắt đầu mọi thiết kế test bằng câu hỏi "lưu lượng thật của tôi là closed model hay open model?". Lưu lượng do con người bấm ngoài đời gần như luôn là open model → thiên về arrival-rate. Hàng đợi công việc nội bộ với số worker cố định lại là closed model → hợp với constant-vus.
Mẹo: Đặt tên scenario bằng tiếng Việt không dấu hoặc tiếng Anh rõ nghĩa (flash_sale_spike, webhook_steady) để báo cáo dễ đọc cho cả team không chuyên.
Bài tập thực hành
- Chuyển đổi mô hình: Lấy một bài test cũ dùng
constant-vus: 200chạy trong 5 phút. Viết lại thànhconstant-arrival-ratesao cho k6 cố ép ~150 req/s. Chạy cả hai, so sánh throughput thực tế và số VU active. Ghi lại: mô hình nào giữ được throughput ổn định khi bạn cố tình gọi một endpoint chậm?
- Ba scenario song song: Thiết kế một bài test mô phỏng một sàn TMĐT Việt Nam với ba luồng: (a) khách duyệt sản phẩm
ramping-vus0→300 trong 3 phút; (b) API tồn khoconstant-arrival-rate40 req/s; (c) webhook thanh toánconstant-arrival-rate15 req/s, dùngstartTime: '1m'. Chạy và xác định luồng nào chịu ảnh hưởng nhiều nhất khi tải tổng lên đỉnh.
- Điều tra insufficient VUs: Cố tình đặt
constant-arrival-ratevớirate: 200, preAllocatedVUs: 10, không đặtmaxVUs. Chạy và quan sát cảnh báo. Sau đó tăng dầnmaxVUscho tới khi k6 giữ đúng rate. Ghi lại con sốmaxVUstối thiểu cần thiết và giải thích vì sao.
Tóm tắt
- Scenario là khối cấu hình mô tả ai chạy, chạy thế nào, với tham số gì; bạn có thể chạy nhiều scenario song song hoặc lệch giờ trong cùng một lần test.
stagesmà bạn học trước đây chỉ là lối tắt cho executorramping-vus— một trong sáu executor của k6.- Executor chia làm hai họ: VU-based (
constant-vus,ramping-vus,shared-iterations,per-vu-iterations) kiểm soát số người dùng đồng thời (closed model); và arrival-rate (constant-arrival-rate,ramping-arrival-rate) kiểm soát tốc độ đến (open model). - Nguyên tắc chọn: SLA theo req/s → arrival-rate; SLA theo số người online → VU-based. Lưu lượng người-dùng-thật thường là open model.
- Với arrival-rate, luôn cấu hình
preAllocatedVUs(và thường cảmaxVUs) đủ lớn, nếu không k6 không giữ được rate mục tiêu và cảnh báo insufficient VUs. - Dùng
execđể mỗi scenario gọi một hàm nghiệp vụ riêng, vàstartTimeđể dàn xếp thứ tự các luồng — đây là chìa khóa để mô phỏng đúng thực tế và tách bạch nguyên nhân khi có vấn đề hiệu năng.