Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

k6 — Group, Tag, debug

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn chạy một bài load test cho website thương mại điện tử với 5.000 người dùng ảo, kịch bản bao gồm: đăng nhập, tìm kiếm sản phẩm, xem chi tiết, thêm vào giỏ, và thanh toán. Test chạy xong, k6 báo cáo http_req_duration trung bình là 480ms. Sếp hỏi: "Vậy bước nào chậm? Đăng nhập hay thanh toán?". Và bạn đứng hình, vì con số 480ms kia là trung bình của TẤT CẢ request trộn lẫn vào nhau. Bạn không biết nổi bước nào đang kéo hệ thống xuống.

Đây chính xác là vấn đề mà bài học hôm nay giải quyết. Trong ba bài trước bạn đã học cách viết request, tham số hóa dữ liệu, và dùng setup/teardown. Nhưng khi kịch bản test lớn dần, dữ liệu đổ về sẽ trở thành một mớ bòng bong nếu bạn không biết cách tổ chứcgắn nhãn nó. Ba công cụ trong bài này — group(), tag, và các kỹ thuật debug — chính là bộ ba giúp bạn biến một đống số liệu vô danh thành một bức tranh rõ ràng: bước nào chậm, request nào lỗi, và tại sao script của bạn không chạy như mong đợi.

Nói cách khác: viết được test là một chuyện, còn đọc hiểu được test và tự sửa được test mới là thứ phân biệt một tester nghiệp dư với một Performance Engineer thực thụ. Bài này dạy bạn cả ba kỹ năng đó.

Khái niệm cốt lõi

group() — gom nhóm request thành bước logic

group() cho phép bạn nhóm nhiều request lại thành một "bước nghiệp vụ" có tên. Thay vì nhìn thấy 20 request rời rạc, bạn nhìn thấy các bước như "Đăng nhập", "Tìm kiếm", "Thanh toán". Mỗi group sẽ có số liệu riêng, giúp bạn biết chính xác bước nào tốn thời gian.

import http from 'k6/http';
import { group, check, sleep } from 'k6';

export default function () { let token;

group('01_dang_nhap', function () { const res = http.post('https://shop.example.vn/api/login', { email: 'user@test.vn', password: '123456', }); check(res, { 'login 200': (r) => r.status === 200 }); token = res.json('token'); });

group('02_tim_kiem_san_pham', function () { const res = http.get('https://shop.example.vn/api/search?q=ao+thun', { headers: { Authorization: Bearer ${token} }, }); check(res, { 'search 200': (r) => r.status === 200 }); });

group('03_thanh_toan', function () { const res = http.post('https://shop.example.vn/api/checkout', JSON.stringify({ cart_id: 999 }), { headers: { Authorization: Bearer ${token} } } ); check(res, { 'checkout 200': (r) => r.status === 200 }); }); }

Điều kỳ diệu là gì? Khi test chạy xong, k6 tự động tạo ra một tag group cho mọi metric bên trong nhóm. Nhờ đó, khi bạn xuất dữ liệu ra Grafana hay JSON, bạn có thể lọc http_req_duration theo từng group. Bước 03_thanh_toan chậm 2 giây trong khi 02_tim_kiem chỉ 80ms — giờ bạn thấy rõ ràng ngay.

Một mẹo nhỏ nhưng cực kỳ hữu ích: đặt tên group bằng tiền tố số (01_, 02_, 03_). Vì các công cụ hiển thị thường sắp xếp theo thứ tự chữ cái, tiền tố số giúp các bước hiển thị đúng thứ tự user journey, không bị xáo trộn.

Lưu ý quan trọng: group() trong k6 thuần túy để tổ chức và đo lường, nó KHÔNG tạo ra điểm dừng đồng bộ (synchronization point) giữa các VU. Đây là điểm khác biệt lớn so với các công cụ như LoadRunner. Các VU chạy độc lập; group chỉ giúp gắn nhãn số liệu, không điều phối luồng chạy.

tag — gắn nhãn để cắt lát dữ liệu

Nếu group() là cách gom nhóm theo bước, thì tag là cách gắn nhãn tùy ý lên bất kỳ metric nào để sau này bạn "cắt lát" (slice & dice) dữ liệu theo nhiều chiều khác nhau.

k6 tự động gắn một số tag hệ thống cho mỗi request: method (GET/POST), status (200/500), name (URL), url, và group (nếu bạn dùng group). Ngoài ra, bạn có thể tự thêm custom tag để phân loại theo logic nghiệp vụ:

// Tag ngay trên từng request
const res = http.get('https://shop.example.vn/api/products', {
    tags: { loai_api: 'catalog', muc_uu_tien: 'cao' },
});

// Kiểm tra riêng theo tag check(res, { 'ok': (r) => r.status === 200 }, { loai_api: 'catalog' });

Bạn cũng có thể gắn tag ở mức cao hơn:

import exec from 'k6/execution';

export default function () { // Tag áp cho TẤT CẢ metric trong iteration này exec.vu.tags['nhom_nguoi_dung'] = 'khach_vip'; }

Và có một cạm bẫy kinh điển về URL động. Giả sử bạn gọi http.get('/product/12'), http.get('/product/47'), http.get('/product/89'). k6 sẽ coi đây là ba metric name khác nhau và làm nổ tung bộ nhớ metric (high-cardinality). Giải pháp là dùng tag name để gộp chúng lại:

for (const id of [12, 47, 89]) {
    http.get(https://shop.example.vn/product/${id}, {
        tags: { name: 'ProductDetail' }, // gộp về 1 tên chung
    });
}

Giờ đây tất cả gom về một metric duy nhất tên ProductDetail, gọn gàng và tiết kiệm bộ nhớ.

debug — nhìn xuyên vào script khi có sự cố

Test performance thất bại thường không phải vì hệ thống chậm, mà vì script của bạn sai — sai header, sai token, parse JSON nhầm field, hoặc server trả 401 mà bạn không hề hay biết. Debug là bộ kỹ năng giúp bạn nhìn xuyên vào bên trong.

Công cụ debug mạnh nhất và đơn giản nhất là hai cờ dòng lệnh:

In ra toàn bộ request + response (HTTP full)

k6 run --http-debug="full" script.js

Chỉ in headers, gọn hơn

k6 run --http-debug script.js

Ngoài ra, console.log() là người bạn thân thiết. Nhưng nhớ: log của k6 mặc định đổ ra stderr, và trong test lớn nếu bạn log mỗi iteration thì màn hình sẽ ngập trong log. Hãy log có điều kiện:

if (res.status !== 200) {
    console.error(Loi ${res.status} tai ${res.url}: ${res.body});
}

Bạn cũng có thể ghi response ra file để phân tích sâu:

k6 run --console-output=debug.log script.js

Và một kỹ thuật cực kỳ quan trọng khi debug với dữ liệu: check() không dừng test khi thất bại (khác với assert trong các framework khác). Một check fail chỉ được ghi nhận là "một check không đạt", còn iteration vẫn chạy tiếp. Vì thế nếu bạn không log lỗi ra, một test có tỉ lệ lỗi 90% vẫn trông "chạy bình thường" trên màn hình. Luôn kết hợp check với log lỗi.

Tình huống thực tế

Ví dụ 1: Tiki và bài toán "trung bình đánh lừa"

Một đội QA tại một sàn TMĐT lớn ở Việt Nam (giả định gọi là "TikiClone") chạy load test cho luồng mua hàng với 3.000 VU. Báo cáo tổng cho thấy http_req_duration p95 là 1.2 giây — nghe có vẻ chấp nhận được, vì SLA của họ là 1.5 giây.

Nhưng khi họ bọc kịch bản trong group() cho từng bước và lọc lại số liệu theo group, sự thật lộ ra: bước 01_trang_chu chỉ 200ms, 02_tim_kiem 350ms, nhưng bước 04_ap_ma_giam_gia có p95 lên tới 4.8 giây. Con số này bị "hòa loãng" trong trung bình chung vì bước áp mã chỉ chiếm một phần nhỏ tổng số request.

Bài học rút ra: trung bình gộp luôn nói dối. Nếu không chia group, bạn sẽ ký duyệt cho lên production một tính năng đang chậm gấp 3 lần SLA. Sau khi phát hiện, đội phát hiện service tính khuyến mãi gọi đồng bộ tới một hệ thống loyalty cũ kỹ, và họ đã cache lại kết quả — kéo p95 bước đó xuống còn 600ms.

Ví dụ 2: Startup fintech và cạm bẫy URL động làm nổ bộ nhớ

Một startup ví điện tử ở TP.HCM chạy soak test 4 tiếng để tìm memory leak. Nhưng chính công cụ test của họ lại "chết" trước hệ thống. Script gọi API lấy lịch sử giao dịch dạng /api/transaction/{uuid}, và với hàng trăm nghìn UUID khác nhau, k6 tạo ra hàng trăm nghìn metric name riêng biệt. Sau 90 phút, tiến trình k6 ngốn hơn 6GB RAM rồi bị hệ điều hành kill.

Kỹ sư của họ tưởng đây là bug của server, mất cả buổi điều tra sai hướng. Cuối cùng phát hiện thủ phạm là high-cardinality tag. Fix chỉ đơn giản là thêm tags: { name: 'TransactionDetail' } vào mọi request có URL động. Sau đó k6 chạy ổn định suốt 4 tiếng chỉ với ~400MB RAM.

Bài học: khi URL chứa ID/UUID/token thay đổi liên tục, LUÔN đặt tag name cố định. Đây là lỗi phổ biến nhất khi chuyển từ test nhỏ sang test quy mô lớn.

Ví dụ 3: Đội Grab-like và giá trị của debug có điều kiện

Một đội QA của nền tảng gọi xe ở Đông Nam Á chạy test API đặt chuyến. Test báo tỉ lệ lỗi 0% và mọi người vui vẻ. Nhưng một kỹ sư cẩn thận thêm dòng console.error khi check fail, và bật --http-debug. Hóa ra 70% response trả về status 200 nhưng body chứa {"success": false, "error": "driver_not_found"}. Về mặt HTTP thì thành công, nhưng về nghiệp vụ thì thất bại hoàn toàn.

Check gốc của họ chỉ kiểm tra r.status === 200, nên không phát hiện lỗi tầng nghiệp vụ. Sau khi thêm check (r) => r.json('success') === true kèm log body khi fail, họ mới thấy hệ thống matching tài xế đang sập dưới tải cao.

Bài học: status HTTP không phải là chân lý. Kết hợp check tầng nghiệp vụ với debug body để không bị "lỗi thầm lặng" qua mặt.

Hướng dẫn từng bước

Hãy cùng xây dựng một script hoàn chỉnh áp dụng cả ba khái niệm cho một luồng mua hàng thực tế.

Bước 1 — Vạch ra các bước nghiệp vụ. Trước khi code, hãy viết ra giấy user journey: (1) Đăng nhập → (2) Duyệt danh mục → (3) Xem sản phẩm → (4) Thêm giỏ → (5) Thanh toán. Mỗi bước sẽ là một group.

Bước 2 — Bọc mỗi bước trong group với tiền tố số.

import http from 'k6/http';
import { group, check, sleep } from 'k6';

const BASE = 'https://shop.example.vn';

export default function () { let token, productId;

group('01_dang_nhap', function () { const res = http.post(${BASE}/api/login, JSON.stringify({ email: 'user@test.vn', password: '123456' }), { headers: { 'Content-Type': 'application/json' } } ); check(res, { 'login thanh cong': (r) => r.status === 200 }); token = res.json('token'); });

const authHeaders = { headers: { Authorization: Bearer ${token} } };

group('02_duyet_danh_muc', function () { const res = http.get(${BASE}/api/products?page=1, authHeaders); check(res, { 'co san pham': (r) => r.json('items.length') > 0 }); productId = res.json('items.0.id'); });

sleep(1); // mô phỏng thời gian user suy nghĩ }

Bước 3 — Thêm custom tag để phân loại theo chiều nghiệp vụ. Ví dụ gắn tag khu_vuc để so sánh hiệu năng theo vùng:

const res = http.get(${BASE}/api/products, {
    headers: { Authorization: Bearer ${token} },
    tags: { khu_vuc: 'mien_bac', loai_api: 'catalog' },
});

Bước 4 — Xử lý URL động bằng tag name.

group('03_xem_san_pham', function () {
    const res = http.get(${BASE}/api/product/${productId}, {
        headers: { Authorization: Bearer ${token} },
        tags: { name: 'ProductDetail' },
    });
    check(res, { 'xem sp ok': (r) => r.status === 200 });
});

Bước 5 — Thêm debug có điều kiện. Thêm một helper để log khi bất kỳ request nào lỗi:

function logNeuLoi(res, buoc) {
    if (res.status >= 400) {
        console.error([${buoc}] status=${res.status} body=${res.body});
    }
}

Gọi logNeuLoi(res, '03_xem_san_pham') sau mỗi request quan trọng.

Bước 6 — Chạy với cờ debug khi phát triển, tắt khi chạy thật. Trong lúc viết script, dùng ít VU và bật debug đầy đủ:

k6 run --vus 1 --iterations 1 --http-debug="full" script.js

Khi script đã đúng, chạy tải thật KHÔNG bật --http-debug (vì nó cực kỳ tốn tài nguyên và làm sai lệch kết quả đo):

k6 run --vus 3000 --duration 10m script.js

Bước 7 — Xuất kết quả theo group/tag để phân tích.

k6 run --out json=ketqua.json script.js

Sau đó bạn có thể lọc metric theo grouptags trong file JSON, hoặc đẩy vào Grafana (nội dung này sẽ được đào sâu ở các bài về dashboard).

Lỗi thường gặp & mẹo

Lỗi 1 — Tưởng group là synchronization point. Nhiều người quen với LoadRunner nghĩ rằng group sẽ làm các VU chờ nhau. KHÔNG. Trong k6, group thuần túy là công cụ đo lường và gắn tag. Đừng dùng nó để mong điều phối luồng chạy.

Lỗi 2 — Quên tag name cho URL động → nổ RAM. Như ví dụ fintech ở trên. Bất kỳ URL nào chứa ID, UUID, timestamp, hay token đều PHẢI có tag name cố định. Nếu không, cardinality sẽ tăng vô hạn và giết tiến trình k6.

Lỗi 3 — Bật --http-debug khi chạy tải thật. Cờ này in mọi request/response ra console, tốn CPU khủng khiếp và làm thời gian đo bị sai lệch. Chỉ dùng khi phát triển với 1-2 VU.

Lỗi 4 — Nghĩ check fail sẽ dừng test. Check chỉ ghi nhận đạt/không đạt rồi chạy tiếp. Nếu bạn cần dừng khi lỗi quá nhiều, hãy dùng thresholds với abortOnFail (nội dung của bài thresholds), chứ đừng trông đợi check tự dừng.

Lỗi 5 — Chỉ check status HTTP. Như ví dụ Grab-like: server trả 200 nhưng nghiệp vụ thất bại. Luôn check thêm nội dung body cho các API quan trọng.

Mẹo — Đặt tên group ngắn, nhất quán, có tiền tố số. 01_login, 02_search... giúp báo cáo sắp xếp đúng thứ tự user journey.

Mẹo — Dùng exec.vu.tags cho tag áp cả iteration. Khi muốn phân loại theo nhóm người dùng (VIP, thường, mới) cho toàn bộ luồng, gán một lần ở đầu function thay vì lặp lại trên từng request.

Mẹo — Kết hợp group lồng nhau khi cần. Bạn có thể lồng group trong group để tạo phân cấp, ví dụ group "Checkout" chứa group con "Apply_Coupon" và "Confirm_Payment". Tag group sẽ ghép đường dẫn dạng Checkout::Apply_Coupon.

Bài tập thực hành

Bài 1 — Cơ bản. Viết một script k6 mô phỏng luồng đọc báo: (1) mở trang chủ, (2) đọc danh sách bài viết, (3) đọc chi tiết một bài. Bọc mỗi bước trong group() với tiền tố số. Chạy với --http-debug và 1 VU, quan sát output.

Bài 2 — Chống high-cardinality. Mở rộng bài 1: bước đọc chi tiết bài viết gọi URL động /article/{id} với 10 id ngẫu nhiên. Chạy và xem k6 tạo bao nhiêu metric name. Sau đó thêm tag name: 'ArticleDetail' và chạy lại, so sánh số lượng metric.

Bài 3 — Debug lỗi thầm lặng. Cố tình gọi một API trả về 200 nhưng body chứa {"error": "..."} (dùng một endpoint giả hoặc mock). Viết check chỉ kiểm tra status, xác nhận test báo 0% lỗi. Sau đó thêm check body và console.error khi fail, xác nhận bạn phát hiện được lỗi.

Bài 4 — Nâng cao. Thêm custom tag nhom_nguoi_dung với hai giá trị vipthuong (chia theo VU id chẵn/lẻ). Xuất kết quả ra JSON và tìm cách so sánh http_req_duration giữa hai nhóm.

Tóm tắt

Bài này trang bị cho bạn ba công cụ để biến dữ liệu test thô thành thông tin hành động được:

  • group() gom request thành các bước nghiệp vụ có tên, giúp bạn biết chính xác bước nào chậm thay vì bị "trung bình gộp" đánh lừa. Nhớ dùng tiền tố số và nhớ rằng group KHÔNG phải synchronization point.
  • tag cho phép cắt lát dữ liệu theo nhiều chiều (method, status, group, và custom tag của bạn). Cạm bẫy lớn nhất là URL động gây high-cardinality — luôn dùng tag name cố định để gộp lại.
  • debug với --http-debug, console.log có điều kiện, và check tầng nghiệp vụ giúp bạn phát hiện lỗi thầm lặng. Nhớ tắt --http-debug khi chạy tải thật.
Ba câu chuyện thực tế — TikiClone bị trung bình đánh lừa, startup fintech nổ RAM vì URL động, và đội gọi xe bị lỗi 200-nhưng-thất-bại qua mặt — đều cho thấy một điều: thu thập số liệu chỉ là một nửa công việc; tổ chức và đọc hiểu số liệu mới là phần tạo ra giá trị. Khi bạn thành thạo group, tag và debug, bạn không còn chỉ "chạy test" nữa — bạn bắt đầu thực sự hiểu hệ thống mình đang kiểm thử.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi