Menu
ESC

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

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

Đang tải...

k6 — Parameterization & test data

Performance Testing with JMeter and k6 Bài 18/60

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

Hãy tưởng tượng bạn viết một kịch bản k6 để test trang đăng nhập của một sàn thương mại điện tử. Bạn hard-code email: "test@example.com"password: "123456" ngay trong request. Chạy với 500 VU (virtual user), kết quả tuyệt đẹp: 5.000 request/giây, latency 40ms. Bạn báo cáo sếp rằng hệ thống chịu tải rất tốt.

Nhưng bạn vừa đo sai. Vì cả 500 VU đều đăng nhập bằng một tài khoản duy nhất. Server cache session của tài khoản đó ngay lần đầu, các lần sau chỉ trả về từ cache, database gần như không phải làm gì. Con số bạn đo được không phản ánh thực tế — nơi 500 người dùng thật là 500 tài khoản khác nhau, 500 lần truy vấn database, 500 phiên riêng biệt.

Đây chính là lý do parameterization (tham số hóa) và test data (dữ liệu test) là kỹ năng phân biệt một người "chạy được k6" với một người "test performance đúng cách". Nếu tất cả VU dùng chung dữ liệu, bạn vô tình tạo ra một điều kiện quá lý tưởng, và mọi kết luận về capacity đều sai lệch. Bài này sẽ dạy bạn cách nạp dữ liệu test một cách hiệu quả về bộ nhớ, phân phối dữ liệu cho từng VU, và tạo dữ liệu động — tất cả trong bối cảnh riêng của k6.

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

Vấn đề bộ nhớ: mỗi VU là một bản sao runtime

Điều quan trọng nhất cần hiểu về k6: mỗi VU chạy trên một JavaScript runtime riêng biệt (goja, một engine JS viết bằng Go). Nếu bạn viết code nạp dữ liệu ở phạm vi toàn cục theo cách ngây thơ, mỗi VU sẽ nạp bản sao riêng của toàn bộ dữ liệu đó vào RAM.

Ví dụ cụ thể: bạn có file users.json chứa 100.000 người dùng, nặng khoảng 50MB khi parse vào bộ nhớ. Nếu bạn chạy 1.000 VU và mỗi VU giữ một bản sao, bạn cần 50GB RAM. Máy của bạn sẽ chết trước khi test bắt đầu. Đây là cái bẫy kinh điển mà hầu như ai mới học k6 cũng vấp phải.

SharedArray — giải pháp chia sẻ bộ nhớ

k6 cung cấp SharedArray từ module k6/data để giải quyết đúng vấn đề này. Dữ liệu trong SharedArray chỉ được nạp một lần duy nhất, lưu ở một vùng bộ nhớ chung, và tất cả VU cùng đọc từ đó (read-only).

import { SharedArray } from 'k6/data';

const users = new SharedArray('users', function () { // Hàm này chỉ chạy MỘT LẦN, không phải mỗi VU return JSON.parse(open('./users.json')); });

export default function () { // Mỗi VU chỉ đọc, không sao chép toàn bộ mảng const user = users[Math.floor(Math.random() * users.length)]; console.log(user.email); }

Vài điểm cần nắm chắc:

  • Tham số đầu tiên ('users') là tên (name) của shared array. Đây là định danh — nếu bạn tạo nhiều SharedArray, mỗi cái phải có tên khác nhau.
  • Hàm callback chỉ được gọi một lần trong toàn bộ đời sống của test, bất kể bao nhiêu VU. Đây là nơi bạn open() và parse file.
  • open() là hàm đặc biệt của k6 để đọc file, và nó chỉ được gọi trong init context (phạm vi ngoài hàm default, hoặc bên trong callback của SharedArray). Bạn không thể gọi open() bên trong hàm default — k6 sẽ báo lỗi.
  • SharedArray là read-only. Bạn đọc users[i] được, nhưng không sửa được phần tử. Đây là chủ ý: chia sẻ an toàn giữa các runtime.

Init context vs default function

Đây là mô hình mà bạn phải khắc cốt ghi tâm. k6 chia code thành hai vùng:

Init context — mọi thứ nằm ngoài hàm export default. Chạy một lần cho mỗi VU khi khởi tạo (nhưng SharedArray callback thì chỉ một lần toàn cục). Đây là nơi duy nhất được open() file và tạo SharedArray.

Default function — chạy lặp đi lặp lại, mỗi lần là một iteration. Đây là nơi bạn gửi request, chọn dữ liệu từ SharedArray, tạo dữ liệu ngẫu nhiên.

// === INIT CONTEXT ===
import { SharedArray } from 'k6/data';
const csvData = new SharedArray('data', function () {
    // được phép open() ở đây
    return parseCsv(open('./data.csv'));
});

// === DEFAULT (VU code) === export default function () { // KHÔNG được open() ở đây const row = csvData[__ITER % csvData.length]; }

Phân phối dữ liệu: mỗi VU một phần dữ liệu

Chọn dữ liệu ngẫu nhiên (Math.random()) là cách đơn giản nhất, nhưng đôi khi bạn muốn mỗi VU dùng một tài khoản riêng, không trùng lặp. k6 cung cấp hai biến toàn cục quý giá:

  • __VU — số thứ tự của VU hiện tại (bắt đầu từ 1).
  • __ITER — số thứ tự iteration của VU đó (bắt đầu từ 0).
export default function () {
    // Mỗi VU luôn dùng một user cố định theo số thứ tự
    const user = users[(__VU - 1) % users.length];

// Hoặc: mỗi iteration một user khác nhau, không lặp lại sớm const uniqueIndex = (__VU * 1000 + __ITER) % users.length; const user2 = users[uniqueIndex]; }

Cách (__VU - 1) % users.length đảm bảo VU số 1 luôn dùng user[0], VU số 2 dùng user[1]... Rất hữu ích khi test tài khoản đăng nhập mà mỗi phiên phải là một tài khoản riêng.

Tạo dữ liệu động

Đôi khi bạn không có file dữ liệu sẵn, mà cần tạo dữ liệu duy nhất cho mỗi request — ví dụ mã đơn hàng, email đăng ký mới, transaction ID. k6 có thư viện k6/crypto và bạn cũng có thể dùng thư viện cộng đồng như k6-utils.

import { uuidv4, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

export default function () { const orderId = ORD-${uuidv4()}; const amount = randomIntBetween(50000, 2000000); // VND const email = user_${__VU}_${__ITER}_${Date.now()}@test.vn; }

Tình huống thực tế

Ví dụ 1 — Tiki test đăng nhập với 200.000 tài khoản thật (đã ẩn danh)

Một đội QA giả định tại một sàn TMĐT lớn ở Việt Nam cần test API đăng nhập chịu được 3.000 người dùng đồng thời trong đợt cao điểm. Ban đầu họ hard-code một tài khoản duy nhất và đo được latency trung bình 25ms — quá đẹp. Nhưng khi lên staging với dữ liệu thật, họ nhận ra bcrypt hashing khi verify mật khẩu tốn ~80ms mỗi lần, và với một tài khoản thì server cache kết quả.

Họ chuyển sang nạp file accounts.json (200.000 tài khoản đã ẩn danh, mật khẩu test) qua SharedArray. File nặng ~90MB. Nhờ SharedArray, dù chạy 3.000 VU, RAM chỉ tăng ~90MB thay vì gấp 3.000 lần. Kết quả thật: latency p95 lên 210ms vì mỗi VU verify một mật khẩu khác nhau, không hit cache.

Bài học: con số 25ms ban đầu là ảo tưởng. Chỉ khi mỗi VU dùng một tài khoản riêng, họ mới thấy đúng chi phí của bcrypt và biết cần scale thêm 2 pod backend cho đợt sale.

Ví dụ 2 — Shopee-style: mã giảm giá dùng một lần

Một đội test tính năng "nhập mã giảm giá" cho chiến dịch 9.9. Đặc thù: mỗi mã chỉ dùng được một lần. Nếu 500 VU cùng dùng một mã, chỉ VU đầu tiên thành công, 499 VU còn lại nhận lỗi "mã đã sử dụng" — và bạn đo nhầm error rate 99% dù server hoàn toàn khỏe.

Giải pháp của họ: nạp file CSV chứa 50.000 mã dùng-một-lần vào SharedArray, rồi phân phối theo __VU__ITER để đảm bảo không mã nào bị dùng hai lần:

const codes = new SharedArray('codes', () => parseCsv(open('./codes.csv')));

export default function () { // Mỗi cặp (VU, ITER) ánh xạ tới một mã duy nhất const idx = (__VU - 1) * MAX_ITER + __ITER; if (idx >= codes.length) return; // hết mã thì dừng const code = codes[idx].value; // gửi request với code này }

Bài học: với dữ liệu "tiêu hao một lần" (single-use), random không đủ — bạn phải cấp phát tuyệt đối duy nhất theo __VU/__ITER, và tính toán để tổng số iteration không vượt quá số mã có sẵn.

Ví dụ 3 — Grab-style: tạo transaction ID động cho thanh toán

Một đội test cổng thanh toán mô phỏng ví điện tử. Mỗi giao dịch cần một transaction_id tuyệt đối duy nhất, nếu trùng thì server từ chối (idempotency key). Không có file nào chứa sẵn hàng triệu ID, nên họ tạo động:

import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

export default function () { const payload = JSON.stringify({ transaction_id: uuidv4(), amount: randomIntBetween(10000, 500000), wallet_id: wallets[__VU % wallets.length].id, // ví thì lấy từ SharedArray }); http.post('https://pay.example.vn/api/charge', payload); }

Họ kết hợp cả hai: wallet_id là dữ liệu cố định lấy từ SharedArray (phải là ví thật đã nạp tiền test), còn transaction_id là dữ liệu động sinh runtime.

Bài học: dữ liệu test thực tế thường lai giữa "cố định lấy từ file" (tài khoản, ví, sản phẩm) và "động sinh mới" (ID giao dịch, timestamp). Biết dùng đúng công cụ cho từng loại là chìa khóa.

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

Giả sử bạn cần test một API tạo đơn hàng, với dữ liệu người dùng từ file JSON.

Bước 1 — Chuẩn bị file dữ liệu. Tạo users.json cùng thư mục với script:

[
  { "email": "u1@test.vn", "token": "abc123" },
  { "email": "u2@test.vn", "token": "def456" }
]

Bước 2 — Nạp qua SharedArray trong init context:

import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

const users = new SharedArray('users', function () { return JSON.parse(open('./users.json')); });

Bước 3 — Cấu hình tải:

export const options = {
    vus: 50,
    duration: '1m',
};

Bước 4 — Chọn và dùng dữ liệu trong default function:

export default function () {
    const user = users[(__VU - 1) % users.length];
    const res = http.post(
        'https://api.example.vn/orders',
        JSON.stringify({ productId: 'SKU-001' }),
        {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': Bearer ${user.token},
            },
        }
    );
    check(res, { 'order created': (r) => r.status === 201 });
}

Bước 5 — Với file CSV, dùng thư viện papaparse từ jslib:

import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';

const csvData = new SharedArray('csv', function () { return papaparse.parse(open('./data.csv'), { header: true }).data; });

Bước 6 — Chạy: k6 run script.js. Quan sát trong output rằng thời gian init không tăng theo số VU — dấu hiệu SharedArray hoạt động đúng.

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

Gọi open() bên trong hàm default. Đây là lỗi phổ biến nhất. k6 chỉ cho phép open() trong init context. Nếu bạn cần file, hãy open() trong callback của SharedArray hoặc ở phạm vi toàn cục, không bao giờ trong hàm iteration.

Không dùng SharedArray, chỉ JSON.parse(open()) toàn cục. Code vẫn chạy, nhưng mỗi VU giữ một bản sao. Với dữ liệu nhỏ vài KB thì không sao, nhưng với file lớn bạn sẽ hết RAM. Thói quen tốt: luôn bọc dữ liệu file trong SharedArray.

Cố sửa phần tử của SharedArray. SharedArray là read-only. Nếu bạn cần biến đổi dữ liệu (ví dụ thêm token sau khi login), hãy copy phần tử ra biến cục bộ trong VU rồi sửa bản copy đó, đừng ghi ngược vào mảng chung.

Quên rằng __VU bắt đầu từ 1, không phải 0. Khi index vào mảng, dùng __VU - 1 để tránh bỏ sót phần tử đầu và lỗi lệch index.

Random gây trùng dữ liệu single-use. Với mã giảm giá, voucher, tài khoản dùng-một-lần, Math.random() sẽ khiến hai VU trúng cùng một phần tử. Dùng cấp phát tuyến tính theo __VU/__ITER thay vì random.

Mẹo — kiểm tra hết dữ liệu. Khi phân phối tuyến tính, luôn thêm if (idx >= data.length) return; hoặc dùng % data.length để tránh truy cập ngoài mảng (undefined).

Mẹo — tách concern. Giữ dữ liệu tĩnh (từ file) trong SharedArray, và tạo dữ liệu động (UUID, timestamp) ngay trong request. Đừng cố nhồi tất cả vào file.

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

  • Nạp cơ bản: Tạo file products.json với 20 sản phẩm (mỗi sản phẩm có idprice). Viết script k6 nạp qua SharedArray, và trong mỗi iteration chọn ngẫu nhiên một sản phẩm rồi log ra id. Chạy với 10 VU trong 30 giây và xác nhận init không chậm đi.
  • Phân phối duy nhất: Tạo codes.csv gồm 100 mã giảm giá dùng-một-lần. Viết script sao cho mỗi mã chỉ được dùng đúng một lần trên toàn bộ test, dùng công thức index theo __VU__ITER. Thêm điều kiện dừng khi hết mã.
  • Dữ liệu lai: Viết script test API tạo đơn hàng, trong đó user_id lấy từ SharedArray (file JSON 50 user), còn order_idcreated_at được sinh động bằng uuidv4()Date.now(). In payload ra console để kiểm tra không có ID nào trùng.
  • Thử nghiệm bộ nhớ: Tạo file JSON ~10MB (có thể generate bằng script Python). Chạy k6 hai lần: một lần với SharedArray, một lần với JSON.parse(open()) toàn cục thường. Dùng k6 run --vus 200 và quan sát mức RAM (qua Activity Monitor hoặc top). Ghi lại sự khác biệt.

Tóm tắt

Parameterization trong k6 xoay quanh một nguyên tắc: dữ liệu test phải phản ánh sự đa dạng của người dùng thật, mà không làm nổ bộ nhớ. Vì mỗi VU chạy trên runtime riêng, bạn phải dùng SharedArray từ k6/data để nạp dữ liệu một lần và chia sẻ read-only cho mọi VU — đây là cách duy nhất để xử lý file lớn mà không cạn RAM.

Nhớ mô hình init context (nơi duy nhất được open()) versus default function (nơi lặp và chọn dữ liệu). Dùng __VU__ITER để phân phối dữ liệu: random cho dữ liệu tái sử dụng, cấp phát tuyến tính cho dữ liệu single-use. Với dữ liệu cần duy nhất tuyệt đối (transaction ID, email đăng ký), sinh động bằng uuidv4() hoặc timestamp thay vì đọc từ file.

Ba tình huống — đăng nhập với bcrypt thật, mã giảm giá dùng-một-lần, và transaction ID động — đều cho thấy cùng một bài học: dữ liệu test sai khiến mọi kết luận về capacity trở nên vô nghĩa. Nắm chắc SharedArray và cách phân phối dữ liệu, và bạn đã có nền tảng để viết những kịch bản k6 đo đúng thực tế.