Strona domowa GDR!a Tor Hidden Service

V 3.8


Celery - wykonywanie czasochłonnych zadań w tle

Problem?

Podczas pisania różnego rodzaju oprogramowania które wchodzi w interakcję z użytkownikiem, a podczas pisania stron WWW w szczególności, często stajemy przed problemem wykonania czasochłonnego zadania. Przykłady takich zadań to spakowanie i wysłanie użytkownikowi dużego pliku, przekodowanie formatu pliku video czy wysłanie dużej ilości e-maili. W przypadku klasycznego programu rozwiązanie byłoby proste - należałoby odpalić w tle wątek bądź pulę wątków i pozwolić im się zająć czasochłonnymi zadaniami. Jak jednak poradzić sobie z tym problemem w przypadku strony WWW (uwaga, będzie modnie: "aplikacji webowej")?

Rozwiązania słabe

Najbardziej oczywistym sposobem jest wykonanie zadania synchronicznie. Użytkownik klika linka lub zatwierdza formularz, jego przeglądarka prosi serwer o /dlugo_wykonujacy_sie_skrypt.php, serwer przez długi czas nie odpowiada, użytkownik zastanawia się co jest grane i albo anuluje akcję i próbuje ją wykonać ponownie, albo po kilkunastu sekundach cierpliwie doczekuje załadowania się strony.

Takie rozwiązanie oprócz najbardziej oczywistej wady - bycia nieprzyjaznym dla użytkownika - ma również kilka innych problemów. Czas wykonania zadania zależy od wielu czynników - na przykład obciążenia serwera czy wielkości zadania (ilości plików do spakowania, długości filmu). Czasem z powodu zmiany tych czynników może urosnąć kilkakrotnie i przeglądarka lub serwer zamknie połączenie z powodu bezczynności, co dla użytkownika będzie wyglądać odpowiednio jak "Could not connect to server" lub "Error 500". W przypadku PHP, można również uderzyć w maksymalny czas wykonania skryptu (max_execution_time). I nie, nie jest to wyczerpująca lista możliwych do napotkania problemów.

Można na różne sposoby próbować minimalizować skutki problemu. Zamiast przekierowywać użytkownika na nową podstronę, można użyć XMLHttpRequest i wyświetlić mu mniej lub bardziej gustowny animowany spinner. Można dzielić zadanie na części i javascriptem wywoływać skrypt z parametrami nakazującymi mu wykonywać kolejne części - powodzenia z debugowaniem. Można wreszcie co sekundę robić echo '.'; flush(); żeby połączenie nie umarło.

Dla większości programistów powinno być oczywiste, że rozwiązanie w którym nad obejściem ograniczeń interpretera trzeba spędzać więcej czasu niż nad domeną problemu, jest rozwiązaniem złym i trzeba poszukać lepszego. Niestety, często jesteśmy ograniczeni do PHP wykonywanego przez serwer WWW (przykład: rozszerzenie do CMS, które musi być proste w instalacji). W takim wypadku raczej nie da się tego zadania wykonać poprawnie i pozostają półśrodki.

W sieci jest sporo desperackich pytań w stylu jak wykonać kod PHP w tle?. Ilość kreatywnych odpowiedzi jest przerażająca, przeważają odpowiedzi typu exec("php $arg1 $arg2"); lub tricki oparte o register_shutdown_function(). Doświadczenia z tego typu zabawami można streścić jednym zdaniem: serwer WWW nie jest odpowiednim miejscem do wykonywania długich, intensywnie korzystających z zasobów operacji.

Rozwiązanie takie sobie

Niezłym półśrodkiem jest wykorzystanie cronjoba oraz tabeli w bazie danych, w której zapisują się zadania do wykonania. Przy bardzo małej liczbie zadań sprawdza się to dość nieźle.

Kiedy klient zgłasza żądanie wykonania zadnia, skrypt odbierający robi INSERT do tabeli z zadaniami oraz zwraca ID rekordu. Następnie skrypt w przeglądarce odpytuje raz na jakiś czas serwer czy zadanie zostało już wykonane.

Z drugiej strony jest sobie skrypt wywoływany okresowo, na przykład co minutę, i robiący SELECT na tabeli z zadaniami. Wykonuje on sobie zadania i zaznacza w tabeli (UPDATE), że zostały wykonane. Może nawet w przypadku błędu w jakiś sposób go zwrócić.

Skrypty crona nie są ograniczone czasowo, więc zadanie może wykonywać się dowolnie długo. Czasem aż tak długo, że cronjob zdąży się odpalić kolejny raz i zacząć wykonywać to samo zadanie, a potem kolejny i kolejny. Można oczywiście oznaczać które zadania zą już "wzięte", albo po prostu zakładać blokadę na czas wykonania cronjoba tak, żeby kolejne odpalone zadanie od razu zakończyło pracę. Pierwsze rozwiązanie tworzy problemy w przypadku nieplanowanego zatrzymania skryptu (to się zdarza, i wcale nie tak rzadko jak może się wydawać), drugie - blokuje wykonanie nowych zadań jeśli jest przetwarzane jakieś starsze. Można zatem rozbudowywać logikę pierwszego sposobu aby uwzględniać kolejne corner cases (przypadki brzegowe?), ale problem wbrew pozorom jest dość złożony i jeśli nie mamy w planie robić badań do rozprawy naukowej na temat teorii kolejek i chcemy jedynie spakować cholerne pliki i wysłać je użytkownikowi, może nie być warto się nim zajmować.

Rozwiązania poprawne

Poprzednie rozwiązanie jest znane jako ghetto queue, co możnaby przetłumaczyć jako bieda-kolejka. Okazuje się bowiem, że nie jesteśmy pierwszymi programistami którzy natrafili na podobny problem, mało tego - został on już dawno temu zgeneralizowany jako problem kolejki. Zostały również napisane programy które rozwiązują go w miarę możliwości najbardziej poprawnie. Nazywają się - niespodzianka - serwerami kolejek.

AMQP

W nie tak znów dawnych czasach kiedy słowo "enterprise" kojarzyło się jeszcze z jakichś niezrozumiałych powodów z dobrym kodem, powstało wiele serwerów kolejek, głównie dla Javy. Były częściami większych serwerów aplikacji, więc nie było żadnego powodu dla którego miałyby móc ze sobą wymieniać dane - aplikację pisze się w końcu używając jednego serwera. Nie będę zresztą udawał że znam się na tej tematyce, istnieje niebezpieczeństwo że artykuł przejrzy kiedyś Kazik, który zna temat na wylot.

W każdym razie, sprawiło to pewien problem w czasach kiedy większości programistów przestało imponować słowo "enterprise" na rzecz słowa "agile" i chcieli użyć serwera kolejek w swoim oprogramowaniu pisanym w Perlu/Rubym/PHP/Pythonie. Doprowadziło to do stworzenia standardowego protokołu komunikacji z serwerem kolejek: AMQP. Skutek jest taki, że do protokołu istnieją klienty w większości popularnych języków, a serwerów które go implementują jest również sporo.

Jeśli chodzi o dokładny sposób działania serwera kolejek opartego o AMQP, proponuję poczytać świetną prezentację o RabbitMQ, ja skupię się na widoku ogólnym.

Kolejka w serwerze kolejek odbiera wiadomości od producentów i dostarcza je konsumentom. Kolejek może być wiele, każda z nich ma swoją nazwę. Producenci to w naszym wypadku skrypty na stronie, które "produkują" żądania spakowania zipa lub przekodowania filmu. Wiadomość - to dowolna struktura danych, np. array("task" => "process", "file_in" => "/tmp/abc.avi", "file_out" => "/home/xxx/abc.flv", "id" => 1234). Konsumenci to wreszcie skrypty przetwarzające wiadomości - konsument odbiera jedną wiadomość na raz i np. wykonuje zawarte w niej zadanie. Konsumenci mogą być uruchomieni w tle przez cały czas i natychmiastowo reagować na wiadomość.

Celery

Konsumenci? Producenci? Brzmi jak zbyt skomplikowane zagadnienie żeby po prostu spakować plik? Spokojnie, z pomocą przybywa Celery. Jest to program w Pythonie, który łączy się z serwerem kolejek, przyjmuje zadania i wykonuje kod dostarczony przez użytkownika, zajmując się wszystkimi szczegółami: obsługą błędów, komunikacją, synchronizacją. Twoim zadaniem jest tylko napisać funkcję która będzie wykonywała zadanie, Celery zajmie się resztą.

Nie będę opisywał instrukcji instalacji serwera kolejek ("brokera"), bo zostało to doskonale opisane w instrukcji. Samo Celery jest instalowalne prostym poleceniem: sudo easy_install Celery bądź sudo pip install Celery. Jeśli brakuje ci w systemie polecenia easy_install, doinstaluj coś o nazwie podobnej do python-setuptools.

Jak użyć Celery? Tworzysz dwa pliki, konfiguracyjny (celeryconfig.py):

BROKER_HOST = "localhost"
BROKER_PORT = 5672
BROKER_USER = "gdr"
BROKER_PASSWORD = "test"
BROKER_VHOST = "wutka"
CELERY_RESULT_BACKEND = "amqp"
CELERY_IMPORTS = ("tasks", )
CELERY_RESULT_SERIALIZER = "json"
oraz kod, właściwe zadanie do wykonania (tasks.py):
#!/usr/bin/python
from celery.task import task
import time

@task
def add(x, y):
    time.sleep(1)
    return x + y
Teraz można uruchomić serwer Celery poleceniem celeryd -l info (opcja -l info powoduje drukowanie większej ilości komunikatów niż normalnie).

W tym momencie Celery uruchomił jakąś liczbę procesów-pracowników (worker), które czekają na zadania do wykonania. Dajmy im zatem coś do roboty. W drugiej konsoli, w tym samym katalogu należy odpalić konsolę Pythona i wykonać najprostsze zadanie:

>>> import tasks
>>> r = tasks.add.delay(2,2)
>>> r.get()
4
tasks.add to funkcja, którą wyżej napisaliśmy. Przez dekorator (@task) zostaje ona zamieniona na obiekt reprezentujący wykonywalne asynchronicznie zadanie. Wywołanie tasks.add.delay() nie zwraca natychmiastowo wyniku, a jedynie obiekt który taki wynik potrafi pobrać - chcemy przecież wykonywać zadania trwające większą ilość czasu.

Metoda get() zwróconego obiektu (klasy AsyncResult) synchronicznie czeka na wykonanie zadania i zwraca wynik jego wykonania. Chcąc wykonać je asynchronicznie, powinniśmy wpierw sprawdzić czy zadanie już skończyło się wykonywać (ready()), a dopiero potem użyć get():

>>> r.ready()
True
>>> r.get()
4

Przechowując obiekt AsyncResult w sesji bądź przekazując do przeglądarki jego ID, możemy odpytywać serwer zadań asynchronicznie wyświetlając użytkownikowi odpowiedni komunikat. Ponieważ Celery wspiera też obsługę callbacków, można zbudować powiadomienia zwrotne przez websockets bądź podobny protokół na zasadzie push, bez odpytywania.

Od niedawna, zadania Celery można wykonywać także z PHP, dzięki bibliotece Celery PHP którą napisałem. W chwili pisania (2011) nie były dostępne żadne serwery kolejek napisane w PHP.

Tak można podsumować podstawy. Więcej szczegółów znajduje się w prezentacji którą dałem na konferencji Open Source Szczecin oraz w dokumentacji projektu. Zachęcam do przejrzenia przynajmniej prezentacji żeby przekonać się, że Celery potrafi więcej niż tylko opisane powyżej podstawy.

URL encoded in QR Code Statystyki:

Email
Comments