Gỡ lỗi các tiến trình busy

Đôi khi chúng ta có thể thấy các tiến trình có trạng thái busy khi chạy lệnh php start.php status, điều này có nghĩa là tiến trình tương ứng đang xử lý công việc, trong điều kiện bình thường, khi công việc xử lý xong, tiến trình sẽ trở lại trạng thái idle. Thông thường, điều này sẽ không gây ra vấn đề gì. Tuy nhiên, nếu tiến trình luôn giữ trạng thái busy mà không trở về trạng thái idle, điều này có thể cho thấy có sự tắc nghẽn hoặc vòng lặp vô hạn trong công việc của tiến trình. Chúng ta có thể sử dụng các phương pháp sau để xác định vị trí tắc nghẽn.

Sử dụng lệnh strace + lsof để xác định vị trí

1. Tìm pid của tiến trình busy trong status
Sau khi chạy lệnh php start.php status, hiển thị như sau

Trong hình, pid của tiến trình busy1172511748.

2. Theo dõi tiến trình bằng strace
Chọn một tiến trình pid (ở đây chọn 11725), chạy lệnh strace -ttp 11725 hiển thị như sau

Có thể thấy tiến trình đang liên tục vòng lặp hệ thống gọi poll([{fd=16, events=.... này đang chờ sự kiện dữ liệu có thể đọc từ mô tả fd 16, tức là đang chờ mô tả này trả về dữ liệu.

Nếu không có bất kỳ hệ thống gọi nào hiển thị, giữ nguyên terminal hiện tại, mở một terminal mới và chạy kill -SIGALRM 11725 (gửi tín hiệu báo thức đến tiến trình), sau đó xem terminal strace có phản hồi hay không, liệu có bị tắc nghẽn trong một hệ thống gọi nào không. Nếu vẫn không hiển thị bất kỳ hệ thống gọi nào, điều này có thể chỉ ra rằng chương trình có thể đang ở trong vòng lặp chết, hãy tham khảo nguyên nhân khác tại phần cuối trang dẫn đến việc tiến trình busy lâu dài - mục 2 để khắc phục.

Nếu hệ thống bị tắc ở lệnh gọi epoll_wait hoặc select là điều bình thường, điều này có nghĩa là tiến trình đã ở trạng thái idle.

3. Sử dụng lsof để xem mô tả tiến trình
Chạy lệnh lsof -nPp 11725 hiển thị như sau

Mô tả fd 16 tương ứng với bản ghi 16u (dòng cuối cùng), có thể thấy mô tả fd=16 này là một kết nối tcp, địa chỉ từ xa là 101.37.136.135:80, cho thấy tiến trình nên đang truy cập một tài nguyên http, vòng lặp poll([{fd=16, events=.... đang chờ http server trả về dữ liệu, điều này lý giải vì sao tiến trình lại ở trạng thái busy.

Giải pháp:
Khi đã biết tiến trình bị tắc ở đâu, việc giải quyết sẽ dễ dàng hơn, ví dụ như trong ví dụ trên, xác định tiến trình có thể đang gọi curl, và url tương ứng không trả dữ liệu trong thời gian dài, dẫn đến tiến trình luôn chờ đợi. Lúc này, bạn có thể tìm người cung cấp url để xác định nguyên nhân trả về chậm, đồng thời nên thêm tham số timeout vào cuộc gọi curl, ví dụ như nếu không nhận được phản hồi trong 2 giây thì sẽ timeout, tránh tình trạng bị tắc nghẽn lâu dài (do đó, tiến trình có thể xuất hiện trạng thái busy khoảng 2 giây).

Các nguyên nhân khác dẫn đến tiến trình busy lâu dài

Ngoài việc tiến trình bị tắc nghẽn dẫn đến trạng thái busy, còn có nguyên nhân khác cũng có thể khiến tiến trình vào trạng thái này.

1. Công việc có lỗi nghiêm trọng khiến tiến trình liên tục thoát
Hiện tượng: Trong trường hợp này, có thể thấy hệ thống có tải cao, load average trong status là 1 hoặc cao hơn. Có thể thấy số lượng exit_count của tiến trình rất cao và tăng liên tục.
Giải pháp: Chạy ở chế độ debug (php start.php start không thêm -d) để xem lỗi của công việc và sửa lỗi đó.

2. Vòng lặp vô hạn trong mã
Hiện tượng: Có thể thấy tiến trình busy chiếm CPU cao trên top, lệnh strace -ttp pid không in ra bất kỳ thông tin hệ thống gọi nào.
Giải pháp: Tham khảo bài viết của Niwanger qua việc định vị với gdb và mã nguồn php, các bước tóm tắt như sau:

  1. Sử dụng php -v để kiểm tra phiên bản.
  2. Tải mã nguồn tương ứng với phiên bản php.
  3. Chạy lệnh gdb --pid=pid tiến trình busy.
  4. Chạy lệnh source đường dẫn đến mã nguồn php/.gdbinit.
  5. Chạy lệnh zbacktrace để in ra stack gọi.
    Cuối cùng, bạn có thể thấy stack gọi mà mã php đang thực thi, tức là vị trí vòng lặp chết trong mã php.
    Lưu ý: Nếu zbacktrace không hiển thị được stack gọi, có thể php của bạn không được biên dịch với tham số -g, cần biên dịch lại php, sau đó khởi động lại workerman để xác định vị trí.

3. Thêm vô hạn bộ hẹn giờ
Mã công việc liên tục thêm bộ hẹn giờ mà không xóa, dẫn đến số lượng bộ hẹn giờ trong tiến trình ngày càng nhiều, cuối cùng dẫn đến tiến trình chạy bộ hẹn giờ vô hạn. Ví dụ như mã sau:

$worker = new Worker;
$worker->onConnect = function($con){
    Timer::add(10, function(){});
};
Worker::runAll();

Mã trên sẽ tăng một bộ hẹn giờ khi có kết nối từ khách hàng, nhưng trong toàn bộ mã công việc không có logic xóa bộ hẹn giờ, do đó theo thời gian, số lượng bộ hẹn giờ trong tiến trình sẽ liên tục gia tăng, cuối cùng dẫn đến tiến trình chạy bộ hẹn giờ vô hạn gây ra trạng thái busy.
Mã đúng:

$worker = new Worker;
$worker->onConnect = function($con){
    $con->timer_id = Timer::add(10, function(){});
};
$worker->onClose = function($con){
    Timer::del($con->timer_id);
};
Worker::runAll();