🔍 Nút thắt cổ chai: vì sao app sập khi đông người dùng

By Better Wise

Đây là bài mở đầu của khóa, nên mình kể trước bằng một đêm trực thật: 21h, traffic gấp 8 lần bình thường, app chết trong 6 phút, và khi mở dashboard ra thì mọi thứ đều đỏ cùng lúc. Việc của người trực không phải là sửa hết, mà là tìm ra thứ đỏ trước tiên. Bài này luyện đúng kỹ năng đó, ở mức nhìn ra chỗ nghẽn chứ chưa đi sâu vào cách sửa (cách sửa nằm ở các bài sau).

Sau bài này, bạn có thể:

  • Giải thích nút thắt cổ chai (bottleneck) bằng ống nước, và vì sao nó quyết định throughput của cả hệ thống.
  • Nhận diện 4 lớp tài nguyên hay cạn nhất: CPU, RAM, connection, I/O.
  • Đọc một tình huống sập app thật và chỉ ra lớp nào chạm trần trước.
  • Ước lượng bằng con số ai cũng tính được: RPS, thời gian giữ tài nguyên, ngưỡng utilization.

Nút thắt cổ chai: tài nguyên cạn trước, hệ thống dừng trước

Hệ thống chạy ổn ở 60 RPS không có nghĩa là nó chạy được 600 RPS. Mỗi hệ thống có một trần, và trần đó do tài nguyên cạn đầu tiên quyết định. Nước chảy qua ống, đoạn ống nhỏ nhất quyết định lưu lượng, cho dù các đoạn khác to gấp mười.

Điều làm app "sập" chứ không "chậm dần đều" là hiệu ứng hàng đợi. Khi một tài nguyên đạt khoảng 70-80% công suất, thời gian chờ bắt đầu tăng phi tuyến: từ 80% lên 95% có thể làm latency gấp 5 lần. Latency vượt timeout, client retry, tải lại càng tăng, và hệ thống tự đẩy mình qua mép vực.

Bốn lớp tài nguyên và cách nhận diện trong 30 giây

flowchart TD
    C["📱<br/>Client"] --> A["🖥️<br/>App server"]
    A --> P["🔌<br/>Connection pool"]
    P --> D["🗄️<br/>Database"]
    A -.-> CPU["⚙️ CPU 100%"]
    A -.-> RAM["🧠 RAM đầy"]
    P -.-> CONN["🚧 Hết connection"]
    D -.-> IO["💽 I/O chờ"]
Lớp Dấu hiệu khi quá tải Con số để nhìn
CPU p95 latency tăng mạnh, throughput không tăng dù tải tăng CPU utilization, load average
RAM Dùng đều tăng theo thời gian, restart thì hết, vài ngày lại sập RSS, swap, số lần GC
Connection CPU thấp mà latency cao, log đầy lỗi timeout chờ connection connection đang dùng / max
I/O Query chậm, CPU không cao, máy "ì" iowait, thời gian đọc đĩa

Tip: CPU thấp mà app vẫn chết là manh mối rất mạnh. Khi đó đừng thêm CPU, hãy nhìn connection pool, ổ đĩa, hoặc một dependency bên ngoài.

Ví dụ 1: CPU là nút thắt đầu tiên

App Node.js chạy trên một server 4 vCPU, 8 GB RAM. Bình thường: 60 RPS, CPU 25%, p95 latency 150ms, lỗi gần 0.

21h có đợt sale: 480 RPS (gấp 8). Kết quả trong 90 giây: CPU lên 100% cả 4 core, event loop lag tăng vọt, p95 latency 4 giây, error rate 18%. Phần lớn lỗi là timeout phía client, và mỗi client retry 2 lần, tức tải thực tế còn cao hơn 480 RPS.

Kết luận: nút thắt đầu tiên là CPU. Nếu vội vàng thêm connection pool lúc này thì vô ích, vì app không có CPU để dùng connection đó.

Ví dụ 2: CPU còn thừa nhưng connection đã cạn

Hôm khác, app khác: 300 RPS, CPU chỉ 45%, nhưng p95 latency 6 giây và log đầy dòng chờ connection.

Pool cấu hình 20 connection. Mỗi request giữ connection khoảng 250ms vì một query thiếu index. Con số cần thiết:

$$ N = RPS \times \text{thời gian giữ connection} = 300 \times 0{,}25 = 75 $$

Cần 75, chỉ có 20. Phần còn lại xếp hàng, và thời gian chờ cộng dồn vào latency.

Cách sửa rẻ nhất không phải tăng pool lên 100, mà là thêm index để query từ 250ms xuống 20ms. Khi đó:

$$ N = 300 \times 0{,}02 = 6 $$

6 connection là đủ cho 300 RPS. Cùng một hệ thống, nút thắt biến mất mà không cần thêm máy.

Ví dụ 3: RAM đầy vì cache không ai dọn

Một app khác cache báo cáo bằng một Map trong bộ nhớ, key theo từng user, không có TTL. 50.000 user, mỗi entry khoảng 200 KB, tổng cộng khoảng 10 GB, trong khi server có 8 GB.

Triệu chứng rất đặc trưng: RAM tăng đều theo thời gian, restart thì sạch, và cứ 2-3 ngày lại sập một lần. Đây không phải bài toán traffic, đây là bài toán rò rỉ bộ nhớ. Nhìn RPS sẽ không thấy gì bất thường.

Tự đoán nút thắt trước khi đọc đáp án

App chạy 100 RPS, CPU 30%, p95 latency 2 giây, RAM ổn định, database CPU 90%, iowait cao. Nút thắt ở đâu?

Nút thắt nằm ở lớp I/O của database, không phải app server. App còn dư CPU, RAM còn dư, nhưng mọi request đều phải chờ đĩa. Hướng điều tra tiếp theo là query nào đang full scan, chứ không phải thêm instance app.

Sửa xong một chỗ, nút thắt tiếp theo sẽ lộ ra

Đây là phần khiến nhiều người bất ngờ: tối ưu mọi thứ cùng lúc là lãng phí, vì bạn không biết chỗ nào đang giới hạn. Nhưng sửa đúng một chỗ cũng không làm bạn xong việc, vì nút thắt chỉ di chuyển.

  • Thêm cache để giảm CPU thì RAM bắt đầu tăng.
  • Tăng pool để hết chờ connection thì database nhận nhiều connection hơn và bắt đầu nghẽn.
  • Chạy thêm instance app để chia tải thì mỗi instance lại mở thêm connection tới database.

Vì vậy cách làm đúng là lặp: đo, tìm nút thắt số một, sửa, đo lại, tìm nút thắt số hai. Mỗi vòng lặp chỉ giải quyết một việc.

Ba quy tắc đọc nút thắt của người trực ca

  1. Đọc theo thứ tự: error rate trước, latency sau, tài nguyên cuối. Error rate cho biết người dùng đang đau đến mức nào; p95 cho biết đau ở đâu; tài nguyên cho biết vì sao.
  2. So sánh với baseline, đừng so với ngưỡng tuyệt đối. CPU 70% là bình thường với app này nhưng là bất thường với app khác. Luôn có một con số "lúc bình thường" để đối chiếu.
  3. Một tài nguyên chạm trần là đủ để kết luận, hai tài nguyên cùng chạm trần thì phải xem cái nào chạm trước. Đừng sửa cả hai cùng lúc, vì bạn sẽ không biết cái nào có tác dụng.

Lỗi phổ biến nhất của người mới: thấy CPU 100% là mua máy to hơn. Trong nhiều sự cố, CPU 100% chỉ là hệ quả của việc app đang retry một dependency đã chết.

Mang theo gì sang bài sau

Bốn điều cần nhớ:

  • Hệ thống sập vì một tài nguyên cạn trước, không phải vì mọi thứ yếu.
  • Bốn lớp cần soi: CPU, RAM, connection, I/O.
  • Latency tăng phi tuyến sau ngưỡng 70-80% utilization, và retry làm mọi thứ tệ hơn.
  • Số connection cần xấp xỉ $RPS \times$ thời gian giữ connection. Con số này tính được, không cần đoán.

Bài sau mình sẽ trả lời câu hỏi tiếp theo: khi đã biết nút thắt, nên mua máy mạnh hơn (scale dọc) hay thêm nhiều máy (scale ngang), và cái giá phải trả của từng hướng.

Front: Nút thắt cổ chai (bottleneck) là gì? Back: Tài nguyên cạn đầu tiên khi lưu lượng tăng, và nó quyết định throughput của cả hệ thống.

Front: Bốn lớp tài nguyên thường chạm trần đầu tiên? Back: CPU, RAM, connection, I/O.

Front: CPU đạt 100% trên app server thường biểu hiện ra sao? Back: p95 latency tăng mạnh, throughput không tăng dù tải tăng, timeout hàng loạt.

Front: RAM đầy trong thời gian dài gây ra hiện tượng gì? Back: Swap hoặc GC liên tục, latency tăng vọt, có thể bị OOM kill.

Front: Vì sao cạn connection pool làm app chết dù CPU còn thấp? Back: Request phải xếp hàng chờ connection, latency dồn lên rồi vượt timeout.

Front: Công thức ước lượng số connection cần thiết? Back: RPS nhân với thời gian giữ connection (tính bằng giây).

Front: Latency bắt đầu tăng phi tuyến ở mức utilization nào? Back: Khoảng 70-80% công suất tài nguyên.

Front: Vì sao không nên tối ưu mọi thứ cùng lúc? Back: Chỉ nút thắt đầu tiên giới hạn throughput, sửa nó xong mới thấy nút thắt tiếp theo.

Front: Dấu hiệu nút thắt nằm ở lớp I/O của database? Back: CPU không cao, iowait cao, query chậm, app chờ đĩa chứ không chờ tính toán.

Front: Thứ tự đọc số khi đang có sự cố? Back: Error rate trước, rồi p95 latency, rồi xem tài nguyên nào chạm trần trước.

Front: Throughput là gì? Back: Số request hệ thống xử lý được trong một giây (RPS).

Front: Vì sao app sập đột ngột lúc cao điểm thay vì chậm dần? Back: Hàng đợi phình, latency vượt timeout, client retry làm tải tăng thêm.

Front: Vì sao CPU 100% có thể chỉ là hệ quả, không phải nguyên nhân? Back: App đang retry một dependency đã chết, CPU cao là do làm lại việc vô ích.

Nút thắt cổ chai là tài nguyên _____ khi lưu lượng tăng, và nó quyết định throughput của cả hệ thống. Bốn lớp tài nguyên thường chạm trần là CPU, RAM, _____ và I/O. Khi utilization vượt khoảng _____%, latency bắt đầu tăng phi tuyến. Số connection cần thiết xấp xỉ bằng _____. Khi có sự cố, đọc theo thứ tự error rate, _____, rồi tài nguyên nào chạm trần trước.