Thông thường, khi gặp tình trạng hệ thống chậm chạp do CPU quá tải, chúng ta thường sử dụng các công cụ như top hoặc pidstat để xác định ngay tiến trình "ngốn" tài nguyên nhất. Tuy nhiên, có những kịch bản mà các công cụ này báo cáo chỉ số CPU hệ thống rất cao (User hoặc System CPU lên tới 80-90%), nhưng danh sách các tiến trình bên dưới lại hiển thị các con số rất thấp, chỉ vài phần trăm. Tại sao lại có sự mâu thuẫn này?
Thiết lập môi trường giả lập
Để hiểu rõ bản chất vấn đề, chúng ta sẽ xây dựng một môi trường giả lập sử dụng Docker. Hệ thống bao gồm một máy chủ Web Nginx xử lý yêu cầu và một backend PHP-FPM thực thi logic nghiệp vụ.
# Chạy container Nginx làm proxy
docker run --name web-server -p 10000:80 -itd feisky/nginx:sp
# Chạy container PHP-FPM kết nối chung network với Nginx
docker run --name php-backend -itd --network container:web-server feisky/php-fpm:sp
Kiểm tra dịch vụ bằng lệnh curl để đảm bảo hệ thống phản hồi bình thường:
curl http://localhost:10000/
# Kết quả mong đợi: It works!
Phân tích hiệu năng bằng các công cụ tiêu chuẩn
Sử dụng công cụ ApacheBench (ab) để tạo áp lực tải giả lập với 100 kết nối đồng thời:
ab -c 100 -n 1000 http://localhost:10000/
Trong khi lệnh ab đang chạy, chúng ta mở một terminal khác và quan sát hệ thống bằng top:
$ top
%Cpu(s): 81.5 us, 14.2 sy, 0.0 ni, 2.5 id, 0.0 wa, 0.0 hi, 1.8 si, 0.0 st
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8120 root 20 0 12450 8200 4100 S 3.2 0.1 0:05.12 docker-containe
8210 www-data 20 0 332400 15800 8200 S 2.5 0.2 0:04.20 php-fpm
8211 www-data 20 0 332400 15800 8200 S 2.2 0.2 0:04.18 php-fpm
Dễ dàng nhận thấy: CPU User (us) đang ở mức 81.5%, nhưng không có tiến trình nào trong danh sách vượt quá 5% CPU. Ngay cả khi cộng dồn toàn bộ danh sách, con số vẫn không khớp với mức 80% mà hệ thống báo cáo.
Truy tìm dấu vết các tiến trình ẩn
Hiện tượng này thường xảy ra do các tiến trình ngắn hạn (Short-lived processes). Đây là những chương trình được sinh ra, thực hiện một tác vụ cực nhanh rồi kết thúc ngay lập tức. Vì top hay pidstat lấy mẫu dữ liệu theo khoảng thời gian (ví dụ 1 giây), chúng thường bỏ lỡ các tiến trình có vòng đời ngắn hơn chu kỳ lấy mẫu.
Hãy quan sát kỹ hơn cột trạng thái (S) và tổng số tác vụ trong top. Nếu số lượng tiến trình ở trạng thái Running (R) thay đổi liên tục hoặc số lượng Total Tasks biến động, đó là dấu hiệu của việc thực thi lệnh bên ngoài.
Sử dụng pstree để xem cấu trúc phân cấp tiến trình:
$ pstree -p | grep php-fpm
|-php-fpm(8209)-+-php-fpm(8210)---sh(9501)---stress(9502)
|-php-fpm(8211)---sh(9503)---stress(9504)
Chúng ta thấy php-fpm đang gọi sh, và sh lại gọi một công cụ có tên là stress. Đây chính là thủ phạm. Kiểm tra mã nguồn ứng dụng PHP:
<?php
// Giả lập xử lý I/O nặng bằng cách gọi lệnh stress từ hệ thống
$exec_cmd = "/usr/local/bin/stress -t 1 -d 1 2>&1";
$output = [];
$status = 0;
exec($exec_cmd, $output, $status);
if ($status !== 0) {
header("HTTP/1.1 500 Internal Server Error");
print_r($output);
} else {
echo "Phản hồi thành công!";
}
?>
Đoạn code trên thực hiện hàm exec() trong mỗi yêu cầu HTTP. Mỗi lần có request, một tiến trình stress mới được tạo ra. Tuy nhiên, nếu kiểm tra log lỗi, ta có thể thấy stress thất bại do thiếu quyền ghi file tạm, dẫn đến việc nó khởi động và thoát cực nhanh, gây tiêu tốn CPU cho việc khởi tạo tiến trình (Process fork/exec overhead) thay vì thực hiện công việc thực tế.
Phân tích chuyên sâu với Perf và Execsnoop
Để xác định chính xác các hàm hệ thống nào đang chiếm dụng tài nguyên, perf là công cụ mạnh mẽ nhất:
# Ghi lại dữ liệu hiệu năng trong 10 giây
perf record -g -a sleep 10
# Phân tích báo cáo
perf report
Trong báo cáo của perf, bạn sẽ thấy các hàm liên quan đến việc tạo tiến trình như do_fork hoặc các hàm xử lý ngắt chiếm tỷ trọng cao.
Ngoài ra, nếu bạn cài đặt bộ công cụ bcc-tools, lệnh execsnoop sẽ giúp bắt trực tiếp các tiến trình ngắn hạn ngay khi chúng vừa xuất hiện:
$ execsnoop
PCOMM PID PPID RET ARGS
sh 10250 8210 0 /bin/sh -c /usr/local/bin/stress -t 1 -d 1
stress 10251 10250 0 /usr/local/bin/stress -t 1 -d 1
sh 10252 8211 0 /bin/sh -c /usr/local/bin/stress -t 1 -d 1
stress 10253 10252 0 /usr/local/bin/stress -t 1 -d 1
execsnoop dựa trên công nghệ eBPF, cho phép theo dõi các lời gọi hàm execve() ở cấp độ nhân (kernel), giúp chúng ta nhìn thấy mọi câu lệnh được thực thi dù nó diễn ra trong mili giây.
Bài học rút ra
Khi gặp vấn đề CPU cao mà danh sách tiến trình "sạch", hãy kiểm tra theo các bước:
- Sử dụng
topquan sát sự biến động của số lượng PID và trạng thái tiến trình (Running vs Sleep). - Dùng
pstreeđể tìm mối quan hệ cha-con, đặc biệt là các tiến trình shell (sh/bash) được gọi từ ứng dụng backend. - Sử dụng
perfhoặc các công cụ truy vết động nhưexecsnoopđể "bắt dính" các tiến trình phù du. - Tránh sử dụng các hàm gọi lệnh hệ thống (như
exec,systemtrong PHP/Python) bên trong các vòng lặp hoặc các endpoint có lưu lượng truy cập lớn.