Ejecute la tarea PHP de forma asincrónica

144

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 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 que esperen hasta que se envíe el correo electrónico, solo quiero comenzar el proceso y devolver un mensaje al usuario de inmediato.

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

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

Lo que parece funcionar, pero me pregunto si hay una mejor manera. Estoy considerando escribir un sistema que ponga en cola las 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 fuera necesario.

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

davr
fuente

Respuestas:

80

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

Rodar el tuyo no es demasiado complicado, aquí hay algunas otras opciones para revisar:

  • GearMan : esta respuesta se escribió en 2009, y desde entonces GearMan parece una opción popular, vea los comentarios a continuación.
  • ActiveMQ si desea una cola de mensajes de código abierto completa.
  • ZeroMQ : esta es una biblioteca de sockets bastante buena que facilita la escritura de código distribuido sin tener que preocuparse demasiado por la programación del socket. Puede usarlo para la cola de mensajes en un solo host: simplemente haría que su aplicación web empuje algo a una cola que una aplicación de consola en funcionamiento continuo consumiría en la próxima oportunidad adecuada
  • beanstalkd : solo encontré esta 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 un contenedor mantenido recientemente (2017) en torno a una variedad de sistemas de colas
  • Finalmente, una publicación de blog sobre el uso de memcached para la cola de mensajes

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

Paul Dixon
fuente
Gracias por todos los consejos. El específico sobre ignore_user_abort realmente no ayuda en mi caso, mi objetivo es evitar demoras innecesarias para el usuario.
davr
2
Si configura el encabezado HTTP Content-Length en su respuesta "Gracias por registrarse", 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é configurado) sin hacer que el usuario final espere. Por supuesto, tendrá que calcular el tamaño del contenido de su respuesta antes de representar los encabezados, pero eso es bastante fácil para respuestas cortas.
Peter
1
Gearman ( gearman.org ) es una gran cola de mensajes de código abierto que es multiplataforma. Puede escribir trabajadores en C, PHP, Perl o casi cualquier otro lenguaje. Existen complementos Gearman UDF para MySQL y también puede usar Net_Gearman de PHP o el cliente gearman pear.
Justin Swanhart
Gearman sería lo que recomendaría hoy (en 2015) sobre cualquier sistema de colas de trabajo personalizado.
Peter
Otra opción es configurar un servidor de nodo js para manejar una solicitud y devolver una respuesta rápida con una tarea intermedia. Muchas cosas dentro de un script de nodo js se ejecutan de forma asíncrona, como una solicitud http.
Zordon
22

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 guión 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 script.php llamado, 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.

Markus
fuente
set_time_limit no tiene efecto si php se ejecuta en modo seguro
Baptiste Pernet
17

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

Luego, en sus scripts accedidos por 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 asunto está envuelto en el servicio y su script solo consume URL. Esto lo 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 otras personas / servicios las consuman (si lo desea) y podría llevarlo más lejos y agregar un servicio de monitoreo en la parte superior para realizar un seguimiento cola y estado de la tarea.

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

rojoca
fuente
1
No me gusta este enfoque porque sobrecarga el servidor web
Oved Yavine
7

He usado Beanstalkd para un proyecto y planeé volver a hacerlo. He encontrado que es una excelente manera de ejecutar procesos asincrónicos.

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

  • Cambio de tamaño de la imagen: y con una cola ligeramente cargada que pasa a un script PHP basado en CLI, cambiar el tamaño de las imágenes grandes (2mb +) funcionó bien, pero tratar de cambiar el tamaño de las mismas imágenes dentro de una instancia de mod_php se encontraba regularmente en problemas de espacio de memoria (I limitó el proceso de PHP a 32 MB, y el cambio de tamaño tomó más que eso)
  • comprobaciones en el futuro cercano: beanstalkd tiene demoras disponibles (haga que este trabajo esté disponible para ejecutarse solo después de X segundos), por lo que puedo activar 5 o 10 comprobaciones para un evento, un poco más tarde

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ó a JSON para inyección en la cola misma.

Luego, un script cli de larga ejecución recogió el trabajo de la cola, lo ejecutó (a través de Zend_Router_Simple) y, si es necesario, ingresó información en memcached para que el sitio web PHP lo 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 según lo planeado, lo haría de inmediato (ejecutándose a través de un script bash). Si hubo un problema y lo hice exit(0)(el valor predeterminado para exit;o die();) primero se pausaría por un par de segundos.

Alister Bulman
fuente
Me gusta el aspecto de beanstalkd, una vez que agregan persistencia, creo que será perfecto.
davr
Eso ya está en la base de código y se está estabilizando. También estoy ansioso por 'trabajos con nombre', para poder tirar cosas allí, pero sé que no se agregarán si ya hay uno allí. Bueno para eventos regulares.
Alister Bulman
@AlisterBulman, ¿podría dar más información o ejemplos para "Un script cli de larga ejecución que luego retiró el trabajo de la cola". Estoy tratando de construir un script cli para mi aplicación.
Sasi varna kumar
7

Si solo se trata de proporcionar tareas costosas, en caso de que se admita php-fpm, ¿por qué no utilizar la 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 requieren mucho tiempo sin dejar abierta la conexión con el cliente.

Realmente no usas la asincronía de esta manera:

  1. Haga todo su código principal primero.
  2. Ejecutar fastcgi_finish_request().
  3. Haz todas las cosas pesadas.

Una vez más se necesita php-fpm.

Denys Gorobchenko
fuente
5

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);
        }
    }
}
Andrew Moore
fuente
4

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 tiene un solo subproceso, por lo que no hay mucho más que pueda hacer.

De hecho, he agregado un nivel adicional a esto y eso es obtener y almacenar la identificación 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 (la identificación del proceso ya no existe). Esto es útil para los casos en que la longitud de la secuencia de comandos haría que el navegador agote el tiempo de espera, pero el usuario debe esperar a que se complete esa secuencia de comandos antes del siguiente paso. (En mi caso, estaba procesando archivos ZIP grandes con archivos similares a CSV que agregan hasta 30 000 registros a la base de datos, después de lo cual el usuario debe confirmar cierta información).

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

Darryl Hein
fuente
1
¿A qué método te refieres en tu primera oración?
Simon East
3

PHP TIENE subprocesos múltiples, simplemente no está habilitado de forma predeterminada, hay una extensión llamada pthreads que hace exactamente eso. Sin embargo, necesitarás PHP compilado con ZTS. (Hilo seguro) Enlaces:

Ejemplos

Otro tutorial

pthreads PECL Extension

Omar S.
fuente
2

Es una gran idea usar cURL como lo sugiere rojoca.

Aquí hay un ejemplo. Puede monitorear text.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);
}

?>
Kjeld
fuente
2
Realmente ayudaría si el código fuente fuera comentado. No tengo idea de lo que está sucediendo allí y qué partes son un ejemplo y qué partes son reutilizables para mi propio propósito.
Thomas Tempelmann
1

Lamentablemente, PHP no tiene ningún tipo de capacidades de subprocesamiento nativo. 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 encontrado formas de simular subprocesos en PHP.

Peter D
fuente
1

Si configura el encabezado HTTP Content-Length en su respuesta "Gracias por registrarse", 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é configurado) para que pueda terminar de funcionar sin hacer esperar al usuario final.

Por supuesto, deberá calcular el tamaño del contenido de su respuesta antes de representar los encabezados, pero eso es bastante fácil para respuestas cortas (escribir la salida en una cadena, llamar a strlen (), llamar a la cabecera (), procesar la cadena).

Este enfoque tiene la ventaja de no obligarlo a administrar una cola "front-end", y aunque es posible que deba trabajar un poco en el back-end para evitar que los procesos secundarios HTTP de carreras se interpongan entre sí, eso es algo que ya debe hacer , de todas formas.

Peter
fuente
Esto no parece funcionar. Cuando lo uso header('Content-Length: 3'); echo '1234'; sleep(5);, a pesar de que el navegador solo toma 3 caracteres, todavía espera 5 segundos antes de mostrar la respuesta. ¿Qué me estoy perdiendo?
Thomas Tempelmann
@ThomasTempelmann: probablemente deba llamar a flush () para forzar que la salida se procese de inmediato, de lo contrario, la salida se almacenará en el búfer hasta que salga el script o se envíen suficientes datos a STDOUT para vaciar el búfer.
Peter
Ya probé muchas formas de enjuagar, que encontré aquí en SO. Ninguna ayuda Y los datos parecen enviarse también sin comprimir, como se puede ver phpinfo(). La única otra cosa que podría imaginar es que primero necesito alcanzar un tamaño mínimo de búfer, por ejemplo 256 o más bytes.
Thomas Tempelmann
@ThomasTempelmann: no veo nada en su pregunta o en mi respuesta sobre gzip (generalmente tiene sentido hacer que el escenario más simple funcione primero antes de agregar capas de complejidad). Para establecer cuándo el servidor está enviando datos, puede usar un sniffer de paquetes del complemento del navegador (como fiddler, tamperdata, etc.). Luego, si encuentra que el servidor web realmente retiene toda la salida del script hasta la salida, independientemente de la descarga, entonces debe modificar la configuración del servidor web (no hay nada que su script PHP pueda hacer en ese caso).
Peter
Utilizo un servicio web virtual, por lo que tengo poco control sobre su configuración. Esperaba encontrar otras sugerencias sobre lo que podría ser el culpable, pero parece que su respuesta simplemente no es tan universalmente aplicable como parece. Demasiadas cosas pueden salir mal, obviamente. Su solución seguramente es mucho más fácil de implementar que todas las otras respuestas aquí. Lástima que no me funcione.
Thomas Tempelmann
1

Si no desea el ActiveMQ completo, le recomiendo considerar RabbitMQ . RabbitMQ es un mensaje ligero que utiliza el estándar AMQP .

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

phpPhil
fuente
0

Creo que deberías probar esta técnica. Ayudará a llamar tantas páginas como quieras. Todas las páginas se ejecutarán a la vez de forma independiente sin esperar que cada respuesta de la página sea así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
    ?>

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

Hassan Saeed
fuente
0

Generando nuevos procesos en el servidor usando exec() o directamente en otro servidor usando curl no escala muy bien en absoluto, si buscamos un ejecutivo, básicamente está llenando su servidor con procesos de larga ejecución que pueden ser manejados por otros servidores que no son web, y usar curl vincula otro servidor a menos que incorpores algún tipo de equilibrio de carga.

He usado Gearman en algunas situaciones y me parece mejor para este tipo de casos de uso. Puedo usar un solo servidor de cola de trabajos para manejar básicamente la puesta en cola de todos los trabajos que el servidor debe realizar y activar los servidores de trabajo, cada uno de los cuales puede ejecutar tantas instancias del proceso de trabajo como sea necesario, y aumentar el número de servidores de trabajo según sea necesario y gírelos cuando no sea necesario También me permite cerrar los procesos de los trabajadores por completo cuando sea necesario y pone en cola los trabajos hasta que los trabajadores vuelven a estar en línea.

Chris Rutherfurd
fuente
-4

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

Su requisito específico aquí es enviar un correo electrónico al usuario. Tengo curiosidad por saber por qué estás 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 está enviando toneladas de correo electrónico y su ISP lo está bloqueando por sospecha de spam, esa podría ser una razón para hacer cola, pero aparte de eso, no puedo pensar en ninguna razón para hacerlo de esta manera.

Marc W
fuente
El correo electrónico fue solo un ejemplo, ya que las otras tareas son más complejas de explicar, y no es realmente el punto de la pregunta. De la forma en que solíamos enviar correos electrónicos, el comando de correo electrónico no volvería hasta que el servidor remoto aceptara el correo. Descubrimos que algunos servidores de correo estaban configurados para agregar retrasos largos (como retrasos de 10-20 segundos) antes de aceptar el correo (probablemente para combatir los robots de spam), y estos retrasos se pasarían a nuestros usuarios. Ahora, estamos utilizando un servidor de correo local para poner en cola los correos que se enviarán, por lo que este no se aplica, pero tenemos otras tareas de naturaleza similar.
davr
Por ejemplo: enviar correos electrónicos a través de Google Apps Smtp con ssl y el puerto 465 tarda más de lo habitual.
Gixty