Back-End

10 jul, 2015

Criando um serviço Daemon PHP

Publicidade

Daemons são aplicativos especiais que podem monitorar e processar uma atividade importante em uma máquina no background.

Leia este artigo para aprender como criar um daemon em PHP puro, lidar com sinais, lidar com múltiplos I/O assíncronos e eventos com libevent, monitorar o desempenho do daemon, iniciar o daemon e distribuir seu aplicativo daemon como um arquivo PHAR.

O que é um Daemon?

O termo daemon foi inventado pelos programadores do Projeto MAC no MIT. É inspirado no demônio de Maxwell responsável pela triagem de moléculas no background. Os sistemas UNIX adotaram essa terminologia para programas daemon.

Ele também se refere a uma personagem da mitologia grega que executa as tarefas que os deuses não querem assumir. Como se afirma no “Administrador do Sistema de Referência UNIX”, na Grécia antiga, o conceito de “demônio pessoal” foi, em parte, comparável ao conceito moderno de “anjo da guarda”. A família de sistemas operacionais BSD usa a imagem como um logotipo do daemon.

Os daemons são iniciados normalmente no momento da inicialização da máquina. No sentido técnico, um daemon é considerado um processo que não tem um terminal de controle e, consequentemente, não há nenhuma interface do usuário. Na maioria das vezes, o processo ancestral do daemon é init – processo root no UNIX, embora muitos daemons rodem a partir de scripts RCD especiais iniciados em um console de terminal.

Richard Stevenson descreve os seguintes passos para escrever daemons:

  1. Reiniciar o modo do arquivo da máscara de criação para função 0 umask (), para mascarar algumas partes de direitos de acesso a partir do processo de inicialização.
  2. Provocar fork () e terminar o processo pai. Isso é feito para que se o processo tiver sido lançado como um grupo, o shell acredita que o grupo terminou ao mesmo tempo, o filho herda o ID do grupo de processo do pai e recebe o seu próprio ID do processo. Isso garante que ele não vai se tornar líder do grupo de processo.
  3. Criar uma nova sessão chamando setsid (). O processo se torna um líder da nova sessão, o líder de um novo grupo de processos e perde o controle do terminal.
  4. Tornar o diretório raiz do diretório de trabalho atual o diretório atual que será montado.
  5. Fechar todos os descritores de arquivos.
  6. Fazer descritores de redirecionamento 0,1 e 2 (STDIN, STDOUT e STDERR) para /dev /null ou arquivos /var/log/project_name.out porque algumas funções da biblioteca padrão usam esses descritores.
  7. Gravar o pid (processo número de identificação) no arquivo pid: /var/run/projectname.pid.
  8. Processar corretamente os sinais e SigTerm SigHup: acabe com a destruição de todos os processos filhos e arquivos pid e/ou reconfiguração.

Como criar daemons em PHP

Para criar daemons em PHP, você precisará usar as extensões pcntl e posix. Para implementar a comunicação rápida dentro de scripts daemon, recomenda-se usar a extensão libevent para I/O assíncrono.

Vamos dar uma olhada no código para iniciar um daemon:

umask(0); // § 1
$pid = pcntl_fork(); // § 2

if ($pid < 0) {
 print('fork failed');
 exit 1; 
}

Depois de um fork, a execução do programa funciona como se houvesse duas ramificações do código, uma para o processo pai e a outra para o processo filho. O que distingue esses dois processos é o valor do resultado retornado à chamada de função fork (). A ID do processo pai recebe o número do processo recém-criado, e o processo filho recebe um 0.

if ($pid > 0) // the parent process
 echo "daemon process started\n";
exit; // Exit
} 
// (pid = 0) child process
 
$sid = posix_setsid();// § 3
if ($sid < 0) exit 2;

chdir('/'); // § 4
file_put_contents($pidFilename, getmypid() ); // § 6

run_process();	// cycle start data

A implementação do passo 5 “para fechar todos os descritores de arquivo” pode ser feita de duas maneiras. Bem, fechar todos os descritores de arquivo é difícil de implementar em PHP. Você só precisa abrir quaisquer descritores de arquivos antes de fork (). Em segundo lugar, você pode substituir a saída padrão para um arquivo de log de erro usando init_set () ou usar o buffer utilizando ob_start () para uma variável e armazená-la no arquivo de log:

ob_start(); // slightly modified, § 5.
var_dump($some_object);  //some conclusions
$content = ob_get_clean(); // takes part of the output buffer and clears it
fwrite($fd_log, $content); // retains some of the data output to the log.

 

Tipicamente, ob_start () é o início do ciclo de vida do daemon, e as chamadas ob_get_clean () e fwrite () são o final. No entanto, você pode substituir diretamente STDIN, STDOUT e STDERR:

ini_set('error_log', $logDir.'/error.log'); // set log file
// $logDir - /var/log/mydaemon
// Closes an open file descriptors system STDIN, STDOUT, STDERR
fclose(STDIN);   
fclose(STDOUT);
fclose(STDERR);
// redirect stdin to /dev/null
$STDIN = fopen('/dev/null', 'r'); 
// redirect stdout to a log file
$STDOUT = fopen($logDir.'/application.log', 'ab');
// redirect stderr to a log file
$STDERR = fopen($logDir.'/application.error.log', 'ab');

Agora, o nosso processo é desconectado do terminal, e a saída padrão é redirecionada para um arquivo de log.

Manipulação de sinais

O processamento de sinal é realizado com os manipuladores que você pode usar, seja através da biblioteca pcntl (pcntl_signal_dispatch ()), seja usando libevent. No primeiro caso, você deve definir um manipulador de sinal:

// signal handler
function sig_handler($signo)
{
 global $fd_log;
 switch ($signo) {
  case SIGTERM:
    // actions SIGTERM signal processing
   fclose($fd_log); // close the log-file
   unlink($pidfile); // destroy pid-file
     exit;
     break;
  case SIGHUP:
    // actions SIGHUP handling
   init_data();// reread the configuration file and initialize the data again
     break;
  default:
     // Other signals, information about errors
 }
}

 // setting a signal handler
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");

Note que os sinais são processados apenas quando o processo está num modo ativo. Os sinais recebidos quando o processo está aguardando entrada ou no modo de suspensão não serão processados. Use a função de espera pcntl_signal_dispatch (). Podemos ignorar o sinal usando flag SIG_IGN: pcntl_signal (SIGHUP, SIG_IGN); Ou, se necessário, restaurar o manipulador de sinal usando a flag SIG_DFL, que foi previamente instalada por padrão: pcntl_signal (SIGHUP, SIG_DFL);

I/O assíncrono com libevent

No caso de você usar bloqueio, o processamento de sinal de entrada/saída não é aplicado. Recomenda-se utilizar a biblioteca libevent que fornece não-bloqueio como entrada/saída, sinais de processamento e temporizadores. A biblioteca libevent fornece um mecanismo simples para iniciar as funções de callback para eventos do descritor de arquivo: Read, Timeout, Signal.

Inicialmente, você tem que declarar um ou mais eventos com um manipulador (função de callback) e anexá-los ao contexto básico dos eventos:

$base = event_base_new();  // create a context for monitoring basic events
// create a context of current events, one context for each type of events
$event = event_new();
$errno = 0;
$errstr = '';
// the observed object (handle)
$socket = stream_socket_server(“tcp://$IP:$port“, $errno, $errstr);
stream_set_blocking($socket, 0);						 // set to non-blocking mode
// set handler to handle
event_set($event, $socket, EV_READ | EV_PERSIST, 'onAccept', $base);

Os manipuladores de função ‘onRead’, ‘onWrite’, ‘onError’ devem implementar a lógica de processamento. Os dados são gravados na memória intermédia, que é obtida no modo de não-bloqueio:

function onRead($buffer, $id) {
    // reading from the buffer to 256 characters or EOF
   while($read = event_buffer_read($buffer, 256)) {
    var_dump($read);
  }
}

O eventos principal loop é executado com a função event_base_loop ($ base);. Com algumas linhas de código, você pode sair do manipulador chamando unicamente event_base_loobreak (); ou após o tempo especificado (timeout) event_loop_exit () ;.

Manipulação de erros lida com falha de eventos:

function onError($buffer, $error, $id) {
    // declare global variables as an option - class variables
    global $id, $buffers, $ctx_connections;
    // deactivate buffer
  event_buffer_disable($buffers[$id], EV_READ | EV_WRITE);
    // free, context buffer
  event_buffer_free($buffers[$id]);
    // close the necessary file / socketed destkriptory
  fclose($ctx_connections[$id]);
    // frees the memory occupied by the buffer
  unset($buffers[$id], $ctx_connections[$id]);
}

Deve-se observar a seguinte sutileza: trabalhar com temporizadores só é possível pelo descritor de arquivo. O exemplo de documentação oficial não funciona. Aqui está um exemplo de processamento que é executado em intervalos regulares.

$event2 = event_new();
// use as an event arbitrary file descriptor of the temporary file
$tmpfile = tmpfile();
event_set($event2, $tmpfile, 0, 'onTimer', $interval);		
$res = event_base_set($event2, $base);		
event_add($event2, 1000000 * $interval);

Com esse código, podemos ter um temporizador trabalhando que é encerrado apenas uma vez. Se precisarmos de um temporizador “permanente”, usando a função ontimer precisamos criar um novo evento de cada vez, e voltar a atribuí-lo para processar através de um “período de tempo”:

function onTimer($tmpfile, $flag, $interval) {
 $global $base, $event2;

 if ($event2) {
    event_delete($event2);
    event_free($event2);
 }

 call_user_function(‘process_data’,$args);

 $event2 = event_new();	
 event_set($event2, $tmpfile, 0, 'onTimer', $interval);		
 $res = event_base_set($event2, $base);		
 event_add($event2, 1000000 * $interval);
}

No final do daemon, devemos liberar todos os recursos previamente alocados:

// delete the context of specific events from the database monitoring is performed for each event
event_delete($event);
// free the context of a particular event is executed for each event
event_free($event);
// free the context of basic events monitoring
event_base_free($base);

// bind event to the base context
event_base_set($event, $base);
// add/activate event monitoring
event_add($event);

Deve-se notar também que, para o manipulador de processamento de sinal, é ajustada a flag EV_SIGNAL: event_set ($ evento, SIGHUP, EV_SIGNAL, ‘onSignal’, $ base);

Caso seja necessário o processamento de sinal constante, é necessário estabelecer uma flag EV_PERSIST. Segue aqui um manipulador para o evento onAccept, que ocorre quando uma nova conexão é aceita em um descritor de arquivo:

// function handler to the emergence of a new connection
function onAccept($socket, $flag, $base) {
  global $id, $buffers, $ctx_connections;
  $id++;
  $connection = stream_socket_accept($socket);
  stream_set_blocking($connection, 0);
// create a new buffer and tying handlers read / write access to the buffer or illustrations of error.
  $buffer = event_buffer_new($connection, 'onRead', NULL, 'onError', $id);
// attach a buffer to the base context
  event_buffer_base_set($buffer, $base);
// exhibiting a timeout if there is no signal from the source
  event_buffer_timeout_set($buffer, 30, 30);
  event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff); // flag is set
  event_buffer_priority_set($buffer, 10); // set priority
  event_buffer_enable($buffer, EV_READ | EV_PERSIST); // flag is set
  $ctx_connections[$id] = $connection;
  $buffers[$id] = $buffer;
}

Monitorando um daemon

É uma boa prática desenvolver o aplicativo de modo que seja possível monitorar o processo do daemon. Os indicadores-chave de monitoramento são o número de itens processados /pedidos no intervalo de tempo, a velocidade de processamento com consultas, o tempo médio para processar um único pedido ou tempo de inatividade.

Com a ajuda dessas métricas, o volume de trabalho do nosso daemon pode ser entendido, e se ele não lidar com a carga ele recebe, você pode executar outro processo em paralelo, ou executar vários processos filhos.

Para determinar essas variáveis, você precisa verificar essas características em intervalos regulares, como uma vez por segundo. Por exemplo, o tempo de inatividade é calculado como a diferença entre o intervalo de medição e o tempo total do daemon.

Tipicamente, o tempo de inatividade é determinado como uma percentagem de um intervalo de medição. Por exemplo, se no primeiro segundo foram executados 10 ciclos com um tempo de processamento total de 50 ms, o tempo será 950ms ou 95%.

A consulta atual enquanto for 10rps (solicitação por segundo). Tempo de processamento médio de uma solicitação: a razão entre o tempo total gasto no processamento de solicitações e o número de solicitações processadas será de 5 ms.

Essas características, bem como os recursos adicionais, como tamanho da fila da pilha de memória, número de transações, o tempo médio para acessar o banco de dados, e assim por diante.

Um monitor externo pode ser obter dados através de uma ligação TCP ou um socket unix, geralmente sob a forma de Nagios ou zabbix, dependendo do sistema de monitoramento. Para fazer isso, o daemon deve usar uma porta de sistema adicional.

Como mencionado acima, se um processo de trabalho não pode lidar com a carga, geralmente rodamos vários processos paralelos. Iniciar um processo paralelo deve ser feito pelo processo mestre pai que usa fork () para lançar uma série de processos filhos.

Por que não processar usando exec () ou system ()? Porque, como regra geral, você deve ter controle direto sobre os processos mestre e filho. Nesse caso, podemos lidar com isso através de sinais de interação. Se você usar o comando exec ou sistema, em seguida inicie o intérprete inicial, e ele já terá começado processos que não são descendentes diretos do processo pai.

Além disso, há um equívoco que você pode fazer em um processo daemon através do comando nohup. Sim, é possível emitir um comando: -master nohup php mydaemon.php >> /var/log/daemon.log 2 >> /var/log/daemon.error.log &

Mas, nesse caso, seria difícil executar a rotação de log, enquanto nohup “captura” descritores de arquivo para STDOUT / STDERR e os libera apenas no final do comando, o que pode sobrecarregar o processo ou a todo o servidor. A sobrecarga do processo daemon pode afetar a integridade do processamento de dados e, eventualmente, causar a perda parcial de alguns dados.

Iniciando um daemon

Iniciar o daemon deve acontecer de forma automática em tempo de boot, ou com a ajuda de um “script de inicialização”.

Todos os scripts de inicialização estão geralmente no diretório /etc/rc.d. O script de inicialização no diretório de serviço é feito em /etc/init.d/. Execute o comando start service myapp ou start group /etc/init.d/myapp, dependendo do tipo de sistema operacional.

Segue um texto script de exemplo:

#! /bin/sh
#
$appdir = /usr/share/myapp/app.php
$parms = --master –proc=8 --daemon
export $appdir
export $parms
if [ ! -x appdir ]; then
 exit 1
fi

if [ -x /etc/rc.d/init.d/functions ]; then
   . /etc/rc.d/init.d/functions
fi

RETVAL=0

start () {
 echo "Starting app"
 daemon /usr/bin/php $appdir $parms 
 RETVAL=$?
 [ $RETVAL -eq 0 ] && touch /var/lock/subsys/mydaemon
 echo
 return $RETVAL
}

stop () {
 echo -n "Stopping $prog: "
 ?????????  killproc /usr/bin/fetchmail
 RETVAL=$?
 [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/mydaemon
 echo
 return $RETVAL
}

case $1 in
 start)
  start
  ;;
 stop)
  stop
  ;;
 restart)
  stop
  start
  ;;
 status)
  status /usr/bin/mydaemon
  ;;
 *)
  echo "Usage: $0 {start|stop|restart|status}"
  ;;
RETVAL=$?
exit $RETVAL

Distribuindo seu daemon PHP

Para distribuir um daemon, é melhor empacotá-lo em um único módulo de arquivo phar. O módulo montado deve incluir todos o PHP necessário e osarquivos .ini.

Segue um exemplo de um script de compilação:

if (is_file('app.phar')) {
 unlink('app.phar');
}
$phar = new Phar('app.phar', 0, 'app.phar');
$phar->compressFiles(Phar::GZ);
$phar->setSignatureAlgorithm (Phar::SHA1);
$files = array();
$files['bootstrap.php'] = './bootstrap.php';
$rd = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.'));
foreach($rd as $file){
 if ($file->getFilename() != '..' && $file->getFilename() != '.' && $file->getFilename() != __FILE__) {
  if ( $file->getPath() != './log'&& $file->getPath() != './script'&& $file->getPath() != '.')	 
    $files[substr($file->getPath().DIRECTORY_SEPARATOR.$file->getFilename(),2)]=$file->getPath().DIRECTORY_SEPARATOR.$file->getFilename();
 }
}
if (isset($opt['version'])) {
 $version = $opt['version'];
 $file = "buildFromIterator(new ArrayIterator($files));
 $phar->setStub($phar->createDefaultStub('bootstrap.php'));
 $phar = null;
}

Além disso, pode ser aconselhável fazer um pacote PEAR como um utilitário de console unix padrão que, quando executado sem nenhum argumento, imprime sua própria instrução de uso:

#php app.phar
myDaemon version 0.1 Debug
  usage:
--daemon – run as daemon
--debug – run in debug mode
--settings – print settings
--nofork – not run child processes
--check – check dependency modules
--master – run as master
--proc=[8] – run child processes

Conclusão

Criar daemons em PHP não é difícil, mas para fazê-los funcionar corretamente é importante seguir os passos descritos neste artigo.

Deixe um comentário aqui se você tiver dúvidas ou comente sobre como criar daemons de serviço em PHP.

***

Dmitry Mamontov faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://www.phpclasses.org/blog/post/270-Creating-a-PHP-Daemon-Service.html