busyプロセスのデバッグ

時々、php start.php status コマンドを実行すると、busy 状態のプロセスが表示されます。これは対応するプロセスが業務処理を行っていることを示しており、通常、業務処理が完了すると対応するプロセスは idle 状態に戻ります。この場合、特に問題はありません。ただし、常に busy 状態が続き、 idle 状態に戻らない場合は、プロセス内で業務がブロックされるか、無限ループに陥っていることを意味します。以下の方法で原因を特定できます。

strace + lsof コマンドを使用して特定

1、status で busy プロセスの pid を探す
php start.php status を実行すると、次のように表示されます。

画像中の busy のプロセスの pid1172511748 です。

2、strace でプロセスをトレース
任意のプロセス pid を選択します(ここでは 11725 を選択します)。次のコマンドを実行します。strace -ttp 11725 と表示されます。

プロセスが poll([{fd=16, events=.... のシステムコールを繰り返し実行しているのが見えます。これは、fd が 16 のディスクリプタの可読イベントを待機していることを示しています。つまり、このディスクリプタがデータを返すのを待っています。

何もシステムコールが表示されない場合は、現在のターミナルを保持し、別のターミナルを新たに開いて kill -SIGALRM 11725 を実行します(プロセスにアラーム信号を送信します)。その後、strace のターミナルが応答するか、特定のシステムコールでブロックしているかを確認します。もし依然として何も表示されない場合、プログラムがビジネスロジックの死循環にある可能性が高いです。ページ下部のプロセスが長時間 busy になる他の原因を参照し、解決策を見つけてください。

epoll_wait または select システムコールでブロックしている場合は、正常な状態です。これは、プロセスがすでに idle 状態に入っていることを示します。

3、lsof でプロセスのディスクリプタを確認
lsof -nPp 11725 を実行すると、次のように表示されます。

ディスクリプタ 16 は、16u のレコード(最後の行)に対応しています。fd=16 のディスクリプタは TCP 接続で、リモートアドレスは 101.37.136.135:80 です。これはプロセスが HTTP リソースにアクセスしていることを示しており、ループしている poll([{fd=16, events=.... は、HTTP サーバーがデータを返すのをずっと待っていることを説明しています。このため、プロセスは busy 状態にあります。

解決:
プロセスがどこでブロックしているかがわかれば、次のステップは簡単です。たとえば、上記の定位から、ビジネスが curl を呼び出している可能性があり、対応する URL が長時間応答していないため、プロセスがずっと待機していることがわかります。この場合、URL の提供者に連絡して、その URL が遅くなっている原因を特定させるべきです。また、curl の呼び出しにタイムアウトパラメータを追加する必要があります。たとえば、2 秒間返答がなければタイムアウトするように設定します。これにより、長時間ブロックされるのを回避することができます(この場合、プロセスは約 2 秒の間 busy 状態になるかもしれません)。

プロセスが長時間 busy になる他の原因

プロセスがブロックされること以外に、以下の理由によってプロセスが busy 状態になる可能性があります。

1、ビジネスに致命的なエラーがあり、プロセスが繰り返し終了する
現象: この場合、システムの負荷が非常に高く、statusload average は 1 以上になります。プロセスの exit_count の数字が非常に高く、継続的に増加しています。
解決: デバッグモードで実行します(php start.php start-d を付けない)workerman を使って、ビジネスエラーを確認し、そのエラーを解決します。

2、コード内に無限死ループが存在する
現象: top で busy プロセスが CPU を高く占有しており、strace -ttp pid コマンドを実行しても、システムコール情報が表示されません。
解決: 鳥哥の記事を参考に、gdb と php ソースコードを用いて特定します。手順は概ね以下の通りです:
1、php -v でバージョンを確認
2、対応する PHP バージョンのソースコードをダウンロード
3、gdb --pid=busyプロセスのpid
4、source phpソースコードパス/.gdbinit
5、zbacktrace を使用してスタックトレースを出力

最後のステップで、現在実行中の PHP コードのスタックトレースが見え、PHP コードの死ループの場所が確認できます。
注意:もし zbacktrace がスタックトレースを出力しない場合、PHP をコンパイルするときに -g オプションが追加されていない可能性があります。再度 PHP をコンパイルし、workerman を再起動して定位を続けてください。

3、無限にタイマーを追加
ビジネスコードがタイマーを追加し続けて削除しない場合、プロセス内のタイマーが増えて、最終的にはプロセスが無限にタイマーを実行することになります。たとえば、以下のコード:

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

上記のコードはクライアントが接続すると、タイマーが 1 つ追加されますが、ビジネスコード全体でタイマーを削除するロジックがありません。こうして時間の経過とともに、プロセス内がどんどんタイマーが増え、最終的にプロセスが無限にタイマーを実行することにより busy 状態になります。
正しいコード:

$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();