Optymalizacja Railsów

Posted by Piotr Sarnacki Sat, 16 Dec 2006 14:30:00 GMT

Dzisiaj chcialem napisać o swoich bojach z liczbą requestów na sekundę.

Po przepisaniu pewnej aplikacji okazało się, że jej szybkość nie powala na podłogę. A właściwie to powala, ale w złym tego wyrażenia znaczeniu.

Tak właściwie to spodziewałem się czegoś takiego podczas pisania. Od jakiegoś czasu próbuję się oduczyć przedwczesnej optymalizacji i pisania zbyt elastycznego kodu, tam gdzie on się nie przydaje. Wielu początkujących programistów pisze kod na wyrost. Gdy trzeba rozwiązać problem, próbują napisać niemalże bibliotekę, którą można będzie wykorzystać później w wielu innych przypadkach. Idea fajna. Tylko w praktyce i tak później używa się tych w bólach powstałych linijek kodu w jednym miejscu, w którym wystarczyłoby proste rozwiązanie. O przedwczesnej optymalizacji kilka słów napisał na swoim blogu Stefan Kaes. Należy jej unikać, ale trzeba dobrze wszystko przemyśleć, żeby później nie skończyć z przepisywaniem połowy aplikacji.

Wracając do sedna (o ile takowe w ogóle istnieje). Przed optymalizacją poczytałem trochę gdzie najłatwiej zyskać upragnione requesty. Od razu można polecić wspomniany wyżej rails express. Oprócz tego bloga pan Google mówi jeszcze o kilku artykułach:

Pierwsza rzecz, o której należy wspomnieć: testy muszą być wykonywane przed i po zmianach. Czasami próby optymalizacji kończą się na zmniejszeniu szykości. A tego byśmy przecież nie chcieli ;-)

Liczbę requestów na sekundę najłatwiej sprawdzić odpalając aplikację w trybie production. Wyświetlając plik log/production.log poleceniem tail -f log/production.log mamy na bieżąco informacje o czasie wtgenerowania strony podzielonym na czas wykonywania zapytań do bazy i renderowania strony:

Processing PageController#index (for 127.0.0.1 at 2006-12-11 23:39:32) [GET] Session ID: 7d4349aab2db813aababe45624ff9a25 Parameters: {"action"=>"index", "controller"=>"admin/page"} Rendering within layouts/application Rendering admin/page/index Completed in 0.11678 (8 reqs/sec) | Rendering: 0.09057 (77%) | DB: 0.02425 (20%) | 200 OK [http://localhost/admin/pages]

W moim wypadku zapytania do bazy danych wykonywały się jakieś 80-90% czasu. W takim wypadku dobrze jest uruchomić aplikację w trybie development i zobaczyć w logach jakie zapytania wykonują się podczas generowania strony. U mnie funkcją wykonującą najwięcej zapyń było tworzenie menu. Klient zażyczył sobie rozwijanego menu javascriptowego. Z reguly odradzam takie rozwiązanie (w większości wypadków jest to niewygodne i niepotrzebne), ale klienci są uparci, więc w trosce o swoje zdrowie psychiczne ustępuję ;-). W jednej z trzech sekcji ma ono około 120 elementów (a liczba ta pewnie będzie się zwiększała z biegiem czasu).

Struktura użyta do budowy menu to oczywiście drzewo - rekurencyjna funkcja wyświetla wszystkie elementy. Najprostsza jej wersja, przy każdym wywołaniu wykonuje jedno zapytanie, łatwo można policzyć, że zaytań było kilkadziesiąt (jedno dla każdego elementu, który posiadał dzieci). Przepisałem funkcję tak, aby kolekcja była pobierana tylko raz, a później przekazywana jako jeden z argumentów. Bingo! Szybkość wzrosła około 2 razy.

Nadal jednak coś było nie tak - metoda children, którą zapewniał plugin acts_as_tree. Już chciałem ją podmienić na swoją, która dostawałaby kolekcję w argumencie, kiedy przypomniałem sobie, że na forum ruby on rails Paweł Kondzior (PaK) pisał o swoim pluginie acts_as_tree_element. Plugin bardzo fajny - generuje tylko jedno zapytanie i zapewnia funkcje, które pracują na pobranej kolekcji - dokładnie to czego szukałem! Paweł pomyślał jeszcze o jednej rzeczy: elementy zapisane jako zmienna instancji są cache’owane w trybie produkcyjnym (o ile config.cache_classes == true). Szybkość wzrosła o kilkanaście procent.

Pojawił się jednak problem - scachowana raz kolekcja pozostaje w pamięci i nie bardzo daje się wyczyścić - nie jest to na rękę, gdy chcemy pobierać różne części menu. Po krótkiej rozmowie na IRC’u Paweł lekko zmodyfikował plugin - cache jest teraz hashem z zapamiętanymi warunkami w zapytaniu. No i git! ;-)

Jakie z tego wnioski? Większość aplikacji w dość dużym stopniu eksploatuje bazę danych. Zadbajmy o to, żeby zapytań było jak najmniej. Dodatkowo warto używać opcji include w metodzie find. Warto też założyć indeksy na kolumnach w bazie. Których? Najprostsza odpowiedź to: na tych, na podstawie których wyszukujemy, lub sortujemy wyniki. Ale takie proste to nie jest - można popytać u pana Google’a, albo w księgarni w celu nabycia dodatkowej wiedzy.

W tym momencie czas wykonywania zapytań spadł do kilku procent. Pomyślałem, że warto jest przyjrzeć się rzeczom, które wykonują się podczas renderowania strony. W artykule Kaes’a czytamy, że dużo czasu zajmuje generowanie adresów za pmocą linkto i urlfor. A co mamy w menu? Około 120 takich linków. Zamieniłem url_for na stringa ze zmieniającym się numerem id artykułu. Szybkość dość znacznie wzrosła.

Uruchomiłem testy Apache Bench (ab -n 1000 -c 20). Wartość, ktorą zobaczyłem bardzo mile mnie zaskoczyła: 40% szybciej od wersji php.

Co jeszcze można zoptymalizować? Zapewne wiele rzeczy, ale mało jest tych naprawdę żrących pamięć miejsc. Najlepiej pomyśleć o cache’owaniu treści - railsy dają możliwość bardzo łatwego cache’owania zarówno całych stron, jak i pojedyńczych akcji, czy fragmentów strony. Jeżeli zmiany są rzadkie na pewno warto pomyśleć o cache’owaniu całych stron - przy odczycie takiej strony serwer nawet nie dotyka railsów. Jeżeli aplikacja jest dynamiczna można pomyśleć o cache’owaniu fragmentów, które rzadko sie zmieniają, albo o memcached (w wypadku naprawdę dużego obciążenia: ~3000 req/s).

Życzę powodzenia w zdobywaniu kolejnych rps’ów ;-)

Posted in  | Tags ,  | no comments

Comments

(leave url/email »)

   Comment Markup Help Preview comment