Executar tarefa PHP de forma assíncrona


144

Eu trabalho em um aplicativo da Web um tanto grande, e o back-end é principalmente em PHP. Existem vários lugares no código em que preciso concluir alguma tarefa, mas não quero fazer o usuário esperar pelo resultado. Por exemplo, ao criar uma nova conta, preciso enviar a eles um email de boas-vindas. Mas quando eles apertam o botão 'Concluir registro', não quero fazê-los esperar até que o email seja realmente enviado, só quero iniciar o processo e retornar uma mensagem ao usuário imediatamente.

Até agora, em alguns lugares, tenho usado o que parece um hack com exec (). Basicamente, fazendo coisas como:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

O que parece funcionar, mas estou me perguntando se existe uma maneira melhor. Estou pensando em escrever um sistema que enfileire tarefas em uma tabela MySQL e um script PHP de execução longa separado que consulta essa tabela uma vez por segundo e executa as novas tarefas que encontrar. Isso também teria a vantagem de me permitir dividir as tarefas entre várias máquinas operadoras no futuro, se eu precisasse.

Estou reinventando a roda? Existe uma solução melhor do que o exec () hack ou a fila do MySQL?

Respostas:


80

Usei a abordagem de enfileiramento e funciona bem, pois você pode adiar esse processamento até a carga do servidor ficar ociosa, permitindo que você gerencie sua carga com bastante eficiência se puder particionar facilmente "tarefas que não são urgentes".

Rolar o seu próprio não é muito complicado, aqui estão algumas outras opções para conferir:

  • GearMan - esta resposta foi escrita em 2009 e, desde então, o GearMan parece uma opção popular, veja os comentários abaixo.
  • ActiveMQ se você desejar uma fila de mensagens de código aberto completa.
  • ZeroMQ - esta é uma biblioteca de soquetes bastante interessante, que facilita a gravação de código distribuído sem ter que se preocupar muito com a programação do soquete. Você pode usá-lo para enfileiramento de mensagens em um único host - basta que seu aplicativo da Web envie algo para uma fila que um aplicativo de console em execução contínua consumiria na próxima oportunidade adequada
  • beanstalkd - encontrou somente este enquanto escrevia esta resposta, mas parece interessante
  • O dropr é um projeto de fila de mensagens baseado em PHP, mas não é mantido ativamente desde setembro de 2010
  • O php-enqueue é um invólucro mantido recentemente (2017) em torno de uma variedade de sistemas de filas
  • Por fim, um post sobre o uso do memcached para enfileiramento de mensagens

Outra abordagem, talvez mais simples, é usar ignore_user_abort - depois de enviar a página ao usuário, você pode fazer o processamento final sem medo de encerramento prematuro, embora isso pareça prolongar o carregamento da página do usuário. perspectiva.


Obrigado por todas as dicas. O específico sobre ignore_user_abort realmente não ajuda no meu caso, meu objetivo é evitar atrasos desnecessários para o usuário.
Davr

2
Se você definir o cabeçalho HTTP de comprimento de conteúdo na resposta "Obrigado por registrar", o navegador deverá fechar a conexão após o recebimento do número especificado de bytes. Isso deixa o processo do lado do servidor em execução (assumindo que ignore_user_abort está definido) sem fazer com que o usuário final espere. Obviamente, você precisará calcular o tamanho do seu conteúdo de resposta antes de renderizar os cabeçalhos, mas isso é muito fácil para respostas curtas.
Peter

1
O Gearman ( gearman.org ) é uma ótima fila de mensagens de código aberto que é multiplataforma. Você pode escrever trabalhadores em C, PHP, Perl ou qualquer outra linguagem. Existem plugins Gearman UDF para MySQL e você também pode usar o Net_Gearman do PHP ou o cliente gearman pear.
Justin Swanhart

O Gearman seria o que eu recomendaria hoje (em 2015) sobre qualquer sistema de enfileiramento de trabalho personalizado.
Peter

Outra opção é configurar um servidor js do nó para manipular uma solicitação e retornar uma resposta rápida com uma tarefa intermediária. Muitas coisas dentro de um script js do nó são executadas de forma assíncrona, como uma solicitação http.
Zordon

22

Quando você deseja apenas executar uma ou várias solicitações HTTP sem precisar esperar pela resposta, também existe uma solução PHP simples.

No script de chamada:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like

No script.php chamado, você pode chamar estas funções PHP nas primeiras linhas:

ignore_user_abort(true);
set_time_limit(0);

Isso faz com que o script continue em execução sem limite de tempo quando a conexão HTTP for fechada.


set_time_limit não tem efeito se php executado em modo de segurança
Baptiste Pernet

17

Outra maneira de dividir os processos é via curl. Você pode configurar suas tarefas internas como um serviço da web. Por exemplo:

Em seguida, nos scripts acessados ​​pelo usuário, faça chamadas para o serviço:

$service->addTask('t1', $data); // post data to URL via curl

Seu serviço pode acompanhar a fila de tarefas com o mysql ou o que você quiser: está tudo encerrado no serviço e seu script está consumindo apenas URLs. Isso libera você para mover o serviço para outra máquina / servidor, se necessário (ou seja, facilmente escalável).

Adicionar autorização http ou um esquema de autorização personalizado (como os serviços da Web da Amazon) permite abrir suas tarefas para serem consumidas por outras pessoas / serviços (se você quiser) e você pode ir além e adicionar um serviço de monitoramento no topo para acompanhar status da fila e da tarefa.

É preciso um pouco de trabalho de configuração, mas há muitos benefícios.


1
Eu não gosto dessa abordagem porque sobrecarrega o servidor web
Oved Yavine

7

Eu usei o Beanstalkd em um projeto e planejei novamente. Descobri que é uma excelente maneira de executar processos assíncronos.

Algumas coisas que fiz com isso são:

  • Redimensionamento de imagem - e com uma fila levemente carregada passando para um script PHP baseado em CLI, o redimensionamento de imagens grandes (2mb +) funcionou muito bem, mas a tentativa de redimensionar as mesmas imagens em uma instância mod_php estava regularmente ocorrendo problemas de espaço de memória (I limitou o processo PHP a 32 MB e o redimensionamento levou mais do que isso)
  • verificações em um futuro próximo - o beanstalkd tem atrasos disponíveis (disponibilize esse trabalho para ser executado após X segundos) - para que eu possa executar 5 ou 10 verificações para um evento, um pouco mais tarde

Eu escrevi um sistema baseado no Zend-Framework para decodificar um URL 'legal', por exemplo, para redimensionar uma imagem que ele chamaria QueueTask('/image/resize/filename/example.jpg'). A URL foi decodificada primeiro em uma matriz (módulo, controlador, ação, parâmetros) e depois convertida em JSON para injeção na própria fila.

Um script cli de longa duração pegou o trabalho na fila, executou-o (via Zend_Router_Simple) e, se necessário, colocou informações em memcached para que o site PHP pegasse conforme necessário quando terminado.

Uma das rugas que eu também coloquei foi que o cli-script rodava apenas 50 loops antes de reiniciar, mas se quisesse reiniciar como planejado, o faria imediatamente (sendo executado através de um script bash). Se houvesse um problema e eu o fizesse exit(0)(o valor padrão para exit;ou die();), ele primeiro seria pausado por alguns segundos.


Eu gosto da aparência do beanstalkd, uma vez que eles adicionam persistência, acho que será perfeito.
Davr

Isso já está na base de código e está sendo estabilizado. Também estou ansioso para 'trabalhos nomeados', para que eu possa lançar coisas lá, mas sei que não será adicionado se já houver um lá. Bom para eventos regulares.
Alister Bulman

@AlisterBulman, você poderia dar mais informações ou exemplos de "Um script cli de longa duração, em seguida, pegou o trabalho na fila". Estou tentando criar um script cli para o meu aplicativo.
precisa saber é o seguinte

7

Se for apenas uma questão de fornecer tarefas caras, no caso de php-fpm ser suportado, por que não usar a fastcgi_finish_request()função?

Esta função libera todos os dados de resposta para o cliente e termina a solicitação. Isso permite que tarefas demoradas sejam executadas sem deixar aberta a conexão com o cliente.

Você realmente não usa assincronismo desta maneira:

  1. Faça todo o seu código principal primeiro.
  2. Execute fastcgi_finish_request().
  3. Faça todas as coisas pesadas.

Mais uma vez é necessário php-fpm.


5

Aqui está uma classe simples que eu codifiquei para meu aplicativo da web. Permite bifurcar scripts PHP e outros scripts. Funciona em UNIX e Windows.

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}

4

Esse é o mesmo método que venho usando há alguns anos e não vi nem encontrei nada melhor. Como as pessoas disseram, o PHP é de thread único, então não há muito mais que você possa fazer.

Na verdade, adicionei um nível extra a isso, que é obter e armazenar a identificação do processo. Isso permite que eu redirecione para outra página e faça com que o usuário fique nessa página, usando o AJAX para verificar se o processo está completo (o ID do processo não existe mais). Isso é útil nos casos em que o tamanho do script levaria o tempo limite ao navegador, mas o usuário precisa aguardar a conclusão do script antes da próxima etapa. (No meu caso, estava processando grandes arquivos ZIP com arquivos CSV, que adicionam até 30.000 registros ao banco de dados, após o qual o usuário precisa confirmar algumas informações.)

Eu também usei um processo semelhante para a geração de relatórios. Não tenho certeza se usaria o "processamento em segundo plano" para algo como um email, a menos que haja um problema real com um SMTP lento. Em vez disso, posso usar uma tabela como uma fila e, em seguida, ter um processo que é executado a cada minuto para enviar os e-mails dentro da fila. Você precisaria ter o cuidado de enviar e-mails duas vezes ou outros problemas semelhantes. Eu consideraria um processo de fila semelhante para outras tarefas também.


1
A que método você se refere na sua primeira frase?
Simon Médio


2

É uma ótima idéia usar o cURL, conforme sugerido pela rojoca.

Aqui está um exemplo. Você pode monitorar o texto.txt enquanto o script está sendo executado em segundo plano:

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>

2
Seria realmente útil se o código fonte fosse comentado. Não tenho idéia do que está acontecendo lá e quais partes são exemplo e quais são reutilizáveis ​​para meu próprio propósito.
Thomas Tempelmann

1

Infelizmente, o PHP não possui nenhum tipo de recurso de encadeamento nativo. Então, acho que nesse caso você não tem escolha a não ser usar algum tipo de código personalizado para fazer o que deseja.

Se você pesquisar na Internet por coisas de encadeamento de PHP, algumas pessoas terão maneiras de simular threads no PHP.


1

Se você definir o cabeçalho HTTP de comprimento de conteúdo na resposta "Obrigado por registrar", o navegador deverá fechar a conexão após o recebimento do número especificado de bytes. Isso deixa o processo do servidor em execução (assumindo que ignore_user_abort está definido) para que ele possa terminar de trabalhar sem fazer com que o usuário final espere.

É claro que você precisará calcular o tamanho do seu conteúdo de resposta antes de renderizar os cabeçalhos, mas isso é muito fácil para respostas curtas (gravar saída em uma string, chamar strlen (), chamar header (), chamar header (), render string).

Essa abordagem tem a vantagem de não forçá-lo a gerenciar uma fila de "front-end" e, embora seja necessário fazer algum trabalho no back-end para impedir que os processos filhos de corrida HTTP se interajam, isso é algo que você já precisava fazer , de qualquer forma.


Isso não parece funcionar. Quando o uso header('Content-Length: 3'); echo '1234'; sleep(5);, mesmo que o navegador leve apenas 3 caracteres, ele ainda aguarda 5 segundos antes de mostrar a resposta. o que estou perdendo?
Thomas Tempelmann

@ThomasTempelmann - Você provavelmente precisará chamar flush () para forçar a saída a ser renderizada imediatamente; caso contrário, a saída será armazenada em buffer até que o script saia ou dados suficientes sejam enviados ao STDOUT para liberar o buffer.
Peter

Eu já tentei várias maneiras de liberar, encontradas aqui no SO. Nenhuma ajuda. E os dados também parecem enviados sem gzip, como se pode ver phpinfo(). A única outra coisa que eu poderia imaginar é que preciso atingir um tamanho mínimo de buffer primeiro, por exemplo, 256 ou mais bytes.
Thomas Tempelmann

@ThomasTempelmann - Não vejo nada na sua pergunta ou na minha resposta sobre o gzip (geralmente faz sentido fazer o cenário mais simples funcionar primeiro antes de adicionar camadas de complexidade). Para determinar quando o servidor está realmente enviando dados, você pode usar um sniffer de pacote do plug-in do navegador (como violinista, tamperdata etc.). Então, se você achar que o servidor da web está realmente mantendo toda a saída do script até a saída, independentemente da liberação, será necessário modificar a configuração do servidor da web (não há nada que seu script PHP possa fazer nesse caso).
Peter

Eu uso um serviço da Web virtual, por isso tenho pouco controle sobre sua configuração. Eu esperava encontrar outras sugestões sobre o que poderia ser o culpado, mas parece que sua resposta simplesmente não é tão universalmente aplicável quanto parece. Muitas coisas podem dar errado, obviamente. Sua solução certamente é muito mais fácil de implementar do que todas as outras respostas fornecidas aqui. Pena que não funciona para mim.
Thomas Tempelmann

1

Se você não deseja o ActiveMQ completo, recomendo considerar o RabbitMQ . O RabbitMQ é um sistema de mensagens leve que usa o padrão AMQP .

Eu recomendo também examinar o php-amqplib - uma popular biblioteca cliente do AMQP para acessar os intermediários de mensagens baseados no AMQP.


0

Eu acho que você deve tentar esta técnica, ajudará a chamar quantas páginas você gosta, todas as páginas serão executadas de uma vez independentemente, sem esperar por cada resposta da página como assíncrona.

cornjobpage.php // página principal

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

PS: se você deseja enviar parâmetros de URL como loop, siga esta resposta: https://stackoverflow.com/a/41225209/6295712


0

Gerar novos processos no servidor usando exec()ou diretamente em outro servidor usando curl não é tão bom assim: se formos executivos, você basicamente estará preenchendo o servidor com processos de execução longa que podem ser gerenciados por outros servidores que não são da Web, e usar curl vincula outro servidor, a menos que você crie algum tipo de balanceamento de carga.

Eu usei o Gearman em algumas situações e acho melhor para esse tipo de caso de uso. Posso usar um único servidor de fila de tarefas para lidar basicamente com o enfileiramento de todas as tarefas que precisam ser executadas pelo servidor e ativar servidores de trabalho, cada um dos quais pode executar quantas instâncias do processo de trabalho forem necessárias e aumentar o número de servidores de trabalho, conforme necessário, e reduza-os quando não forem necessários. Também me permite desligar completamente os processos de trabalho quando necessário e enfileirar os trabalhos até que os trabalhadores voltem a ficar online.


-4

O PHP é uma linguagem de thread único, portanto, não há maneira oficial de iniciar um processo assíncrono com ele, além de usar execor popen. Há um post sobre isso aqui . Sua idéia para uma fila no MySQL também é uma boa idéia.

Seu requisito específico aqui é enviar um email ao usuário. Estou curioso para saber por que você está tentando fazer isso de forma assíncrona, pois o envio de um email é uma tarefa bastante trivial e rápida de executar. Suponho que, se você está enviando toneladas de e-mail e seu ISP está bloqueando você por suspeita de spam, esse pode ser um motivo para fazer fila, mas, além disso, não consigo pensar em nenhum motivo para fazê-lo dessa maneira.


O e-mail foi apenas um exemplo, pois as outras tarefas são mais complexas de explicar, e esse não é realmente o ponto da questão. Da maneira como costumávamos enviar email, o comando email não retornaria até o servidor remoto aceitar o email. Descobrimos que alguns servidores de correio foram configurados para adicionar atrasos longos (como atrasos de 10 a 20 segundos) antes de aceitar emails (provavelmente para combater spambots), e esses atrasos seriam repassados ​​aos nossos usuários. Agora, estamos usando um servidor de correio local para enfileirar os e-mails a serem enviados, para que este em particular não se aplique, mas temos outras tarefas de natureza semelhante.
Davr

Por exemplo: enviar e-mails pelo Google Apps Smtp com ssl e porta 465 leva mais tempo que o normal.
Gixty
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.