Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Jak poradzić sobie z czasochłonnym skryptem ?
Forum PHP.pl > Forum > PHP
wasilek
Witam,
to mój pierwszy post, mam nadzieję, że w dobrym dziale, a jak coś z nim nie tak to przepraszam z góry . smile.gif
A do rzeczy. Piszę skrypt pobierający/aktualizujący dużą ilość danych (kilkanaście tysięcy rekordów), które następnie są wprowadzane do bazy danych MySQL. Skrypt wywoływany przez cron.
Tylko tutaj pojawia się problem (nie przy cron, tylko przy przetwarzaniu tych danych). Przy mniejszej ilości (testowej) wszystko jest ok, przy większej - docelowej - oczywiście skrypt wyrzuca błąd o zbyt długim czasie działania. set_time_limit nie wchodzi w grę, bo nie wiem jaka będzie górna granica działania skryptu, a w skrypcie nie ma co optymalizować już. Myślałem żeby jakoś dane wejściowe dzielić na paczki mniejsze, żeby skrypt się "nie zapychał" (przyszło mi do głowy jakieś kombinowanie z wykonywaniem części zapytań i przenoszeniem przez header() z jakims parametrem, żeby dalszą część przetwarzał, ale jakoś nie przekonany do tego byłem, żeby to było najlepsze rozwiązanie), ale nie wiem czy to dobry pomysł, jak w ogole się do tego zabrać, i z taką prośbą o pomoc z tym zwracam się do Was. smile.gif
Starałem się szukać odpowiedzi na forum i Google, ale z mizernym skutkiem, bo nawet nie wiem jak to dokładnie nazwać...

dziękuję z góry za pomoc.
wrzasq
set_time_limit(0); - bez limitu
wasilek
Cytat(wrzasq @ 16.07.2008, 23:17:06 ) *
set_time_limit(0); - bez limitu


Faktycznie, działa tak. Tylko zastanawiam się czy na hostingu będzie działać, czy może być zmiana tego jakoś zabroniona, że wtedy nie będzie to ustawienie działało? I jeśli tak to jak wtedy można sobie jakoś z tym poradzić? Tak pytam, bo chcę się przy okazji czegoś nauczyć. smile.gif
cbagov
pokaz co pobierasz, moze robisz to zle
wrzasq
jak powiesz dokladnie co i jak pobierasz, to na pewno da sie jakos podzielic na czesci. na hostingu moze niedzialac (chociazby na home.pl).
wasilek
Cytat(wrzasq @ 17.07.2008, 00:52:23 ) *
jak powiesz dokladnie co i jak pobierasz, to na pewno da sie jakos podzielic na czesci. na hostingu moze niedzialac (chociazby na home.pl).


Właśnie na home sprawdzałem i nie działało. Pobieram dane z Allegro przez WebAPI i SOAP.

  1. <?php
  2. for($l=0;$l<count($UserArray);$l++)
  3. {
  4. $UserID_array = array('country-id'=>_COUNTRY_ID_,'user-login'=>$UserArray[$l]);
  5. $UserID = $soap->__Call('doGetUserID',$UserID_array);
  6. $liczba_aukcji_array = array('user_id'=>$UserID,'webapi-key'=>_WEBAPI_KEY_,'country-id'=>_COUNTRY_ID_,'offset'=>0);
  7. $liczba_aukcji = $soap->__Call('doGetUserItems',$liczba_aukcji_array);
  8. for($a=0;$a<floor($liczba_aukcji['user-item-count']/25);$a++)
  9. {
  10. $params = array('user_id'=>$UserID,'webapi-key'=>_WEBAPI_KEY_,'country-id'=>_COUNTRY_ID_,'offset'=>$a);
  11. $result = $soap->__Call('doGetUserItems',$params);
  12. for($i=0;$i<count($result['user-item-list']);$i++)
  13. {
  14. mysql_query("INSERT INTO items (`it-id` ,`it-name` ,`it-price` ,`it-buy-now-price` ,`it-bid-count` ,`it-time-left`)VALUES ('".$result['user-item-list'][$i]->{'it-id'}."', '".$result['user-item-list'][$i]->{'it-name'}."', '".$result['user-item-list'][$i]->{'it-price'}."', '".$result['user-item-list'][$i]->{'it-buy-now-price'}."', '".$result['user-item-list'][$i]->{'it-bid-count'}."', '".$result['user-item-list'][$i]->{'it-time-left'}."');",$link);
  15. }
  16. }
  17. }
  18. ?>


To więc tak to wygląda. To jest pobierane po 25 rekordów na jeden przebieg pętli for(i), bo tyle maksymalnie przekazuje WebAPI.
wrzasq
no to pobieraj po jednej porcji i z kazdym wywolaniem skryptu przeskakuj do nastepnej smile.gif, przeciez sam masz juz dane podzielone, po prostu nie rob tego w petli, a trzymaj gdzies (na przyklad w bazie danych) offset ostatniego przebiegu i po przejsciu do konca zeruj go. na home.pl mozna tworzyc pliki do cron'a z odstepem 5 minut, sadze ze wystarczy. zreszta powinno sie dac rade wiecej niez jedna porcja jednoczesnie zmiescic.

obejmij inserty transakcja i co kazda porcje wywoluj COMMIT; i zwiekszaj licznik offsetu w bazie.
wasilek
Jeśli dobrze zrozumiałem, to proponujesz żeby na każde wywołanie skryptu przez cron'a wykonywał powiedzmy ileś tam porcji, tak żeby zmieścić się w limicie czasu, wcześniej wrzucam w bazę danych dane do kolejnych wywołań skryptu. Na takiej zasadzie to ma działać, dobrze pojąłem? smile.gif
Tylko w sumie nie chciałbym wywoływać cron'a co 5 minut, tylko np raz na godzinę lub dwie, i żeby przetwarzał powiedzmy te 10-20 minut w zależności od ilości nowych danych, danych do aktualizacji. Tak się teraz zastanawiałem nad dwiema sprawami. Pierwsza to taka, że jak bym zrobił jak powyżej, czyli cron co 5 minut i przetwarza kolejne partie danych, a maksymalny czas wykonywania skryptu na serwerze powiedzmy to 120s (a póki nie mogę sobie pozwolić na jakiś dedykowany to trzeba korzystać z np. home) to przez kolejne 3 minuty do kolejnego wywołania cron'a serwer nic nie robi, a przez ten czas mogłby kolejna partie danych przetworzyć. Druga sprawa to taka, że jeśli będzie ustalony cron co 5 minut to z jednej strony mogłoby być tak, iż przykładowo ustalam wywołanie na 0:00, 0:05, 0:10, 0:15 i powiedzmy wystarcza te 4 wywołania na przetworzenie wszystkiego, ale jak więcej będę miał już danych to nie starczą i dopiero przy kolejnych wywołaniach by dokończyło, co by troche przesunęło dostarczanie danych. Z drugiej strony jeśli bym ustalił cron co 5 min przez 24 h to po przetworzeniu od razu by zaczynało od nowa przetwarzać co troszkę dla mnie jest bez sensu. Wolałbym, żeby wywołanie było raz na godzinę i przetwarzało "od początku do końca" całość materiałów (ale oczywiście dzieląc na mniejsze partie żeby mieścić się w limicie).
Jeśli bym poprzez header() wywoływał kolejne przebiegi dla danych to wszystkie razem muszą zmieścić się w czasie czy poszczególne z osobna?

Poczytałem o tych transakcjach jak napisałeś, bo nie znałem tego jeszcze i powiem, że chyba zrozumiałem idee tego i nawet całkiem przydatne mogłoby być. winksmiley.jpg Ale wtedy muszę zmienić silnik dla tabeli na InnoDB tak?

Troszkę się rozpisałem, ale pisząc jakieś takie przemyślenia na ten temat mnie wzięły. Mam nadzieję, że dość zrozumiale napisałem. winksmiley.jpg
wrzasq
Location nie zadziala na cron'a, bo dziala przez CLI. mozesz sie conajwyzej pobawic, w generowanie zapytan HTTP do tego pliku. ale jesli twoj skrypt przekroczy czas wykonywania to nie wygeneruje zadnego zapytania tongue.gif. ogolem musisz sie na cos zdecydowac, hostingi, gdzie nie masz mozliwosci manipulowania platforma raczej sie nie nadaja do tego typu specyficznych zastosowan, dlatego niestety musisz problem obchodzic na okolo.
.radex
Lepiej podzielić to na kilka wykonań, bo jak czasochłonny skrypt, to prawdopodobnie też pamiecio- i procesorożerny. A na hostingach są limity na wykorzystanie RAMu i procka.
wasilek
Hmm, dziękuje Panowie. winksmiley.jpg
Myślałem, że może są jakieś inne sposoby na ugryzienie tego. winksmiley.jpg

To się zastanowię jeszcze, jak to rozwiązać.

A tak się jeszcze spytam, przykładowo jeśli miałbym serwer dedykowany i jak dam set_time_limit(0), i po prostu wykonywanie skryptu, to to byłoby najlepsze rozwiązanie, najwydajniejsze, najoczywistsze czy lepiej też dzielić na partie?
mrok
nie wiem co udostepnia home, ale jesli tylko aktualizujesz baze to moze lepiej napisac cos w perlu lub pythonie?questionmark.gif
nie powinno byc wtedy problemow z odpaleniem
.radex
Cytat(wasilek @ 17.07.2008, 19:54:55 ) *
A tak się jeszcze spytam, przykładowo jeśli miałbym serwer dedykowany i jak dam set_time_limit(0), i po prostu wykonywanie skryptu, to to byłoby najlepsze rozwiązanie, najwydajniejsze, najoczywistsze czy lepiej też dzielić na partie?


Najlepsze rozwiązanie, chyba, że to jest skrypt, który czym dłużej się wykonuje, tym więcej potrzebuje RAMu.
wasilek
Cytat(mrok @ 17.07.2008, 20:12:49 ) *
nie wiem co udostepnia home, ale jesli tylko aktualizujesz baze to moze lepiej napisac cos w perlu lub pythonie? questionmark.gif
nie powinno byc wtedy problemow z odpaleniem


home udostepnia perl'a, wiec może to faktycznie dobry pomysł.

radex_p - myślę, że to nie wyjdzie na taki skrypt, jak analizuję to myślę, że nie będzie zwiększał zapotrzebowania na RAM.
sarat20
Na ograniczenia czasowe na home.pl mam taki sposób. Załóżmy, że limit to 60 sek. Zapisuję microtime() na początku skryptu, po wykonaniu zapytania sprawdzam czy minęło 50 sek, jeśli tak to skrypt kończy wykonywanie, jednocześnie wywołując się sam ponownie:

A) dla skryptów, które wywołuję ręcznie wystarczy header() albo window.location='' z poziomu javascript (użycie javascript ma taką zaletę, że jeżeli sprawdzanie microtime() zawiedzie i skrypt da fatal error, to javascriptowy setTimeout() i tak zrobi redirect po x sekundach, no i można wyświetlać logi informujące o postępie wykonywania - header już wtedy wywołać nie można i na redirect zostaje tylko javascript).

B) dla skryptów cron można uzyskać ten sam efekt używając np. CURL z opcją na pobranie pierwszych x bajtów.

W przypadku cron'a wypadałoby też dać jakieś ograniczenie w pętli wywołań, żeby nie było sytuacji, że minęło 20 min i nowy skrypt cron zostanie wywołany, a w międzyczasie ciągle stary cron dalej leci.
dr_bonzo
Cytat
no to pobieraj po jednej porcji i z kazdym wywolaniem skryptu przeskakuj do nastepnej , przeciez sam masz juz dane podzielone, po prostu nie rob tego w petli, a trzymaj gdzies (na przyklad w bazie danych) offset ostatniego przebiegu i po przejsciu do konca zeruj go. na home.pl mozna tworzyc pliki do cron'a z odstepem 5 minut, sadze ze wystarczy. zreszta powinno sie dac rade wiecej niez jedna porcja jednoczesnie zmiescic.

Tez bym cos podobnego polecil

1. zaczynasz, pierwsze uruchomienie skryptu
  1. SELECT id FROM users INTO tabela_allegrowa_do_update

i masz w tabela_allegrowa_do_update ID userow ktorych dane chcesz pobrac

2. pobierasz kilka IDkow i robisz SOAPa do allegro, zapisujesz dane czy co tam z nimi robisz, i usuwasz! to ID usera, bo juz go przetworzyles i commit na tranzakcje

3. powtarzasz to dla pozostalych ID az ci sie skoncza [w bazie], jak skoncza ci sie te pobrane to pobierasz kolejna porcje

4. jak ci sie skonczy czas wykonania skryptu, to opalasz go kolejny raz, bierzesz kolejne IDki i przetwarzasz jak wyzej, wiesz ktore rekordy przetworzyles a ktore nie.

proste?


Co do zuzycia pamiecie, memory_get_peak_usage() na koniec skryptu i zobacz jak wyniki sie zmieniaja w zaleznosci od ilosci przetwarzanych userow [czyli wlasciwie na koniec petli morzesz to wrzucic].
wasilek
To więc tak, zrobiłem małe testy czasu wykonania skryptu i pamięci do liczby aukcji.

308 aukcji - 14,43s - z 2 460 816B do 2 600 272B
825 aukcji - 34,96s - z 2 460 816B do 2 600 496B
1417 aukcji - 61s - z 2 460 816B do 2 600 608B
1894 aukcji - 85,33s - z 2 460 816B do 2 600 808B
4444 aukcji - 194,06s - z 2 461 112B do 2 603 104B

Pierwsze 4 pomiary dla 4 różnych pojedynczych użytkowników, ostatni pomiar dla wszystkich razem.
Ja wnioskuję (jeśli błędnie to mnie poprawcie), że różnica w pamięci nie jest wielka, więc mam nadzieję, że zasobów nie przekroczę, nawet przy tych 10 000 aukcjach do pobrania.

Z pomysłem dr_bonzo to tabela musiałaby chyba zawierać jeszcze dane powiedzmy liczby offsetów i zmniejszać je do zera przy przetworzeniu, a gdy równa zero usunąć użytkownika z tablicy 'do zrobienia'. Bo jak widać, pojedynczy użytkownik co ma 1800 aukcji przetwarzany jest ponad limit powiedzmy 60 sekundowy, więc chyba tak trzeba by to załatwić.

A jak miałby wyglądać ten pomysl z wykorzystaniem CURL'a? Chodzi mi o samą ideę.

A z zabezpieczeniem przed wywołaniem przez cron kolejnego skryptu to myślę, że po prostu sprawdzenie czy tabela 'do zrobienia' jest pusta, jeśli tak to pobiera dane, a jak nie to przerywa wykonanie. Dobrze myślę?
sarat20
Cytat(wasilek @ 18.07.2008, 14:01:48 ) *
A jak miałby wyglądać ten pomysl z wykorzystaniem CURL'a? Chodzi mi o samą ideę.


Za pomocą CURL możesz wykonywać żądania HTTP, czyli np pobrać plik http://test.home.pl/moj-cron.php

php.net/curl

Z opcją CURLOPT_RANGE (http://pl2.php.net/curl_setopt) - żeby pobrać tylko pierwsze bajty, a nie czekać aż cały skrypt się wykona. Nie pamiętam czy ten range zawiera nagłówki, jeśli nie to w skrypcie należałoby zrobić echo paru bajtów tekstu i flush() żeby natychmiast to wyświetlić. Dla pewności odpal też ini_set('zlib.output_compression', 0).
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.