Google chce przyśpieszyć internet

Posted by Piotr Sarnacki Fri, 26 Jun 2009 19:11:00 GMT

Nie od dziś wiadomo, że prędkość ładowania strony ma wpływ na akcje użytkowników. Badania prowadzone miedzy innymi przez Yahoo, Google i Amazon wskazały, że zmniejszenie czasu ładowania strony ma bezpośredni wpływ na ilość akcji wykonanych przez użytkowników (wyszukiwań lub w wypadku Amazona zakupionych przedmiotów).

Google odpalił niedawno stronę, na której zachęca i uczy jak przyśpieszyć działanie naszych stron i serwisów

Dodatkowo Google wypuścił narzędzie Page Speed, które jest rozszerzeniem do Firebuga i podobnie jak YSlow pomaga zbadać stronę pod kątem prędkości ładowania się poszczególnych komponentów. Oba te narzędzia dostarczają od razu praktycznych porad i opisów pomagających zrozumieć jak zoptymalizować stronę. Dla mnie są to niezbędne narzędzia przy pracy z serwisami internetowymi. Do tej pory używałem głównie YSlow, ale przy następnych optymalizacjach na pewno skorzystam z Page Speed.

Jeżeli jeszcze nie znacie tych narzędzi, gorąco polecam zapoznanie się z nimi.

Posted in ,  | Tags , , ,  | no comments | no trackbacks

Cachowanie treści cz. I

Posted by Piotr Sarnacki Fri, 22 Dec 2006 13:30:00 GMT

Pastwiłem się ostatnio nad pewną aplikacją wykonując proces zwany, jakże trafnie, optymalizacją. Prawdopodobnie możnaby jeszcze dużo z niej wycisnąć, ale nie lubię się za bardzo przemęczać, a nie ma już teraz części, która jakoś znacznie wyróżniałaby się czasem wykonywania.

Ruby on Rails daje nam bardzo proste mechanizmy obsługujące cachowanie treści (page, action i fragment caching). W kilku słowach:

  • page caching - zapisywanie całych stron - podczas pobierania serwer nawet nie tyka railsów, serwowany jest statyczny plik
  • action caching - podobne do page caching, z tym że framework jest wywoływany - przydatne jeżeli chcemy mieć dostęp do filtrów (autentykacja, autoryzacja, pewnie jakieśinne acje ;-) )
  • fragment caching - zapisywane są fragmenty stron - najmniejszy skok szybkości, ale może się przydać przy dynamicznych stronach, możemy wtedy skeszować tylko te fragmenty, które nie podlegają częstym zmianom

Postanowiłem zacząć od fragment cachingu. Elementem, który generował się najdłużej, nadal było menu. Rekurancja i bardzo dużo iteracji nie jest tym, co tygryski lubią najbardziej ;-)

Menu tworzy metodarender_menu(). Zcachujmy ją więc:

<% cache do %>
  <%= render_menu(3, nil, nil) %>
<% end %>

W katalogu tmp/cache (w naszej apliakcji) stworzył się plik: localhost.3000/zycie.cache (zycie to jedna z sekcji). Fajnie. Powstaje tylko jeden mały problem - fragment cachowany jest w pliku z layoutem, więc nie jest przypisany do żadnej akcji. Co jeżeli będziemy chcieli zcachować inny fragment? Z pomocą przychodzi możliwość określenia miejsca przechowywania:

<% cache(:controller => 'abstract', :action => 'menu', :part => 'zycie') do %>
  <%= render_menu(3, nil, nil) %>
<% end %>

Kontroler i akcja określane w tym miejscu nie są w żaden sposób powiązane z kontrolerami w naszej aplikacji. Dzięki temu możemy dowolnie określić miejsce zapisania fragmentu (co później się jeszcze przyda). Po odświeżeniu strony pojawił się plik:

tmp/cache/localhost.3006/zycie/abstract/menu.part=zycie.cache

Przed kontrolerem widać jeszcze swoisty namespace określany w routes.rb na podstawie argumentu section w adresie (w tym wypadku jest to sekcja “zycie”). Jeżeli nie ma w aplikacji podobnych myków, w ścieżce nie będzie w ogóle części zycie.

A teraz czas ładowania się strony:

  • z generowaniem się menu: Completed in 1.52972 (0 reqs/sec) | Rendering: 1.50603 (98%) | DB: 0.01315 (0%) | 200 OK (czas jest dość długi, bo moja machina nie jest najwyższej klasy, a odpalonych jest parę aplikacji)
  • z menu odczytanym z fragmentu: Completed in 0.04565 (21 reqs/sec) | Rendering: 0.01936 (42%) | DB: 0.01816 (39%) | 200 OK

Skok szybkości jest ogromny. Jest to też największy dział w serwisie. Dla innej “sekcji” różnica jest mniejsza, ale cały czas bardzo duża:

  • z generowanym menu: Completed in 0.27923 (3 reqs/sec) | Rendering: 0.21727 (77%) | DB: 0.03056 (10%) | 200 OK
  • z pamięci cache: Completed in 0.03424 (29 reqs/sec) | Rendering: 0.01521 (44%) | DB: 0.01059 (30%) | 200 OK

Bingo! :)

Teraz wystarczy tylko zapewnić czyszczenie pamięci podczas modyfikacji. Przeznaczona jest do tego funkcja expire_fragment. W kontrolerze modyfikującym menu dodałem prywatną metodę expire_menu, w której czyszczę fragment:

def expire_menu
  expire_fragment(%r{/#{section_name(params[:section])}/abstract/menu/*})
end

Skorzystałem tutaj z możliwości użycia wyrażeń regularnych. Przy określeniu położenia pliku (expire_fragment(:controller => 'abstract', :action => 'menu', :part => 'zycie')) coś się gryzło z owymi sekcjami, o których pisałem na początku - fragment nie chciał się usuwać. Prawdopodobnie w większości przypadków nie trzeba uciekać do takich rozwiązań. Wyrażenia regularne mają jeszcze tą zaletę, że możemy w razie potrzeby wyczyścić wiele fragmentów na raz.

Teraz wystarczy wywołać metodę dla akcji, które modyfikują menu:

after_filter :expire_menu, :only => ['create', 'update', 'destroy', 'move_up', 'move_down']

W tym wypadku sprawa była ułatwiona, gdyż treść, którą chciałem skeszować była generowana w helperze. Co jeżeli chcemy zapisać na przykład listę artykułów? Mamy zapewne jakąś pętlę for article in @articles, którą opatulimy metodą cache. I ok. Ale w kontrolerze nadal pobiera się lista artykułów, która przy odczytywaniu skeszowanej treści nie będzie do niczego potrzebna. W takich wypadkach powinno się używać metody read_fragment. Pobiera ona te same argumenty co expire_fragment i zwraca true jeżeli fragment istnieje. Czyli wszystko można załatwić jest krótkim kodem:

@articles = Article.find(:all) unless read_fragment(:controller => 'articles', :action => 'list')

Jest coraz lepiej, ale moja sadystyczna natura nie pozwala mi przedwcześnie zakończyć znęcania sie nad aplikacją ;-)

Dodałem jeszcze page cahing - daje on największy wzrost prędkości. Dlaczego nie zrobiłem tylko page cachingu olewając fragment caching? Przecież na skeszowanej stronie i tak menu nie będzie się generowało. Odpowiedź jest prosta. Moje menu zapisze się teraz do pliku przy pierwszym wywołaniu dowolnej strony z layoutem default. A całe strony będą się cachowały tylko z danymi argumentami - czyli dla każdego z 250 artykułów i kilkudziesięciu newsów oddzielnie. Ale o tym już w następnej notce :)

Update: Uważajcie na niektóre teksty w sieci ;) Nie twierdzę, że moje rozwiązanie jest jedyne i słuszne, ale wpadłem dzisiaj przypdakiem na posta, w którym autor twierdzi, że trzeba wrzucić do cron’a skrypt, który będzie przeładowywał fragment cache co kilka minut i raczej nie keszować akcji z dużą ilością pobieranych danych. Czyli po prostu nie wie do czego służą metody read_fragment i expire_fragment. I jeszcze taki śmieszny cytacik z końca owego posta :)

Conclusion

Rails has a bunch of built-in page caching mechanisms, but they aren’t THAT useful out of the box. You need to tweak and play around to get what you need, and most of the time you will NOT use the simple solutions.

For our large scale sites we still love the event-driven memcached approaches.

Zastanawiam się co to za large scale sites :)

Posted in  | Tags , , , ,  | no comments

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