Uruchom zadanie PHP asynchronicznie

144

Pracuję nad dość dużą aplikacją internetową, a backend jest głównie w PHP. W kodzie jest kilka miejsc, w których muszę wykonać jakieś zadanie, ale nie chcę, aby użytkownik czekał na wynik. Na przykład podczas tworzenia nowego konta muszę wysłać im powitalną wiadomość e-mail. Ale kiedy klikną przycisk „Zakończ rejestrację”, nie chcę zmuszać ich do czekania, aż wiadomość e-mail zostanie faktycznie wysłana, po prostu chcę rozpocząć proces i od razu zwrócić wiadomość do użytkownika.

Do tej pory w niektórych miejscach używałem czegoś, co wygląda jak hack z exec (). Zasadniczo robienie rzeczy takich jak:

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

Co wydaje się działać, ale zastanawiam się, czy istnieje lepszy sposób. Rozważam napisanie systemu, który kolejkowałby zadania w tabeli MySQL, oraz osobnego, długo działającego skryptu PHP, który raz na sekundę odpytuje tę tabelę i wykonuje nowe zadania, które znajdzie. Miałoby to również tę zaletę, że pozwoliłoby mi w przyszłości podzielić zadania między kilka maszyn roboczych, jeśli zajdzie taka potrzeba.

Czy wymyślam koło na nowo? Czy istnieje lepsze rozwiązanie niż hack exec () lub kolejka MySQL?

davr
źródło

Odpowiedzi:

80

Użyłem metody kolejkowania i działa ona dobrze, ponieważ możesz odłożyć to przetwarzanie do czasu, gdy obciążenie serwera będzie bezczynne, co pozwala dość efektywnie zarządzać obciążeniem, jeśli możesz łatwo podzielić „zadania, które nie są pilne”.

Samo rozwijanie nie jest zbyt trudne, oto kilka innych opcji do sprawdzenia:

  • GearMan - ta odpowiedź została napisana w 2009 roku i od tego czasu GearMan wygląda na popularną opcję, patrz komentarze poniżej.
  • ActiveMQ, jeśli chcesz mieć pełną kolejkę komunikatów typu open source.
  • ZeroMQ - jest to całkiem fajna biblioteka gniazd, która ułatwia pisanie rozproszonego kodu bez konieczności przejmowania się samym programowaniem gniazd. Można go użyć do kolejkowania wiadomości na jednym hoście - wystarczy, że aplikacja internetowa wypchnie do kolejki coś, co stale działająca aplikacja konsolowa zużyje przy następnej odpowiedniej okazji
  • beanstalkd - znalazłem ten tylko podczas pisania tej odpowiedzi, ale wygląda interesująco
  • dropr to projekt kolejki wiadomości oparty na PHP, ale nie był aktywnie obsługiwany od września 2010
  • php-enqueue jest ostatnio (2017) utrzymywanym opakowaniem dla różnych systemów kolejek
  • Na koniec post na blogu o używaniu memcached do kolejkowania wiadomości

Innym, być może prostszym, podejściem jest użycie ignore_user_abort - po wysłaniu strony do użytkownika możesz wykonać końcowe przetwarzanie bez obawy o przedwczesne zakończenie, chociaż wydaje się, że wydłuża wczytywanie strony przez użytkownika perspektywiczny.

Paul Dixon
źródło
Dzięki za wszystkie wskazówki. Ten konkretny o ignore_user_abort tak naprawdę nie pomaga w moim przypadku, moim celem jest uniknięcie niepotrzebnych opóźnień dla użytkownika.
davr
2
Jeśli ustawisz nagłówek HTTP Content-Length w odpowiedzi „Dziękujemy za rejestrację”, przeglądarka powinna zamknąć połączenie po odebraniu określonej liczby bajtów. Pozostawia to uruchomiony proces po stronie serwera (zakładając, że ustawiono ignore_user_abort) bez konieczności czekania przez użytkownika końcowego. Oczywiście będziesz musiał obliczyć rozmiar treści odpowiedzi przed renderowaniem nagłówków, ale w przypadku krótkich odpowiedzi jest to dość łatwe.
Peter,
1
Gearman ( gearman.org ) to świetna kolejka wiadomości typu open source, która jest wieloplatformowa. Możesz pisać pracowników w C, PHP, Perl lub w jakimkolwiek innym języku. Istnieją wtyczki Gearman UDF dla MySQL, a także możesz użyć Net_Gearman z PHP lub klienta gruszki gearman.
Justin Swanhart
Gearman byłby tym, co poleciłbym dzisiaj (w 2015) w stosunku do dowolnego niestandardowego systemu kolejkowania pracy.
Peter
Inną opcją jest skonfigurowanie serwera node js do obsługi żądania i zwracania szybkiej odpowiedzi z zadaniem pomiędzy nimi. Wiele rzeczy wewnątrz skryptu node js jest wykonywanych asynchronicznie, na przykład żądanie http.
Zordon
22

Jeśli chcesz tylko wykonać jedno lub kilka żądań HTTP bez czekania na odpowiedź, istnieje również proste rozwiązanie PHP.

W skrypcie wywołującym:

$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

W nazwie script.php możesz wywołać te funkcje PHP w pierwszych wierszach:

ignore_user_abort(true);
set_time_limit(0);

Powoduje to kontynuowanie działania skryptu bez ograniczeń czasowych po zamknięciu połączenia HTTP.

Markus
źródło
set_time_limit nie ma wpływu, jeśli php działa w trybie awaryjnym
Baptiste Pernet
17

Innym sposobem na rozwidlenie procesów jest zawijanie. Możesz skonfigurować swoje wewnętrzne zadania jako usługę sieciową. Na przykład:

Następnie w skryptach, do których użytkownik uzyskuje dostęp, wywołuje usługę:

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

Twoja usługa może śledzić kolejkę zadań za pomocą mysql lub cokolwiek chcesz: wszystko jest opakowane w usługę, a Twój skrypt po prostu zużywa adresy URL. Dzięki temu możesz w razie potrzeby przenieść usługę na inny komputer / serwer (tj. Łatwo skalowalną).

Dodanie autoryzacji http lub niestandardowego schematu autoryzacji (takiego jak usługi internetowe Amazon) umożliwia otwarcie zadań do wykorzystania przez inne osoby / usługi (jeśli chcesz) i możesz pójść dalej i dodać usługę monitorowania na wierzchu, aby śledzić stan kolejki i zadania.

To wymaga trochę pracy związanej z konfiguracją, ale jest wiele korzyści.

rojoca
źródło
1
Nie podoba mi się to podejście, ponieważ przeciąża serwer WWW
Oved Yavine
7

Użyłem Beanstalkd w jednym projekcie i planowałem ponownie. Odkryłem, że to doskonały sposób na uruchamianie procesów asynchronicznych.

Kilka rzeczy, które z nim zrobiłem, to:

  • Zmiana rozmiaru obrazu - i przy lekko załadowanej kolejce przechodzącej do skryptu PHP opartego na CLI, zmiana rozmiaru dużych (2 MB +) obrazów działała dobrze, ale próba zmiany rozmiaru tych samych obrazów w instancji mod_php regularnie powodowała problemy z pamięcią (I ograniczył proces PHP do 32 MB, a zmiana rozmiaru zajęła więcej)
  • testy w najbliższej przyszłości - beanstalkd ma dostępne opóźnienia (udostępnij to zadanie do uruchomienia dopiero po X sekundach) - dzięki czemu mogę odpalić 5 lub 10 czeków dla zdarzenia, nieco później

Napisałem system oparty na Zend-Framework, aby zdekodować „ładny” adres URL, na przykład, aby zmienić rozmiar obrazu, który by wywołał QueueTask('/image/resize/filename/example.jpg'). Adres URL został najpierw zdekodowany do tablicy (moduł, kontroler, akcja, parametry), a następnie przekonwertowany na format JSON w celu wstrzyknięcia do samej kolejki.

Długo działający skrypt CLI odebrał zadanie z kolejki, uruchomił je (przez Zend_Router_Simple) i, jeśli było to wymagane, umieścił informacje w memcached, aby PHP mogło je pobrać zgodnie z wymaganiami po zakończeniu.

Jedną z pomyłek, które również zrobiłem, było to, że skrypt cli działał tylko przez 50 pętli przed ponownym uruchomieniem, ale jeśli chciałby uruchomić ponownie zgodnie z planem, zrobiłby to natychmiast (uruchamiany przez skrypt bash). Jeśli wystąpił problem, a ja to zrobiłem exit(0)(domyślna wartość exit;lub die();), najpierw zatrzymywał się na kilka sekund.

Alister Bulman
źródło
Podoba mi się wygląd łodygi fasoli, kiedy dodadzą wytrwałości, myślę, że będzie idealny.
davr
To już w kodzie źródłowym i jest stabilizowane. Nie mogę się też doczekać „nazwanych ofert pracy”, więc mogę wrzucić tam rzeczy, ale wiem, że nie zostaną dodane, jeśli już są. Dobre na regularne imprezy.
Alister Bulman
@AlisterBulman mógłbyś podać więcej informacji lub przykładów na temat „Długo działający skrypt CLI, a następnie odebrał zadanie z kolejki”. Próbuję zbudować taki skrypt CLI dla mojej aplikacji.
Sasi varna kumar
7

Jeśli chodzi tylko o zapewnienie drogich zadań, w przypadku gdy obsługiwane jest php-fpm, dlaczego nie skorzystać z fastcgi_finish_request()funkcji?

Ta funkcja opróżnia wszystkie dane odpowiedzi do klienta i kończy żądanie. Pozwala to na wykonywanie czasochłonnych zadań bez pozostawiania otwartego połączenia z klientem.

Tak naprawdę nie używasz asynchroniczności w ten sposób:

  1. Najpierw utwórz cały kod główny.
  2. Wykonaj fastcgi_finish_request().
  3. Zrób wszystkie ciężkie rzeczy.

Po raz kolejny potrzebne jest php-fpm.

Denys Gorobchenko
źródło
5

Oto prosta klasa, którą zakodowałem dla mojej aplikacji internetowej. Pozwala na rozwidlanie skryptów PHP i innych skryptów. Działa na systemach UNIX i 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
źródło
4

To ta sama metoda, której używam od kilku lat i nie widziałem ani nie znalazłem nic lepszego. Jak ludzie powiedzieli, PHP jest jednowątkowy, więc niewiele więcej możesz zrobić.

Właściwie dodałem do tego jeden dodatkowy poziom, czyli pobieranie i przechowywanie identyfikatora procesu. To pozwala mi przekierować na inną stronę i pozwolić użytkownikowi usiąść na tej stronie, używając AJAX do sprawdzenia, czy proces się zakończył (identyfikator procesu już nie istnieje). Jest to przydatne w przypadkach, gdy długość skryptu spowodowałaby przekroczenie limitu czasu przeglądarki, ale użytkownik musi poczekać na zakończenie skryptu przed następnym krokiem. (W moim przypadku było to przetwarzanie dużych plików ZIP z plikami podobnymi do CSV, które dodają do bazy danych do 30 000 rekordów, po czym użytkownik musi potwierdzić pewne informacje.)

Użyłem również podobnego procesu do generowania raportów. Nie jestem pewien, czy użyłbym „przetwarzania w tle” do czegoś takiego jak e-mail, chyba że istnieje prawdziwy problem z powolnym SMTP. Zamiast tego mógłbym użyć tabeli jako kolejki, a następnie uruchomić proces, który uruchamia się co minutę, aby wysłać e-maile w kolejce. Musisz być ostrożny w wysyłaniu e-maili dwa razy lub innych podobnych problemach. Rozważałbym podobny proces kolejkowania również dla innych zadań.

Darryl Hein
źródło
1
Do jakiej metody odwołujesz się w swoim pierwszym zdaniu?
Simon East
3

PHP MA wielowątkowość, po prostu nie jest domyślnie włączona, istnieje rozszerzenie o nazwie pthreads, które robi dokładnie to. Będziesz jednak potrzebował php skompilowanego z ZTS. (Bezpieczne wątki) Linki:

Przykłady

Kolejny samouczek

Rozszerzenie pthreads PECL

Omar S.
źródło
2

To świetny pomysł, aby użyć cURL zgodnie z sugestią rojoca.

Oto przykład. Możesz monitorować text.txt, gdy skrypt działa w tle:

<?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
źródło
2
Naprawdę pomogłoby, gdyby kod źródłowy został skomentowany. Nie mam pojęcia, co się tam dzieje i które części są przykładowe, a które można ponownie wykorzystać do własnych celów.
Thomas Tempelmann
1

Niestety PHP nie ma żadnych natywnych możliwości obsługi wątków. Więc myślę, że w tym przypadku nie masz innego wyjścia, jak tylko użyć jakiegoś niestandardowego kodu, aby zrobić to, co chcesz.

Jeśli szukasz w sieci rzeczy związanych z wątkami PHP, niektórzy wymyślili sposoby na symulowanie wątków w PHP.

Peter D.
źródło
1

Jeśli ustawisz nagłówek HTTP Content-Length w odpowiedzi „Dziękujemy za rejestrację”, przeglądarka powinna zamknąć połączenie po odebraniu określonej liczby bajtów. Powoduje to, że proces po stronie serwera działa (zakładając, że ustawiono ignore_user_abort), dzięki czemu może zakończyć pracę bez czekania użytkownika końcowego.

Oczywiście będziesz musiał obliczyć rozmiar zawartości odpowiedzi przed renderowaniem nagłówków, ale jest to całkiem łatwe w przypadku krótkich odpowiedzi (zapisywanie danych wyjściowych do ciągu znaków, wywołanie strlen (), wywołanie nagłówka (), renderowanie ciągu).

To podejście ma tę zaletę, że nie zmusza cię do zarządzania kolejką „front-end” i chociaż może być konieczne wykonanie pewnych prac na zapleczu, aby zapobiec nadeptywaniu na siebie wyścigowych procesów potomnych HTTP, to już trzeba było zrobić , tak czy siak.

Piotr
źródło
To nie działa. Kiedy używam header('Content-Length: 3'); echo '1234'; sleep(5);tego, mimo że przeglądarka przyjmuje tylko 3 znaki, nadal czeka 5 sekund przed wyświetleniem odpowiedzi. czego mi brakuje?
Thomas Tempelmann
@ThomasTempelmann - Prawdopodobnie będziesz musiał wywołać flush (), aby wymusić natychmiastowe renderowanie wyjścia, w przeciwnym razie dane wyjściowe będą buforowane do momentu zakończenia skryptu lub wysłania wystarczającej ilości danych do STDOUT, aby opróżnić bufor.
Piotr
Próbowałem już wielu sposobów spłukiwania, znalezionych tutaj na SO. Żadna pomoc. Wydaje się, że dane również nie zostały wysłane gzipem, jak można się zorientować phpinfo(). Jedyną inną rzeczą, jaką mogę sobie wyobrazić, jest to, że najpierw muszę osiągnąć minimalny rozmiar bufora, np. 256 lub więcej bajtów.
Thomas Tempelmann
@ThomasTempelmann - nie widzę nic w twoim pytaniu ani mojej odpowiedzi na temat gzip (zwykle warto najpierw uruchomić najprostszy scenariusz przed dodaniem warstw złożoności). Aby ustalić, kiedy serwer faktycznie wysyła dane, możesz użyć sniffera pakietów wtyczki przeglądarki (np. Fiddler, tamperdata itp.). Następnie, jeśli okaże się, że serwer sieciowy faktycznie przechowuje wszystkie dane wyjściowe skryptu do momentu zakończenia pracy, niezależnie od opróżniania, musisz zmodyfikować konfigurację serwera WWW (nie ma nic, co w takim przypadku może zrobić twój skrypt PHP).
Peter
Korzystam z wirtualnej usługi internetowej, więc mam niewielką kontrolę nad jej konfiguracją. Miałem nadzieję, że znajdę inne sugestie dotyczące przyczyny, ale wydaje się, że Twoja odpowiedź nie jest tak uniwersalna, jak się wydaje. Oczywiście zbyt wiele rzeczy może się nie udać. Twoje rozwiązanie z pewnością jest znacznie łatwiejsze do wdrożenia niż wszystkie inne podane tutaj odpowiedzi. Szkoda, że ​​to nie działa dla mnie.
Thomas Tempelmann
1

Jeśli nie chcesz pełnego ActiveMQ, polecam rozważyć RabbitMQ . RabbitMQ to uproszczona obsługa wiadomości korzystająca ze standardu AMQP .

Polecam również zajrzeć do php-amqplib - popularnej biblioteki klienta AMQP, aby uzyskać dostęp do brokerów wiadomości opartych na AMQP.

phpPhil
źródło
0

Myślę, że powinieneś wypróbować tę technikę, pomoże ona wywołać dowolną liczbę stron, wszystkie strony będą działać jednocześnie niezależnie, bez czekania na każdą odpowiedź strony jako asynchroniczną.

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: jeśli chcesz wysłać parametry adresu URL jako pętlę, postępuj zgodnie z tą odpowiedzią: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
źródło
0

Odradzanie nowych procesów na serwerze przy użyciu exec()lub bezpośrednio na innym serwerze przy użyciu curl wcale nie skaluje się zbyt dobrze, jeśli zdecydujemy się na exec, w zasadzie wypełniasz swój serwer długotrwałymi procesami, które mogą być obsługiwane przez inne serwery niezwiązane z siecią, a użycie curl wiąże inny serwer, chyba że zastosujesz jakiś rodzaj równoważenia obciążenia.

Użyłem Gearmana w kilku sytuacjach i uważam, że jest to lepsze dla tego rodzaju przypadków użycia. Mogę użyć pojedynczego serwera kolejki zadań, aby w zasadzie obsłużyć kolejkowanie wszystkich zadań, które muszą być wykonane przez serwer, i uruchomić serwery robocze, z których każdy może uruchamiać dowolną liczbę wystąpień procesu roboczego, i skalować w górę liczbę serwery robocze w razie potrzeby i wyłączaj je, gdy nie są potrzebne. Pozwala mi również całkowicie zamknąć procesy robocze w razie potrzeby i kolejkować zadania, aż pracownicy wrócą do trybu online.

Chris Rutherfurd
źródło
-4

PHP jest językiem jednowątkowym, więc nie ma innego oficjalnego sposobu na rozpoczęcie procesu asynchronicznego niż użycie execlub popen. Jest blogu o tym tutaj . Twój pomysł na kolejkę w MySQL jest również dobrym pomysłem.

Twoim konkretnym wymaganiem jest wysłanie wiadomości e-mail do użytkownika. Ciekawi mnie, dlaczego próbujesz to zrobić asynchronicznie, ponieważ wysłanie wiadomości e-mail jest dość banalnym i szybkim zadaniem do wykonania. Przypuszczam, że jeśli wysyłasz mnóstwo e-maili, a Twój dostawca usług internetowych blokuje Cię podejrzenie spamowania, może to być jeden z powodów do kolejki, ale poza tym nie mam żadnego powodu, aby robić to w ten sposób.

Marc W.
źródło
E-mail był tylko przykładem, ponieważ inne zadania są bardziej skomplikowane do wyjaśnienia i tak naprawdę nie o to chodzi. W sposób, w jaki wysyłaliśmy e-maile, polecenie e-mail nie było zwracane, dopóki serwer zdalny nie zaakceptował wiadomości. Odkryliśmy, że niektóre serwery pocztowe zostały skonfigurowane tak, aby dodawać duże opóźnienia (np. 10-20 sekundowe opóźnienia) przed przyjęciem poczty (prawdopodobnie w celu zwalczania spamu), a te opóźnienia były następnie przekazywane naszym użytkownikom. Teraz używamy lokalnego serwera poczty do kolejkowania wiadomości do wysłania, więc ten konkretny nie ma zastosowania, ale mamy inne zadania o podobnym charakterze.
davr
Na przykład: wysyłanie wiadomości e-mail przez Google Apps Smtp z protokołem SSL i portem 465 trwa dłużej niż zwykle.
Gixty,