Esegui attività PHP in modo asincrono


144

Lavoro su un'applicazione Web piuttosto grande e il backend è principalmente in PHP. Esistono diversi punti nel codice in cui devo completare alcune attività, ma non voglio che l'utente attenda il risultato. Ad esempio, quando creo un nuovo account, devo inviare loro un'e-mail di benvenuto. Ma quando premono il pulsante 'Termina registrazione', non voglio farli aspettare fino a quando l'e-mail non viene effettivamente inviata, voglio solo iniziare il processo e restituire immediatamente un messaggio all'utente.

Fino ad ora, in alcuni punti ho usato quello che sembra un hack con exec (). Fondamentalmente facendo cose come:

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

Il che sembra funzionare, ma mi chiedo se c'è un modo migliore. Sto pensando di scrivere un sistema che mette in coda le attività in una tabella MySQL e uno script PHP di lunga durata separato che interroga quella tabella una volta al secondo ed esegue tutte le nuove attività che trova. Ciò avrebbe anche il vantaggio di permettermi di dividere i compiti tra più macchine di lavoro in futuro, se necessario.

Sto reinventando la ruota? Esiste una soluzione migliore dell'hack exec () o della coda MySQL?

Risposte:


80

Ho usato l'approccio di accodamento e funziona bene poiché puoi rimandare l'elaborazione fino a quando il carico del tuo server non è inattivo, permettendoti di gestire il carico in modo abbastanza efficace se puoi partizionare facilmente "attività che non sono urgenti".

Rotolare il tuo non è troppo complicato, ecco alcune altre opzioni per verificare:

  • GearMan : questa risposta è stata scritta nel 2009 e da allora GearMan sembra un'opzione popolare, vedi i commenti qui sotto.
  • ActiveMQ se si desidera una coda di messaggi open source completa.
  • ZeroMQ - questa è una libreria socket piuttosto interessante che semplifica la scrittura di codice distribuito senza doversi preoccupare troppo della programmazione del socket stesso. Potresti usarlo per l'accodamento dei messaggi su un singolo host: avresti semplicemente il tuo webapp spingere qualcosa in una coda che un'app console in esecuzione continua consumerebbe alla prossima occasione adatta
  • beanstalkd : ho trovato questo solo mentre scrivevo questa risposta, ma sembra interessante
  • dropr è un progetto di coda di messaggi basato su PHP, ma non è stato attivamente mantenuto da settembre 2010
  • php-enqueue è un wrapper gestito di recente (2017) attorno a una varietà di sistemi di coda
  • Infine, un post sul blog sull'uso di memcached per l'accodamento dei messaggi

Un altro approccio, forse più semplice, consiste nell'utilizzare ignore_user_abort : una volta che hai inviato la pagina all'utente, puoi eseguire l'elaborazione finale senza timore di una chiusura prematura, sebbene ciò abbia l'effetto di apparire per prolungare il caricamento della pagina da parte dell'utente prospettiva.


Grazie per tutti i suggerimenti. Quello specifico su ignore_user_abort non aiuta davvero nel mio caso, il mio obiettivo è quello di evitare ritardi inutili per l'utente.
David,

2
Se si imposta l'intestazione HTTP Content-Length nella risposta "Grazie per la registrazione", il browser dovrebbe chiudere la connessione dopo aver ricevuto il numero specificato di byte. Questo lascia in esecuzione il processo lato server (presupponendo che ignore_user_abort sia impostato) senza far attendere l'utente finale. Ovviamente dovrai calcolare la dimensione del contenuto della risposta prima di eseguire il rendering delle intestazioni, ma è abbastanza facile per le risposte brevi.
Peter,

1
Gearman ( gearman.org ) è una grande coda di messaggi open source che è multipiattaforma. Puoi scrivere lavoratori in C, PHP, Perl o in qualsiasi altra lingua. Ci sono plugin Gearman UDF per MySQL e puoi anche usare Net_Gearman da PHP o il client di Pearman Gear.
Justin Swanhart,

Gearman sarebbe ciò che consiglierei oggi (nel 2015) su qualsiasi sistema di accodamento del lavoro personalizzato.
Peter,

Un'altra opzione è quella di impostare un server js nodo per gestire una richiesta e restituire una risposta rapida con un'attività in mezzo. Molte cose all'interno di uno script js del nodo vengono eseguite in modo asincrono come una richiesta http.
Zordon,

22

Quando vuoi solo eseguire una o più richieste HTTP senza dover attendere la risposta, c'è anche una semplice soluzione PHP.

Nello script chiamante:

$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

Sul chiamato script.php, puoi invocare queste funzioni PHP nelle prime righe:

ignore_user_abort(true);
set_time_limit(0);

Questo fa sì che lo script continui l'esecuzione senza limiti di tempo quando la connessione HTTP viene chiusa.


set_time_limit non ha alcun effetto se php funziona in modalità provvisoria
Baptiste Pernet il

17

Un altro modo per eseguire processi a forcella è tramite l'arricciatura. È possibile impostare le attività interne come servizio Web. Per esempio:

Quindi negli script a cui hai effettuato l'accesso gli utenti effettuano chiamate al servizio:

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

Il tuo servizio può tenere traccia della coda di attività con mysql o qualunque cosa ti piaccia il punto: è tutto racchiuso nel servizio e il tuo script consuma solo URL. Ciò consente di spostare il servizio su un'altra macchina / server, se necessario (ovvero facilmente scalabile).

L'aggiunta dell'autorizzazione http o di uno schema di autorizzazione personalizzato (come i servizi Web di Amazon) ti consente di aprire le tue attività affinché vengano consumate da altre persone / servizi (se lo desideri) e potresti portarlo oltre e aggiungere un servizio di monitoraggio per tenere traccia di coda e stato dell'attività.

Ci vuole un po 'di lavoro di installazione ma ci sono molti vantaggi.


1
Non mi piace questo approccio perché sovraccarica il web server
Oved Yavine,

7

Ho usato Beanstalkd per un progetto e pianificato di nuovo. Ho trovato che è un modo eccellente per eseguire processi asincroni.

Un paio di cose che ho fatto sono:

  • Ridimensionamento delle immagini - e con una coda leggermente caricata che passa a uno script PHP basato su CLI, il ridimensionamento delle immagini di grandi dimensioni (2mb +) ha funzionato bene, ma il tentativo di ridimensionare le stesse immagini all'interno di un'istanza mod_php si imbatteva regolarmente in problemi di spazio di memoria (I limitato il processo di PHP a 32 MB e il ridimensionamento ha richiesto più di questo)
  • controlli per il prossimo futuro - beanstalkd ha dei ritardi disponibili (rendi questo lavoro disponibile per l'esecuzione solo dopo X secondi) - così posso eseguire 5 o 10 controlli per un evento, un po 'più tardi nel tempo

Ho scritto un sistema basato su Zend-Framework per decodificare un URL "carino", quindi ad esempio per ridimensionare un'immagine che chiamerebbe QueueTask('/image/resize/filename/example.jpg') . L'URL è stato prima decodificato in un array (modulo, controller, azione, parametri), quindi convertito in JSON per l'iniezione nella coda stessa.

Uno script cli di lunga durata ha quindi raccolto il lavoro dalla coda, lo ha eseguito (tramite Zend_Router_Simple) e, se necessario, ha messo le informazioni in memcached per il sito Web PHP per raccogliere come richiesto quando è stato fatto.

Una delle rughe che ho anche inserito è che lo script cli ha funzionato solo per 50 loop prima di riavviare, ma se voleva riavviare come previsto, lo farebbe immediatamente (essendo eseguito tramite uno script bash). Se ci fosse un problema e l'ho fatto exit(0)(il valore predefinito per exit;o die();) si fermerebbe prima per un paio di secondi.


Mi piace l'aspetto di beanstalkd, una volta che aggiungono persistenza penso che sarà perfetto.
davr

Questo è già nella base di codice ed è stabilizzato. Non vedo l'ora di 'nominare i lavori', quindi posso aggiungere qualcosa, ma so che non verrà aggiunto se ce n'è già uno lì. Buono per eventi regolari.
Alister Bulman,

@AlisterBulman potresti fornire ulteriori informazioni o esempi per "Uno script cli di lunga durata ha poi raccolto il lavoro dalla coda". Sto cercando di creare un tale script cli per la mia applicazione.
Sasi varna kumar,

7

Se si tratta solo di fornire compiti costosi, in caso di php-fpm è supportato, perché non usare la fastcgi_finish_request()funzione?

Questa funzione scarica tutti i dati di risposta sul client e termina la richiesta. Ciò consente di eseguire attività dispendiose in termini di tempo senza lasciare aperta la connessione al client.

Non usi davvero l'asincronicità in questo modo:

  1. Crea prima tutto il tuo codice principale.
  2. Esegui fastcgi_finish_request().
  3. Fai tutte le cose pesanti.

Ancora una volta è necessario php-fpm.


5

Ecco una semplice classe che ho codificato per la mia applicazione web. Permette di creare script PHP e altri script. Funziona su 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

Questo è lo stesso metodo che uso da un paio d'anni e non ho visto o trovato niente di meglio. Come è stato detto, PHP è a thread singolo, quindi non c'è molto altro che puoi fare.

In realtà ho aggiunto un ulteriore livello a questo e che sta ottenendo e memorizzando l'id di processo. Ciò mi consente di reindirizzare a un'altra pagina e di far sedere l'utente su quella pagina, utilizzando AJAX per verificare se il processo è completo (ID processo non esiste più). Ciò è utile nei casi in cui la lunghezza dello script causerebbe il timeout del browser, ma l'utente deve attendere il completamento dello script prima del passaggio successivo. (Nel mio caso stava elaborando file ZIP di grandi dimensioni con file simili a CSV che aggiungono fino a 30.000 record al database, dopodiché l'utente deve confermare alcune informazioni.)

Ho anche usato un processo simile per la generazione di report. Non sono sicuro che utilizzerei "elaborazione in background" per qualcosa come un'e-mail, a meno che non ci sia un vero problema con un SMTP lento. Invece potrei usare una tabella come una coda e quindi avere un processo che viene eseguito ogni minuto per inviare le e-mail all'interno della coda. Avresti bisogno di diffidare di inviare e-mail due volte o altri problemi simili. Considererei un processo di accodamento simile anche per altre attività.


1
A quale metodo ti riferisci nella tua prima frase?
Simon East,


2

È un'ottima idea usare cURL come suggerito dal rojoca.

Ecco un esempio Puoi monitorare text.txt mentre lo script è in esecuzione in background:

<?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
Sarebbe davvero utile se il codice sorgente fosse commentato. Non ho idea di cosa stia succedendo lì e quali parti siano di esempio e quali parti siano riutilizzabili per il mio scopo.
Thomas Tempelmann,

1

Sfortunatamente PHP non ha alcun tipo di funzionalità di threading nativo. Quindi penso che in questo caso non hai altra scelta che usare una sorta di codice personalizzato per fare quello che vuoi fare.

Se cerchi in rete elementi di threading PHP, alcune persone hanno escogitato dei modi per simulare i thread su PHP.


1

Se si imposta l'intestazione HTTP Content-Length nella risposta "Grazie per la registrazione", il browser dovrebbe chiudere la connessione dopo aver ricevuto il numero specificato di byte. Questo lascia in esecuzione il processo lato server (presupponendo che ignore_user_abort sia impostato) in modo che possa finire di funzionare senza far attendere l'utente finale.

Ovviamente dovrai calcolare la dimensione del contenuto della risposta prima di eseguire il rendering delle intestazioni, ma è abbastanza facile per le risposte brevi (scrivere l'output su una stringa, chiamare strlen (), chiamare header (), render string).

Questo approccio ha il vantaggio di non costringerti a gestire una coda di "front-end" e, sebbene possa essere necessario fare un po 'di lavoro sul back-end per evitare che i processi figlio HTTP da corsa si calpestino l'un l'altro, è qualcosa che devi già fare , Comunque.


Questo non sembra funzionare. Quando lo uso, header('Content-Length: 3'); echo '1234'; sleep(5);anche se il browser richiede solo 3 caratteri, attende comunque 5 secondi prima di mostrare la risposta. Cosa mi sto perdendo?
Thomas Tempelmann,

@ThomasTempelmann - Probabilmente dovrai chiamare flush () per forzare l'effettivo rendering dell'output immediatamente, altrimenti l'output verrà bufferizzato fino alla chiusura dello script o all'invio di dati sufficienti a STDOUT per svuotare il buffer.
Peter,

Ho già provato molti modi per scaricare, trovato qui su SO. Nessuno aiuta. E i dati sembrano essere inviati anche non compressi, come si può capire phpinfo(). L'unica altra cosa che potrei immaginare è che devo prima raggiungere una dimensione minima del buffer, ad esempio 256 byte circa.
Thomas Tempelmann,

@ThomasTempelmann - Non vedo nulla nella tua domanda o nella mia risposta su gzip (di solito ha senso far funzionare lo scenario più semplice prima di aggiungere livelli di complessità). Per stabilire quando il server sta effettivamente inviando i dati è possibile utilizzare uno sniffer di pacchetti del plug-in del browser (come fiddler, tamperdata, ecc.). Quindi, se scopri che il server web sta davvero trattenendo tutto l'output dello script fino all'uscita indipendentemente dal flushing, allora devi modificare la configurazione del tuo server web (non c'è nulla che il tuo script PHP possa fare in quel caso).
Peter,

Uso un servizio Web virtuale, quindi ho poco controllo sulla sua configurazione. Speravo di trovare altri suggerimenti su quello che potrebbe essere il colpevole, ma sembra che la tua risposta semplicemente non sia così universalmente applicabile come sembra. Troppe cose possono andare storte, ovviamente. La tua soluzione sicuramente è molto più semplice da implementare rispetto a tutte le altre risposte fornite qui. Peccato che non funzioni per me.
Thomas Tempelmann,

1

Se non vuoi un ActiveMQ completo, ti consiglio di prendere in considerazione RabbitMQ . RabbitMQ è un sistema di messaggistica leggero che utilizza lo standard AMQP .

Consiglio di esaminare anche php-amqplib , una popolare libreria client AMQP per accedere ai broker di messaggi basati su AMQP.


0

penso che dovresti provare questa tecnica che ti aiuterà a chiamare tutte le pagine che ti piacciono tutte le pagine verranno eseguite contemporaneamente in modo indipendente senza attendere che ogni risposta della pagina sia asincrona.

cornjobpage.php // pagina principale

    <?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 desideri inviare i parametri url come loop, segui questa risposta: https://stackoverflow.com/a/41225209/6295712


0

Generare nuovi processi sul server usando exec()o direttamente su un altro server usando curl non si adatta affatto così bene, se andiamo per i dirigenti stai sostanzialmente riempiendo il tuo server di processi a lungo termine che possono essere gestiti da altri server non web, e l'uso di arricciatura lega un altro server a meno che non si crei una sorta di bilanciamento del carico.

Ho usato Gearman in alcune situazioni e lo trovo migliore per questo tipo di caso d'uso. Posso utilizzare un singolo server delle code dei lavori per gestire sostanzialmente l'accodamento di tutti i lavori che devono essere eseguiti dal server e far girare i server dei lavoratori, ognuno dei quali può eseguire tutte le istanze del processo di lavoro necessarie e aumentare il numero di server di lavoro in base alle esigenze e disattivarli quando non sono necessari. Mi permette anche di chiudere completamente i processi di lavoro quando necessario e di mettere in coda i lavori fino a quando i lavoratori tornano online.


-4

PHP è un linguaggio a thread singolo, quindi non esiste un modo ufficiale per avviare un processo asincrono con esso diverso dall'uso di execo popen. C'è un post sul blog qui . Anche la tua idea per una coda in MySQL è una buona idea.

Il tuo requisito specifico qui è per l'invio di una e-mail all'utente. Sono curioso di sapere perché stai cercando di farlo in modo asincrono poiché l'invio di un'e-mail è un'operazione piuttosto banale e veloce da eseguire. Suppongo che se stai inviando tonnellate di e-mail e il tuo ISP ti sta bloccando con il sospetto di spamming, questo potrebbe essere un motivo per fare la coda, ma a parte questo non riesco a pensare a nessun motivo per farlo in questo modo.


L'e-mail era solo un esempio, poiché le altre attività sono più complesse da spiegare e non è proprio il punto della domanda. Come abbiamo usato per inviare e-mail, il comando e-mail non sarebbe tornato fino a quando il server remoto non ha accettato la posta. Abbiamo scoperto che alcuni server di posta erano configurati per aggiungere lunghi ritardi (come ritardi di 10-20 secondi) prima di accettare la posta (probabilmente per combattere gli spambots) e che questi ritardi sarebbero stati passati ai nostri utenti. Ora stiamo usando un server di posta locale per mettere in coda i messaggi da inviare, quindi questo particolare non si applica, ma abbiamo altri compiti di natura simile.
David,

Ad esempio: l'invio di e-mail tramite Google Apps Smtp con SSL e la porta 465 richiede più tempo del solito.
Gixty,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.