Bỏ qua để đến nội dung
Knowledge Hub

Load testing với k6: đo RPS thật thay vì số đẹp của vòng for Trung cấp

Thời gian đọc ước tính: 7 phút

Tuần sau có flash sale. Đội sản phẩm dự kiến 150 đơn/giây vào POST /orders và nhiều lượt xem GET /products/:id. Mục tiêu: p95 dưới 300 ms, lỗi dưới 1%.

Lần trước đội đo bằng vòng for gọi tuần tự rồi lấy trung bình. Số rất đẹp, nhưng vòng for chỉ có một request đang bay: server không bao giờ thấy hàng đợi.

Bài này trả lời: làm sao biết API chịu được bao nhiêu RPS với p95 < 300 ms, và vì sao cách sinh tải quyết định con số bạn thấy?

Theo tài liệu k6 “Load test types”; mọi loại dùng hình dạng ramp-up, giữ, ramp-down.

Tình huống Chọn Điều kiện / hình dạng tải
Vừa viết script, muốn biết script và hệ thống chạy được Smoke Tải rất thấp, vài giây đến vài phút
Kiểm tra tải dự kiến bình thường (ngày thường) Average-load Tải trung bình production, 5–60 phút, có ramp-up
Kiểm tra tải cao hơn bình thường (flash sale) Stress Tải trên trung bình, 5–60 phút
Tìm điểm gãy, sức chứa tối đa Breakpoint Tăng dần đến khi hệ thống gãy
Traffic bật lên đột ngột (mở bán lúc 0h) Spike Tải rất cao, tăng gấp, vài phút
Rò rỉ memory, connection, đầy disk theo thời gian Soak Tải trung bình, kéo dài nhiều giờ
Cần giữ đúng RPS mục tiêu Executor constant-arrival-rate / ramping-arrival-rate Mô hình mở (open model)
Mô phỏng N người dùng đồng thời có thời gian nghỉ Executor constant-vus / ramping-vus Mô hình đóng; không dùng để kết luận RPS chịu được

VU (virtual user) là một vòng lặp chạy hàm default của script. Iteration là một lần chạy hàm đó. Cách k6 quyết định khi nào bắt đầu iteration mới chia hai mô hình:

Closed model Open model
Iteration mới bắt đầu khi Iteration trước của VU đó xong Đến lịch, không chờ iteration trước
Executor k6 constant-vus, ramping-vus constant-arrival-rate, ramping-arrival-rate
Server chậm lại thì Tải gửi đi giảm theo Tải giữ nguyên, hàng đợi phình

Server chậm làm iteration closed model dài hơn, tốc độ đến giảm. Tài liệu k6 gọi đây là coordinated omission: bộ sinh tải bỏ qua đúng những request lẽ ra phải gửi lúc server nghẽn.

Người dùng flash sale không chờ người trước được phục vụ rồi mới bấm “Đặt hàng”. Vì vậy câu hỏi “chịu được 150 RPS không” cần open model.

Ba metric built-in cần đọc:

  • http_req_duration (Trend): sending + waiting + receiving, không gồm DNS và mở connection.
  • iteration_duration (Trend): thời gian một iteration trọn vẹn.
  • dropped_iterations (Counter): iteration không bắt đầu được vì hết VU (với arrival-rate executor). Khác 0 nghĩa là k6 không đạt RPS bạn đặt.

Demo dùng một server giả lập: POST /orders cần một trong 4 slot (giống connection pool DB), mỗi đơn giữ slot 40 ms. Sức chứa lý thuyết: 4 × 1000 / 40 = 100 đơn/giây. Thiếu slot thì request xếp hàng.

Cùng server, hai cách sinh tải cho hai kết luận ngược nhau:

Server chậm lại: closed model tự giảm tải và pass, open model giữ 150/s và lộ hàng đợi
Server chậm lại: closed model tự giảm tải và pass, open model giữ 150/s và lộ hàng đợiBước 1/4: Cùng server có 4 slot DB, mỗi đơn 40 ms. Closed model giữ 10 request đang bay; open model bắn 150 request/giây.thời gian chạy test10 request đang bay4 slot bậnxếp hàng ≤ 6150 request/giâyClosed modelconstant-vus, 10 VUServer /orders4 slot × 40 ms ≈ 100/sOpen modelconstant-arrival-rate 150/s
Bước 1/4

Cùng server có 4 slot DB, mỗi đơn 40 ms. Closed model giữ 10 request đang bay; open model bắn 150 request/giây.

Trạng thái sau từng bước:

  1. Server bão hoà ở ~100/s: VU closed chờ response nên gửi muộn hơn; open model vẫn đúng lịch.
  2. Phân kỳ: closed model về ≈ 98 RPS, hàng đợi server tối đa 5. Open model đẩy hàng đợi lên 195, hết 200 VU, bỏ 1372 iteration.
  3. Kết luận: closed model pass (p95 = 124 ms) dù chưa từng gửi 150 RPS; open model fail (p95 = 2,08 s).

Định luật Little giải thích: RPS = số request đồng thời / latency = 10 / 0,102 s ≈ 98. Latency tăng thì RPS tự giảm.

Thresholds là tiêu chí pass/fail của cả lần chạy. Threshold fail thì k6 thoát mã khác 0 (demo thấy 99), đủ để CI chặn release:

thresholds: {
http_req_duration: ['p(95)<300'], // ms, áp cho Trend
http_req_failed: ['rate<0.01'], // dưới 1% request lỗi
},

Checks chỉ ghi nhận đúng/sai từng response, không làm test fail. Muốn checks chặn CI thì thêm threshold checks: ['rate>0.99'].

Các điểm khác:

  • constant-arrival-rate cần rate, duration, preAllocatedVUs; maxVUs mặc định bằng preAllocatedVUs. Hết VU thì k6 cảnh báo Insufficient VUs.
  • Nhiều endpoint: mỗi endpoint một scenario, threshold theo tag ('http_req_duration{scenario:orders}') để GET /products/:id nhanh không kéo p95 của đơn hàng xuống.
  • Đọc percentile, không đọc trung bình. Lần chạy open model có avg=1.77s nhưng min=40ms: trung bình trộn request đầu test với request kẹt hàng đợi, không mô tả người dùng nào cả.

Threshold nên lấy từ SLO: SLO “99% request dưới 300 ms” thì p(95) là quá lỏng, dùng p(99)<300. Load test kiểm tra trước release; error budget đo sau release (xem SLI, SLO).

Demo nằm ở examples/src/load-testing-k6/: server server.ts (port 7020) và ba script k6.

Terminal window
npx --prefix examples ts-node examples/src/load-testing-k6/server.ts
cd examples/src/load-testing-k6
docker run --rm -i grafana/k6:2.3.0 run - < closed-vus.js
docker run --rm -i grafana/k6:2.3.0 run - < open-arrival.js
docker run --rm -i grafana/k6:2.3.0 run -e RATE=90 - < open-arrival.js
Output thật (rút gọn) của ba lần chạy
# closed-vus.js — constant-vus, 10 VU, 30s
✓ 'p(95)<300' p(95)=123.88ms
✓ 'rate<0.01' rate=0.00%
http_req_duration: avg=101.99ms min=43.64ms med=95.22ms max=160.83ms p(90)=123.31ms p(95)=123.88ms
http_reqs........: 2942 97.706456/s
exit=0 server: {"served":2942,"maxQueue":5}
# open-arrival.js — constant-arrival-rate 150/s, preAllocatedVUs 50, maxVUs 200, 30s
level=warning msg="Insufficient VUs, reached 200 active VUs and cannot initialize more"
✗ 'p(95)<300' p(95)=2.08s
http_req_duration: avg=1.77s min=40.02ms med=2.02s max=2.15s p(90)=2.04s p(95)=2.08s
http_reqs........: 3129 97.611322/s
dropped_iterations: 1372 42.80049/s
level=error msg="thresholds on metrics 'http_req_duration' have been crossed"
exit=99 server: {"served":3129,"maxQueue":195}
# open-arrival.js — RATE=90
✓ 'p(95)<300' p(95)=48.36ms
http_reqs........: 2701 89.865909/s
exit=0 server: {"served":2701,"maxQueue":9}

So sánh:

Lần chạy RPS thật p95 dropped_iterations Exit
constant-vus 10 VU 97,7 124 ms — 0 (pass)
constant-arrival-rate 150/s 97,6 2,08 s 1372 99
constant-arrival-rate 90/s 89,9 48 ms 0 0

Hai lần đầu đạt cùng ~98 RPS vì server chỉ làm được thế; closed model giấu việc phần tải còn lại phải chờ bao lâu.

Máy và giới hạn: Apple M5, Docker 29.5.3, Node 24.17, k6 2.3.0; k6 và server chạy cùng một máy. Số tuyệt đối đổi mỗi lần chạy; chỉ xu hướng closed/open là đáng tin.

Script stress-ramp.js tìm điểm gãy: ramping-arrival-rate tăng từ 50 lên 150/s trong 60 giây, threshold p95 < 300 ms có abortOnFail. Output thật (rút gọn):

running (0m38.0s), 056/061 VUs, 3035 complete and 0 interrupted iterations
http_req_duration: med=41.89ms p(95)=436.25ms dropped_iterations: 11
level=error msg="thresholds on metrics 'http_req_duration' were crossed; at least one has abortOnFail enabled, stopping test prematurely"

Số iteration hoàn thành mỗi giây dừng ở ~100/s trong khi tải mục tiêu tiếp tục tăng; số VU đang chờ tăng vọt, p95 vượt ngưỡng và test tự dừng ở giây 38 (mục tiêu lúc đó ~113/s), exit code 99. Điểm gãy khớp với trần 4 slot × 40 ms của server.

Open model (arrival rate)

  • Giữ đúng RPS mục tiêu, không bị server chậm kéo tải xuống.
  • Lộ hàng đợi và latency đuôi thật, khớp hành vi người dùng đến độc lập.

Điều cần lưu ý

  • Phải tính preAllocatedVUs/maxVUs; quá thấp thì không đạt RPS, quá cao tốn RAM máy k6.
  • Server sập thì VU dồn lên nhanh, máy sinh tải có thể nghẽn trước server.
  1. Ghi rõ câu hỏi của test (RPS mục tiêu, endpoint, ngưỡng p95/p99, tỷ lệ lỗi) lấy từ SLO trước khi viết script.
  2. Dùng constant-arrival-rate hoặc ramping-arrival-rate khi kết luận về RPS; theo dõi dropped_iterations bằng 0.
  3. Đặt thresholds cho http_req_duration và http_req_failed, cho checks nếu cần; để CI fail theo exit code.
  4. Chạy k6 trên máy tách khỏi server, gần mạng production; theo dõi CPU/RAM của máy k6 trong lúc chạy.
  5. Dùng dữ liệu giống production (nhiều product, phân bố ID thật); không gọi mãi một ID nằm sẵn trong cache.
  6. Có warm-up (ramp-up) để JIT, cache, pool ổn định; không đọc số trong vài giây đầu như số chính thức.
  7. Bật monitoring phía server (DB pool, queue, CPU) cùng lúc để tìm nút cổ chai, không chỉ nhìn summary k6.

Lỗi phổ biến nhất:

Hỏi về RPS nhưng sinh tải bằng VU

Hay gặp

scenarios: { orders: { executor: 'constant-vus', vus: 10, duration: '30s' } }
// pass p95=124ms, nhưng chỉ ~98 RPS

Nên viết

scenarios: { orders: { executor: 'constant-arrival-rate', rate: 150,
timeUnit: '1s', duration: '30s', preAllocatedVUs: 50, maxVUs: 200 } }

Vì sao

Closed model tự giảm tải khi server chậm; open model giữ 150/s nên p95 thật lộ ra.

Lỗi thứ hai hay gặp khi đọc output:

Cách hiểu sai Hệ quả Cần làm
Vòng for tuần tự đo được sức chịu tải Chỉ 1 request đồng thời, không có hàng đợi Dùng arrival-rate executor
http_reqs đạt nghĩa là đạt RPS đặt ra Bỏ qua dropped_iterations So http_reqs/s với rate, kiểm tra dropped
Tăng retry trong script khi lỗi Retry nhân tải, che lỗi thật Đo lỗi; xem retry, backoff, jitter
  1. constant-vus 10 VU, latency tăng gấp đôi thì RPS gửi đi thay đổi thế nào, vì sao?
  2. Trong lần chạy 150/s, http_reqs chỉ 97,6/s. Metric nào cho biết phần còn lại đi đâu?
  3. Vì sao checks 100% xanh mà k6 vẫn thoát mã 99?
  4. SLO là “99% request dưới 300 ms”. Threshold p(95)<300 có đủ không, vì sao?