Product Management
Đăng nhập
ESC

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

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

Test e-commerce flash sale (12.12)

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

Nếu bạn đã đi qua các bài trước — biết cách viết Thread Group trong JMeter, cấu hình stages trong k6, đặt Threshold làm SLA gate — thì bài này là nơi tất cả kiến thức đó bị đẩy đến giới hạn khắc nghiệt nhất. Flash sale không phải là "load test bình thường nhưng đông hơn". Nó là một loại quái vật riêng, với hình dạng traffic mà hầu hết các bài học nhập môn cố tình né tránh vì nó khó mô phỏng.

Hãy nghĩ về đêm 12.12 trên Shopee, hay 0h ngày Black Friday, hay khoảnh khắc mở bán vé concert Blackpink tại Hà Nội. Trong 60 giây trước giờ G, hệ thống chạy êm ru với vài nghìn người dùng đang lảng vảng. Rồi đúng 0h00, đồng hồ điểm, và trong vòng 3-5 giây, lượng request nhảy vọt từ 2.000 lên 50.000 mỗi giây. Không có đường cong tăng dần đẹp đẽ. Nó là một vách đá dựng đứng.

Với một Performance Engineer, đây là bài kiểm tra danh dự. Nếu hệ thống thương mại điện tử của bạn sập trong 30 giây đầu của flash sale, công ty mất doanh thu đỉnh điểm của cả năm, khách hàng chửi rủa trên mạng xã hội, và đối thủ được đà cười. Bài học này dạy bạn cách mô phỏng đúng cái vách đá đó — không phải một cái dốc — để đội kỹ thuật kịp vá lỗ hổng trước khi người dùng thật tìm ra chúng.

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

Traffic profile của flash sale — vách đá, không phải cái dốc

Điểm khác biệt lớn nhất giữa flash sale test và một load test thông thường nằm ở hình dạng đường traffic theo thời gian. Một load test bình thường tăng dần (ramp-up mượt), giữ ổn định, rồi giảm. Flash sale thì như thế này:

RPS
  ^
50k |          ______________
    |         /              \
    |        /                \
    |       /                  \
 2k |______/                    \______
    |
    +----------------------------------------> time
     idle   spike    sustained     drop
     (60s)  (3-5s)   (1-2 giờ)     (vài phút)

Bốn giai đoạn cần mô phỏng: idle (nền thấp trước giờ G), spike (bùng nổ gần như tức thời, 0 → peak trong vài giây), sustained (giữ đỉnh 1-2 giờ khi khách vẫn tranh nhau mua), và drop (hàng hết hoặc hết giờ, traffic về nền). Nếu test của bạn chỉ mô phỏng phần "sustained" — tức là ramp-up từ từ lên 50k rồi giữ — bạn đã bỏ lỡ đúng khoảnh khắc nguy hiểm nhất: cú sốc khởi động.

Vì sao cú spike lại giết hệ thống

Cú spike phá hủy hệ thống theo những cách mà tải-cao-đều-đặn không bao giờ làm được:

  • Connection pool cạn tức thì. Database connection pool cấu hình 200 kết nối. Ở tải đều, connection được tái sử dụng nhịp nhàng. Khi 50k request ập tới cùng lúc, tất cả tranh nhau xin connection, pool cạn trong mili-giây, request xếp hàng, timeout dây chuyền.
  • Auto-scaling không kịp. Kubernetes HPA hay AWS Auto Scaling phản ứng theo metric trung bình trong 30-60 giây. Cú spike xảy ra trong 3 giây — pod mới còn chưa kịp Ready thì trận đánh đã kết thúc.
  • Cache stampede (thundering herd). Cache của trang sản phẩm flash sale vừa hết hạn đúng lúc spike. 50k request cùng miss cache, cùng lao vào database để rebuild — database gục.
  • Cold start. Serverless function (Lambda) chưa warm. Hàng nghìn cold start đồng thời, mỗi cái thêm 1-3 giây latency.

Cái mà flash sale test đo — và không đo

Mục tiêu của flash sale test không chỉ là "hệ thống có sập không". Bạn cần trả lời cụ thể:

  • Ở đỉnh spike, error rate là bao nhiêu? Tỷ lệ request lỗi trong 10 giây đầu tiên thường tệ hơn nhiều so với phần sustained.
  • Recovery time — sau cú spike, mất bao lâu để p95 latency trở về ngưỡng chấp nhận được?
  • Có race condition không? Đây là đặc thù riêng của flash sale: oversell. Còn 100 iPhone mà bán ra 130 đơn vì logic trừ kho không atomic dưới tải đồng thời cực cao.
Điểm cuối cùng đặc biệt quan trọng và hay bị bỏ sót: flash sale test không chỉ đo tốc độ, nó phải kiểm chứng tính đúng đắn của tồn kho dưới đồng thời cao. Đây là chỗ mà một load test thuần "đo latency" trở nên vô dụng.

Mô hình open vs closed — vì sao spike cần open model

Trong k6 (bài 15 đã nói về executor), spike test bắt buộc dùng mô hình open — tức ramping-arrival-rate hoặc constant-arrival-rate, nơi bạn kiểm soát tốc độ đến của request (RPS), chứ không phải mô hình closed (ramping-vus) nơi bạn kiểm soát số VU. Lý do: người dùng thật không "đợi response xong rồi mới gửi request tiếp". Họ bấm F5 điên cuồng. Nếu server chậm, request thật vẫn tiếp tục dồn về — chứ không dừng lại chờ như mô hình closed. Chọn sai model, bạn sẽ vô tình "che giấu" chính vấn đề mình đang muốn phát hiện.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT "MuaNhanh" và cú sập 12.12

MuaNhanh (tên giả định, một sàn TMĐT tầm trung ở Việt Nam) chuẩn bị cho 12.12 với chương trình "iPhone giá 1.000đ — 50 suất, đúng 0h". Đội QA chạy load test truyền thống: ramp-up mượt lên 40.000 RPS trong 5 phút, giữ 30 phút. Kết quả đẹp: p95 = 800ms, error rate 0.3%. Cả đội tự tin.

Đêm 12.12, đúng 0h00, hệ thống nhận 55.000 RPS trong 4 giây. Trong 90 giây đầu, error rate leo lên 34%, trang trắng liên tục, và tệ hơn — database log cho thấy 72 đơn "iPhone 1.000đ" được tạo cho 50 suất hàng. Race condition trong logic UPDATE stock SET qty = qty - 1 WHERE qty > 0 không có lock, khiến nhiều request cùng đọc qty = 5 rồi cùng trừ.

Bài học: Load test ramp mượt đã ru ngủ cả đội. Nó không bao giờ tạo ra cú sốc connection pool và cũng không tạo đủ đồng thời để lộ race condition. Sau sự cố, họ viết lại test bằng k6 ramping-arrival-rate với stage đầu tiên target: 55000, duration: '4s', kèm một check kiểm tra tổng số đơn tạo ra không vượt quá tồn kho. Test mới lộ ngay cả hai vấn đề trong môi trường staging.

Ví dụ 2 — Tiki và chiến lược "virtual waiting room"

Một sàn lớn hơn (lấy cảm hứng từ cách Tiki và nhiều sàn xử lý sự kiện lớn) nhận ra rằng chống đỡ trực diện 50k RPS vào tầng nghiệp vụ là bất khả thi về chi phí. Họ triển khai phòng chờ ảo (virtual waiting room): khi spike ập tới, người dùng bị đưa vào một hàng đợi ở tầng edge (CDN/nginx), chỉ nhả ra tầng backend với tốc độ mà backend chịu được — ví dụ 5.000 req/s được "nhả" mỗi giây.

Với hệ thống này, flash sale test phải mô phỏng hai lớp: tải thô đập vào waiting room (55k RPS) và tải đã được điều tiết chảy vào backend (5k RPS ổn định). Đội performance dùng JMeter mô phỏng lớp ngoài để kiểm chứng waiting room không sập, và k6 với thread data giả lập token hàng đợi để test lớp trong.

Bài học: Không phải lúc nào giải pháp cũng là "scale backend to hơn". Đôi khi kiến trúc chống spike là điều tiết (throttling/queueing) ở tầng ngoài, và test của bạn phải phản ánh đúng kiến trúc đó — nếu không bạn sẽ tối ưu nhầm chỗ.

Ví dụ 3 — Bán vé concert và "thundering herd" lúc 10h00

Một nền tảng bán vé sự kiện ở Đông Nam Á mở bán vé show ca nhạc lúc 10h00 sáng. Khác flash sale TMĐT, ở đây gần như 100% traffic đổ vào đúng 1 endpoint duy nhất (/checkout/seat-map) trong 2 giây. Team để cache seat-map với TTL 5 giây. Đúng 10h00, cache vừa hết hạn, 80.000 request cùng miss, cùng lao vào query dựng lại seat-map từ Postgres — cache stampede kinh điển. Database CPU chạm 100%, latency vọt lên 12 giây.

Bài học: Flash sale test phải cố tình đặt endpoint trọng điểm vào đúng thời điểm cache expiry để lộ stampede. Giải pháp về sau là request coalescing (chỉ 1 request được phép rebuild cache, số còn lại chờ kết quả) — và test được viết lại để verify rằng dù 80k request cùng miss, số query xuống database vẫn ≈ 1.

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

Dưới đây là cách dựng một flash sale test hoàn chỉnh bằng k6 (khuyến nghị cho spike vì open model gọn gàng), kèm ghi chú JMeter tương đương.

Bước 1 — Xác định traffic profile bằng số thật

Đừng bịa con số. Lấy từ dữ liệu sự kiện trước, hoặc ước lượng từ marketing: bao nhiêu người dự kiến tham gia, tỷ lệ bấm mua, RPS đỉnh. Ví dụ: 500.000 người, 60% bấm vào trong 5 giây đầu, mỗi người ~3 request → đỉnh ~50.000 RPS.

Bước 2 — Viết test spike bằng open model

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

export const options = { scenarios: { flash_sale: { executor: 'ramping-arrival-rate', startRate: 2000, // nền idle: 2000 req/s timeUnit: '1s', preAllocatedVUs: 3000, // cấp sẵn VU đủ lớn maxVUs: 20000, stages: [ { target: 2000, duration: '60s' }, // idle trước giờ G { target: 50000, duration: '4s' }, // VÁCH ĐÁ: spike { target: 50000, duration: '30m' }, // sustained { target: 2000, duration: '2m' }, // drop ], }, }, thresholds: { http_req_failed: ['rate<0.05'], // SLA: lỗi < 5% http_req_duration: ['p(95)<1500'], // p95 < 1.5s 'http_req_duration{stage:spike}': ['p(95)<3000'], // nới cho spike }, };

Điểm mấu chốt là stage { target: 50000, duration: '4s' } — đây là cái vách đá. Và preAllocatedVUs phải đủ lớn: nếu k6 phải allocate VU giữa lúc spike, chính công cụ test của bạn thành bottleneck.

Bước 3 — Thêm kiểm chứng tính đúng đắn (không chỉ tốc độ)

export default function () {
  const res = http.post('https://staging.muanhanh.vn/api/flash-buy', 
    JSON.stringify({ sku: 'IP15-1K', qty: 1 }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  check(res, {
    'status is 200 or 409': (r) => r.status === 200 || r.status === 409,
    'no 500 error': (r) => r.status < 500,
  });
}

Lưu ý status 409 Conflict (hết hàng) là kết quả hợp lệ và mong muốn — nó chứng tỏ hệ thống từ chối đơn thừa đúng cách, chứ không phải lỗi. Sau khi test chạy xong, truy vấn database để xác nhận: số đơn thành công ≤ số hàng tồn. Đây là phần verify quan trọng nhất mà nhiều người quên.

Bước 4 — Với JMeter, dùng Synchronizing Timer hoặc Ultimate Thread Group

JMeter mặc định khó tạo vách đá tức thời. Hai cách:

  • Synchronizing Timer: gom N thread lại rồi thả cùng lúc, tạo burst thật sự đồng thời.
  • Ultimate Thread Group (plugin): vẽ trực tiếp đường traffic có ramp-up = 0 để mô phỏng bậc thang dựng đứng.

Bước 5 — Chạy phân tán nếu 1 máy không đủ

50.000 RPS thường vượt sức một máy load generator. Với k6, dùng k6 Cloud hoặc chạy nhiều instance; với JMeter, dùng distributed testing master/slave (bài 23). Luôn theo dõi CPU của chính máy generate tải — nếu nó > 80%, số liệu của bạn không đáng tin.

Bước 6 — Quan sát cả hai phía

Trong lúc test, mở dashboard hệ thống đích (Grafana — bài 22): DB connection pool usage, cache hit rate, CPU auto-scaling, queue depth. Cú spike thú vị không nằm ở con số k6 báo, mà ở chỗ nó làm gì với hệ thống bên kia.

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

  • Dùng ramp-up mượt thay cho vách đá. Đây là lỗi số một, đúng như ví dụ MuaNhanh. Ramp mượt không tạo ra cú sốc connection pool và không lộ race condition. Spike phải là bậc thang gần như thẳng đứng.
  • Dùng closed model (ramping-vus) cho spike. Khi server chậm, VU "đứng chờ" response, vô tình giảm tải — che giấu vấn đề. Spike cần open model (arrival-rate).
  • Quên preAllocatedVUs / maxVUs. k6 dành thời gian allocate VU giữa spike, làm méo kết quả. Cấp sẵn dư ra.
  • Chỉ đo latency, quên đo tính đúng đắn. Flash sale đặc trưng ở race condition tồn kho. Luôn verify số đơn ≤ số hàng sau test.
  • Coi status 4xx là "lỗi hệ thống". 429 Too Many Requests409 Conflict là hành vi bảo vệ đúng đắn. Đừng để chúng làm hỏng threshold http_req_failed một cách oan uổng — hãy tách metric riêng.
  • Test trên production trong giờ thật. Flash sale test phải chạy trên staging giống production, hoặc production ngoài giờ với dữ liệu cô lập. Đừng bao giờ bắn 50k RPS vào production đang có khách thật.
  • Bỏ qua cache expiry timing. Cố tình canh cho endpoint trọng điểm hết cache đúng lúc spike để lộ stampede — nếu không, test qua nhưng đời thật sập.
  • Mẹo — test cả cú "double spike". Đôi khi sau flash sale suất 1 hết, hệ thống mở suất 2 sau 5 phút → spike lần hai chồng lên hệ thống chưa hồi phục. Mô phỏng cả kịch bản này.

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

  • Dựng traffic profile. Cho một flash sale giả định: 300.000 khách, 70% bấm mua trong 3 giây đầu, mỗi khách 4 request. Tính RPS đỉnh và vẽ biểu đồ 4 giai đoạn (idle/spike/sustained/drop) trên giấy.
  • Viết k6 spike test với ramping-arrival-rate mô phỏng đúng con số bạn vừa tính, spike đạt đỉnh trong ≤ 5 giây, sustained 20 phút. Đặt threshold: error rate < 5%, p95 < 2s (nới lên 4s riêng cho stage spike).
  • Thêm kiểm chứng tồn kho. Với một endpoint /flash-buy (dùng một mock server hoặc httpbin để tập), thêm check phân biệt 200/409/500 và một metric custom (Counter) đếm số đơn thành công. Sau test, so sánh với "tồn kho" giả định.
  • So sánh open vs closed. Chạy cùng kịch bản một lần bằng ramping-arrival-rate, một lần bằng ramping-vus. Ghi lại chênh lệch RPS thực tế và error rate. Viết 3-4 câu giải thích vì sao closed model "che giấu" vấn đề.
  • Thiết kế double spike (nâng cao). Thêm một stage spike thứ hai vào 5 phút sau spike đầu, khi hệ thống chưa hồi phục hoàn toàn. Quan sát recovery time có xấu đi không.

Tóm tắt

Flash sale test là dạng performance test khắc nghiệt và đặc thù nhất trong TMĐT: traffic có hình vách đá — 0 lên đỉnh trong vài giây, giữ 1-2 giờ, rồi rơi. Cú spike, chứ không phải tải cao kéo dài, mới là thứ giết hệ thống: cạn connection pool, auto-scaling không kịp, cache stampede, cold start. Vì thế bạn phải mô phỏng đúng bậc thang dựng đứng bằng open model (ramping-arrival-rate trong k6 hoặc Synchronizing/Ultimate Thread Group trong JMeter), cấp sẵn đủ VU, và đặt threshold nới riêng cho giai đoạn spike.

Điểm khác biệt cốt tử: flash sale test không chỉ đo latency mà còn phải kiểm chứng tính đúng đắn của tồn kho dưới đồng thời cao — chống oversell và race condition, coi 409/429 là hành vi bảo vệ đúng đắn chứ không phải lỗi. Ba câu chuyện MuaNhanh (race condition ẩn dưới ramp mượt), waiting room (điều tiết ở tầng edge), và bán vé concert (cache stampede) cho thấy: dựng đúng hình dạng cú sốc và verify đúng cái cần verify quan trọng hơn nhiều so với việc chỉ bắn thật nhiều request. Ở bài tiếp theo, bạn sẽ tập trung sâu vào việc test các cổng thanh toán Việt Nam dưới tải — mảnh ghép còn lại của một flash sale thật sự.

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