Отладка занятых процессов

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

Поиск с использованием команды strace+lsof

1. Найти PID занятого процесса в статусе
После выполнения команды 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, указывающий на блокировку в каком-либо системном вызове. Если по-прежнему не отображается никаких системных вызовов, это означает, что программа, скорее всего, находится в бесконечном цикле бизнес-логики, см. пункт 2 раздела «Решение длительного состояния занятости процесса» в конце страницы.

Если система блокируется на системных вызовах epoll_wait или select, это означает, что процесс уже находится в состоянии idle.

3. Просмотр дескрипторов процесса с помощью lsof
Выполните команду lsof -nPp 11725, результат будет подобен следующему:

Дескриптор 16 соответствует записи 16u (последняя строка), где можно увидеть, что дескриптор fd=16 представляет собой TCP-соединение с удаленным адресом 101.37.136.135:80, что указывает на то, что процесс, вероятно, ожидает ответа от HTTP-сервера. Это объясняет, почему процесс находится в состоянии busy.

Решение:
После определения местонахождения блокировки в бизнес-логике необходимо легко решить проблему. Например, в случае, описанном выше, процесс, вероятно, ждет ответ от сервера при вызове curl, и провайдеру url следует устранить причину медленного ответа по url. Также при вызове curl следует добавить параметр таймаута, например, если ответ не был получен в течение 2 секунд, тогда запрос завершится, что предотвратит длительную блокировку.

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

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

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

2. Бесконечный цикл в коде
Симптомы: Вывод команды top покажет, что занятый процесс потребляет большое количество CPU, и команда strace -ttp pid не выведет информацию о системных вызовах.
Решение: Следуйте инструкциям из статьи от "птичьего пера" с использованием gdb и исходного кода PHP. В общем, шаги выглядят так:

  1. Используйте команду php -v для просмотра версии.
  2. Скачайте исходный код PHP.
  3. Выполните команду gdb --pid=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();

В этом коде при каждом соединении клиента добавляется новый таймер, и если логика не включает удаление таймеров, то с течением времени в процессе будет накапливаться все больше и больше таймеров, что в конечном итоге приведет к постоянному исполнению таймеров и состоянию занятости.
Вот исправленный вариант кода:

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