Depurando processos em estado busy
Às vezes, podemos observar processos com o estado busy ao executar o comando php start.php status, o que indica que esses processos estão lidando com alguma tarefa. Normalmente, após o término do processamento, os processos correspondentes retornam ao estado idle. Em geral, isso não apresenta problemas. No entanto, se um processo continuar em estado busy e não retornar ao estado idle, isso pode indicar que há um bloqueio ou um loop infinito na lógica do processo. Abaixo estão alguns métodos para identificar o problema.
Localizando com os comandos strace+lsof
1. Encontre o pid do processo busy
Execute php start.php status e observe a saída a seguir:

Na imagem, o pid do processo com estado busy é 11725 e 11748.
2. Rastreie o processo com strace
Escolha um dos pids (neste caso, vamos selecionar 11725) e execute strace -ttp 11725. A saída será semelhante ao seguinte:

Podemos observar que o processo está em um loop constante de chamadas do sistema poll([{fd=16, events=.... esperando por eventos de leitura do descritor fd 16, ou seja, está aguardando que esse descritor retorne dados.
Se não houver nenhuma chamada de sistema sendo exibida, mantenha o terminal atual aberto, abra um novo terminal e execute kill -SIGALRM 11725 (para enviar um sinal de alarme para o processo) e então verifique se o terminal strace apresenta alguma resposta, indicando que o processo está bloqueado em alguma chamada de sistema. Se ainda assim não houver nenhuma informação de chamadas de sistema, isso sugere que o programa está provavelmente em um loop infinito. Consulte a seção inferior da página sobre outras causas que podem tornar o processo busy por um longo período.
Se o sistema estiver bloqueado em chamadas de sistema como epoll_wait ou select, isso é um sinal normal, o que indica que o processo está em estado idle.
3. Verifique os descritores de processo com lsof
Execute lsof -nPp 11725 e a saída será:

O descritor 16 corresponde ao registro de 16u (última linha). É possível ver que o descritor fd=16 é uma conexão tcp, com o endereço remoto 101.37.136.135:80, o que indica que o processo está acessando um recurso http. O loop poll([{fd=16, events=.... está constantemente aguardando o servidor http retornar dados, o que explica por que o processo está em estado busy.
Solução:
Sabendo onde o processo está bloqueado, a resolução se torna mais fácil. Por exemplo, a análise levou à conclusão de que a lógica do negócio está chamando curl, e que a url correspondente não está retornando dados há muito tempo, fazendo com que o processo continue aguardando. Nessa situação, podemos contatar o fornecedor da url para entender a lentidão no retorno, e também é recomendável adicionar parâmetros de timeout na chamada curl, como por exemplo, se em 2 segundos não houver resposta, isso é tratado como um timeout, evitando bloqueios prolongados (assim o processo pode experimentar um estado busy por cerca de 2 segundos).
Outras causas que fazem um processo ficar busy por muito tempo
Além do bloqueio de processos que leva a um estado busy, existem outras causas que podem resultar em um processo permanecer nesse estado.
1. Erros fatais na lógica do negócio que levam o processo a sair continuamente
Sintomas: Nessa situação, o sistema apresenta alta carga, com a média de carga (load average) no status sendo 1 ou maior. Observam-se números elevados do exit_count do processo, que continuam a aumentar.
Solução: Execute o Worker em modo de depuração (php start.php start sem a opção -d) e veja as mensagens de erro, corrigindo os problemas conforme necessário.
2. Loop infinito no código
Sintomas: No top, conseguimos ver que o processo busy está consumindo uma quantidade significativa de CPU e o comando strace -ttp pid não imprime nenhuma informação sobre chamadas de sistema.
Solução: Consulte o artigo de Niao Ge para localizar usando gdb e o código fonte do php. Os passos resumidos são aproximadamente os seguintes:
- Execute
php -vpara verificar a versão. - Baixe o código fonte correspondente à versão do php.
- Execute
gdb --pid=pid do processo busy. - Em seguida, execute
source caminho_do_codigo_fonte_php/.gdbinit. - Finalmente, utilize
zbacktracepara imprimir a pilha de chamadas.
A última etapa permitirá que você veja a pilha de chamadas que o código php está executando atualmente, revelando a localização do loop infinito.
Atenção: sezbacktracenão retornar a pilha de chamadas, pode ser que sua compilação do php não inclua o parâmetro-g, sendo necessário recompilar o php e reiniciar o workerman para orientações adicionais.
3. Adição infinita de timers
O código do negócio continua a adicionar timers sem removê-los, resultando em um aumento contínuo dos timers dentro do processo, o que eventualmente causa execução infinita de timers. Por exemplo, o seguinte código:
$worker = new Worker;
$worker->onConnect = function($con){
Timer::add(10, function(){});
};
Worker::runAll();
O código acima adiciona um timer sempre que uma conexão de cliente é estabelecida, mas não inclui lógica para remover o timer, levando a um aumento contínuo nos timers ao longo do tempo, causando eventual execução infinita de timers e resultando em estado busy.
Código correto:
$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();