Product Management
Đăng nhập
ESC

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

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

Realistic test scenarios — user journey simulation

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 thương mại điện tử. Kết quả đẹp long lanh: 5.000 requests/giây, p95 latency chỉ 80ms, error rate 0%. Bạn tự tin báo cáo với sếp rằng hệ thống "chịu được 5.000 user đồng thời". Rồi đến ngày sale 12.12, hệ thống sập chỉ với 800 user thật. Chuyện gì đã xảy ra?

Vấn đề nằm ở chỗ bài test của bạn chỉ bắn liên tục vào một endpoint GET /products — một request nhẹ nhàng, có cache, không đụng database ghi, không giữ session, không gọi thanh toán. Trong khi user thật thì mở trang chủ, tìm kiếm, xem chi tiết sản phẩm, thêm vào giỏ, đăng nhập, điền địa chỉ, rồi mới thanh toán. Mỗi bước là một endpoint khác nhau, với chi phí tài nguyên khác nhau, và quan trọng nhất — chúng có thời gian suy nghĩ (think time) giữa các bước.

Đây chính là khác biệt giữa synthetic test (test tổng hợp, giả tạo) và realistic user journey simulation (mô phỏng hành trình người dùng thực). Một bài test không giống cách user thật hành xử thì con số nó cho ra là con số ảo — bạn đang đo một hệ thống không tồn tại. Bài học này dạy bạn cách xây dựng kịch bản test phản ánh đúng hành vi thật, để những con số bạn báo cáo có thể tin được và ra quyết định được. Đây là kỹ năng phân biệt một performance tester "bắn số" với một performance engineer thực thụ.

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

Anti-pattern: bài test "synthetic" đơn endpoint

Kịch bản sai điển hình mà gần như ai mới học cũng viết:

import http from 'k6/http';

export const options = { vus: 500, duration: '5m' };

export default function () { // Mọi VU chỉ làm đúng một việc: bắn liên tục vào một URL http.get('https://shop.example.vn/api/products'); // Không sleep, không think time, không state }

Bài này sai ở ba điểm chết người:

  • Chỉ chạm một endpoint. Trong thực tế, /products có thể được cache ở CDN và trả về trong 10ms. Nhưng /checkout phải khóa tồn kho, ghi database, gọi cổng thanh toán — nặng gấp 50 lần. Test một endpoint nhẹ rồi kết luận về toàn hệ thống là ngụy biện.
  • Không có think time. User thật không bấm liên tục 20 request/giây. Họ đọc mô tả sản phẩm 15 giây, xem ảnh 8 giây, đắn đo trước khi thêm vào giỏ. Bỏ think time khiến bạn tạo ra một tải dồn dập không thực tế — hoặc quá nặng (mỗi VU bắn quá nhanh), hoặc sai lệch tỷ lệ tài nguyên.
  • Không có state và tỷ lệ. User thật không ai cũng thanh toán. Thường 100 người vào xem thì chỉ 3 người mua (conversion 3%). Nếu bạn cho mọi VU đều đi thẳng tới /checkout, bạn đang test một thế giới nơi 100% khách mua hàng — vừa không thật, vừa làm cổng thanh toán quá tải giả.

User journey là gì

Một user journey (hành trình người dùng) là chuỗi hành động có thứ tự mà một user thực hiện để đạt mục tiêu. Với sàn TMĐT, các journey phổ biến gồm:

  • Browser (người xem): trang chủ → tìm kiếm → xem sản phẩm → rời đi. Chiếm ~70% traffic.
  • Add-to-cart (người cân nhắc): xem sản phẩm → thêm giỏ → xem giỏ → rời đi. ~25%.
  • Buyer (người mua): toàn bộ phễu tới thanh toán thành công. ~5%.
Mô phỏng thực tế nghĩa là tái tạo cả thành phần (các bước trong mỗi journey) lẫn tỷ lệ phân bổ (bao nhiêu % user theo journey nào).

Ba trụ cột của một kịch bản thực tế

Think time (thời gian suy nghĩ): khoảng dừng giữa các hành động. Nên dùng giá trị ngẫu nhiên chứ không cố định, vì user thật không đều nhau. Ví dụ sleep(Math.random() * 5 + 3) để dừng 3–8 giây.

Pacing (nhịp độ): kiểm soát mỗi VU hoàn thành bao nhiêu journey mỗi giờ, phản ánh đúng tần suất truy cập.

State & data (trạng thái & dữ liệu): mỗi VU giữ session riêng (cookie, token đăng nhập), dùng dữ liệu khác nhau (mỗi user tìm từ khóa khác, mua sản phẩm khác) để tránh cache "ăn gian" và tránh xung đột tồn kho giả.

Tình huống thực tế

Ví dụ 1: Tiki và cú sập vì bỏ quên think time

Một đội QA (bối cảnh giả định dựa trên tình huống phổ biến ở các sàn lớn như Tiki) chuẩn bị cho đợt sale. Họ viết k6 với 2.000 VU chạy full phễu mua hàng nhưng không có think time — mỗi VU vừa xong checkout là lập tức bắt đầu journey mới. Kết quả test: hệ thống "chịu được" vì họ chỉ dám chạy 2.000 VU.

Nhưng nhìn kỹ throughput: 2.000 VU không think time tạo ra ~4.500 đơn hàng/phút. Trong khi thực tế, 2.000 user thật với think time trung bình 40 giây/phễu chỉ tạo ~3.000 đơn/phút. Đội QA đã ước lượng thừa tải checkout 50% nhưng lại ước lượng thiếu số VU cần thiết. Khi sale thật diễn ra, số user đồng thời lên tới 12.000 (vì mỗi user "ngồi lâu" trên site), vượt xa 2.000 VU đã test. Kết nối database cạn kiệt vì mỗi user thật giữ session lâu hơn nhiều so với VU chạy vội.

Bài học: Không có think time, bạn không thể ánh xạ số VU sang số user thật. VU là "công nhân bận rộn", user là "người rảnh rỗi". Muốn mô phỏng 12.000 user với think time 40s và mỗi phễu 20s hành động, bạn cần khoảng 12000 × 20 / (20+40) = 4.000 VU. Bỏ think time làm sai toàn bộ phép quy đổi này.

Ví dụ 2: Shopee-style — trộn tỷ lệ journey đúng thực tế

Một sàn giả định "MuaSam.vn" phân tích Google Analytics và thấy phân bổ traffic ngày thường: 68% chỉ xem, 27% thêm giỏ rồi bỏ, 5% mua thật. Đội performance viết kịch bản k6 dùng cơ chế phân nhánh theo tỷ lệ:

export default function () {
  const r = Math.random();
  if (r < 0.68)      browseJourney();      // 68% chỉ xem
  else if (r < 0.95) addToCartJourney();   // 27% thêm giỏ
  else               checkoutJourney();    // 5% mua thật
}

Kết quả đầu tiên bất ngờ: dù tổng request nhiều, tải lên cổng thanh toán chỉ bằng 5% — đúng thực tế, và họ phát hiện database search mới là điểm nghẽn thật (vì 68% user đều search), chứ không phải checkout như họ tưởng. Nhờ trộn tỷ lệ đúng, họ tối ưu đúng chỗ: thêm Elasticsearch cho search thay vì nâng cấp cổng thanh toán vô ích. Nếu họ chạy synthetic test toàn checkout, họ đã đổ tiền sai chỗ.

Bài học: Tỷ lệ journey quyết định điểm nghẽn thật nằm ở đâu. Lấy tỷ lệ từ dữ liệu thật (Analytics, access log) chứ đừng đoán.

Ví dụ 3: Ví điện tử VN — session state khiến kết quả đảo chiều

Một ví điện tử (bối cảnh kiểu MoMo/ZaloPay) test API chuyển tiền. Ban đầu họ viết synthetic: mỗi request POST /transfer độc lập, dùng chung một token. Kết quả: 8.000 TPS, mọi thứ ổn.

Khi chuyển sang journey thực (mỗi VU: đăng nhập → lấy token riêng → kiểm tra số dư → chuyển tiền → xem lịch sử), throughput sụt còn 1.900 TPS. Lý do: bước đăng nhập gọi hàm băm mật khẩu (bcrypt) rất nặng CPU, và mỗi user giữ một session Redis riêng. Synthetic test dùng chung một token đã "giấu" hoàn toàn chi phí đăng nhập và quản lý session — những thứ user thật buộc phải trả.

Bài học: State thật (login, token, session) thường là phần đắt nhất mà synthetic test vô tình bỏ qua. Mỗi VU phải sống một "cuộc đời" hoàn chỉnh.

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

Bước 1 — Khai thác dữ liệu hành vi thật. Vào Google Analytics / access log, xác định các journey chính, tỷ lệ mỗi journey, và think time trung bình giữa các trang (dùng "Average time on page"). Đừng bỏ qua bước này — nó là nền móng của mọi thứ.

Bước 2 — Mô hình hóa từng journey thành hàm. Mỗi journey là một chuỗi request có thứ tự, có think time, có kiểm tra kết quả:

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

function think() { sleep(Math.random() * 5 + 3); } // 3-8 giây

function browseJourney() { group('Browse', () => { http.get('https://shop.vn/'); think(); const res = http.get('https://shop.vn/search?q=giày'); think(); check(res, { 'search ok': (r) => r.status === 200 }); http.get('https://shop.vn/product/1234'); think(); }); }

Bước 3 — Cho mỗi VU state riêng. Dùng CSV data set để mỗi VU đăng nhập bằng tài khoản khác, tìm từ khóa khác. Điều này tránh cache "ăn gian" và mô phỏng đúng độ đa dạng:

import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';

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

function checkoutJourney() { const u = users[Math.floor(Math.random() * users.length)]; const jar = http.cookieJar(); http.post('https://shop.vn/login', { email: u.email, pass: u.pass }); // cookie/session tự động được jar giữ cho các request sau http.post('https://shop.vn/cart', { sku: u.sku }); think(); http.post('https://shop.vn/checkout', { address: u.addr }); }

Bước 4 — Trộn journey theo tỷ lệ thật như ví dụ 2 ở trên (dùng Math.random() phân nhánh, hoặc dùng nhiều scenarios trong k6 với exec khác nhau và rate khác nhau — cách này sạch hơn cho tỷ lệ chính xác).

Bước 5 — Đặt think time và pacing hợp lý. Random hóa think time. Đảm bảo tổng thời gian một phễu (hành động + think time) khớp với thời gian user thật ở trên site.

Bước 6 — Quy đổi VU sang user thật. Dùng công thức: VU ≈ user_đồng_thời × thời_gian_hành_động / (thời_gian_hành_động + think_time). Ghi rõ giả định này trong báo cáo để người đọc hiểu con số nghĩa là gì.

Bước 7 — Validate kịch bản. So sánh phân bổ request trong report của bạn với phân bổ request trong access log production. Nếu production có 70% request là /search mà test của bạn chỉ có 20%, kịch bản chưa thực tế — chỉnh lại tỷ lệ.

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

Lỗi: Bỏ think time cho "an toàn". Nhiều người nghĩ bỏ think time = test khắc nghiệt hơn = an toàn hơn. Sai. Nó tạo tải sai hình dạng và làm phép quy đổi VU→user vô nghĩa. Nếu muốn test khắc nghiệt, hãy tăng số VU với think time thật, đừng bóp think time về 0.

Lỗi: Dùng chung một tài khoản/một dữ liệu cho mọi VU. Gây cache hit giả (server trả cache thay vì xử lý thật) và xung đột khóa giả (mọi VU tranh cùng một dòng tồn kho). Luôn tham số hóa dữ liệu.

Lỗi: Hardcode tỷ lệ theo cảm tính. "Chắc khoảng 50% mua hàng" — không, hãy lấy số thật từ Analytics. Conversion thật thường chỉ 1–5%.

Lỗi: Quên xử lý cookie/session. Nếu không dùng cookie jar, mỗi request bị coi là user mới, server không giữ giỏ hàng, phễu vỡ giữa chừng. k6 tự quản cookie theo VU, nhưng hãy kiểm tra login thật sự thành công bằng check.

Mẹo: Dùng k6 scenarios cho tỷ lệ chính xác. Thay vì Math.random(), khai báo nhiều scenario với exec riêng và arrival-rate khác nhau — bạn kiểm soát tỷ lệ chuẩn hơn và report tách bạch từng journey.

Mẹo: Tag mỗi journey. Gắn tag (tags: { journey: 'checkout' }) để khi đọc report, bạn xem được latency/error theo từng loại journey — biết ngay journey nào là điểm nghẽn.

Mẹo: Bắt đầu từ access log production. Cách nhanh nhất để có journey thật là "phát lại" (replay) mẫu request từ log production một ngày điển hình.

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

  • Phân tích dữ liệu: Lấy access log hoặc Google Analytics của một website bạn biết. Liệt kê 3 journey chính và ước lượng tỷ lệ phần trăm mỗi journey cùng think time trung bình.
  • Viết kịch bản đa journey: Dùng k6 viết một script mô phỏng 3 journey (browse / add-to-cart / checkout) với tỷ lệ 70/25/5, có think time random 3–8 giây, và tham số hóa từ khóa tìm kiếm bằng một file CSV ít nhất 10 dòng.
  • So sánh synthetic vs realistic: Chạy hai bài test cùng số VU — một bài chỉ bắn GET /products không think time, một bài chạy journey thực. So sánh throughput, phân bổ request, và điểm nghẽn phát hiện được. Viết một đoạn ngắn giải thích vì sao kết luận của hai bài khác nhau.
  • Quy đổi VU→user: Với think time trung bình 30 giây và thời gian hành động mỗi phễu 15 giây, tính số VU cần để mô phỏng 6.000 user đồng thời. Ghi rõ giả định.

Tóm tắt

Test một endpoint không phải là test hệ thống. User thật đi qua nhiều bước, có tỷ lệ chuyển đổi thấp, có thời gian suy nghĩ, và giữ session riêng — và chính những đặc điểm này quyết định điểm nghẽn thật của hệ thống. Một bài realistic user journey simulation đúng nghĩa cần ba trụ cột: think time (dừng ngẫu nhiên giữa các bước, để quy đổi VU sang user thật có ý nghĩa), tỷ lệ journey đúng thực tế (lấy từ dữ liệu Analytics/log, không đoán), và state & dữ liệu riêng cho mỗi VU (login, token, cookie, tham số hóa để tránh cache và xung đột giả).

Ba ví dụ trong bài cho thấy hậu quả rõ ràng của việc bỏ qua từng trụ cột: bỏ think time làm sai phép quy đổi VU→user và gây sập ngoài đời; trộn sai tỷ lệ khiến bạn tối ưu nhầm chỗ; bỏ state thật giấu đi chi phí đắt nhất là đăng nhập và session. Quy trình bảy bước — từ khai thác dữ liệu thật, mô hình hóa journey, gán state riêng, trộn tỷ lệ, đặt think time, quy đổi VU, đến validate với log production — giúp bạn xây kịch bản mà con số nó cho ra là con số đáng tin để ra quyết định. Hãy nhớ: performance engineer giỏi không phải người tạo ra số đẹp nhất, mà là người tạo ra số thật nhất.

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