Xử lý sự cố CPU hệ thống tăng cao nhưng không tìm thấy tiến trình chiếm dụng

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:

  1. Sử dụng top quan sát sự biến động của số lượng PID và trạng thái tiến trình (Running vs Sleep).
  2. 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.
  3. Sử dụng perf hoặc các công cụ truy vết động như execsnoop để "bắt dính" các tiến trình phù du.
  4. Tránh sử dụng các hàm gọi lệnh hệ thống (như exec, system trong 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.

Thẻ: linux-kernel performance-tuning cpu-analysis eBPF docker

Đăng vào ngày 27 tháng 7 lúc 17:09