¿La mejor manera de administrar un script php de larga ejecución?

80

Tengo un script PHP que tarda mucho (5-30 minutos) en completarse. Por si acaso importa, el script utiliza curl para extraer datos de otro servidor. Ésta es la razón por la que está tardando tanto; tiene que esperar a que se cargue cada página antes de procesarla y pasar a la siguiente.

Quiero poder iniciar el script y dejarlo estar hasta que esté listo, lo que establecerá una bandera en una tabla de base de datos.

Lo que necesito saber es cómo poder finalizar la solicitud http antes de que termine de ejecutarse el script. Además, ¿es un script php la mejor manera de hacer esto?

kbanman
fuente
1
Aunque no lo mencionó en los idiomas admitidos por su servidor, supongo que si tiene la capacidad de ejecutar Ruby y Perl, probablemente podría agregar Node.js, y esto me suena como un caso de uso perfecto para Javascript. : su script pasará la mayor parte del tiempo esperando que se completen las solicitudes, que es un área en la que sobresale el paradigma asincrónico. Sin hilos significa sincronización fácil, concurrencia significa velocidad.
djfm
Puede hacer esto con PHP. Usaría Gouttee Guzzleimplementaría subprocesos de concurrencia. También puede consultar cómo Gearmanlanzar solicitudes paralelas en forma de trabajadores.
Andre García

Respuestas:

114

Ciertamente, se puede hacer con PHP, sin embargo, NO debe hacer esto como una tarea en segundo plano: el nuevo proceso debe disociarse del grupo de procesos donde se inició.

Dado que la gente sigue dando la misma respuesta incorrecta a estas preguntas frecuentes, he escrito una respuesta más completa aquí:

http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html

De los comentarios:

La versión corta es, shell_exec('echo /usr/bin/php -q longThing.php | at now');pero las razones por las que son un poco largas para incluirlas aquí.

symcbean
fuente
Esta publicación de blog es la verdadera respuesta. El ejecutivo y el sistema de PHP tienen demasiados peligros potenciales.
incredimike
2
¿Alguna posibilidad de copiar los detalles relevantes en la respuesta? hay demasiadas respuestas antiguas que enlazan con blogs muertos. Ese blog no está muerto (todavía) pero lo estará algún día.
Murphy
5
La versión corta es, shell_exec('echo /usr/bin/php -q longThing.php | at now');pero las razones por las que son un poco largas para incluirlas aquí.
symcbean
1
Respuesta muy votada a una pregunta muy votada, pero la respuesta no contiene mucho más que un enlace a una publicación de blog. Agregue la respuesta real, según meta.stackexchange.com/questions/8231/… y / o el centro de ayuda
Nanne
1
¿Puedo saber qué está haciendo esta opción -q?
Kiren Siva
11

La forma rápida y sucia sería usar la ignore_user_abortfunción en php. Esto básicamente dice: No importa lo que haga el usuario, ejecute este script hasta que esté terminado. Esto es algo peligroso si se trata de un sitio público (porque es posible que termine teniendo 20 ++ versiones del script ejecutándose al mismo tiempo si se inicia 20 veces).

La forma "limpia" (al menos en mi humilde opinión) es establecer una bandera (en la base de datos, por ejemplo) cuando desea iniciar el proceso y ejecutar un cronjob cada hora (más o menos) para comprobar si esa bandera está activada. Si está configurado, se inicia el script de larga ejecución, si NO está configurado, no ocurre nada.

FlorianH
fuente
Entonces, el método "ignore_user_abort" permitiría al usuario cerrar la ventana del navegador, pero ¿hay algo que pueda hacer para que devuelva una respuesta HTTP al cliente antes de que termine de ejecutarse?
kbanman
1
@kbanman Sí. Es necesario para cerrar la conexión: header("Connection: close", true);. Y no te olvides de flush ()
Benubird
8

Puede usar exec o system para comenzar un trabajo en segundo plano y luego hacer el trabajo en eso.

Además, existen mejores enfoques para raspar la web que el que está utilizando. Puede usar un enfoque de subprocesos (varios subprocesos en una página a la vez), o uno que usa un bucle de eventos (un subproceso que hace varias páginas a la vez). Mi enfoque personal usando Perl sería usar AnyEvent :: HTTP .

ETA: symcbean explicó cómo separar correctamente el proceso en segundo plano aquí .

Leon Timmermans
fuente
5
Casi correcto. El simple hecho de usar exec o system volverá a morderte el trasero. Vea mi respuesta para más detalles.
symcbean
5

No, PHP no es la mejor solución.

No estoy seguro acerca de Ruby o Perl, pero con Python podría reescribir su raspador de página para que sea multiproceso y probablemente se ejecute al menos 20 veces más rápido. Escribir aplicaciones de múltiples subprocesos puede ser un desafío, pero la primera aplicación de Python que escribí fue un raspador de páginas de múltiples subprocesos. Y puede simplemente llamar al script Python desde su página PHP usando una de las funciones de ejecución de shell.

Jamieb
fuente
La parte de procesamiento real de mi raspado es muy eficiente. Como mencioné anteriormente, es la carga de cada página lo que me mata. Lo que me preguntaba es si PHP está destinado a ejecutarse durante períodos tan largos.
kbanman
Estoy un poco sesgado porque desde que aprendí Python detesto PHP. Sin embargo, si está raspando más de una página (en serie), es casi seguro que obtendrá un mejor rendimiento si lo hace en paralelo con una aplicación multiproceso.
Jamieb
1
¿Alguna posibilidad de que pueda enviarme un ejemplo de dicho raspador de página? Me ayudaría mucho ya que todavía no he tocado Python.
kbanman
Si tuviera que reescribirlo, usaría eventlet. Hace que mi código sea 10 veces más simple: eventlet.net/doc
jamieb
5

Sí, puedes hacerlo en PHP. Pero además de PHP, sería aconsejable utilizar un Administrador de colas. Esta es la estrategia:

  1. Divida su gran tarea en tareas más pequeñas. En su caso, cada tarea podría cargar una sola página.

  2. Envía cada pequeña tarea a la cola.

  3. Ejecute sus trabajadores de cola en algún lugar.

El uso de esta estrategia tiene las siguientes ventajas:

  1. Para tareas de ejecución prolongada, tiene la capacidad de recuperarse en caso de que ocurra un problema fatal en el medio de la ejecución, no es necesario comenzar desde el principio.

  2. Si sus tareas no tienen que ejecutarse secuencialmente, puede ejecutar varios trabajadores para ejecutar tareas simultáneamente.

Tiene una variedad de opciones (estas son solo algunas):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Si está utilizando el marco de Laravel, las colas están integradas ( https://laravel.com/docs/5.4/queues ), con controladores para AWS SES, Redis, Beanstalkd
aljo f
fuente
3

PHP puede ser o no la mejor herramienta, pero usted sabe cómo usarlo, y el resto de su aplicación está escrito usándolo. Estas dos cualidades, combinadas con el hecho de que PHP es "suficientemente bueno", hacen un caso bastante sólido para usarlo, en lugar de Perl, Ruby o Python.

Si su objetivo es aprender otro idioma, elija uno y utilícelo. Cualquier idioma que mencione funcionará, no hay problema. Me gusta Perl, pero lo que a ti te gusta puede ser diferente.

Symcbean tiene buenos consejos sobre cómo gestionar procesos en segundo plano en su enlace.

En resumen, escriba un script PHP CLI para manejar los bits largos. Asegúrese de que informe el estado de alguna manera. Cree una página php para manejar las actualizaciones de estado, ya sea usando AJAX o métodos tradicionales. Su script de inicio iniciará el proceso en ejecución en su propia sesión y devolverá la confirmación de que el proceso está en marcha.

Buena suerte.

daotoad
fuente
1

Estoy de acuerdo con las respuestas que dicen que esto debería ejecutarse en un proceso en segundo plano. Pero también es importante que informe sobre el estado para que el usuario sepa que se está realizando el trabajo.

Al recibir la solicitud de PHP para iniciar el proceso, puede almacenar en una base de datos una representación de la tarea con un identificador único. Luego, inicie el proceso de eliminación de pantalla, pasándole el identificador único. Informe a la aplicación de iPhone que la tarea se ha iniciado y que debe verificar una URL específica, que contiene la nueva identificación de la tarea, para obtener el estado más reciente. La aplicación de iPhone ahora puede sondear (o incluso "sondear largamente") esta URL. Mientras tanto, el proceso en segundo plano actualizaría la representación de la base de datos de la tarea a medida que funcionaba con un porcentaje de finalización, paso actual o cualquier otro indicador de estado que desee. Y cuando haya terminado, establecerá una bandera completa.

Jacob
fuente
1

Puede enviarlo como una solicitud XHR (Ajax). Los clientes no suelen tener tiempo de espera para las XHR, a diferencia de las solicitudes HTTP normales.

JAL
fuente
1

Me doy cuenta de que esta es una pregunta bastante antigua, pero me gustaría intentarlo. Este script intenta abordar tanto la llamada de inicio inicial para terminar rápidamente como para dividir la carga pesada en partes más pequeñas. No he probado esta solución.

<?php
/**
 * crawler.php located at http://mysite.com/crawler.php
 */

// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);


function get_remote_sources_to_crawl() {
  // Do a database or a log file query here.

  $query_result = array (
    1 => 'http://exemple.com',
    2 => 'http://exemple1.com',
    3 => 'http://exemple2.com',
    4 => 'http://exemple3.com',
    // ... and so on.
  );

  // Returns the first one on the list.
  foreach ($query_result as $id => $url) {
    return $url;
  }
  return FALSE;
}

function update_remote_sources_to_crawl($id) {
  // Update my database or log file list so the $id record wont show up
  // on my next call to get_remote_sources_to_crawl()
}

$crawling_source = get_remote_sources_to_crawl();

if ($crawling_source) {


  // Run your scraping code on $crawling_source here.


  if ($your_scraping_has_finished) {
    // Update you database or log file.
    update_remote_sources_to_crawl($id);

    $ctx = stream_context_create(array(
      'http' => array(
        // I am not quite sure but I reckon the timeout set here actually
        // starts rolling after the connection to the remote server is made
        // limiting only how long the downloading of the remote content should take.
        // So as we are only interested to trigger this script again, 5 seconds 
        // should be plenty of time.
        'timeout' => 5,
      )
    ));

    // Open a new connection to this script and close it after 5 seconds in.
    file_get_contents('http://' . $_SERVER['HTTP_HOST'] . '/crawler.php', FALSE, $ctx);

    print 'The cronjob kick off has been initiated.';
  }
}
else {
  print 'Yay! The whole thing is done.';
}
Francisco Luz
fuente
@symcbean Leí la publicación que sugirió y me gustaría escuchar su opinión sobre esta solución alternativa.
Francisco Luz
En primer lugar, me ha dado una idea inicial para mi primer bot (teehee). En segundo lugar, ¿cómo encontró el rendimiento de su solución? ¿Has trabajado más con él y has aprendido algo más? Estoy interesado en implementar algo parecido a dragar a través de 26.000 imágenes (1,3GB), realizar varias operaciones, etc. Va a llevar un tiempo. La suya es la única solución que no parece hacky, use exec () shudder o requiera Linux (algunos de nosotros los perdedores todavía tenemos que usar Windows). Prefiero aprender de tus golpes de cabeza, en lugar de los míos: P
Just Plain High
@HighPriestessofTheTech Hola amigo, no he ido más lejos. En el momento en que escribí esto, solo estaba realizando un experimento mental.
Francisco Luz
1
Oh cielos ... Así que aprenderé de mis propios cabezazos ... les haré saber cómo va;)
Just Plain High
1
Probé esto y lo encuentro bastante útil.
Alex
1

Me gustaría proponer una solución que sea un poco diferente de la de symcbean, principalmente porque tengo el requisito adicional de que el proceso de larga ejecución debe ejecutarse como otro usuario, y no como un usuario de apache / www-data.

Primera solución usando cron para sondear una tabla de tareas en segundo plano:

  • La página web PHP se inserta en una tabla de tareas en segundo plano, indica 'ENVIADO'
  • cron se ejecuta una vez cada 3 minutos, utilizando otro usuario, ejecutando un script PHP CLI que verifica la tabla de tareas en segundo plano para las filas 'ENVIADAS'
  • PHP CLI actualizará la columna de estado en la fila en 'PROCESAMIENTO' y comenzará a procesar, una vez completado, se actualizará a 'COMPLETADO'

Segunda solución usando la instalación inotify de Linux:

  • La página web PHP actualiza un archivo de control con los parámetros establecidos por el usuario y también proporciona una identificación de tarea
  • El script de shell (como un usuario que no tiene www) ejecutando inotifywait esperará a que se escriba el archivo de control
  • después de escribir el archivo de control, se generará un evento close_write y el script de shell continuará
  • El script de shell ejecuta PHP CLI para realizar el proceso de ejecución prolongada.
  • PHP CLI escribe la salida en un archivo de registro identificado por el ID de la tarea o, alternativamente, actualiza el progreso en una tabla de estado
  • La página web PHP podría sondear el archivo de registro (según la identificación de la tarea) para mostrar el progreso del proceso de larga ejecución, o también podría consultar la tabla de estado

Se puede encontrar información adicional en mi publicación: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

YudhiWidyatama
fuente
0

He hecho cosas similares con Perl, double fork () y separándome del proceso principal. Todo el trabajo de búsqueda de http debe realizarse en un proceso bifurcado.

Alexandr Ciornii
fuente
0

Utilice un proxy para delegar la solicitud.

zerodina
fuente
0

lo que SIEMPRE uso es una de estas variantes (porque los diferentes sabores de Linux tienen diferentes reglas sobre el manejo de la salida / algunos programas tienen una salida diferente):

Variante I @exec ('./ myscript.php \ 1> / dev / null \ 2> / dev / null &');

Variante II @exec ('php -f myscript.php \ 1> / dev / null \ 2> / dev / null &');

Variante III @exec ('nohup myscript.php \ 1> / dev / null \ 2> / dev / null &');

Es posible que no tenga que instalar "nohup". Pero, por ejemplo, cuando estaba automatizando las conversaciones de video FFMPEG, la interfaz de salida de alguna manera no se manejó al 100% al redirigir los flujos de salida 1 y 2, así que usé nohup Y redirigí la salida.

dr quema
fuente
0

si tiene un script largo, divida el trabajo de la página con la ayuda del parámetro de entrada para cada tarea. (luego cada página actúa como hilo) es decir, si la página tiene 1 lac product_keywords ciclo de proceso largo, entonces en lugar del ciclo haga lógica para una palabra clave y pase esta palabra clave de magic o cornjobpage.php (en el siguiente ejemplo)

y para el trabajador en segundo plano, creo que debería probar esta técnica; le ayudará a llamar a tantas páginas como desee, todas las páginas se ejecutarán a la vez de forma independiente sin esperar la respuesta de cada página como asincrónica.

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

No es el mejor enfoque, como muchos afirman aquí, pero esto podría ayudar:

ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes

// Long running script here
Lucas Bustamante
fuente
0

Si la salida deseada de su script es algún procesamiento, no una página web, entonces creo que la solución deseada es ejecutar su script desde shell, simplemente como

php my_script.php

MrMartin
fuente