długie działanie procedury MySQL = serwer wisi

Witam,

Piszę system, w którym konieczne jest generowanie skomplikowanych raportów, wykorzystujących duże ilości rekordów bazy danych (kilka - kilkanaście tys.). Dodatkowo przy raportach trzeba zbierać informacje z wielu różnych tabel (równie dużych). Napisałem odpowiednią procedurę MySQL aby szybko generowała raport. Niestety wykonuje się ona około 2-4 minut. Cała aplikacja w czasie wykonywania procedury wisi - żaden inny użytkownik nie może z niej korzystać dopóki procedura całkowicie się nie wykona. Próbowałem zrobić odpowiadającą tej procedurze metodę w samym kontrolerze, ale dzieje się to samo. Zarówno procedura jak i metoda muszą przejść po kilku tysiącach elementów i zebrać z bazy danych potrzebne informacje dla każdego z tych elementów. Nie wiem jak sobie poradzić z tym żeby serwer nie zawieszał się podczas generowania takiego raportu.

Z góry dziękuję za pomoc

Działa moja teoria: każda aplikacja po osiągnięciu odpowiedniego stopnia złożoności zaczyna potrzebować możliwości odpalenia dłuższych procesów w tle :wink:

Zacznij od tego – bardzo fajny plugin ułatwiający delegację tego typu zadań z aplikacji railsowe do najpopularniejszych obecnie backendów zajmujących się “czarną robotą”:
http://playtype.net/past/2008/2/6/starling_and_asynchrous_tasks_in_ruby_on_rails/
W dużym skrócie interesuje cię (zapytania do googla) “background tasks in rails”, “asynchronous tasks in rails” z naciskiem na biblioteki: spawn, starling, rabbitmq, backgroundrb.

Sam sobie przeczysz :wink:

A tak poważnie, przenieś wykonywanie na backgroundrb, starling/workling albo napisz własnego demona, ale na pewno nie wykonuj tego w obrębie aplikacji.

[edit]
Oops, Tomash mnie wyprzedził :slight_smile:

A ja bym zaczął od sprawdzenia czemu tak długo się to wykonuje. Kilkanaście tysięcy rekordów to żadna duża ilość. Być może wystarczy poprawcować nad indeksami w bazie, żeby było dużo szybciej.

Napisał że musi zbierać rekordy z kilku dużych tabel, czyli złączenia. Tuning konfiguracji MySQL (właśnie, googlnij ściągnij i odpal “mysql tuning primer”) oczywiście może pomóc, ale jeśli zapytanie ma szansę zająć więcej niż 1-2 sekundy, to sorry Winnetou, ale wynosimy Cię poza aplikację.

Z reguły można to wyeliminować używając :include i :join. Ale to racja, że to i tak pewnie nie da czasów mniejszych niż sekunda.

Moja strategia byłaby taka:

  1. Optymalizacja samego zapytania na bazie. Sprawdź czy klucze obce mają indeksy(migracja rails automatycznie ich nie zakłada), Dodaj indeksy dla kolumn filtrujących zapytanie.
  2. Jeżeli jest to możliwe użyj cachowania wyników. W zależności od charakterystyki danych:
  • zadanie crona wypełniające dodatkową tabelę raportu, odpalane co pewien czas
  • użyj memcached lub inny cache, cache będzie napełniany przez pierwsze zapytanie, metody inwalidacji cache’a musisz opracować w zależności od rodzaju danych
  1. Co do zawieszania aplikacji to odpal dodatkowy proces mongrela (nie wiem jak wygląda to w przypadku passangera). Wywołania rails są asynchroniczne i pojedynczy serwer jest blokowany do czasu zakończenia zapytania.

Ała.
Nie, nie i zdecydowanie nie jest to dobre podejście do tematu.
“Uda się” jeśli generowanie raportu będzie trwało 20 sekund, ale przy 30 już może (Apacz albo przeglądarka), zależnie od konfiguracji, dostać timeout error.
Naprawdę, jeśli coś ma (może) trwać dłużej niż sekundę-dwie, to musi być wykonane asynchronicznie w osobnym procesie.

Panowie, problem został postawiony jasno:

Owszem, optymalizacja generowania raportu to dobry pomysł, ale ja zakładam, że osoba go robiąca wiedziała co robi i o indeksach słyszała.
Skoro serwer przyjmuje tylko 1 żądanie to wnioskuję że używasz mongrela/thina i odpalasz tylko 1 proces. Albo musisz odpalać kilka procesów tych serwerów (np. na portach 3000-3005) i odpowiednio ustawić proxowanie w apache/nginx lub (IMHO łatwiej) użyć passengera i ustawić mu maksymalną ilość procesów > 1 (domyślnie tak właśnie jest). Aby jednak długo trwający proces nie kolejkował nadchodzących żądań musisz włączyć opcję PassengerUseGlobalQueue.

Można zawsze przenieść aplikację na railsy 2.3 i dodać config.threadsafe! do production.rb.

Ale tak jak pisze Tomash - żadna akcja nie powinna się wykonywać więcej niż sekundę czy dwie.

Swoją drogą przypomniało mi to problem szybkiego wyszukiwania client side na flickrze - http://code.flickr.com/blog/2009/03/18/building-fast-client-side-searches/

Bardzo pouczający artykuł. Rekordy są cache’owane w tabeli (cache’owanie jest kolejkowane offline) w wersji przygotowanej do szybkiego odczytania przez javascript.

Dzięki wielkie za pomoc… zaraz zabieram się do sprawdzenia tych rozwiązań

Witam,

Wszystko działa super. Używam Starling + Workling, ale mam pewne obawy.
Potrzebuję odpalać w tle metody, które generują raporty. Raport generuje sie kilkanascie - kilkadziesiąt sekund. Co jeżeli wielu użytkowników zacznie generować wiele raportów jednoczesnie? Czy starling umożliwi rownoczesne dzialanie metod dla kazdego uzytkownika? Jaki to ma wpływ na obciążenie systemu?

Zakladajac że Starling tylko kolejkuje zadania, czy jest jakiś sposób, aby umożliwić sprawne generowanie raportów przez wielu użytkowników jednocześnie? (bez odpalania wielu wątków). W sytuacji kiedy tworzy się kolejka, przy kilkudziesięciu użytkownikach, ktorzy jenoczesnie zaczeli generowac raporty, ostatni uzytkownik bedzie czekal nawet kilkanascie minut na swoj raport.

pozdrawiam

Komputery to takie urządzenia, które potrafią wykonać określoną liczbę operacji na sekundę. Jeśli komputerowi wykonującemu X operacji na sekundę każesz wykonać 5X operacji, zajmie mu owa seria co najmniej 5 sekund. Niezależnie od tego, jak owe operacje poustawiasz czy poprzeplatasz.

A teraz jeszcze prościej i mniej złośliwie:
Jeśli zrobienie 100 raportów ma zająć 10 minut, to naprawdę lepiej robić je po kolei na 100% mocy każdy – dzięki temu ostatni owszem, wypadnie po 10 minutach, ale pierwszy już po kilku sekundach. Jeśli odpalisz wszystkie na raz (wątki, procesy, cokolwiek) będą się robiły dłużej niż 10 minut (narzut na przełączanie zadań) i każdy z nich wyleci dopiero w ostatniej minucie. Co z tego że w ciągu ostatniej minuty uzyskasz nagle 100 raportów, skoro w tym momencie każdy użytkownik na swój raport czekać musiał pełne 10 minut?

Mam nadzieję że to przekonuje Cię co do wyższości kolejkowania nad multizadaniowością :wink:

No właśnie… chyba masz rację. Jeżeli będzie taka potrzeba to ustawi się drugi server i jakiś load-balancer żeby zadania przerzucał w kolejki.

Tak czy inaczej wielkie dzięki za pomoc, bo to rozwiązanie dużo mi pomogło.

pozdrawiam

A te raporty to są generowane z bieżących danych? W sensie jak użytkownik sobie generuje raport i potem znowu za pół godziny to one się jakoś bardzo różnią?

Nie myślałeś może, żeby stworzyć jakiś zupełnie oddzielny proces co by generował te raporty po kolei dla wszystkich użytkowników i je gdzie zapisywał na dysku albo w bazie, a potem jak użytkownik by chciał raport to poprsotu byś go odczytywał z dysku czy tam z bazy.

Mógłbyś też spróbować robić jakieś wstępne przetwarzanie danych w osobnym procesie, tak żeby przy tworzenie raportu nie trzeba było wszystkiego liczyć od nowa, tylko odczytywać gotowe dane.

Nie no oczywiście raporty są zapisywane do bazy. Mógłbym rzeczywiście pomyśleć nad jakimś mechanizmem ktory by mi porównywał zmiany z poprzednim raportem i wykorzystywał stare dane, ale nie wiem czy sprawdzanie powtórzonych danych nie zajeło by wiecej czasu niz generowanie wszystkiego od nowa.

Siedze i szukam i znaleźć nie moge.

Czy możliwe jest ustawienie (korzystajac ze Starling/Workling) oddzielnej kolejki dla każdego Workera? Z tym, że musiało by być sprecyzowane że dla tego Workera jest ta kolejka i żadna inna. Czy możliwe jest coś takiego?

pozdrawiam

http://railscasts.com/episodes/171-delayed-job
http://railscasts.com/episodes/128-starling-and-workling
http://railscasts.com/episodes/129-custom-daemon

to w sumie mi nic nie daje bo znam te linki ale może inaczej. W jaki sposób ustawić dwie kolejki i jak z nich korzystać?
W pliku workling.yml mam:

production:
listens_on: localhost:15151,localhost:15152

Odpalam dwie instancje Srarlinga:

#starling -p 15151 -d
#starling -p 15152 -d

Odpalam worklinga:

#script/workling_client run

Czy powinienem mieć dwie instancje worklinga?

W controllerze odpalam metodę z Workera:

def start
AnalyticsWorker.async_moja_metoda(:id => params[:id])
end

Jak, mając już dwie kolejki (zakładając że mam rowniez dwie instancje worklinga) odpalać metody z workera na konkretnych kolejkach?

pozdrawiam