Ejecutar Tareas PHP De Forma Asíncrona


Trabajo en una aplicación web algo grande, y el backend está principalmente en PHP. Hay varios lugares en el código donde necesito completar alguna tarea, pero no quiero hacer que el usuario espere el resultado. Por ejemplo, al crear una nueva cuenta, necesito enviarles un correo electrónico de bienvenida. Pero cuando presionan el botón' Finalizar registro', no quiero hacerlos esperar hasta que se envíe el correo electrónico, solo quiero iniciar el proceso y devolver un mensaje al usuario correcto lejos.

Hasta ahora, en algunos lugares he estado usando lo que se siente como un hack con exec(). Básicamente haciendo cosas como:

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

Que parece funcionar, pero me pregunto si hay una mejor manera. Estoy considerando escribir un sistema que ponga en cola tareas en una tabla MySQL, y un script PHP independiente de larga duración que consulta esa tabla una vez por segundo, y ejecuta cualquier tarea nueva que encuentre. Esto también tendría la ventaja de permitirme dividir las tareas entre varias máquinas de trabajo en el futuro si lo necesitaba.

¿Estoy reinventando la rueda? ¿Hay una solución mejor que el hackeo exec () o la cola MySQL?

Author: davr, 0000-00-00

15 answers

He utilizado el enfoque de cola, y funciona bien, ya que puede diferir ese procesamiento hasta que la carga del servidor esté inactiva, lo que le permite administrar su carga de manera bastante efectiva si puede particionar fácilmente las "tareas que no son urgentes".

Rodar por su cuenta no es demasiado complicado, aquí hay algunas otras opciones para comprobar:

  • GearMan - esta respuesta fue escrita en 2009, y desde entonces GearMan parece una opción popular, ver comentarios a continuación.
  • ActiveMQ si quieres una cola de mensajes de código abierto completa.
  • ZeroMQ - esta es una biblioteca de sockets bastante genial que facilita la escritura de código distribuido sin tener que preocuparse demasiado por la programación de sockets en sí. Podría usarlo para message queue server en un solo host: simplemente haría que su aplicación web enviara algo a una cola que una aplicación de consola en ejecución continua consumiría en la próxima oportunidad adecuada
  • beanstalkd - solo encontré este mientras escribía esta respuesta, pero parece interesante
  • dropr es un proyecto de cola de mensajes basado en PHP, pero no se ha mantenido activamente desde septiembre de 2010
  • php-enqueue es una envoltura mantenida recientemente (2017) alrededor de una variedad de sistemas de cola
  • Finalmente, una entrada de blog sobre el uso de memcached para message queue server

Otro enfoque, quizás más simple, es usar ignore_user_abort - una vez que haya enviado la página al usuario, puede hacer su procesamiento sin miedo a la terminación prematura, aunque esto tiene el efecto de parecer prolongar la carga de la página desde la perspectiva del usuario.

 71
Author: Paul Dixon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-30 11:28:54

Cuando solo desea ejecutar una o varias solicitudes HTTP sin tener que esperar la respuesta, también hay una solución PHP simple.

En el script de llamada:

$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

En el script llamado.php, puede invocar estas funciones PHP en las primeras líneas:

ignore_user_abort(true);
set_time_limit(0);

Esto hace que el script continúe ejecutándose sin límite de tiempo cuando se cierra la conexión HTTP.

 19
Author: Markus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-29 21:05:25

Otra forma de bifurcar procesos es a través de curl. Puede configurar sus tareas internas como un servicio web. Por ejemplo:

Luego, en los scripts a los que accedió el usuario, realice llamadas al servicio:

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

Su servicio puede realizar un seguimiento de la cola de tareas con mysql o lo que quiera, el punto es: todo está envuelto dentro del servicio y su script solo consume URLs. Esto te libera para mover el servicio a otra máquina/servidor si es necesario (es decir, fácilmente escalable).

Agregar autorización http o un esquema de autorización personalizado (como los servicios web de Amazon) le permite abrir sus tareas para que sean consumidas por otras personas / servicios (si lo desea) y podría llevarlo más allá y agregar un servicio de monitoreo en la parte superior para realizar un seguimiento de la cola y la tarea estatus.

Toma un poco de trabajo de configuración, pero hay muchos beneficios.

 17
Author: rojoca,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-13 23:31:59

He usado Beanstalkd para un proyecto, y lo planeé de nuevo. He encontrado que es una excelente manera de ejecutar procesos asincrónicos.

Un par de cosas que he hecho con él son:

  • Redimensionamiento de imágenes-y con una cola ligeramente cargada pasando a un script PHP basado en CLI, redimensionar imágenes grandes (2mb+) funcionó bien, pero tratar de redimensionar las mismas imágenes dentro de una instancia mod_php se estaba ejecutando regularmente en problemas de espacio de memoria (Limité el proceso PHP a 32MB, y el redimensionamiento tomó más que eso)
  • chequeos de futuro cercano-beanstalkd tiene retrasos disponibles (hacer que este trabajo esté disponible para ejecutarse solo después de X segundos) - por lo que puedo disparar 5 o 10 chequeos para un evento, un poco más tarde en el tiempo

Escribí un sistema basado en Zend-Framework para decodificar una url 'agradable', por ejemplo, para cambiar el tamaño de una imagen que llamaría QueueTask('/image/resize/filename/example.jpg'). La URL se decodificó primero en una matriz (módulo, controlador,acción, parámetros), y luego se convirtió en JSON para la cola misma.

Un script cli de larga ejecución luego recogió el trabajo de la cola, lo ejecutó (a través de Zend_Router_Simple), y si es necesario, puso información en memcached para que el sitio web PHP la recogiera cuando fuera necesario.

Una arruga que también puse fue que el cli-script solo se ejecutó durante 50 bucles antes de reiniciar, pero si quería reiniciar como estaba previsto, lo haría inmediatamente (se ejecuta a través de un bash-script). Si hubo un problema y lo hice exit(0) (el valor predeterminado para exit; o die();) primero se pausaría durante un par de segundos.

 7
Author: Alister Bulman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-18 10:16:22

Si solo se trata de proporcionar tareas costosas, en el caso de php-fpm es compatible, ¿por qué no utilizar fastcgi_finish_request() ¿función?

Esta función vacía todos los datos de respuesta al cliente y finaliza la solicitud. Esto permite realizar tareas que consumen mucho tiempo sin dejar abierta la conexión con el cliente.

Realmente no usas asincronicidad de esta manera:

  1. Primero haga todo su código principal.
  2. Ejecutar fastcgi_finish_request().
  3. Make todo pesado.

Una vez más se necesita php-fpm.

 7
Author: Denys Gorobchenko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-01 22:07:52

Aquí hay una clase simple que codifiqué para mi aplicación web. Permite bifurcar scripts PHP y otros scripts. Funciona en UNIX y 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);
        }
    }
}
 5
Author: Andrew Moore,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-13 20:44:08

Este es el mismo método que he estado usando durante un par de años y no he visto ni encontrado nada mejor. Como la gente ha dicho, PHP es un subproceso simple, por lo que no hay mucho más que pueda hacer.

En realidad he añadido un nivel extra a esto y que es conseguir y almacenar el id del proceso. Esto me permite redirigir a otra página y hacer que el usuario se siente en esa página, usando AJAX para verificar si el proceso está completo (el id del proceso ya no existe). Esto es útil para los casos en los que el la longitud del script causaría que el tiempo de espera del navegador, pero el usuario necesita esperar a que ese script se complete antes del siguiente paso. (En mi caso estaba procesando archivos ZIP grandes con CSV como archivos que suman hasta 30 000 registros a la base de datos después de lo cual el usuario necesita confirmar alguna información.)

También he utilizado un proceso similar para la generación de informes. No estoy seguro de usar "procesamiento en segundo plano" para algo como un correo electrónico, a menos que haya un problema real con un SMTP lento. En su lugar, podría usar una tabla como una cola y luego tener un proceso que se ejecuta cada minuto para enviar los correos electrónicos dentro de la cola. Usted tendría que ser cauteloso de enviar correos electrónicos dos veces u otros problemas similares. Consideraría un proceso de cola similar para otras tareas también.

 4
Author: Darryl Hein,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-13 17:02:01

PHP TIENE multithreading, simplemente no está habilitado por defecto, hay una extensión llamada pthreads que hace exactamente eso. Sin embargo, necesitará php compilado con ZTS. (Hilo seguro) Enlaces:

Ejemplos

Otro tutorial

Extensión Pthreads PECL

 3
Author: Omar A. Shaban,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-25 11:59:35

Es una gran idea usar rIZO como sugiere rojoca.

Aquí hay un ejemplo. Puede monitorear el texto.txt mientras el script se ejecuta en 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
Author: Kjeld,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-29 19:07:20

Desafortunadamente PHP no tiene ningún tipo de capacidades nativas de threading. Así que creo que en este caso no tienes más remedio que usar algún tipo de código personalizado para hacer lo que quieres hacer.

Si busca en la red cosas de subprocesos de PHP, algunas personas han ideado formas de simular subprocesos en PHP.

 1
Author: Peter D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-13 16:20:27

Si establece el encabezado HTTP Content-Length en su respuesta "Gracias por registrarse", entonces el navegador debe cerrar la conexión después de recibir el número especificado de bytes. Esto deja el proceso del lado del servidor en ejecución (suponiendo que ignore_user_abort esté establecido) para que pueda terminar de funcionar sin hacer esperar al usuario final.

Por supuesto, necesitará calcular el tamaño del contenido de su respuesta antes de renderizar los encabezados, pero eso es bastante fácil para respuestas cortas (escribir salida a una cadena, llamada strlen (), llamada header (), cadena de renderizado).

Este enfoque tiene la ventaja de que no lo obliga a administrar una cola "front end", y aunque es posible que deba hacer algo de trabajo en el back-end para evitar que los procesos hijos HTTP de carreras se pisen entre sí, eso es algo que ya necesitaba hacer, de todos modos.

 1
Author: Peter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-05 23:09:43

Si no quieres el ActiveMQ completo, te recomiendo considerar RabbitMQ. RabbitMQ es una mensajería ligera que utiliza el estándar AMQP .

Recomiendo también buscar en php-amqplib - una popular biblioteca cliente AMQP para acceder a los corredores de mensajes basados en AMQP.

 1
Author: phpPhil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-13 00:11:10

Creo que deberías probar esta técnica que te ayudará a llamar a tantas páginas como te gusten todas las páginas se ejecutarán a la vez de forma independiente sin esperar a que cada respuesta de página sea asincrónica.

Cornjobpage.php //mainpage

    <?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);
            }
            ?>

Página de prueba.php

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

PD: si desea enviar parámetros de url como bucle, siga esta respuesta: https://stackoverflow.com/a/41225209/6295712

 0
Author: Hassan Saeed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:08

Generar nuevos procesos en el servidor usando exec() o directamente en otro servidor usando curl no escala tan bien en absoluto, si vamos por exec, básicamente está llenando su servidor con procesos de larga ejecución que pueden ser manejados por otros servidores no orientados a la web, y usando curl ata otro servidor a menos que construya algún tipo de equilibrio de carga.

He usado alemán en algunas situaciones y me parece mejor para este tipo de caso de uso. Puedo usar un solo servidor de cola de trabajos para básicamente, manejar la cola de todos los trabajos que deben ser realizados por el servidor y girar servidores de trabajo, cada uno de los cuales puede ejecutar tantas instancias del proceso de trabajo como sea necesario, y escalar el número de servidores de trabajo según sea necesario y girar hacia abajo cuando no sea necesario. También me permite cerrar los procesos de los trabajadores por completo cuando sea necesario y poner en cola los trabajos hasta que los trabajadores vuelvan a estar en línea.

 0
Author: Chris Rutherfurd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-31 06:56:02

PHP es un lenguaje de subproceso simple, por lo que no hay forma oficial de iniciar un proceso asincrónico con él que no sea usando exec o popen. Hay una entrada de blog sobre eso aquí. Su idea para una cola en MySQL es una buena idea también.

Su requisito específico aquí es enviar un correo electrónico al usuario. Tengo curiosidad por saber por qué está tratando de hacer eso de forma asíncrona, ya que enviar un correo electrónico es una tarea bastante trivial y rápida de realizar. Supongo que si usted está enviando toneladas de el correo electrónico y su ISP lo están bloqueando por sospecha de spam, esa podría ser una razón para hacer cola, pero aparte de eso no se me ocurre ninguna razón para hacerlo de esta manera.

 -4
Author: Marc W,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-13 16:21:03