Domanda

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 all'effettivo invio dell'e-mail, voglio solo avviare il processo e restituire immediatamente un messaggio all'utente.

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

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

Il che sembra funzionare, ma mi chiedo se esiste un modo migliore.Sto pensando di scrivere un sistema che metta in coda le attività in una tabella MySQL e uno script PHP separato e di lunga esecuzione che interroghi quella tabella una volta al secondo ed esegua tutte le nuove attività che trova.Ciò avrebbe anche il vantaggio di permettermi di dividere i compiti tra diverse macchine operaie in futuro, se necessario.

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

È stato utile?

Soluzione

Ho utilizzato l'approccio dell'accodamento e funziona bene poiché puoi rinviare l'elaborazione fino a quando il carico del server non è inattivo, consentendoti di gestire il carico in modo abbastanza efficace se puoi suddividere facilmente "attività non urgenti".

Rollare il tuo non è troppo complicato, ecco alcune altre opzioni da verificare:

  • GearMan - questa risposta è stata scritta nel 2009 e da allora GearMan sembra un'opzione popolare, vedi i commenti qui sotto.
  • ActiveMQ se desideri una coda di messaggi open source completa.
  • ZeroMQ - questa è una libreria di socket piuttosto interessante che semplifica la scrittura di codice distribuito senza doversi preoccupare troppo della programmazione del socket stessa.Potresti usarlo per l'accodamento di messaggi su un singolo host: faresti semplicemente in modo che la tua webapp spinga qualcosa in una coda che un'app console in esecuzione continua consumerebbe alla successiva occasione adatta
  • beanstalkd - ho trovato solo questo mentre scrivevo questa risposta, ma sembra interessante
  • dropr è un progetto di coda di messaggi basato su PHP, ma non è stato mantenuto attivamente 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'utilizzo memcached per l'accodamento dei messaggi

Un altro approccio, forse più semplice, è quello di utilizzare ignora_utente_abort - una volta inviata la pagina all'utente, puoi eseguire l'elaborazione finale senza timore di una chiusura prematura, anche se ciò ha l'effetto di prolungare il caricamento della pagina dal punto di vista dell'utente.

Altri suggerimenti

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

Nello script di chiamata:

$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

Al chiamato script.php, è possibile richiamare queste funzioni PHP nelle prime righe:

ignore_user_abort(true);
set_time_limit(0);

Questo fa sì che lo script di continuare a funzionare senza limiti di tempo quando la connessione HTTP viene chiusa.

Un altro modo di generare i processi è via ricciolo. È possibile impostare le attività interne come un webservice. Ad esempio:

Poi, nel tuo script utente accede effettuare chiamate al servizio:

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

Il servizio può tenere traccia della coda di compiti con MySQL o qualsiasi altra cosa che ti piace il punto è: è tutto avvolto all'interno del servizio e lo script è solo consumando URL. In questo modo si libera fino a spostare il servizio a un altro computer / server, se necessario (cioè facilmente scalabile).

L'aggiunta di autorizzazione http o un regime di autorizzazione personalizzato (come i servizi web di Amazon) consente di aprire i vostri compiti a essere consumati da altre persone / servizi (se volete) e si poteva prendere ulteriormente e aggiungere un servizio di monitoraggio in cima alla tenere traccia di coda e il compito di stato.

Ci vuole un po 'di lavoro di set-up, ma ci sono un sacco di vantaggi.

Ho usato Beanstalkd per un progetto, e pianificato di nuovo. Ho trovato ad essere un ottimo modo per eseguire processi asincroni.

Un paio di cose che ho fatto con esso sono:

  • ridimensionamento delle immagini - e con una coda leggermente caricato spacciando per uno script PHP CLI-based, il ridimensionamento di grandi dimensioni (2mb +) immagini funzionava bene, ma cercando di ridimensionare le stesse immagini all'interno di un'istanza mod_php è stato regolarmente in esecuzione nella memoria-spazio problemi (I limitano il processo di PHP per 32 MB, e il ridimensionamento ha preso più di quello)
  • vicino-futuri controlli - beanstalkd ha ritardi a sua disposizione (rendere questo lavoro disponibile per eseguire solo dopo X secondi) - in modo da poter sparare 5 o 10 assegni per un evento, un po 'più avanti nel tempo

ho scritto un sistema basato Zend Framework-per decodificare un URL 'bella', quindi, ad esempio, per ridimensionare un'immagine si chiamerebbe QueueTask('/image/resize/filename/example.jpg'). L'URL è stato decodificato a un array (modulo, controllore, azione, parametri), e quindi convertito in JSON iniettabile alla coda stessa.

Uno script CLI lunga esecuzione poi prese il lavoro dalla coda, lo gestisce (tramite Zend_Router_Simple) e, se necessario, mettere le informazioni in memcached per il sito web PHP per raccogliere, come richiesto quando è stato fatto.

Una ruga ho anche messo in era che il cli-script è stato eseguito solo per 50 cicli prima di riavviare, ma se si voleva riavviare come previsto, farei immediatamente (gestito tramite un bash-script). Se ci fosse un problema e ho fatto exit(0) (il valore predefinito per exit; o die();) sarebbe prima pausa per un paio di secondi.

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

  

Questa funzione svuota tutti i dati di risposta al client e termina la richiesta. Questo permette di tempo compiti da svolgere senza lasciare la connessione al client aperto.

In realtà non usa asincronicità in questo modo:

  1. Fai tutto il codice principale prima.
  2. Esegui <=>.
  3. Fai tutta roba pesante.

è necessaria una volta php-fpm.

Ecco una semplice classe ho codificato per la mia applicazione web. Permette di forking 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);
        }
    }
}

Questo è lo stesso metodo che ho usato per un paio di anni e non ho visto o trovato niente di meglio. Come è già stato detto, PHP è a thread singolo, quindi non c'è molto altro si può fare.

Ho effettivamente aggiunto un ulteriore livello di questo e quello di ottenere e memorizzare l'ID del processo. Questo mi permette di reindirizzare a un'altra pagina e che l'utente sedersi su quella pagina, utilizzando AJAX per verificare se il processo è completo (id processo non esiste più). Questo è utile per i casi in cui la lunghezza dello script causerebbe il browser per timeout, ma l'utente deve attendere che lo script per completare prima del passaggio successivo. (Nel mio caso è stato l'elaborazione di grandi file ZIP con CSV come i file che si aggiungono fino a 30 000 record al database dopo che l'utente ha bisogno di confermare alcune informazioni.)

Ho anche usato un processo simile per la generazione di report. Non sono sicuro che userei "elaborazione in background" per qualcosa, come una e-mail, a meno che non ci sia un problema reale con uno SMTP lento. Invece potrei utilizzare una tabella come una coda e poi hanno un processo che viene eseguito ogni minuto per inviare le e-mail all'interno della coda. Si avrebbe bisogno di essere warry di inviare e-mail due volte o altri problemi simili. Vorrei prendere in considerazione un processo di messa in coda simile per altre attività come bene.

PHP ha multithreading, il suo solo non abilitato di default, c'è un'estensione chiamata pthreads , che fa esattamente questo. Avrete bisogno di php compilato con ZTS però. (Thread-safe) Link:

Esempi

un altro tutorial

pthreads PECL estensione

E 'una grande idea di utilizzare cURL come suggerito da rojoca.

Ecco un esempio. È possibile 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);
}

?>

Purtroppo PHP non ha alcun tipo di funzionalità di threading native. Quindi penso che in questo caso si ha altra scelta che utilizzare una sorta di codice personalizzato per fare quello che vuoi fare.

Se si ricerca intorno alla rete per PHP filettatura roba, alcune persone hanno escogitato modi per simulare le discussioni su PHP.

Se si imposta l'intestazione HTTP Content-Length nella vostra risposta "Grazie per la registrazione", quindi il browser dovrebbe chiudere la connessione dopo il numero specificato di byte vengono ricevuti. Questo lascia il processo lato server in esecuzione (assumendo che ignore_user_abort è impostato) in modo che possa finire a lavorare senza fare l'utente finale attesa.

Naturalmente è necessario per calcolare la dimensione del contenuto della risposta prima del rendering delle intestazioni, ma questo è abbastanza facile per le risposte brevi (uscita in scrittura a una stringa, chiamata strlen (), intestazione call (), render stringa).

Questo approccio ha il vantaggio di non ti costringe a gestire una coda di "front end", e anche se potrebbe essere necessario fare un certo lavoro sul back-end per evitare processi figli corsa HTTP da calpestare l'altro, questo è qualcosa che si doveva fare già, in ogni caso.

Se non si desidera che i ActiveMQ in piena regola, vi consiglio di prendere in considerazione RabbitMQ . RabbitMQ è di messaggistica leggero che utilizza il AMQP serie .

Vi consiglio di guardare anche in php-amqplib - una biblioteca AMQP famoso client per accedere AMQP broker di messaggistica basati.

penso che si dovrebbe provare questa tecnica vi aiuterà a chiamare come molti come pagine gradite tutte le pagine verrà eseguito in una sola volta in modo indipendente, senza attendere per ogni risposta pagina l'asincrona.

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

testpage.php

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

PS: se si desidera inviare parametri URL come anello poi seguire questa risposta: https://stackoverflow.com/a/41225209 / 6295712

La deposizione delle uova nuovi processi sul server utilizzando exec() o direttamente su un altro server utilizzando ricciolo non scala tanto bene a tutti, se andiamo per exec si sono fondamentalmente riempiendo il server con lunghi processi in esecuzione, che possono essere gestiti da altri non web rivolto verso i server, e l'utilizzo di legami rannicchiarsi un altro server a meno che non si costruisce in una sorta di bilanciamento del carico.

Ho usato Gearman in alcune situazioni e lo trovo di meglio per questo tipo di caso d'uso. Posso usare un singolo server coda di lavoro per gestire praticamente in coda di tutti i lavori che hanno bisogno di essere fatto dal server e girare i server di lavoratori, ognuno dei quali possono essere eseguiti come molte istanze del processo di lavoro in base alle esigenze, e scalare il numero di i server dei lavoratori in base alle esigenze e girano giù quando non serve. E 'anche lasciare che sono io chiudo i processi di lavoro del tutto quando necessario e code i lavori fino a quando gli operai tornano in linea.

PHP è un linguaggio a thread singolo, quindi non c'è alcun modo ufficiale per avviare un processo asincrono con esso altro che usare exec o popen. C'è un post sul blog su questo qui . La tua idea per una coda in MySQL è una buona idea pure.

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top