Menu
ESC

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

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

Đang tải...

Performance test for AI/ML APIs

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

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

Suốt cả khóa học này, bạn đã luyện tay với các API kiểu CRUD: đăng nhập, thêm sản phẩm vào giỏ, thanh toán, tra cứu đơn hàng. Những endpoint đó thường trả về trong vài chục mili-giây, tiêu tốn rất ít CPU, và scale theo chiều ngang một cách khá tuyến tính — cứ thêm pod là chịu thêm tải. Nhưng thế giới năm 2026 đã khác. Gần như mọi sản phẩm phần mềm đều đang gắn thêm một endpoint dạng /chat, /embeddings, /classify, /recommend hay /generate-image. Đằng sau những endpoint đó không phải là một câu SELECT chạy trong 5ms, mà là một mô hình machine learning (ML) đang chạy inference — quá trình mô hình "suy nghĩ" để cho ra kết quả.

Và inference có một tính cách hoàn toàn khác với CRUD. Latency của nó không phải vài chục mili-giây mà là hàng trăm đến hàng nghìn mili-giây. Nó không I/O-bound (chờ đĩa, chờ mạng) mà compute-bound — nghẽn ở GPU hoặc CPU. Nó có hiện tượng cold start khủng khiếp: lần gọi đầu tiên sau khi mô hình chưa được nạp có thể mất 30–60 giây chỉ để load model vào bộ nhớ GPU. Và với các mô hình sinh văn bản (LLM), thời gian phản hồi còn phụ thuộc vào độ dài output — trả về 50 token nhanh hơn hẳn 2000 token.

Nếu bạn mang tư duy load test CRUD sang test một AI/ML API, bạn sẽ đọc sai gần như mọi con số. Bạn sẽ ngạc nhiên vì sao throughput thấp thảm hại, vì sao p99 latency nhảy loạn xạ, vì sao thêm server chẳng giúp gì. Bài này dạy bạn cách nhìn đúng, đo đúng, và viết kịch bản test đúng cho lớp API đặc biệt này.

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

1. Inference là compute-bound, không phải I/O-bound

Một API CRUD điển hình dành phần lớn thời gian chờ: chờ database, chờ cache, chờ service khác. CPU của nó rảnh rỗi, nên bạn có thể nhồi hàng nghìn request đồng thời trên một core. Ngược lại, một request inference chiếm dụng GPU (hoặc nhiều core CPU) trong toàn bộ thời gian xử lý. Điều này có một hệ quả trực tiếp: concurrency không miễn phí. Một GPU chỉ xử lý được một số lượng batch nhất định cùng lúc; vượt qua ngưỡng đó, request không chạy song song mà xếp hàng chờ. Đây là lý do vì sao khi tăng số virtual user (VU), latency của ML API tăng gần như tuyến tính chứ không phẳng như CRUD.

2. Batching thay đổi hoàn toàn đường cong hiệu năng

Các server inference hiện đại (vLLM, TensorRT-LLM, Triton Inference Server, TorchServe) đều dùng dynamic batching: gom nhiều request đến trong một khoảng thời gian ngắn thành một batch để đưa vào GPU cùng lúc, tận dụng tính song song của phần cứng. Hệ quả rất phản trực giác: throughput của ML API thường TĂNG khi concurrency tăng, tới một điểm bão hòa. Nghĩa là gửi 1 request/giây có thể cho latency thấp nhưng throughput tệ; gửi 50 request đồng thời lại cho throughput cao hơn nhiều nhờ batch đầy. Nhiệm vụ của performance engineer là tìm ra điểm ngọt (sweet spot) giữa latency và throughput, chứ không đơn thuần "càng ít VU càng nhanh".

3. Cold start và warm-up

Mô hình ML nặng hàng GB. Khi container mới khởi động, hoặc khi một serverless function (AWS SageMaker Serverless, Google Cloud Run với GPU) scale từ 0, mô hình phải được nạp từ đĩa/mạng vào VRAM. Quá trình này — cold start — có thể mất 20–90 giây. Trong test, nếu bạn không tách riêng giai đoạn warm-up, những request đầu tiên sẽ có latency dị thường kéo p99 lên trời và làm hỏng toàn bộ phân tích.

4. Chi phí thực = tiền, không chỉ là server

Với ML API, đặc biệt khi bạn gọi API bên thứ ba (OpenAI, Anthropic, Google Gemini, hay các nhà cung cấp Việt Nam như VinAI, Zalo AI, FPT.AI), mỗi request tốn tiền thật theo token. Một load test 100.000 request lên GPT-4 có thể tốn vài trăm đến vài nghìn USD. Đây là điểm khác biệt sống còn: bạn không thể "cứ bơm tải cho đến khi sập" như với API nội bộ. Phải tính chi phí trước, và thường phải test trên môi trường staging với mô hình rẻ hơn hoặc self-hosted.

5. Các metric đặc thù cho LLM

Ngoài latency/throughput/error rate quen thuộc, ML API — nhất là LLM streaming — cần đo thêm:

  • TTFT (Time To First Token): thời gian từ lúc gửi request đến khi nhận token đầu tiên. Đây là metric trải nghiệm người dùng quan trọng nhất cho chatbot.
  • TPOT / ITL (Time Per Output Token / Inter-Token Latency): thời gian trung bình giữa các token — quyết định tốc độ chữ "chạy ra" trên màn hình.
  • Tokens/second: throughput thực của mô hình, đo bằng token chứ không phải request.
  • Cost per request: chi phí trung bình mỗi lệnh gọi, dựa trên input + output token.

Tình huống thực tế

Tình huống 1 — Startup fintech TP.HCM và cái bẫy "thêm server không giúp gì"

Một startup fintech ở Quận 1 xây dựng tính năng chatbot tư vấn khoản vay, dùng một LLM open-source (Llama 3 8B) self-host trên một server có 2 GPU A10G, phục vụ qua vLLM. QA lead — tên Trâm — chạy load test bằng k6 với 200 VU và thấy p95 latency vọt lên 8 giây, error rate 0%. Phản xạ đầu tiên của team dev là "thêm một GPU nữa".

Nhưng khi Trâm nhìn kỹ, cô phát hiện GPU utilization đã ở mức 98% và batch size trung bình đã đầy. Vấn đề không phải thiếu tài nguyên mà là request bị xếp hàng: 200 VU vượt xa capacity của mô hình ở độ dài prompt đó. Cô chạy lại một loạt test bậc thang (10, 25, 50, 100, 150 VU) và vẽ đường cong latency vs throughput. Kết quả: tại 50 VU, throughput đạt gần đỉnh (khoảng 42 request/giây) với p95 chỉ 2,1 giây; vượt 50 VU thì throughput tăng không đáng kể còn latency thì leo dốc.

Bài học: với ML API, "thêm server" chỉ đúng khi bạn đã ở trên đường bão hòa của phần cứng hiện tại. Trước tiên phải tìm điểm ngọt concurrency. Team kết luận: một server 2-GPU chịu được khoảng 50 người dùng đồng thời ở mức trải nghiệm chấp nhận được, và họ đặt rate limit ở tầng gateway thay vì đổ tiền mua GPU thứ ba.

Tình huống 2 — Sàn TMĐT và hóa đơn OpenAI 1.900 USD sau một đêm

Một sàn thương mại điện tử vừa tích hợp tính năng "tìm sản phẩm bằng ngôn ngữ tự nhiên", gọi thẳng API text-embeddinggpt-4o-mini của OpenAI. Trước ngày sale, đội QA muốn "test cho chắc", viết một script JMeter bơm 100.000 request lên môi trường... production, vì embedding index nằm ở production. Sáng hôm sau, tài khoản OpenAI báo hóa đơn 1.900 USD và bị chạm rate limit của tổ chức, khiến tính năng thật bị lỗi trong giờ cao điểm.

Nguyên nhân: họ quên rằng mỗi request là tiền thật, và họ không mock nhà cung cấp bên thứ ba. Cách làm đúng lẽ ra là: (1) test tầng ứng dụng của mình bằng một mock server trả về response giả với độ trễ mô phỏng (dùng WireMock hoặc một endpoint Flask giả), để kiểm tra khả năng chịu tải của chính hệ thống mình — connection pooling, timeout, retry, hàng đợi; (2) chỉ chạy một smoke test rất nhỏ (vài trăm request) lên API thật để xác nhận độ trễ và rate limit thực tế; (3) tính trước ngân sách token.

Bài học: performance test cho ML API bên thứ ba phải tách bạch "test hệ thống của tôi" (mock nhà cung cấp) và "đo nhà cung cấp" (smoke test có kiểm soát ngân sách). Không bao giờ bơm tải lớn lên API tính phí ở production.

Tình huống 3 — Ngân hàng số và cold start giết chết p99

Một ngân hàng số ở Hà Nội triển khai mô hình chấm điểm gian lận (fraud scoring) trên SageMaker Serverless để tiết kiệm chi phí — scale về 0 khi không có traffic. Test tải ban đầu cho kết quả đẹp: p50 latency 180ms. Nhưng khi lên production, khách hàng phàn nàn giao dịch đầu ngày (sau đêm không traffic) bị treo 40–50 giây.

Đội performance chạy lại test có chủ đích mô phỏng pattern thật: để hệ thống idle 15 phút, rồi bắn một request. TTFT là 47 giây — chính là cold start nạp mô hình. Họ nhận ra test cũ đã vô tình "làm ấm" endpoint trước khi đo, nên không bao giờ thấy hiện tượng này. Giải pháp: đặt provisioned concurrency (giữ tối thiểu 1 instance ấm) trong giờ giao dịch, và thiết kế test riêng cho kịch bản cold start để đo đúng cái giá của việc scale-to-zero.

Bài học: nếu không tách warm-up ra khỏi phép đo, cold start sẽ ẩn mình trong p99 và chỉ lộ ra trên production — đúng lúc gây thiệt hại nhất.

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

Dưới đây là quy trình thực hành để performance test một AI/ML API. Ta minh họa bằng k6 vì nó xử lý streaming và metric tùy biến gọn hơn.

Bước 1 — Phân loại API cần test. Trả lời ba câu: Mô hình self-host hay gọi bên thứ ba (tiền)? Response có streaming không (cần TTFT)? Có scale-to-zero không (cần test cold start)? Câu trả lời quyết định thiết kế test.

Bước 2 — Chuẩn bị test data thực tế. Prompt/input phải phản ánh phân bố thật: độ dài input đa dạng, không dùng cùng một prompt lặp lại (dễ bị cache đánh lừa). Chuẩn bị một file CSV/JSON gồm nhiều prompt với độ dài khác nhau.

Bước 3 — Thiết kế warm-up riêng. Trước giai đoạn đo chính, cho một phase ramp nhẹ 30–60 giây để nạp mô hình và làm ấm batch. Đánh tag các request warm-up để loại chúng khỏi phân tích.

Bước 4 — Định nghĩa metric tùy biến cho TTFT và token. Trong k6, dùng Trend để đo TTFT và tokens/second thay vì chỉ dựa vào http_req_duration:

import http from 'k6/http';
import { Trend, Counter } from 'k6/metrics';
import { check } from 'k6';

const ttft = new Trend('llm_ttft', true); const tokensPerReq = new Counter('llm_output_tokens');

export const options = { scenarios: { warmup: { executor: 'constant-vus', vus: 2, duration: '30s', tags: { phase: 'warmup' }, gracefulStop: '5s', }, steady: { executor: 'ramping-vus', startTime: '30s', startVUs: 0, stages: [ { duration: '1m', target: 25 }, { duration: '3m', target: 25 }, { duration: '1m', target: 0 }, ], tags: { phase: 'steady' }, }, }, thresholds: { 'llm_ttft{phase:steady}': ['p(95)<2000'], // SLA: token đầu < 2s 'http_req_failed{phase:steady}': ['rate<0.01'], }, };

export default function () { const start = Date.now(); const res = http.post('https://api.internal/v1/chat', JSON.stringify({ prompt: pickPrompt(), max_tokens: 256 }), { headers: { 'Content-Type': 'application/json' }, timeout: '120s' } ); // Với non-streaming, xấp xỉ TTFT bằng tổng thời gian; với streaming, đo mốc token đầu. ttft.add(Date.now() - start); const body = res.json(); if (body && body.usage) tokensPerReq.add(body.usage.completion_tokens); check(res, { 'status 200': (r) => r.status === 200 }); }

Bước 5 — Chạy test bậc thang để tìm điểm ngọt. Thay vì một lần bơm tải lớn, chạy nhiều mức concurrency (10, 25, 50, 100) và ghi lại throughput cùng p95 latency ở mỗi mức. Vẽ hai đường lên cùng một biểu đồ. Điểm ngọt là nơi throughput gần chạm đỉnh còn latency chưa leo dốc.

Bước 6 — Đối chiếu với GPU/CPU utilization. Song song với k6, giám sát GPU (bằng nvidia-smi, DCGM exporter) và CPU. Kết luận "nghẽn ở đâu" chỉ đáng tin khi bạn nhìn được cả tải phía server, không chỉ latency phía client.

Bước 7 — Test cold start riêng. Nếu có scale-to-zero, viết một test riêng: để idle đủ lâu cho hệ thống scale về 0, rồi bắn một request và đo TTFT lạnh. So sánh với TTFT ấm để lượng hóa cái giá của cold start.

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

  • Vô tình đo trên cache. Nếu lặp lại cùng một prompt, nhiều server (hoặc CDN, hoặc semantic cache) trả kết quả cache, cho latency đẹp giả tạo. Luôn dùng data đa dạng, và thêm một tham số ngẫu nhiên nếu cần vô hiệu hóa cache khi mục tiêu là đo inference thật.
  • Quên loại warm-up khỏi phân tích. Cold start và batch chưa ấm sẽ kéo p99 lên. Luôn tách phase và tag rõ ràng.
  • Đọc throughput theo request thay vì token. Với LLM, một request trả 50 token và một request trả 2000 token là hai gánh nặng hoàn toàn khác nhau. Đo tokens/second mới phản ánh đúng tải GPU.
  • Đặt timeout quá ngắn. ML API có thể mất nhiều giây; timeout mặc định 30s của nhiều tool sẽ cắt oan request và thổi phồng error rate. Đặt timeout 60–120s và phân biệt rõ "lỗi thật" với "chưa xong".
  • Bơm tải lớn lên API tính phí ở production. Như tình huống 2 — mock nhà cung cấp bên thứ ba, chỉ smoke test API thật với ngân sách token định trước.
  • Bỏ qua max_tokens khi so sánh. Muốn so sánh công bằng giữa các lần test hoặc giữa hai mô hình, phải cố định max_tokens (hoặc đo tokens/second), vì độ dài output ảnh hưởng trực tiếp latency.
  • Mẹo — dùng constant-arrival-rate cho SLA theo RPS. Nếu mục tiêu là "hệ thống chịu được 30 request/giây", dùng executor constant-arrival-rate của k6 thay vì constant-vus; nó giữ đúng tốc độ đến bất kể latency, giúp bạn thấy hàng đợi phình ra khi vượt capacity.
  • Mẹo — luôn kèm dashboard GPU. Không có utilization GPU/CPU, mọi kết luận về "nghẽn ở đâu" chỉ là phỏng đoán.

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

  • Dựng mock ML API. Viết một endpoint đơn giản (Flask/FastAPI) trả JSON sau một khoảng sleep ngẫu nhiên 100–800ms để giả lập inference, và thêm cơ chế cố ý "cold start" 10 giây cho request đầu tiên sau 60 giây idle. Đây là sân tập an toàn, không tốn tiền.
  • Viết script k6 tìm điểm ngọt. Với mock ở trên, chạy test ở 5 mức concurrency (5, 15, 30, 60, 100 VU), ghi lại throughput và p95 latency mỗi mức, rồi xác định điểm ngọt. Trình bày bằng một bảng nhỏ.
  • Đo cold start. Viết một test riêng: để idle 70 giây, rồi bắn 1 request, ghi lại TTFT lạnh. Lặp lại nhưng warm-up trước, so sánh hai con số và giải thích chênh lệch.
  • Metric token. Cho mock trả về trường usage.completion_tokens ngẫu nhiên 50–500. Bổ sung Counter và tính tokens/second cho toàn bộ test. Nhận xét vì sao đo theo request có thể gây hiểu lầm.
  • (Nâng cao) Tính chi phí. Giả sử bạn test một API thật giá 0,15 USD / 1M input token và 0,60 USD / 1M output token. Với kịch bản 10.000 request, mỗi request ~300 input + 200 output token, hãy ước tính chi phí trước khi chạy. Đây là bước bắt buộc trước mọi test lên API tính phí.

Tóm tắt

AI/ML API không phải là CRUD API "chậm hơn" — nó là một lớp workload có bản chất khác: compute-bound (nghẽn GPU/CPU chứ không phải I/O), hưởng lợi từ batching nên throughput tăng theo concurrency tới điểm bão hòa, chịu cold start hàng chục giây khi scale-to-zero, và với các nhà cung cấp bên thứ ba thì mỗi request là tiền thật. Vì vậy, cách test cũng phải khác: tìm điểm ngọt concurrency thay vì bơm tải mù, đo thêm các metric đặc thù (TTFT, TPOT, tokens/second, cost per request), tách warm-up khỏi phép đo, mock nhà cung cấp khi test hệ thống của mình và chỉ smoke test có ngân sách khi đo API thật, đặt timeout đủ dài, và luôn đối chiếu latency phía client với GPU/CPU utilization phía server. Ba tình huống — startup fintech với bẫy "thêm server", sàn TMĐT với hóa đơn 1.900 USD, và ngân hàng số bị cold start giết p99 — đều minh họa cùng một điều: mang tư duy CRUD sang test ML API sẽ dẫn đến kết luận sai. Nắm đúng bản chất inference, bạn sẽ đọc đúng con số và đưa ra khuyến nghị đáng tin.