Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa chạy một bài load test cho website của công ty. Script chạy xong, k6 in ra một bảng số liệu dài: p95 latency, request rate, số byte truyền đi... Bạn nhìn vào đó và tự hỏi: "Vậy rốt cuộc là ĐẠT hay KHÔNG ĐẠT?" Nếu bạn phải ngồi dò từng dòng số rồi tự phán xét, thì bài test của bạn mới chỉ làm được một nửa công việc.
Đây chính là chỗ mà Checks và Thresholds trở thành hai công cụ quan trọng bậc nhất trong k6. Chúng biến một bài test từ "chạy ra số rồi con người tự đọc" thành "chạy ra kết quả PASS/FAIL rõ ràng, máy tự phán xét". Nói cách khác, chúng là cách bạn viết ra hợp đồng chất lượng cho hệ thống: "Hệ thống của tôi PHẢI trả về 95% request dưới 500ms, tỉ lệ lỗi PHẢI dưới 1%". Nếu vi phạm, bài test thất bại.
Sự khác biệt này cực kỳ quan trọng khi bạn muốn đưa performance test vào quy trình tự động (mà các bài sau trong khóa học sẽ đi sâu). Một pipeline CI/CD không thể "đọc bảng số rồi suy nghĩ" — nó cần một tín hiệu nhị phân: pass thì cho deploy, fail thì chặn lại. Thresholds chính là cái công tắc đó. Trong bài này, chúng ta sẽ mổ xẻ kỹ cả hai khái niệm, khi nào dùng cái nào, và cách kết hợp chúng để biến bài test của bạn thành một SLA gate thực thụ.
Khái niệm cốt lõi
Checks — "assertion mềm" cho từng response
check() trong k6 giống như một câu hỏi có/không mà bạn đặt ra cho mỗi response nhận về: "Response này có đúng như tôi mong đợi không?". Điểm mấu chốt cần nhớ ngay: check KHÔNG làm bài test thất bại. Nó chỉ ghi nhận kết quả — bao nhiêu lần đúng, bao nhiêu lần sai — rồi báo cáo lại. Vì thế người ta gọi nó là "soft assertion" (assertion mềm).
Cú pháp cơ bản như sau:
import http from 'k6/http';
import { check } from 'k6';export default function () {
const res = http.get('https://api.example.vn/products');
check(res, {
'status là 200': (r) => r.status === 200,
'thời gian phản hồi dưới 500ms': (r) => r.timings.duration < 500,
'body chứa danh sách sản phẩm': (r) => r.body.includes('products'),
'trả về đúng JSON': (r) => r.headers['Content-Type'].includes('application/json'),
});
}
Mỗi dòng trong object là một check riêng, với tên mô tả (bên trái) và một hàm trả về true/false (bên phải). k6 gọi hàm đó với response r, ghi nhận kết quả, rồi tiếp tục chạy. Kể cả khi check "status là 200" trả về false vì server sập, k6 vẫn tiếp tục vòng lặp bình thường — nó chỉ đếm số lần fail.
Khi test kết thúc, bạn sẽ thấy trong báo cáo phần trăm check pass, ví dụ checks.........: 98.34% ✓ 29502 ✗ 498. Con số này nói với bạn: có 498 lần trong tổng 30000 lần kiểm tra không đạt kỳ vọng.
Vậy check dùng để làm gì? Để xác nhận tính đúng đắn về mặt logic của từng phản hồi. Trong lúc chịu tải nặng, hệ thống có thể vẫn trả HTTP 200 nhưng body lại rỗng, hoặc trả về trang lỗi thay vì dữ liệu thật. Chỉ đo latency thôi không phát hiện được điều này — bạn cần check nội dung.
Thresholds — "SLA gate" cho toàn bộ bài test
Nếu check là câu hỏi cho từng response, thì threshold là tiêu chuẩn đỗ/trượt cho cả bài test. Đây mới là thứ quyết định exit code của k6 — tức là quyết định pipeline CI/CD của bạn sẽ pass hay fail.
Threshold được khai báo trong phần options, dựa trên các metric mà k6 thu thập (khái niệm metric — latency, throughput, error rate — đã được bàn ở các bài trước; ở đây ta dùng chúng làm đối tượng để đặt ngưỡng):
export const options = {
thresholds: {
// 95% request phải dưới 500ms, 99% phải dưới 1000ms
http_req_duration: ['p(95)<500', 'p(99)<1000'],
// tỉ lệ request lỗi phải dưới 1%
http_req_failed: ['rate<0.01'],
},
};
Cú pháp là: tên metric ánh xạ tới một mảng các biểu thức điều kiện. Nếu MỌI điều kiện đều đúng khi test kết thúc, threshold đó pass. Chỉ cần một điều kiện sai, cả bài test bị đánh dấu thất bại và k6 thoát với exit code khác 0.
Bạn có thể dùng nhiều hàm tổng hợp (aggregation) trong threshold:
avg— trung bìnhmin,max— nhỏ nhất, lớn nhấtmed— trung vịp(N)— phân vị thứ N (percentile), ví dụp(90),p(95),p(99)rate— tỉ lệ (dùng cho metric kiểu Rate nhưhttp_req_failed)count— số lượng
Vì sao dùng percentile chứ không dùng trung bình?
Đây là một điểm mà nhiều người mới hay sai. Nếu bạn đặt threshold theo avg, một vài request cực nhanh có thể "kéo" trung bình xuống đẹp, che giấu thực tế là 5% người dùng đang chờ đợi mòn mỏi. Trung bình nói dối. Percentile thì thành thật hơn: p(95)<500 nghĩa là "95% người dùng có trải nghiệm dưới 500ms", và nó phơi bày cái đuôi chậm (long tail) mà người dùng thực sự cảm nhận. Trong thực tế công nghiệp, SLA hầu như luôn được viết theo percentile, không phải trung bình.
Kết hợp check với threshold — bộ đôi hoàn chỉnh
Sức mạnh thật sự đến khi bạn kết hợp cả hai. Bạn dùng check để đánh dấu điều gì đúng/sai ở từng response, rồi đặt threshold LÊN CHÍNH tỉ lệ pass của check đó:
export const options = {
thresholds: {
// ít nhất 99% check phải pass, nếu không cả bài test fail
checks: ['rate>0.99'],
http_req_duration: ['p(95)<500'],
},
};
Với dòng checks: ['rate>0.99'], bạn đã "nâng cấp" các soft assertion thành một hard gate: nếu tỉ lệ check pass rớt xuống dưới 99%, bài test thất bại. Đây là mẫu (pattern) được dùng phổ biến nhất trong thực tế.
abortOnFail — dừng sớm khi thảm họa xảy ra
Threshold còn có một tùy chọn nâng cao rất hữu ích: dừng test ngay khi ngưỡng bị vi phạm, thay vì chạy cho hết thời gian và lãng phí tài nguyên:
export const options = {
thresholds: {
http_req_failed: [
{ threshold: 'rate<0.05', abortOnFail: true, delayAbortEval: '30s' },
],
},
};
Ở đây, nếu tỉ lệ lỗi vượt 5%, k6 sẽ hủy bài test — nhưng delayAbortEval: '30s' yêu cầu chờ 30 giây đầu (giai đoạn khởi động) trước khi bắt đầu đánh giá, để tránh hủy oan vì hệ thống chưa "nóng máy".
Tình huống thực tế
Ví dụ 1: Sàn TMĐT ở TP.HCM — 200 OK nhưng giỏ hàng trống
Một đội QA tại một sàn thương mại điện tử ở TP.HCM chạy load test cho API "thêm vào giỏ hàng" trước đợt khuyến mãi. Ban đầu họ chỉ đặt một threshold duy nhất: http_req_failed: ['rate<0.01']. Test chạy xanh mướt, mọi người vui vẻ ra về.
Đến ngày khuyến mãi thật, khách hàng ồ ạt phàn nàn: bấm "thêm vào giỏ" nhưng giỏ hàng vẫn trống. Điều tra ra mới thấy: dưới tải nặng, một service phía sau bị nghẽn kết nối database, nên API trả về HTTP 200 kèm một body báo lỗi mềm dạng {"success": false, "reason": "timeout"}. Vì status vẫn là 200, http_req_failed không hề tăng — threshold của họ hoàn toàn "mù" trước loại lỗi này.
Sau sự cố, họ bổ sung check kiểm tra nội dung body và nâng nó thành threshold:
check(res, {
'thêm giỏ hàng thành công': (r) => r.json('success') === true,
});
// trong options:
// checks: ['rate>0.995']
Bài học: status code chỉ là bề nổi. Với các luồng nghiệp vụ quan trọng (đặt hàng, thanh toán, đăng nhập), luôn dùng check để xác nhận body nói lên thành công thật sự, rồi khóa nó bằng threshold.
Ví dụ 2: Ví điện tử — trung bình đẹp nhưng đuôi chậm giết chết trải nghiệm
Một fintech vận hành ví điện tử tại Việt Nam đặt threshold cho API xác thực OTP là http_req_duration: ['avg<300']. Báo cáo luôn hiện trung bình khoảng 180ms, đội ngũ rất tự tin.
Nhưng bộ phận chăm sóc khách hàng liên tục nhận khiếu nại "chờ OTP mãi không thấy". Khi họ đổi threshold sang percentile để nhìn kỹ hơn, sự thật lộ ra:
http_req_duration.....: avg=182ms p(95)=890ms p(99)=2400ms
Trung bình 182ms rất đẹp, nhưng 5% người dùng phải chờ gần 900ms, và 1% phải chờ hơn 2.4 giây — đủ để họ bực bội và thử lại, tạo thêm tải. Đội QA sửa threshold thành p(95)<500 và p(99)<1000. Lần chạy tiếp theo threshold FAIL, buộc đội backend phải tối ưu phần xử lý gửi OTP bất đồng bộ.
Bài học: đặt threshold theo avg là tự lừa mình. Luôn ưu tiên p(95) và p(99) để bắt được cái đuôi chậm — thứ mà người dùng thực sự cảm nhận và phàn nàn.
Ví dụ 3: Startup logistics — threshold gắn vào CI/CD chặn bản deploy tệ
Một startup giao vận ở Hà Nội đưa k6 vào pipeline GitLab CI. Mỗi lần merge code vào nhánh chính, một bài smoke performance test tự chạy với threshold:
thresholds: {
http_req_duration: ['p(95)<400'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
}
Một hôm, một lập trình viên vô tình thêm một truy vấn N+1 vào API danh sách đơn hàng. Khi pipeline chạy, p95 vọt lên 720ms, threshold p(95)<400 FAIL, k6 trả exit code 1, và GitLab tự động chặn merge. Lỗi bị bắt trong 2 phút, ngay trước khi kịp lên production, thay vì đợi khách hàng phàn nàn tuần sau.
Bài học: threshold là cầu nối để performance test trở thành một "cổng chất lượng" tự động. Vì k6 trả exit code khác 0 khi threshold fail, mọi công cụ CI đều hiểu được tín hiệu này mà không cần cấu hình phức tạp.
Hướng dẫn từng bước
Hãy cùng xây dựng một bài test hoàn chỉnh với check và threshold, từ đơn giản đến đầy đủ.
Bước 1 — Viết check cho các điểm quan trọng của response. Xác định những gì thể hiện "thành công" cho luồng nghiệp vụ của bạn: status code, thời gian, và nội dung body.
import http from 'k6/http';
import { check } from 'k6';export default function () {
const res = http.get('https://api.example.vn/orders');
check(res, {
'status là 200': (r) => r.status === 200,
'có dữ liệu đơn hàng': (r) => Array.isArray(r.json('data')),
'phản hồi dưới 500ms': (r) => r.timings.duration < 500,
});
}
Bước 2 — Thêm threshold cho các metric hệ thống. Đây là các ngưỡng cứng quyết định pass/fail của cả bài test.
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
},
};
Bước 3 — Nâng check thành hard gate. Thêm ngưỡng lên metric checks để đảm bảo các assertion nghiệp vụ cũng được thực thi nghiêm túc.
thresholds: {
// ...các threshold trên...
checks: ['rate>0.99'],
}
Bước 4 — Chạy và đọc exit code. Sau khi chạy k6 run script.js, kiểm tra tín hiệu pass/fail của máy:
k6 run script.js
echo $? # 0 = tất cả threshold pass; khác 0 = có threshold fail
Bước 5 — Tinh chỉnh threshold theo metric riêng (tùy chọn nâng cao). Nếu muốn đặt ngưỡng riêng cho một nhóm request, bạn có thể dùng threshold gắn với tag (kỹ thuật tag sẽ được đào sâu ở bài về Group & Tag). Ví dụ chỉ áp ngưỡng cho request có tag type:checkout:
thresholds: {
'http_req_duration{type:checkout}': ['p(95)<800'],
}
Cách làm này cho phép bạn đặt SLA khác nhau cho các loại endpoint khác nhau trong cùng một bài test — API tìm kiếm có thể được phép chậm hơn API thanh toán.
Lỗi thường gặp & mẹo
Lỗi 1: Nhầm check với threshold. Rất nhiều người mới nghĩ rằng viết check() là đủ để làm bài test fail. Không phải. Check chỉ đếm, không bao giờ tự làm test thất bại. Nếu bạn muốn CI/CD chặn lại khi check hỏng, BẮT BUỘC phải thêm checks: ['rate>...'] vào threshold. Đây là cạm bẫy phổ biến nhất.
Lỗi 2: Đặt threshold quá lỏng hoặc quá chặt. Ngưỡng p(95)<5000 (5 giây) thì gần như không bao giờ fail — vô nghĩa. Ngược lại p(95)<50 cho một API gọi qua Internet ở Việt Nam thì gần như luôn fail vì độ trễ mạng. Mẹo: chạy một bài baseline khi hệ thống rảnh để biết con số thực tế, rồi đặt threshold cao hơn baseline một chút làm ngưỡng cảnh báo.
Lỗi 3: Dùng avg thay vì percentile. Như ví dụ ví điện tử ở trên, trung bình che giấu cái đuôi chậm. Mặc định hãy nghĩ tới p(95) và p(99) trước tiên.
Lỗi 4: Đặt tên check khó hiểu. Tên như 'check1' hay 'test' khiến báo cáo trở nên vô dụng khi có 20 check cùng fail. Hãy đặt tên mô tả rõ: 'đăng nhập trả về token hợp lệ'. Báo cáo sẽ tự đọc được như một danh sách yêu cầu.
Lỗi 5: Gọi r.json() mà không kiểm tra response có phải JSON không. Nếu server trả về trang HTML lỗi 500, r.json('data') sẽ ném exception và làm hỏng cả iteration. Mẹo an toàn: kiểm tra status trước, hoặc bọc trong logic phòng thủ, ví dụ (r) => r.status === 200 && r.json('success') === true.
Mẹo hữu ích: Bạn có thể để threshold rỗng chỉ để hiển thị metric trong summary: 'http_reqs': ['count>=0']. Điều này ép k6 in ra dòng metric đó ngay cả khi bạn không thực sự muốn ràng buộc — tiện cho việc quan sát.
Mẹo về logic controller trong check: không nên nhồi quá nhiều logic phức tạp vào hàm check. Hàm check nên nhanh và đơn giản vì nó chạy sau MỖI request. Tính toán nặng trong đó sẽ làm sai lệch chính kết quả đo hiệu năng của bạn.
Bài tập thực hành
- Viết bộ check cơ bản. Tạo một script k6 gọi tới một API công khai (ví dụ
https://test-api.k6.io/public/crocodiles/). Viết ít nhất 3 check: status là 200, response dưới 400ms, và body là một mảng có ít nhất 1 phần tử. Chạy và đọc dòngcheckstrong summary.
- Thêm SLA gate. Bổ sung threshold:
http_req_duration: ['p(95)<500'],http_req_failed: ['rate<0.01'], vàchecks: ['rate>0.99']. Chạy lại và kiểm traecho $?để xác nhận exit code.
- Buộc một threshold fail có chủ đích. Cố tình đặt
http_req_duration: ['p(95)<1'](1ms — bất khả thi). Chạy và quan sát: k6 báo threshold crossed màu đỏ, exit code khác 0. Đây chính là tín hiệu mà CI/CD sẽ bắt.
- Phân biệt avg và percentile. Đặt cùng lúc hai threshold
http_req_duration: ['avg<300', 'p(99)<300']trên một API thật. Quan sát xem cái nào fail trước và suy ngẫm tại sao — đây là cách cảm nhận trực tiếp về "cái đuôi chậm".
- Thử abortOnFail. Viết threshold
http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }]và chạy test với một URL không tồn tại. Quan sát k6 tự hủy test khi ngưỡng lỗi bị vượt.
Tóm tắt
- Check là soft assertion — kiểm tra từng response (status, thời gian, nội dung body), chỉ đếm đúng/sai và KHÔNG bao giờ tự làm bài test thất bại.
- Threshold là hard gate — đặt ngưỡng lên metric tổng hợp (percentile, rate, count...), quyết định exit code của k6 và do đó quyết định pass/fail của cả pipeline.
- Kết hợp cả hai bằng cách đặt threshold
checks: ['rate>0.99']để nâng các assertion nghiệp vụ thành cổng chất lượng bắt buộc. - Luôn ưu tiên percentile (
p(95),p(99)) thay vìavg, vì trung bình che giấu cái đuôi chậm mà người dùng thực sự cảm nhận. - Status 200 không đồng nghĩa với thành công — hãy dùng check để xác minh body cho các luồng nghiệp vụ quan trọng.
- Chính nhờ việc k6 trả exit code rõ ràng khi threshold fail mà performance test có thể trở thành một SLA gate tự động, sẵn sàng cắm vào CI/CD.