Trabalhando com processos de longa duração no PHP

Processos de longa duração

Trabalhando com processos de longa duração no PHP

Atenção! Essa postagem foi escrita há mais de 2 anos. Na informática tudo evolui muito rápido e algumas informações podem estar desatualizadas. Embora o conteúdo possa continuar relevante, lembre-se de levar em conta a data de publicação enquanto estiver lendo. Caso tenha sugestões para atualizá-la, não deixe de comentar!

Para trabalhar com processos de longa duração, como daemons e serviços que ficam rodando o tempo todo no servidor, utilizando o PHP, é necessário ter alguns conhecimentos específicos.

Gostaria de apresentar dois exemplos reais que requerem processos PHP que rodem por longos períodos de tempo (ou até indefinidamente):

  1. Processamento de dados: Todas as noites o seu servidor precisa vasculhar milhões de dados para atualizar rankings, tabelas e estatísticas.
  2. Dados da API de Streaming do Twitter: Requer uma conexão constante com a API do Twitter para receber as mensagens conforme elas são postadas.

As ferramentas

Uma das melhores ferramentas que temos é o PHP CLI (Command Line Interface). Essa ferramenta permite que possamos rodar o PHP diretamente da linha de comando (mais conhecido como Terminal) sem possuir um tempo limite. Todas as dores com o set_time_limit() e as brigas com o php.ini vão embora!

Se você vai trabalhar com linha de comando, provavelmente você vai precisar saber um pouquinho de bash scripting. Deixo aqui uma dica: http://aurelio.net/shell/ – uma fonte ilimitada de conhecimento sobre shell/bash.

Outra coisa bacana é o cron (crontab / cron job)

SSH vs Cron Jobs

Você precisa saber rodar algo em uma sessão SSH é diferente do que configurar um cronjob pra fazer algo pra você. Uma sessão SSH pode ser um bom lugar para testar scripts e rodá-los uma vez apenas, enquanto um cronjob é a maneira correta de configurar um script que você deseja que rode regularmente.

Se eu escrever isso na minha sessão SSH

php meuScript.php

irei executar o meuScript.php. Contudo, meu terminal ficará bloqueado até que a execução seja encerrada.

Você pode passar por isso segurando Ctrl + Z (pausa a execução) e utilizando o comando bg (envia o processo para o background).

Para processos longos, pode ser bacana, mas se você perder a sessão SSH, a execução do script será encerrada.

Você pode driblar esse problema utilizando o nohup (no hangup – “não deixar cair”).

nohup php meuScript.php

O comando nohup permite que a execução continue mesmo que você perca a sessão. Isso quer dizer que se você utilizar esse comando, enviar o processo para o background e perder a conexão com o SSH, seu comando continuará rodando.

Mas ainda acho que a melhor maneira de executar um processo diretamente no background é utilizando um & (ampersand – “e comercial”). Você adiciona no final do comando e ele vai diretamente para o background, sem precisar de nenhum atalho.

nohup php meuScript.php &

É claro que tudo isso apenas importa se você estiver rodando comandos manualmente. Se você scripts com uma certa regularidade e usando os cronjobs, então você não precisa se preocupar com esses problemas. Uma vez que é o próprio servidor quem os executa, a sessão SSH não interessa pra nada.

Algumas vezes, você pode esquecer se rodar o processo utilizando o comando nohup e, mesmo assim, querer que ele continue rodando após a desconexão do SSH. Você pode tentar rodar scripts de madrugada, por achar que serão mais rápidos, e acabar descobrindo que eles demoram muito mais. Aqui fica uma pequena dica que vai te ajudar a rodar o script como daemon, assim não se encerrará ao término da sessão SSH.

  1. Ctrl + Z para pausar o programa e voltar ao shell
  2. bg para rodar no background
  3. disown -h [num-job], onde [num-job] é o número do job que está rodando (exemplo: %1 para o primeiro job rodando; você pode listar os jobs usando o comando jobs). Isso retirará sua propriedade do processo e ele não se encerrará após a saída do terminal.

Créditos da solução ao usuário Node no StackOverflow

Processando dados com PHP

Para quem roda scripts regularmente, é interessante criar um bash script para ser executado por um job agendado.

Exemplo de um bash script que, na verdade, roda um script PHP:

Example bash script which actually runs the PHP script:

#!/bin/sh
php /caminho/para/script.phpb

Exemplo de um cronjob:

0 23 * * * /bin/sh /caminho/para/bashScript.sh

Se você não sabe onde colocar o código acima, utilize o comando crontab -e para editar a sua tabela do cron e salvá-la. O 0 23 * * * indica que o script rodará aos 0 minutos, 23 horas de qualquer dia, em qualquer mês e em qualquer dia da semana (agradeço ao mbodock, que me alertou um detalhe que eu havia deixado passar aqui).

# *    *    *    *    *  comando
# ┬    ┬    ┬    ┬    ┬
# │    │    │    │    │
# │    │    │    │    │
# │    │    │    │    └───── dia da semana (0 - 6) (sunday - saturday)
# │    │    │    └────────── mês (1 - 12)
# │    │    └─────────────── dia do mês (1 - 31)
# │    └──────────────────── hora (0 - 23)
# └───────────────────────── minuto (0 - 59)

Exemplo extraído do artigo Cron da Wikipedia.

Vale também ressaltar que existem alguns nomes pré-definidos que você pode utilizar no lugar do horário de agendamento:

Abreviação Descrição Equivalente a
@yearly (ou @annually) Uma vez por ano, à meia-noite de 1º de Janeiro 0 0 1 1 *
@monthly Uma vez por mês, à meia-noite do primeiro dia do mês 0 0 1 * *
@weekly Uma vez por semana, à meia-noite de domingo 0 0 * * 0
@daily Uma vez por dia, à meia-noite 0 0 * * *
@hourly Uma vez por hora, no começo da hora 0 * * * *
@reboot Quando a máquina é inicializada @reboot

Bom, agora nós temos um script básico que rodará todas as noites às 11h. Não importa quanto tempo levará para executar, apenas irá começar às 23h e só encerrará quando terminar.

Twitter Streaming API

O segundo problema é mais interessante, porque o script PHP precisa estar rodando para coletar dados. Queremos que esteja rodando o tempo todo.

Para fazer o trabalho de capturar os dados da API do Twitter, podemos usar a excelente biblioteca Phirehose (fica a dica).

A partir daí, teremos um script que manterá permanentemente uma conexão aberta com a API do Twitter, mas não podemos garantir que estará sempre rodando. O servidor pode reiniciar, podemos ter algum tipo de problema na execução ou erros de qualquer outra natureza.

Então uma solução que podemos lançar mão é: criar um bash script para ter certeza de que o processo está rodando. E se não estiver, que comece a rodá-lo.

#!/bin/sh

ps aux | grep '[m]euScript.php'
if [ $? -ne 0 ]
then
php /caminho/para/meuScript.php
fi

Agora vamos dar uma olhada, linha por linha, o que faz esse script:

#!/bin/sh

Inicia o script indicando qual é o path do shell.

ps aux | grep '[m]euScript.php'

O comando ps lista os processos. Depois há um pipe (|) que fará com que seja executado um comando sobre o resultado da saída. O comando grep irá procurar por ‘[m]euScript.php’. Usei a expressão regular [m] para que o comando não encontre a ele mesmo. O grep lançará um processo com meuScript.php no comando, então você sempre encontrará um resultado se não colocar alguma coisa nos brackets.

if [ $? -ne 0 ]

Verifica o retorno do último comando. Então se nada for retornado, significa que não existe nenhum resultado para [m]euScript na lista de processos.

then
php /caminho/para/meuScript.php
fi

Essas linhas são executadas se o script php não for encontrado rodando. É esse comando que irá rodar nosso script php. Então a condicional é encerrada com fi.

Agora vamos criar um cron job para executar o script acima:

* * * * * /bin/sh rodarPraSempre.sh

Isso fará com que o sistema fique checando o tempo todo se o nosso script está rodando. Se não estiver, fará rodar.

Considerações finais

Dá pra perceber que o script de streaming do Twitter é uma versão mais avançada do processamento de dados. Ambas as versões de produção, possuem muito mais coisas, obviamente, mas vão além do objetivo proposto nesse artigo. Se você estiver interessado em estender esses scripts, é uma boa ideia ativar o sistema de logs, pois vai te ajudar a ter uma noção de como o seu sistema está se comportando.

Um abraço a todos e fiquem com Deus!
Rafael Jaques

Texto adaptado do original: http://reviewsignal.com/blog/2013/08/22/long-running-processes-in-php/