Xử lý sự cố

Website chậm nhưng CPU thấp: nên kiểm tra những gì tiếp theo?

Cách tìm điểm nghẽn khi website chậm nhưng CPU thấp, từ request queue, database lock và disk I/O đến memory pressure hoặc API bên ngoài.

Website chậm nhưng CPU thấp
Mục lục

Tình trạng website phản hồi chậm chạp trong khi CPU máy chủ chỉ dao động ở mức 15–30% là hiện tượng kỹ thuật rất phổ biến. Mức sử dụng CPU thấp chỉ khẳng định các lõi vi xử lý không bận tính toán logic; nó hoàn toàn không chứng minh hệ thống đang vận hành trơn tru.

Trong hầu hết các trường hợp, các luồng xử lý không hề thiếu năng lực tính toán mà đang phải dừng lại chờ đợi một tài nguyên khác: chờ ổ đĩa đọc ghi xong dữ liệu (I/O wait), chờ database giải phóng khóa bảng (lock contention), chờ hàng đợi worker giải phóng kết nối, hoặc chờ phản hồi từ một API bên thứ ba qua mạng Internet.

Ngoài ra, con số CPU trung bình toàn máy còn có thể che giấu các điểm nghẽn cục bộ: một lõi đơn bị nghẽn 100% trong khi các lõi khác rảnh rỗi, hoặc container bị giới hạn bởi hạn ngạch CPU quota. Thay vì thắc mắc vì sao CPU không tăng, câu hỏi kỹ thuật chuẩn xác cần đặt ra là: Yêu cầu của người dùng đang bị giam giữ thời gian ở đâu?

Bước 1: Xác định hành trình request bị chậm ở chặng nào

Một yêu cầu web từ người dùng trải qua nhiều chặng kế tiếp nhau:

Phân giải DNS → Bắt tay TCP/TLS → Mạng CDN → Nginx Reverse Proxy → Ứng dụng Backend → Database/Cache → Truyền tải dữ liệu về trình duyệt

Để biết độ trễ phát sinh từ đâu, bạn cần bóc tách các mốc thời gian. Với Nginx, cấu hình access log ghi lại 3 biến upstream timing sẽ chỉ ra chính xác chặng bị nghẽn:

Chỉ số thời gian tăng vọtĐiểm nghẽn kỹ thuật thực tếHướng kiểm tra ưu tiên
$upstream_connect_timeBắt tay kết nối giữa Nginx và backend bị chậmHàng đợi kết nối (TCP backlog) bị đầy, hết file descriptors, hoặc container backend bị quá tải
$upstream_header_timeNginx kết nối xong nhưng backend phản hồi byte đầu tiên rất lâuBackend bị nghẽn tính toán, database query chạy chậm, deadlock, hoặc đang chờ API bên ngoài
$upstream_response_time (chênh lệch lớn so với header time)Nhận header nhanh nhưng thời gian truyền toàn bộ nội dung kéo dàiDung lượng tệp tin phản hồi quá lớn, nghẽn băng thông mạng máy chủ, hoặc ổ đĩa đọc dữ liệu chậm

Nếu trang chủ (được CDN hoặc Nginx lưu cache) phản hồi dưới 100ms trong khi các trang động (gọi vào backend) mất tới 5–10 giây, bạn có thể loại trừ hoàn toàn các nghi vấn về đường truyền mạng hay năng lực của web server Nginx.

Bước 2: Kiểm tra tình trạng hàng đợi Worker Pool

Mỗi ứng dụng web (như PHP-FPM, Node.js cluster, Python Gunicorn) đều có một số lượng luồng (thread) hoặc tiến trình con (worker) giới hạn để phục vụ các yêu cầu đồng thời.

Khi toàn bộ các worker đều đang bận xử lý (hoặc đang nằm chờ database/API ngoài), các yêu cầu mới gửi tới sẽ bị dồn ứ vào hàng đợi (listen.backlog). Trong suốt thời gian nằm chờ trong hàng đợi, người dùng phải đối mặt với màn hình xoay tròn, nhưng CPU của máy chủ vẫn ở mức rất thấp vì các worker thực chất không thực hiện phép tính nào.

Kiểm tra số lượng tiến trình và trạng thái worker:

  • Với PHP-FPM: Bật trang trạng thái (/status) của PHP-FPM để xem số lượng active processes và listen queue. Nếu active processes chạm ngưỡng max_children, worker pool của bạn đã cạn kiệt.
  • Với Node.js: Kiểm tra xem event loop có bị nghẽn bởi một hàm xử lý đồng bộ nặng (như parse file JSON hàng chục megabyte hoặc mã hóa mật khẩu) hay không.

Lưu ý: Không tùy tiện tăng số lượng worker lên gấp đôi khi thấy hàng đợi tăng. Tăng worker mà không tính toán dung lượng RAM sẽ dẫn tới nguy cơ sập máy do OOM Killer như đã phân tích trong bài VPS hết RAM và OOM Killer: dấu hiệu, kiểm tra và phòng ngừa.

Bước 3: Database đang chạy hay đang chờ khóa dữ liệu?

Trong các ứng dụng website động thông thường, hơn 80% thời gian phản hồi của một request là thời gian chờ đợi cơ sở dữ liệu. CPU máy chủ thấp không đồng nghĩa với việc database đang rảnh rỗi.

Hai kịch bản database làm chậm hệ thống mà không đẩy CPU lên cao:

  1. Tranh chấp khóa dữ liệu (Lock Contention / Deadlock): Một giao dịch (transaction) ghi dữ liệu cập nhật tồn kho hoặc chốt đơn hàng giữ khóa bảng dữ liệu quá lâu. Toàn bộ các câu lệnh SELECT hoặc UPDATE khác muốn truy cập vào cùng bảng đó đều phải xếp hàng chờ đợi giải phóng khóa.
  2. Cạn kiệt Connection Pool: Ứng dụng chỉ được cấu hình mở tối đa 20 kết nối tới database. Khi cả 20 kết nối này bị chiếm giữ bởi các tác vụ chạy chậm, các kết nối mới từ ứng dụng sẽ bị treo cứng ở tầng kết nối.

Kiểm tra các câu truy vấn và sự kiện chờ trên MySQL:

sql
-- Xem danh sách các truy vấn đang thực thi
SHOW FULL PROCESSLIST;

-- Kiểm tra chi tiết trạng thái khóa và giao dịch InnoDB
SHOW ENGINE INNODB STATUS\G

Trên PostgreSQL, bạn hãy kiểm tra cột wait_event_type và wait_event trong bảng pg_stat_activity để biết tiến trình database đang thực sự tính toán (CPU) hay đang dừng lại chờ ổ đĩa (IO) hoặc chờ khóa (Lock).

Bước 4: Nghẽn tốc độ I/O ổ đĩa cứng

Một máy chủ có thể còn trống hàng chục gigabyte dung lượng ổ đĩa nhưng các tiến trình vẫn bị treo cứng do ổ đĩa bị nghẽn thông lượng đọc ghi (I/O Bottleneck).

Khi một câu truy vấn database không có index buộc phải quét toàn bộ bảng dữ liệu hàng triệu dòng (Full Table Scan), hoặc khi tác vụ sao lưu nén file chạy cùng lúc với đợt ghi log của ứng dụng, số lượng yêu cầu đọc ghi mỗi giây (IOPS) của ổ đĩa sẽ chạm trần phần cứng. Lúc này, CPU sẽ phải chuyển sang trạng thái chờ I/O (%wa trong lệnh top).

Kiểm tra xem tiến trình nào đang chiếm dụng băng thông đọc ghi đĩa:

bash
# Cài đặt và chạy iotop để theo dõi tốc độ đọc ghi theo từng tiến trình
sudo iotop -o -b -n 3

Nếu bạn thấy chỉ số Load Average trên máy chủ tăng cao (ví dụ load 5.0 trên VPS 2 vCPU) nhưng mức dùng CPU trong top chỉ có 10–20%, nguyên nhân gần như chắc chắn nằm ở hàng đợi I/O đĩa cứng.

Bước 5: Áp lực bộ nhớ và hiện tượng Swap Thrashing

Khi máy chủ bị thiếu hụt dung lượng RAM khả dụng (MemAvailable), nhân Linux sẽ liên tục hoán đổi dữ liệu giữa RAM và phân vùng Swap trên ổ đĩa để duy trì hoạt động cho các tiến trình mới.

Do tốc độ truy xuất dữ liệu của ổ cứng (dù là SSD NVMe) vẫn chậm hơn RAM vật lý hàng trăm lần, việc đọc ghi swap liên tục (swap in/out) sẽ khiến toàn bộ hệ thống bị nghẽn cổ chai I/O nghiêm trọng. Các yêu cầu web bị kéo dài thời gian phản hồi từ 100ms lên tới 10–20 giây, trong khi CPU vẫn hiển thị mức sử dụng rất thấp.

Kiểm tra hiện tượng hoán đổi swap bằng lệnh:

bash
vmstat 2 5

Nếu hai cột si (swap-in) và so (swap-out) liên tục hiển thị các giá trị lớn hơn 0, hệ thống của bạn đang bị nghẽn do thiếu bộ nhớ RAM trầm trọng.

Bước 6: Treo kết nối do gọi các API bên ngoài

Nếu tính năng bị chậm rơi vào các thao tác như thanh toán đơn hàng, gửi email thông báo, xác thực OTP hoặc đồng bộ dữ liệu sang phần mềm quản lý kho (ERP), thủ phạm rất có thể nằm ở các dịch vụ bên ngoài máy chủ.

Trong mã nguồn ứng dụng, nếu một lời gọi hàm HTTP sang dịch vụ bên thứ ba không được thiết lập thời gian timeout cụ thể (hoặc timeout mặc định để tới 60 giây), tiến trình backend sẽ bị treo giữ kết nối suốt thời gian đó để đợi phản hồi qua mạng. Trong lúc chờ đợi, tiến trình này không hề tiêu tốn CPU, nhưng nó chiếm dụng một worker và khiến người dùng phải chờ đợi cho tới khi Nginx trả về lỗi 504.

Nguyên tắc xử lý:

  • Luôn đặt giới hạn timeout ngắn hạn (dưới 5–10 giây) cho mọi kết nối ra API bên ngoài.
  • Tách các tác vụ không cần thiết trả kết quả tức thì (như gửi email, đồng bộ CRM) sang hàng đợi xử lý nền bất đồng bộ (Background Queue) như đã phân tích trong bài Lỗi 504 Gateway Timeout: nguyên nhân thường nằm ở đâu?.

Bước 7: Bottleneck cục bộ bị con số CPU trung bình che khuất

Con số CPU hiển thị trên bảng điều khiển tổng thể thường là giá trị trung bình của toàn bộ các lõi. Điều này có thể che giấu những điểm nghẽn nghiêm trọng sau:

  1. Ứng dụng đơn luồng chạy hết công suất 1 core: Một tiến trình Node.js hoặc Python chạy đơn luồng chỉ có thể sử dụng tối đa 1 core CPU. Trên một VPS có 4 vCPU, khi tiến trình này chạy hết 100% công suất của 1 core, CPU trung bình toàn máy chỉ hiển thị 25%. Mức 25% trông có vẻ rất an toàn, nhưng thực tế ứng dụng của bạn đã bị quá tải hoàn toàn trên luồng xử lý đó.
  2. Container chạm giới hạn CPU Quota: Trong Docker hoặc Kubernetes, một container có thể bị giới hạn hạn ngạch CPU (ví dụ cpus: 0.5). Khi container đạt mức 0.5 core, nó sẽ bị cơ chế cgroup bóp nghẽn thời gian tính toán (CPU Throttling), khiến ứng dụng bị chậm dù VPS bên ngoài vẫn còn dư thừa nhiều core nhàn rỗi.

Kiểm tra mức sử dụng CPU chi tiết trên từng core riêng biệt:

bash
# Mở top và bấm phím 1 để hiển thị chi tiết từng core CPU
top

Bảng tra cứu nhanh nguyên nhân và hướng xử lý

Dấu hiệu quan sát đượcNguyên nhân gốc rễ dự kiếnHành động can thiệp ưu tiên
Load average cao, CPU thấp, %wa caoỔ cứng bị nghẽn đọc ghi I/ODùng iotop tìm tiến trình ghi đĩa; tối ưu index câu truy vấn database quét bảng lớn
Chỉ 1 core chạy 100%, các core khác rảnh rỗiỨng dụng xử lý đơn luồng chạm trầnTối ưu thuật toán trong code; cấu hình chạy đa tiến trình (Cluster mode)
Chỉ tính năng thanh toán/đặt hàng bị chậmKhóa dữ liệu database (Lock contention)Kiểm tra SHOW ENGINE INNODB STATUS, tối ưu phạm vi khóa trong transaction
Chỉ tính năng gửi mail/gọi cổng thanh toán chậmChờ phản hồi từ API bên ngoàiCấu hình timeout cho HTTP client; chuyển tác vụ sang hàng đợi nền (Queue)
si và so trong vmstat nhảy liên tụcThiếu RAM, phát sinh Swap ThrashingBổ sung RAM hoặc giảm worker pool để giải phóng áp lực bộ nhớ
Mọi trang tĩnh đều nhanh, trang động bị xếp hàngWorker pool của ứng dụng bị bão hòaKiểm tra cấu hình pm.max_children của PHP-FPM hoặc connection pool

Checklist điều tra sự cố website chậm nhưng CPU thấp

  • ☐ Đã tách biệt kiểm tra giữa trang tĩnh/cache và trang động để xác định tầng phát sinh độ trễ.
  • ☐ Đã kiểm tra 3 mốc thời gian $upstream_connect_time, $upstream_header_time, $upstream_response_time trong log Nginx.
  • ☐ Đã kiểm tra chi tiết từng core CPU bằng cách bấm phím 1 trong lệnh top để phát hiện nghẽn đơn luồng.
  • ☐ Đã kiểm tra trạng thái worker pool và hàng đợi kết nối của ứng dụng (PHP-FPM status, Node.js event loop).
  • ☐ Đã kiểm tra danh sách câu truy vấn đang thực thi (SHOW PROCESSLIST) và tình trạng khóa dữ liệu trên database.
  • ☐ Đã kiểm tra chỉ số %wa và dùng iotop xác định tiến trình chiếm dụng băng thông đọc ghi đĩa cứng.
  • ☐ Đã kiểm tra áp lực hoán đổi bộ nhớ Swap bằng lệnh vmstat 2 5.
  • ☐ Đã rà soát các kết nối gọi ra API bên ngoài xem có được cấu hình thời gian timeout ngắn hạn hay không.

Website phản hồi chậm khi CPU thấp không phải là một bài toán bí ẩn. Năng lực tính toán của CPU chỉ là một mắt xích trong chuỗi cung ứng dịch vụ. Khi website bị chậm trên production và bạn cần khoanh vùng theo thứ tự ưu tiên mà không làm mất dấu vết, hãy áp dụng Quy trình 30 phút đầu khi website gặp sự cố. Bằng cách bình tĩnh bóc tách từng chặng thời gian của request, bạn sẽ tìm ra chính xác nút thắt cổ chai đang kìm hãm tốc độ hệ thống của mình.

Nguồn tham khảo

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp

Đọc tiếp

Bài nên đọc tiếp

Xem tất cả bài về Xử lý sự cố