Najlepszy sposób na zarządzanie długo działającym skryptem php?

80

Mam skrypt PHP, którego wykonanie zajmuje dużo czasu (5-30 minut). Na wszelki wypadek, skrypt używa curl do pobierania danych z innego serwera. To jest powód, dla którego trwa to tak długo; musi czekać na załadowanie każdej strony, zanim ją przetworzy i przejdzie do następnej.

Chcę mieć możliwość zainicjowania skryptu i pozostawienia go, dopóki nie zostanie ukończony, co spowoduje ustawienie flagi w tabeli bazy danych.

Muszę wiedzieć, jak zakończyć żądanie http przed zakończeniem działania skryptu. Czy skrypt PHP jest najlepszym sposobem na to?

kbanman
źródło
1
Chociaż nie wspomniałeś o tym w językach obsługiwanych przez twój serwer, zgadnę, że jeśli masz możliwość uruchomienia Rubiego i Perla, prawdopodobnie możesz dodać Node.js, a to brzmi dla mnie jak idealny przypadek użycia dla JavaScript : Twój skrypt spędza większość czasu na oczekiwaniu na zakończenie żądań, co jest obszarem, w którym wyróżnia się paradygmat asynchroniczny. Brak wątków oznacza łatwą synchronizację, współbieżność oznacza spead.
djfm
Możesz to zrobić za pomocą PHP. Używałbym Gouttei Guzzleimplementował wątki współbieżności. Możesz również spojrzeć na Gearmanuruchamianie równoległych żądań w postaci pracowników.
Andre Garcia

Odpowiedzi:

114

Z pewnością można to zrobić za pomocą PHP, jednak NIE należy tego robić w tle - nowy proces należy oddzielić od grupy procesów, w której jest inicjowany.

Ponieważ ludzie ciągle udzielają tej samej złej odpowiedzi na to często zadawane pytania, tutaj napisałem pełniejszą odpowiedź:

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

Z komentarzy:

Krótka wersja to shell_exec('echo /usr/bin/php -q longThing.php | at now');tylko powody, dla których umieszczenie tutaj jest trochę długie.

symcbean
źródło
Ten wpis na blogu jest prawdziwą odpowiedzią. Exec & system PHP ma zbyt wiele potencjalnych pułapek.
Incredimike
2
jest szansa na skopiowanie odpowiednich szczegółów do odpowiedzi? jest zbyt wiele starych odpowiedzi, które prowadzą do martwych blogów. Ten blog nie umarł (jeszcze), ale kiedyś będzie.
Murphy
5
Krótka wersja to shell_exec('echo /usr/bin/php -q longThing.php | at now');tylko powody, dla których umieszczenie tutaj jest trochę długie.
symcbean
1
Wysoce głosowana odpowiedź na wysoko ocenione pytanie, ale odpowiedź nie zawiera nic więcej niż link do posta na blogu. Dodaj rzeczywistą odpowiedź, zgodnie z meta.stackexchange.com/questions/8231/ ... i / lub centrum pomocy
Nanne
1
czy mogę wiedzieć, co robi ta opcja -q?
Kiren Siva
11

Szybkim i nieczystym sposobem byłoby użycie ignore_user_abortfunkcji w php. To w zasadzie mówi: Nie obchodzi mnie, co robi użytkownik, uruchamiaj ten skrypt, dopóki nie zostanie zakończony. Jest to nieco niebezpieczne, jeśli jest to witryna publiczna (ponieważ jest możliwe, że w końcu uruchomisz 20 ++ wersji skryptu w tym samym czasie, jeśli zostanie zainicjowany 20 razy).

"Czystym" sposobem (przynajmniej IMHO) jest ustawienie flagi (na przykład w bazie danych), gdy chcesz zainicjować proces i uruchamiać cronjob co godzinę (lub tak), aby sprawdzić, czy ta flaga jest ustawiona. Jeśli jest ustawiony, uruchamia się długo działający skrypt, jeśli NIE jest ustawiony, nic się nie dzieje.

FlorianH
źródło
Tak więc metoda „ignore_user_abort” pozwoliłaby użytkownikowi zamknąć okno przeglądarki, ale czy jest coś, co mógłbym zrobić, aby zwracał odpowiedź HTTP do klienta przed zakończeniem działania?
kbanman
1
@kbanman Yep. Trzeba zamknąć połączenie: header("Connection: close", true);. I nie zapomnij o flush ()
Benubird
8

Możesz użyć exec lub system, aby rozpocząć zadanie w tle, a następnie wykonać tę pracę.

Istnieją również lepsze metody skrobania sieci niż ta, której używasz. Możesz zastosować podejście wielowątkowe (wiele wątków wykonujących jedną stronę na raz) lub jedno wykorzystujące pętlę zdarzeń (jeden wątek wykonujący wiele stron jednocześnie). Moim osobistym podejściem do używania Perla byłoby użycie AnyEvent :: HTTP .

ETA: symcbean wyjaśnił, jak prawidłowo odłączyć proces w tle tutaj .

Leon Timmermans
źródło
5
Prawie dobrze. Samo użycie exec lub systemu wróci, aby ugryźć cię w dupę. Zobacz moją odpowiedź po szczegóły.
symcbean
5

Nie, PHP nie jest najlepszym rozwiązaniem.

Nie jestem pewien co do Rubiego czy Perla, ale w Pythonie mógłbyś przepisać skrobak strony, aby był wielowątkowy i prawdopodobnie działałby co najmniej 20x szybciej. Pisanie aplikacji wielowątkowych może być pewnym wyzwaniem, ale pierwsza napisana przeze mnie aplikacja Pythona była wielowątkowym skrobakiem stron. Możesz po prostu wywołać skrypt Pythona ze strony PHP, używając jednej z funkcji wykonywania powłoki.

jamieb
źródło
Faktyczna część mojego skrobania jest bardzo wydajna. Jak wspomniałem powyżej, zabija mnie ładowanie każdej strony. Zastanawiałem się, czy PHP ma działać przez tak długi czas.
kbanman
Jestem trochę stronniczy, ponieważ od czasu nauki Pythona po prostu nie znoszę PHP. Jeśli jednak zeskrobujesz więcej niż jedną stronę (w serii), prawie na pewno uzyskasz lepszą wydajność, robiąc to równolegle z aplikacją wielowątkową.
jamieb
1
Jest jakaś szansa, że ​​mógłbyś mi przesłać przykład takiego skrobaka? Pomogłoby mi to w dużej mierze, ponieważ nie dotknąłem jeszcze Pythona.
kbanman
Gdybym musiał to przepisać, użyłbym po prostu eventlet. Dzięki temu mój kod jest około 10x prostszy: eventlet.net/doc
jamieb
5

Tak, możesz to zrobić w PHP. Ale oprócz PHP rozsądnie byłoby użyć menedżera kolejek. Oto strategia:

  1. Podziel duże zadanie na mniejsze zadania. W twoim przypadku każde zadanie może ładować jedną stronę.

  2. Wyślij każde małe zadanie do kolejki.

  3. Uruchom gdzieś swoich pracowników w kolejce.

Korzystanie z tej strategii ma następujące zalety:

  1. W przypadku długotrwałych zadań ma możliwość regeneracji w przypadku wystąpienia poważnego problemu w połowie biegu - nie trzeba zaczynać od początku.

  2. Jeśli zadania nie muszą być uruchamiane sekwencyjnie, możesz uruchomić wielu pracowników, aby wykonywać zadania jednocześnie.

Masz wiele opcji (to tylko kilka):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Jeśli używasz frameworka Laravel, kolejki są wbudowane ( https://laravel.com/docs/5.4/queues ), ze sterownikami dla AWS SES, Redis, Beanstalkd
aljo f
źródło
3

PHP może być najlepszym narzędziem lub nie, ale wiesz, jak go używać, a reszta aplikacji jest napisana przy jego użyciu. Te dwie cechy, w połączeniu z faktem, że PHP jest „wystarczająco dobry”, stanowią całkiem mocne argumenty przemawiające za jego użyciem, zamiast Perla, Ruby czy Pythona.

Jeśli Twoim celem jest nauczenie się innego języka, wybierz jeden i używaj go. Każdy język, o którym wspomniałeś, spełni swoje zadanie, nie ma problemu. Tak się składa, że ​​lubię Perla, ale to, co lubisz, może być inne.

Symcbean ma pod swoim łączem kilka dobrych rad dotyczących zarządzania procesami w tle.

Krótko mówiąc, napisz skrypt CLI PHP do obsługi długich bitów. Upewnij się, że w jakiś sposób zgłasza stan. Utwórz stronę php do obsługi aktualizacji statusu, używając AJAX lub tradycyjnych metod. Twój skrypt kickoff uruchomi proces działający we własnej sesji i zwróci potwierdzenie, że proces trwa.

Powodzenia.

daotoad
źródło
1

Zgadzam się z odpowiedziami, które mówią, że powinno to być uruchamiane w tle. Ale ważne jest również, aby zgłaszać stan, aby użytkownik wiedział, że praca jest wykonywana.

Po otrzymaniu żądania PHP, aby rozpocząć proces, możesz przechowywać w bazie danych reprezentację zadania z unikalnym identyfikatorem. Następnie rozpocznij proces skrobania ekranu, przekazując mu unikalny identyfikator. Zgłoś ponownie aplikacji na iPhone'a, że ​​zadanie zostało uruchomione i że powinna sprawdzić określony adres URL zawierający nowy identyfikator zadania, aby uzyskać najnowszy stan. Aplikacja na iPhone może teraz sondować (lub nawet „długo sondować”) ten adres URL. W międzyczasie proces w tle zaktualizowałby reprezentację zadania w bazie danych, tak jak działało, z procentem ukończenia, bieżącym krokiem lub innymi wskaźnikami stanu, które chcesz. A kiedy skończy, ustawia flagę ukończenia.

Jakub
źródło
1

Możesz wysłać go jako żądanie XHR (Ajax). Klienci zwykle nie mają limitu czasu dla XHR, w przeciwieństwie do zwykłych żądań HTTP.

JAL
źródło
1

Zdaję sobie sprawę, że jest to dość stare pytanie, ale chciałbym spróbować. Ten skrypt próbuje zająć się zarówno początkowym wezwaniem, aby szybko zakończyć i pociąć duże obciążenie na mniejsze kawałki. Nie testowałem tego rozwiązania.

<?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
źródło
@symcbean Przeczytałem post, który zasugerowałeś i chciałbym poznać Twoje przemyślenia na temat tego alternatywnego rozwiązania.
Francisco Luz
Po pierwsze, podałeś mi pomysł na początek dla mojego pierwszego bota (teehee). Po drugie, jak oceniasz wydajność swojego rozwiązania? Czy pracowałeś z tym dalej i nauczyłeś się czegoś więcej? Jestem zainteresowany wdrożeniem czegoś podobnego do pogłębiania 26 000 obrazów (1,3 GB), wykonywania różnych operacji itp. To trochę potrwa. Twoje jest jedynym rozwiązaniem, które nie wydaje się zepsute, użyj exec () dreszcz lub wymaga Linuksa (niektórzy z nas, przegrani, nadal muszą używać Windows). Wolę uczyć się z twojego bicia głową niż własnego: P
Just Plain High
@HighPriestessofTheTech Cześć, nie poszedłem dalej. W czasie, gdy to pisałem, właśnie przeprowadzałem eksperyment myślowy.
Francisco Luz
1
Ojej ... Więc będę się uczył z własnego bicia głowy ... Powiem ci, jak leci;)
Just Plain High
1
Próbowałem tego i uważam, że jest to całkiem przydatne.
Alex
1

Chciałbym zaproponować rozwiązanie, które trochę różni się od Symcbean, głównie dlatego, że mam dodatkowe wymaganie, aby długo działający proces musiał być uruchamiany jako inny użytkownik, a nie jako użytkownik apache / www-data.

Pierwsze rozwiązanie wykorzystujące cron do sondowania tabeli zadań w tle:

  • Strona internetowa PHP wstawia do tabeli zadań w tle, stan „SUBMITTED”
  • cron działa raz na 3 minuty, używając innego użytkownika, uruchamiając skrypt PHP CLI, który sprawdza tabelę zadań w tle pod kątem wierszy „SUBMITTED”
  • Interfejs wiersza polecenia PHP zaktualizuje kolumnę stanu w wierszu do „PRZETWARZANIE” i rozpocznie przetwarzanie, a po zakończeniu zostanie zaktualizowany do „ZAKOŃCZONO”

Drugie rozwiązanie wykorzystujące funkcję inotify w systemie Linux:

  • Strona internetowa PHP aktualizuje plik kontrolny z parametrami ustawionymi przez użytkownika, a także podając identyfikator zadania
  • Skrypt powłoki (jako użytkownik inny niż www) działający w trybie inotifywait będzie czekał na zapisanie pliku kontrolnego
  • po zapisaniu pliku kontrolnego zostanie zgłoszone zdarzenie close_write i skrypt powłoki będzie kontynuowany
  • skrypt powłoki wykonuje PHP CLI w celu wykonania długotrwałego procesu
  • PHP CLI zapisuje dane wyjściowe do pliku dziennika identyfikowanego przez identyfikator zadania lub alternatywnie aktualizuje postęp w tabeli stanu
  • Strona internetowa PHP może sondować plik dziennika (na podstawie identyfikatora zadania), aby pokazać postęp długotrwałego procesu, lub może również sprawdzać tabelę stanu

Dodatkowe informacje można znaleźć w moim poście: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

YudhiWidyatama
źródło
0

Zrobiłem podobne rzeczy z Perlem, podwójnym widelcem () i odłączeniem od procesu rodzica. Wszystkie prace związane z pobieraniem http powinny być wykonywane w procesie rozwidlonym.

Alexandr Ciornii
źródło
0

Użyj serwera proxy do delegowania żądania.

zerodin
źródło
0

ZAWSZE używam jednego z tych wariantów (ponieważ różne wersje Linuksa mają różne zasady dotyczące obsługi danych wyjściowych / niektóre programy działają inaczej):

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

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

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

Być może będziesz musiał zainstalować "nohup". Ale na przykład, kiedy automatyzowałem konwersje wideo FFMPEG, interfejs wyjściowy nie był w 100% obsługiwany przez przekierowywanie strumieni wyjściowych 1 i 2, więc użyłem nohup I przekierowałem wyjście.

dr pali
źródło
0

jeśli masz długi skrypt, podziel pracę strony za pomocą parametru wejściowego dla każdego zadania. (wtedy każda strona działa jak wątek) tj. jeśli strona ma 1 lac product_keywords długa pętla procesu, to zamiast pętli stwórz logikę dla jednego słowa kluczowego i przekaż to słowo kluczowe from magic lub cornjobpage.php (w poniższym przykładzie)

a dla pracownika pracującego w tle 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 oczekiwania 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

Nie jest to najlepsze podejście, jak wielu tutaj stwierdziło, ale może to pomóc:

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
źródło
0

Jeśli pożądanym wyjściem twojego skryptu jest przetwarzanie, a nie strona internetowa, to uważam, że pożądanym rozwiązaniem jest uruchomienie skryptu z powłoki, tak jak

php my_script.php

MrMartin
źródło