Menu
ESC

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

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

Đang tải...

Bottleneck Analysis — đâu là điểm nghẽn?

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

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

Bạn chạy một bài load test, biểu đồ hiện lên: throughput đạt 1.200 request/giây rồi… đứng im, không tăng thêm dù bạn đổ thêm virtual user. Latency bắt đầu leo dốc, error rate nhích lên. Câu hỏi lúc này không phải "hệ thống chịu được bao nhiêu" — bạn đã biết con số đó rồi. Câu hỏi thật sự là: cái gì đang giữ hệ thống lại ở mức 1.200 req/s? Đó chính là bottleneck — điểm nghẽn.

Đây là kỹ năng phân biệt một người "biết bấm chạy tool" với một Performance Engineer thực thụ. Ai cũng dựng được Thread Group trong JMeter hay viết stages trong k6. Nhưng khi con số xấu hiện ra, chỉ người biết đọc ngược từ triệu chứng về nguyên nhân mới đưa ra được kết luận có giá trị cho đội dev và ops. Một report chỉ nói "hệ thống chậm khi 500 user" là vô dụng. Một report nói "connection pool tới database bị cạn ở 480 user, mỗi request phải chờ trung bình 800ms để lấy connection" mới là thứ khiến người ta ra tay sửa.

Bài này dạy bạn tư duy và phương pháp để trả lời câu hỏi "đâu là điểm nghẽn?". Chúng ta sẽ không đi sâu vào cách đọc từng chỉ số (bài 6 đã lo phần metrics, bài 24 lo phần đọc kết quả thô), cũng không viết findings report (bài 38). Ở đây, trọng tâm là truy tìm nguyên nhân gốc: đi qua từng lớp của hệ thống, dùng bằng chứng để loại trừ, và xác định lớp nào đang là nút thắt cổ chai.

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

Bottleneck là gì — định nghĩa đúng

Bottleneck (điểm nghẽn, hay "nút thắt cổ chai") là thành phần giới hạn throughput của toàn hệ thống. Nguyên lý quan trọng nhất bạn phải khắc cốt ghi tâm: một hệ thống chỉ nhanh bằng thành phần chậm nhất của nó. Giống như một chuỗi ống nước — dòng chảy bị chặn ở đoạn ống hẹp nhất, dù mọi đoạn khác rộng bao nhiêu cũng vô nghĩa.

Hệ quả thực tế: tại một thời điểm, hệ thống của bạn chỉ có một bottleneck chi phối. Bạn sửa nó, throughput tăng lên, rồi một thành phần khác trở thành bottleneck mới. Đây là quá trình lặp — "đuổi bottleneck". Đừng cố sửa mười thứ cùng lúc; hãy tìm ra thứ đang giới hạn ngay bây giờ.

Bản đồ các lớp cần soi

Mọi request đi qua một chuỗi các lớp. Bottleneck có thể nằm ở bất kỳ đâu trên chuỗi này:

[Client — k6 / JMeter]          ← chính máy sinh tải có thể là điểm nghẽn!
        ↓
[Network / Load Balancer / CDN] ← băng thông, số connection, SSL handshake
        ↓
[Web / App Server]              ← CPU, thread pool, worker process, GC
        ↓
[Application Code]              ← lock, xử lý đồng bộ, thuật toán tệ
        ↓
[Cache — Redis / Memcached]     ← cache miss, hot key, kết nối
        ↓
[Database]                      ← connection pool, slow query, lock, disk I/O
        ↓
[External services / API bên thứ 3] ← rate limit, độ trễ mạng ngoài

Nhiệm vụ của bạn là đi dọc chuỗi này và hỏi từng lớp: "Có phải mày đang no không?". "No" ở đây nghĩa là saturation — một tài nguyên nào đó đã dùng hết công suất (CPU 100%, pool cạn, disk đọc ghi tối đa).

Bốn nhóm tài nguyên gốc (mô hình USE)

Dù bottleneck nằm ở lớp nào, gốc rễ luôn quy về một trong bốn loại tài nguyên vật lý/logic. Cách nhớ hay nhất là mô hình USE của Brendan Gregg — với mỗi tài nguyên, kiểm tra ba thứ:

  • Utilization (mức sử dụng): tài nguyên bận bao nhiêu phần trăm thời gian? CPU 95% là dấu hiệu.
  • Saturation (mức quá tải): có bao nhiêu việc đang xếp hàng chờ? Run queue của CPU dài, connection đang chờ trong pool.
  • Errors (lỗi): có timeout, connection refused, packet drop không?
Bốn tài nguyên gốc là: CPU, Memory, Disk I/O, Network. Cộng thêm các "tài nguyên logic" thường bị quên nhưng hay gây nghẽn nhất: connection pool, thread pool, file descriptor, lock.

Chữ ký của từng loại bottleneck

Đây là phần quý giá nhất — mỗi loại điểm nghẽn để lại một "chữ ký" (signature) nhận biết được từ dữ liệu:

  • CPU-bound: CPU app server cắm 90–100%, latency tăng đều theo tải, throughput đạt trần. Nhìn vào là biết code hoặc số worker cần tối ưu.
  • Connection pool cạn: throughput đứng ở một mức phẳng lì, latency có một thành phần "chờ" lớn và ổn định (thời gian chờ lấy connection), nhưng CPU cả app lẫn DB đều thấp. Đây là bẫy kinh điển: mọi thứ đều "rảnh" mà hệ thống vẫn chậm.
  • Database slow query / lock: latency có "đuôi dài" (p99 cao gấp nhiều lần p50), DB CPU hoặc disk I/O cao, số active connection ở DB tăng vọt.
  • Memory / GC: latency có những "gai" (spike) theo chu kỳ đúng lúc Garbage Collection chạy; memory tăng dần rồi hệ thống khựng.
  • Network / băng thông: throughput tính theo byte đạt trần (ví dụ 1 Gbps) trong khi throughput tính theo request vẫn thấp — thường gặp khi trả về payload lớn.

Tình huống thực tế

Ví dụ 1 — Tiki và cái pool 20 connection

Một đội QA giả định tại một sàn TMĐT lớn kiểu Tiki chạy load test cho API danh mục sản phẩm. Kịch bản: tăng dần từ 100 lên 1.000 virtual user bằng k6. Kết quả gây bối rối — throughput leo đẹp tới khoảng 480 user rồi đứng phắt ở ~950 req/s, không nhúc nhích dù đổ tới 1.000 user. Latency p95 nhảy từ 120ms lên 2.100ms.

Phản xạ đầu tiên của đội là đổ lỗi cho database "chậm". Nhưng khi mở dashboard hạ tầng ra soi: CPU app server chỉ 35%, CPU database chỉ 22%, disk I/O gần như bằng không. Mọi tài nguyên vật lý đều rảnh rỗi. Nếu chỉ nhìn "hệ thống chậm" thì vô lý — máy nào cũng nhàn.

Manh mối nằm ở phân tích latency. Họ bóc thời gian một request thành các phần và phát hiện: thời gian xử lý query thật chỉ 15ms, nhưng có một khoảng chờ 1.900ms trước khi query bắt đầu. Chờ cái gì? Chờ lấy một connection từ pool. Kiểm tra cấu hình: connection pool của app tới DB được đặt cứng max = 20. Với 480+ user gọi đồng thời, 20 connection bị giành giật, phần còn lại xếp hàng dài dằng dặc.

Bài học: throughput phẳng lì + mọi CPU đều thấp + latency có thành phần "chờ" lớn = cạn tài nguyên logic (pool), không phải tài nguyên vật lý. Họ tăng pool lên 100, throughput vọt lên 3.400 req/s, và bottleneck chuyển sang… database CPU. Đúng như lý thuyết: sửa xong nút này, nút khác lộ ra.

Ví dụ 2 — Fintech VN và cây query thiếu index

Một công ty fintech ở TP.HCM test API tra cứu lịch sử giao dịch trước mùa cao điểm. Ở tải trung bình mọi thứ ổn, nhưng khi vượt 300 user, họ thấy một chữ ký rất đặc trưng: latency p50 chỉ 90ms nhưng p99 lên tới 4.500ms — cái "đuôi dài" kinh điển. Nghĩa là đa số request vẫn nhanh, nhưng một nhóm nhỏ bị kéo lê thê.

Đi dọc bản đồ lớp: app CPU 60% (cao nhưng chưa max), và đáng chú ý — database CPU cắm 98%, disk read tăng vọt. Đây là chữ ký DB-bound rõ ràng. Họ bật slow query log của MySQL và bắt được thủ phạm: một câu query lọc giao dịch theo user_id và khoảng thời gian, chạy full table scan trên bảng 40 triệu dòng vì thiếu composite index.

Vì sao p50 vẫn nhanh mà p99 thảm? Vì những user có ít giao dịch thì scan nhanh, còn vài "khách VIP" có hàng chục nghìn giao dịch khiến query quét lê thê, đúng lúc trùng tải cao thì DB nghẽn hẳn. Thêm một composite index (user_id, created_at), p99 rơi từ 4.500ms xuống 210ms.

Bài học: đuôi dài của latency (p99 >> p50) cộng DB CPU/disk cao = slow query. Đừng bao giờ chỉ nhìn con số trung bình — trung bình che giấu chính những request đang giết trải nghiệm người dùng thật.

Ví dụ 3 — Máy sinh tải mới là bottleneck

Một startup giao đồ ăn ở Đông Nam Á test API đặt đơn. Họ chạy JMeter từ một chiếc laptop, cấu hình 2.000 thread, và báo cáo hoảng hốt: "Hệ thống chỉ chịu được 600 req/s, quá tệ!". May thay có người tỉnh táo hỏi lại: máy chạy JMeter thế nào?

Kiểm tra chính chiếc laptop chạy test: CPU của JMeter 100%, JVM liên tục Full GC. Vấn đề không nằm ở hệ thống được test mà nằm ở client sinh tải. JMeter chạy GUI mode với hàng đống Listener bật sẵn đang ngốn hết RAM và CPU của máy tester, khiến nó không đủ sức bắn request đi. Con số "600 req/s" không phải giới hạn của server — nó là giới hạn của cái laptop.

Họ chuyển sang non-GUI mode, tắt bớt Listener, và chạy phân tán từ vài máy. Server thật ra chịu được 4.100 req/s. Bài học sống còn: luôn kiểm tra máy sinh tải trước tiên. Nếu client no, mọi kết luận về server đều sai. Đây là lỗi phổ biến đến mức nó phải là bước số 0 trong mọi cuộc điều tra bottleneck.

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

Đây là quy trình có kỷ luật để truy tìm điểm nghẽn. Làm đúng thứ tự.

Bước 0 — Kiểm tra máy sinh tải (loại trừ client). Trước khi tin bất cứ con số nào, xác nhận CPU/RAM của máy chạy k6/JMeter chưa bị no. Nếu client đã max, dừng lại, mở rộng client hoặc chuyển non-GUI/phân tán rồi chạy lại. Không có bước này, mọi thứ phía sau là xây nhà trên cát.

Bước 1 — Xác định điểm bão hòa (saturation point). Chạy một bài load tăng dần và tìm ngưỡng nơi throughput ngừng tăng dù bạn thêm user, còn latency bắt đầu vọt lên. Đây chính là "cái knee" của biểu đồ — nơi hệ thống bắt đầu nghẽn. Ghi lại con số user và throughput tại điểm đó.

Bước 2 — Đọc chữ ký từ đường cong. Nhìn hình dạng: throughput phẳng lì hay lao dốc? Latency tăng đều hay có gai chu kỳ? p99 có tách xa p50 không? Error xuất hiện ở loại nào (timeout, 5xx, connection refused)? Mỗi hình dạng gợi ý một nhóm nguyên nhân, dựa vào bảng chữ ký ở phần khái niệm.

Bước 3 — Đi dọc bản đồ lớp, thu thập metric hạ tầng. Đây là mấu chốt: bạn phải có dữ liệu server-side, không chỉ số liệu client. Với mỗi lớp (LB → app → cache → DB), thu ba thứ theo mô hình USE: utilization, saturation, errors. Cụ thể cần: CPU và memory của app và DB; số active connection ở DB và độ dài hàng chờ connection pool; disk I/O; slow query log; số lỗi ở load balancer.

Bước 4 — Định vị lớp no đầu tiên. Đi từ ngoài vào trong, tìm lớp đầu tiên chạm trần tài nguyên. Đó là ứng viên bottleneck. Cẩn thận phân biệt "no vật lý" (CPU/disk max) với "no logic" (pool/thread cạn dù CPU thấp).

Bước 5 — Xác nhận bằng thí nghiệm loại trừ. Đừng dừng ở phỏng đoán — hãy chứng minh. Nếu nghi pool: tăng pool và chạy lại, xem throughput có nhảy không. Nếu nghi query: thêm index trên môi trường staging rồi đo lại. Nếu bottleneck di chuyển hoặc biến mất khi bạn tác động đúng chỗ, giả thuyết được xác nhận. Đây là tư duy khoa học: một thay đổi, một phép đo.

Bước 6 — Lặp lại. Sau khi hạ được bottleneck, throughput tăng và một bottleneck mới xuất hiện. Quay lại Bước 1. Dừng khi hệ thống đạt mục tiêu năng lực đề ra, chứ không phải khi hết bottleneck (không bao giờ hết).

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

Lỗi 1 — Chỉ nhìn số liệu phía client. k6 và JMeter cho bạn latency, throughput, error rate — nhưng chúng chỉ nói "hệ thống chậm", không nói "chậm ở đâu". Không có metric server-side (CPU, connection, slow log), bạn chỉ đoán mò. Mẹo: luôn dựng monitoring hạ tầng song song trước khi test.

Lỗi 2 — Tin vào con số trung bình. Average latency là kẻ nói dối. Một hệ thống có p50 = 80ms trông tuyệt vời, nhưng nếu p99 = 5.000ms thì 1% người dùng đang có trải nghiệm thảm họa — và 1% của một sàn TMĐT ngày sale là hàng nghìn người. Luôn nhìn percentile và cái đuôi.

Lỗi 3 — Bỏ qua chính máy sinh tải. Đã nói ở Ví dụ 3 nhưng phải nhắc lại vì nó phổ biến kinh khủng: client no khiến bạn "phát hiện" một bottleneck không tồn tại. GUI mode của JMeter đặc biệt hay là thủ phạm.

Lỗi 4 — Sửa nhiều thứ cùng lúc. Tăng pool, thêm index, đổi cấu hình worker — tất cả trong một lần. Rồi throughput tăng và bạn không biết nhờ đâu. Một thay đổi, một phép đo. Kỷ luật này giúp bạn thật sự hiểu hệ thống thay vì gặp may.

Lỗi 5 — Nhầm triệu chứng với nguyên nhân. DB CPU cao có thể là nguyên nhân (slow query), nhưng cũng có thể là triệu chứng của việc app gửi quá nhiều query vì thiếu cache (bài toán N+1). Luôn hỏi thêm một tầng "vì sao".

Mẹo vàng — Bật correlation ID. Gắn một ID xuyên suốt request từ client qua các lớp giúp bạn bóc tách được thời gian tiêu ở từng chặng. Đây là cách nhanh nhất để biết "chờ" nằm ở đâu.

Mẹo — Test từng lớp cô lập khi bí. Nếu không xác định được lớp nào, hãy test riêng: bắn thẳng vào DB, rồi bắn vào app có cache, rồi qua LB. So sánh trần throughput của từng chặng sẽ lộ ra chặng yếu nhất.

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

Bài 1 — Đọc chữ ký. Bạn có ba kết quả test. Với mỗi cái, đoán loại bottleneck và giải thích:

  • (a) Throughput phẳng ở 1.100 req/s, app CPU 40%, DB CPU 30%, latency có thành phần chờ ổn định 900ms.
  • (b) p50 = 100ms, p99 = 6.000ms, DB CPU 96%, disk read cao.
  • (c) Latency có gai đều mỗi 30 giây, memory app tăng dần theo hình răng cưa.
(Gợi ý đáp án: a = pool/thread cạn; b = slow query; c = Garbage Collection/memory.)

Bài 2 — Dựng monitoring tối thiểu. Trên một app mẫu (Node/Java/PHP tùy chọn) chạy local, cài công cụ quan sát CPU/memory của app và DB (có thể dùng htop, docker stats, hoặc dashboard đơn giản). Chạy một bài k6 tăng dần và ghi lại điểm saturation. Xác định lớp nào no đầu tiên.

Bài 3 — Tạo và bắt một bottleneck. Cố tình đặt connection pool = 5 trên app mẫu. Chạy load test, quan sát throughput phẳng và latency chờ. Sau đó tăng pool lên 50, chạy lại, ghi lại throughput mới và bottleneck kế tiếp xuất hiện ở đâu. Viết 5 dòng mô tả quá trình "đuổi bottleneck" này.

Tóm tắt

Bottleneck analysis là kỹ năng đi ngược từ triệu chứng ("hệ thống chậm ở mức X req/s") về nguyên nhân gốc ("connection pool cạn ở 480 user"). Nguyên lý nền tảng: hệ thống chỉ nhanh bằng thành phần chậm nhất, và tại mỗi thời điểm chỉ có một bottleneck chi phối — sửa nó xong, cái tiếp theo lộ ra.

Ba điều cần nhớ. Thứ nhất, luôn kiểm tra máy sinh tải trước — client no khiến bạn bịa ra bottleneck không có thật. Thứ hai, số liệu client chỉ nói "chậm", số liệu server-side mới nói "chậm ở đâu" — hãy đi dọc bản đồ lớp (LB → app → cache → DB) và dùng mô hình USE để tìm lớp bão hòa đầu tiên. Thứ ba, mỗi loại bottleneck có chữ ký riêng: pool cạn thì throughput phẳng mà CPU thấp; slow query thì p99 tách xa p50 và DB CPU cao; GC thì latency có gai theo chu kỳ.

Cuối cùng, làm việc như một nhà khoa học: một giả thuyết, một thay đổi, một phép đo, rồi lặp lại. Đó là con đường biến những biểu đồ khó hiểu thành kết luận mà đội kỹ thuật có thể hành động ngay.