Отладка процессов в состоянии busy

Иногда с помощью команды php start.php status мы можем увидеть процессы в состоянии busy, что означает, что соответствующий процесс обрабатывает бизнес-логику. В нормальных условиях, после завершения обработки бизнес-логики, соответствующий процесс должен вернуться в состояние idle. Обычно это не вызывает проблем. Однако если процесс все еще находится в состоянии busy и не возвращается в состояние idle, это может указывать на блокировку или бесконечный цикл внутри процесса. Ниже представлены методы для диагностики таких ситуаций.

Использование команд strace и lsof для диагностики

1. Найдите pid процесса в состоянии busy в статусе
Запустив команду php start.php status, вы получите следующий вывод:

В процессе с состоянием busy pid равен 11725 и 11748.

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, чтобы выяснить причину медленного ответа, а также добавить параметр тайм-аута в вызов curl, например, если ответ не пришел в течение 2 секунд, то завершить ожидание. Это поможет избежать длительных блокировок (таким образом процесс может находиться в состоянии busy около 2 секунд).

Другие причины длительного состояния busy у процессов

Помимо блокировки процессов, которая может привести к состоянию busy, существуют и другие причины, способные вызвать такую ситуацию.

1. Фатальная ошибка в бизнес-логике, приводящая к постоянному завершению процесса
Явление: В таких случаях можно наблюдать высокую нагрузку на систему, значение load average в status равно 1 или больше. Видно, что цифра exit_count у процесса высокая и постоянно растет.
Решение: Запустите workerman в режиме отладки (php start.php start без добавления -d) и посмотрите, возникли ли ошибки в бизнес-логике. Устраните выявленные ошибки.

2. Бесконечный цикл в коде
Явление: В top можно видеть, что процесс busy занимает много CPU, команда strace -ttp pid не выводит никаких сообщений о системных вызовах.
Решение: Обратитесь к статье Нiao Гэ по локализации с помощью gdb и исходного кода PHP, шаги изложены примерно следующим образом:

  1. php -v для проверки версии.
  2. Скачивание соответствующего исходного кода PHP.
  3. gdb --pid=busy进程的pid.
  4. source php源码路径/.gdbinit.
  5. zbacktrace для печати стека вызовов.
    На последнем шаге можно увидеть текущий стек вызовов PHP кода и, таким образом, узнать местоположение бесконечного цикла.
    Обратите внимание: если zbacktrace не выводит стек вызовов, возможно, при компиляции PHP не был добавлен параметр -g, потребуется пересобрать PHP и перезапустить workerman для диагностики.

3. Бесконечное добавление таймеров
Бизнес-код постоянно добавляет таймеры, не удаляя их, что приводит к тому, что их становится все больше, в конечном итоге это вызывает бесконечное выполнение таймеров в процессе. Например, следующий код:

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

Этот код добавляет таймер при каждом подключении клиента, но в целом бизнес-код не содержит логики для удаления таймеров. Таким образом, с течением времени в процессе будет постоянно добавляться таймер, что в конечном итоге приведет к бесконечному запуску таймеров и долгому состоянию 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();