[Drupal] Optymalizacja wydajności w Drupal CMS

Data dodania wpisu: 10-01-2011

Jakiś czas temu na GoldenLine obiecałem (w nawiązaniu do tematu dotyczącego Drupal 7), że jak znajdę wolną chwilę, to opiszę metody optymalizacji Drupal CMS. Skuteczne metody:)

 

EDIT: Sponsorem niniejszego artykułu jest mój autorski sklep online oparty o Drupala w wersji 6 - dostępny pod adresem www.smakprostoty.pl

 

Można poklikać, popatrzać jak bangla. Tylko nie zepsuć! ;) :D

Panel zawężania wg cech dostępny od drugiego poziomu kategorii (pierwszy poziom to zbyt wiele danych, bez sensu, żeby tutaj zawężać, co zostało zresztą zatwierdzone przez właścicieli sklepu).

 

Ubercart i E-commerce nie spełniły wymagań więc dla klienta powstało dedykowane rozwiązanie, które bardzo dobrze się sprawdza jak na razie. Niestety, w tej chwili zaprezentuję tylko liczby, bez konkretnych przykładów i bardziej szczegółowych screenshotów - sklep jest w trakcie zapełniania bazy produktowej i nie jest jeszcze dostępny pod domeną docelową. Jak zacznie działać, zapewne zapodam w określonych sekcjach tego artykułu konkretne linki dla zobrazowania przykładów.

 

Czas więc zacząć ten nieco przydługi artykuł;) (Nie! Nie podzielę go na mniejsze części!:P)

 

Zapewne nieraz słyszałeś, że Twoje strony oparte o Drupal CMS czasami ładują się w nieskończoność. I nie jest ważne na jakim serwerze stronka stoi, bo Drupal potrafi przymulić równie dobrze na hostingu współdzielonym, jak i dedykowanym serwerze. Okrutne, ale prawdziwe.

 

Drupala, jak każde inne oprogramowanie, można przyspieszyć tak, aby zarówno generowanie strony i wywoływanie zapytań do bazy było realizowane szybciej, ale i żeby frontend strony był renderowany w bardziej zadowalającym czasie.

 

Optymalizacja renderowania w przeglądarce

 

Drupal poza charakterystycznymi wcięciami w kodzie, ma również sporo śmieci. Bardzo sporo. A już tym bardziej, gdy zainstalujemy i wykorzystujemy masowo moduły typu Views lub Panels. Albo co gorsza - templatki w stylu Aqcuia Prosper. Uniwersalne. Ale jak wiadomo, jak coś jest do wszystkiego, to jest do niczego. Ale nie o tym temat.

 

Skupmy się na elementach wypluwanych przez Drupal CMS po pobraniu wszystkich wymaganych danych (plików) potrzebnych do wyrenderowania strony w przeglądarce. Tak, mowa o kaskadowych arkuszach styli CSS oraz plikach JavaScript. Zależnie od ilości zainstalowanych modułów, ilość obu partii plików potrafi być naprawdę imponująca. W negatywnym znaczeniu.

 

Jak wiadomo, każdy plik, do kolejne zapytanie HTTP do serwera. Im więcej zapytań, tym więcej przeglądarka męczy się podczas renderowania strony. Oczywistym jest, że problem rozwiązuje zminimalizowanie ilości zapytań HTTP poprzez zminimalizowanie ilości plików CSS oraz JS. Drupal standardowo posiada możliwość kompaktowania plików CSS oraz JS tak, aby wszystkie wylądowały w jednym pliku. Co nam to daje? 1 (słownie: jedno) zapytanie HTTP dla styli CSS i jedno dla JS.

Dodatkowo warto, aby pliki JS dodawać w kodzie strony przed domknięciem tagu <body> zamiast umieszczania ich w sekcji <head>. Powoduje to, że cały kod HTML zostanie załadowany bez oczekiwania na pobranie wstępnie niepotrzebnych plików JS. JavaScripty i tak odpalamy po załadowaniu contentu;)

 

    <!-- content -->
    <?php print $scripts ?>
    <?php print $closure ?>
  </body>
</html>

 

Ale nie tylko pliki CSS i JS wymagają zapytań HTTP. Przecież w erze Web2.0 nie można obyć się bez obrazków. A każdy kolejny obrazek o kolejne wywołanie HTTP. Ok, w przypadku np. zdjęć nic nie zdziałamy, ale jeśli layout strony składa się z wielu niekoniecznie małych elementów, możemy zastosować technikę CSS Sprites. Dzięki połączeniu wielu obrazków w jeden, możemy zminimalizować ilość wywołań HTTP o kolejne X-razy.

 

Pliki strony, a pamięć podręczna przeglądarek

 

Niektóre pliki strony (np. obrazki, pliki JS, pliki CSS, PDF) zmieniają się rzadko, dlatego, aby nie zmuszać przeglądarek do pobierania ich zawartości przy każdym odświeżeniu strony, warto z góry nałożyć nagłówki informujące przeglądarkę, że określone typy plików mają określony okres ważności. Dlatego - jeśli serwer nie protestuje - warto dopisać kilka reguł w pliku .htaccess, dzięki którym pliki o określonych formatach będą miały czas istnienia określony na np. dzień, tydzień, miesiąc. Dodatkowo, podpowiemy serwerowi, aby domyślnie kompresował dane metodą gZip.

 

<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
    Header set Cache-Control "max-age=345600, public"
</FilesMatch>
<FilesMatch "\.(xml|txt)$">
    Header set Cache-Control "max-age=86400, public, must-revalidate"
</FilesMatch>
<FilesMatch "\.(html|htm)$">
    Header set Cache-Control "max-age=3600, must-revalidate"
</FilesMatch>

<ifModule mod_gzip.c>
  mod_gzip_on Yes
  mod_gzip_dechunk Yes
  mod_gzip_item_include file \.(html?|txt|css|js|php|pl)$
  mod_gzip_item_include handler ^cgi-script$
  mod_gzip_item_include mime ^text/.*
  mod_gzip_item_include mime ^application/x-javascript.*
  mod_gzip_item_exclude mime ^image/.*
  mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifModule>

 

Jakie narzędzia wykorzystać, aby szukać frontend-owych usterek?

 

Firefox + PageSpeed + Firebug + YSlow for Firebug

 

Jak wiadomo, jakiś czas temu Google wypuściło fajną wtyczkę dla Firefoxa - PageSpeed. Dzięki niej można szybko sprawdzić jakie elementy wymagają poprawy, aby strona teoretycznie ładowała i renderowała się szybciej. Dzięki PageSpeed możemy dowiedzieć się, że np. pliki CSS / JS są nieskompresowane, obrazki nie mają określonych wymiarów (IE sucks), treść nie jest kompresowana (można zaaplikować ustawienie w htaccess, jeśli serwer zezwala na kompresję gZip) czy też że niektóre pliki "pobierane" przez przeglądarkę i załączone gdzieś w kodzie tak naprawdę nie istnieją (404), co znacznie spowalnia wywołania HTTP.

 

 

Drugim narzędziem jest YSlow, wtyczka, dzięki której możemy szybko dowiedzieć się ile faktycznie zajmuje cała uruchomiona podstrona, a ile poszczególne pliki, jaki jest dla nich czas dostępu, pobierana całego dokumentu, itp. Dzięki temu można wywnioskować, co potencjalnie trwa zbyt długo podczas pobierana strony. Dodatkowo, YSlow posiada zintegrowane "pakery" plików CSS i JS, dzięki czemu możemy wygenerować skompresowane wersje tych danych.

 

 

Na szczęście, w przypadku Drupala i włączonej opcji kompaktowania plików CSS i JS w zakładce "Wydajność", te opcje (YSlow Tools) nie są konieczne.

 

Wybrać Drupala, czy szybszego Drupala?

 

Śmieszne pytanie;) Może niewiele z niego wynika, dopóki nie napiszę: Pressflow:)

 

Pressflow jest dystrybucją Drupala (aktualna wersja stabilna pokrywa się z najnowszą rewizją D6 - 6.20). Jest to wersja zawierająca zoptymalizowane kawałki kodu i dodatkowe moduły usprawniające system pamięci podręcznej dla pobierania aliasów adresów.

 

Dlaczego właśnie Pressflow?

 

Zacznijmy badać sprawę od wykorzystania modułu Devel w standardowej wersji D6. Po włączeniu logowania zapytań, można śmiało stwierdzić, że średnia ilość zapytań podczas pojedynczego wywołania strony przeraża. Biorąc na stół sklep o którym była mowa: średni zakres ilości zapytań na podstronę to 250-400 (w przypadku instalacji opartej o Drupal 6). Z czego średnio 150 to zapytania realizowane przez funkcje drupal_path_alias_lookup(). Każde z nich to czas mniej więcej 0.3ms. Sumarycznie daje nam to całkiem pokaźną ilość czasu dla ogółu zapytań do bazy. A już tym bardziej będzie to działanie niepożądane, jeśli serwer obsługuje określoną ilość zapytań w ciągu godziny / doby. Dystrybucja Pressflow posiada moduł Path Alias Cache, który skuteczne wrzuca całą listę zapytań do pamięci podręcznej, aby pobrać je wszystkie za pomocą 1 (słownie: jednego) zapytania. Spora różnica, prawda?

 

Jak to wygląda na przykładzie mojego sklepu dla np. strony głównej, bez włączonych mechanizmów cache (które będą omówione w kolejnych częściach tego artykułu)?

 

  Drupal 6 Pressflow
Ilość zapytań 187 73
Czas wykonywania zapytań 178ms 72ms
Czas generowania strony 410ms 308ms

 

 

Spora różnica, prawda?;) A to tylko jeden moduł:)

 

Dodatkowo, Pressflow posiada moduł Cookie bypass, który pozwala na wyłączenie ciastka dla określonej treści, jeśli została ona przed chwilą zmieniona (co za tym idzie, użytkownik otrzyma zawsze aktualną wersję strony, a pamięć podręczna zostanie odświeżona).

 

UWAGA: w przypadku stosowania dystrybucji Pressflow, nie ma konieczności instalowania modułu PathCache, do obsługi pamięci podręcznej adresów. Sprawę załatwia Path Alias Cache z tej dystrybucji.

 

Pamięć podręczna bloków

 

Jasne, że jest taka opcja w zakładce "Wydajność". I kończy się na tym, że bloki są ładowane w pamięć podręczną w bazie danych.

 

Ale co, jeśli chcemy, aby określone bloki były cachowane w zależności od warunków typu użytkownik, podstrona itp.? Tutaj z pomocą przychodzi moduł BlockCache Alter. Dzięki niemu, możemy dla każdego bloku określić, w jaki sposób blok ma być zapisywany w pamięci podręcznej Drupala. Standardowo dostępne opcje to m.in. taki sam zapis dla wszystkich podstron, osobny zapis dla każdej podstrony, czy osobny zapis na użytkownika. Dzięki temu możemy być pewni, że bloki np. zawierające menu z elementami aktywnymi, mimo pamięci podręcznej zawsze będą wyświetlane poprawnie, niezależnie na jakiej stronie wylądujemy.

 

 

Boost up your Drupal, czyli moduły Boost i Authcache

 

Dwa moduły, które w pełni załatwiają sprawę stosowania kompletnej pamięci podręcznej w zasadzie dla każdego typu strony www opartej o Drupal CMS. Pierwszy dla userów anonimowych, drugi dla zalogowanych.

 

Dzięki modułowi Boost można generować pamięć podręczną całego serwisu (bloki, treść, całe strony) jak i generować pliki statyczne pamięci podręcznej, oczywiście z ustawieniem ich czasu ważności.

 

Należy pamiętać, że aby odczyt wygenerowanych plików statycznych był możliwy, należy z zakładki "Wydajność -> Boost htaccess rules generation" skopiować reguły do pliku .htaccess bezpośrednio za regułą "Rewrite Base/".

 

Co więcej, moduł Boost posiada rewelacyjne rozwiązanie: Cron Crawler. Jest to opcja, która umożliwia automatyczne przeskanowanie całej strony podczas uruchomienia zadań CRON, podczas którego każda "przeskanowana" podstrona ma tworzony cache przez demona CRON'a. Dzięki temu użytkownik odwiedzający stronę od razu otrzymuje wygenerowany cache, bez konieczności oczekiwania na jego wygenerowanie podczas uruchomienia podstrony. Ustawienie Cron Crawler jest powiązane z opcją Crawl All URL's in the url_alias table, dzięki której mamy pewność, że CRON przeskanuje wszystkie podstrony zapisane w serwisie, bazując na liście przyjaznych adresów. W przypadku przykładowego sklepu, wygenerowanie pełnego cache dla 3500 adresów (produkty, node'y, terminy taksonomii) zajęło na serwerze typu RPSIV w OVH około 45min około 15 minut po zoptymalizowaniu struktury bazy danych dzięki użyciu indeksów w tabelach podległych mojemu sklepowi:) Indeksy alleluja:) Niemniej, CRON'a na pełne generowanie cache najlepiej ustawić na noc, gdzie odwiedzialność strony jest relatywnie niska. Poza tym cache będzie stale odświeżany dzięki modułowi Boost w przypadku, gdy użytkownik odwiedzi stronę, której cache wygasł.

 

Należy pamiętać, że oprócz generowania plików, również baza danych dostaje kopa z pełną strukturą pamięci podręcznej podstron, bloków i menu, co niestety potrafi zająć trochę miejsca (w moim przypadku, pełen cache to około 400MB w bazie).

 

Nawiązując do kilku poprzednich akapitów (BlockCache Alter, różne typy ustawień, zależnie od typu bloku, generowanie cache dla 3500 podstron i 450MB w bazie), mogę przedstawić pokrótce w liczbach, jak wygląda wygenerowana ilość rekordów z pamięcią podręczną:

 

- pamięć podręczna bloków: ponad 23.000 rekordów (różnicowe ze względu na activetrail w taxonomy menu zależnie od aktywnej kategorii),

- pamięć podręczna atrybutów produktów (mój cache dla sklepu): tyle co produktów, około 1930,

- pamięć podręczna treści - tyle co node'ów (włącznie z produktami), czyli około 2400,

- pamięć podręczna dla bloku nawigacji warstowej: tyle, co kategorii z przypisanymi produktami + każdy możliwy stopień zawężania (w teorii może być tego w cholerę, obecnie w cache dostało się ponad 1300 poziomów zawężeń, przy czym bazuje to na ogólnej liczbie możliwych kombinacji, która wynosi obecnie ponad 14.000 i stale rośnie, bo Google dopiero zaczyna stronę zasysać:)),

- pamięć podręczna menu (dla podstron typu node + taksonomia + inne z modułów): ponad 21.000,

- pamięć podręczna pełnych podstron (node + taksonomia + inne z modułów): 3.500

 

Trzeba zauważyć, że znaczna część wygenerowanej pamięci podręcznej, to pamięć bloków (ok 200MB z 450MB ogólnie, zatem prawie połowa), które w dużej mierze są powtarzalne. Jednak ustawienie 'per page' zapewnia nas, że każda podstrona będzie posiadać poprawnie wygenerowany indywidualny cache dla każdego bloku.

 

Dodatkowo, w konfiguracji modułu Boost możemy określić które podstrony mają nie być cacheowane (w przypadku sklepu będą to np. strony panelu klienta, strony koszyka, czy też strony panelu realizacji zamówienia).

 

Link do konfiguracji modułu Boost, której ja używam: Konfiguracja modułu Boost.

Tutaj tylko wspomnę o dwóch punktach, czyli zarżnięciu serwera;) Nie patrzyłem w źródła, ale wydaje się że Boost dopasowuje ustawienia dotyczące szybkości "generowania" cache na podstawie osiągów serwera (??). W każdym razie, jeżeli serwer jest wykorzystywany dość mocno (wysoki poziom odwiedzalności blabla), to dobrze jest zmniejszyć liczbę Crawler Batch Size (liczba jednoczesnie generowanych cache podstron) niż ta podawana przez Boosta standardowo. Dodatkowo można wpisać odstęp czasowy międyz kolejnymi wywołaniami Crawlera (Crawler Throttle), tak, aby nie zapierdzielał on non-stop - jak mamy duży ruch, sporą część cache generują za nas userzy:)

 

Zamiast modułu Boost (lub wraz z nim) można również wykorzystać moduł Authcache, dzięki któremu możemy określić które grupy autoryzowanych użytkowników będą otrzymywać renderowany do plików statycznych cache, co równie skutecznie odciąża serwer.

 

 

Poniżej proste porównanie - czas wyświetlania strony przed wygenerowaniem pamięci podręcznej oraz po wygenerowaniu pamięci podręcznej (pobranie wyłącznie pliku z serwera z wygenerowanym cache całej strony, ilość zapytań do bazy? 0 (słownie: zero))

 

Przed (czas renderowania: 1442ms):

 

 

Po (czas renderowania: 10.62ms !!! Spadek czasu renderowania strony o ponad 13 tysięcy procent !!! ):

 

 

 

Boosted Drupal a dynamiczna zawartość bloków - czyli np. minikoszyk :)

No dobra, fajnie, mamy stronę wrzuconą w cache. Serwer dostarcza nam wygenerowanego gZipa z gotowym kodem HTML. Dodaliśmy produkt do koszyka. I zonk - boks minikoszyka jak był pusty, tak dalej świeci oczami. I tutaj z pomocą przychodzi AJAX. A konkretnie moduł: AJAXBlocks. Zasada jest prosta - po instalacji, na stronie edycji bloku, pojawia się dodatkowa opcja umożliwiająca oznaczenie bloku jako ładowanego AJAXowo, po zakończeniu ładowania strony. Dzięki temu, zawartość tego bloku, zostanie zapodana userowi zawsze z aktualnym contentem. Przykłady bloków, dla których AJAX może być przydatny: minikoszyk w sklepach, blok ankiety, blok statystyk (np. na forum).

 

 

hook_exit()  ;)

 

Mam nadzieję, że artykuł się przyda, pewnie jak będzie więcej czasu, to opiszę kilka metod programistycznych na własne systemy pamięci podręcznej dla własnych modułów, ale to w następnym, bliżej nieokreślonym wolnym czasie:)

 

 

EDIT 09-03-2011: Jak piszecie własne moduły korzystające ze sporej ilości danych, o JOINach już nie wspominając - używajcie indeksów w tabelach dla encji, które używacie do JOINowania tabel. Dlaczemu? A dlatemu - prosty przykład, ze SmakuProstoty rodem wzięty: generowanie panelu zawężarki na podstawie aktualnej kolekcji produktów (w "czystej" kategorii, czy tez już podczas zawężania) - zależnie od złożoności, zapytanie wyciągające cechy produktów, ciągnęło nieraz po 3-4 sekundy (masakra!). Po zarzuceniu indeksów na encje w tabelach używane w JOINach zapytanek, czasy wyciągania tych samych danych skróciły się do, uwaga uwaga: 0,0003s. Nie - nie pomyliłem ilości zer :) Ave indexes;)

 

EDIT 2: Jeżeli piszecie własne moduły do strony opartej o Drupala z Boostem na pokładzie, i Wasze moduły łączą się ze sklepem poprzez wywołanie adresu z POSTem/GETem czy też jakieś WebService - zawsze na końcu skryptu / strony, którą wywołujecie / przesyłacie dane - doklejać ?nocache=1. Dlaczemu? A dlatemu, że Boost wówczas tej strony nie cachuje. A przeca nie chcemy się złapać na otrzymaniu cacheowanych danych przy zdalnym wywołaniu;) Ja się złapałem - przy systemach płatności online. Oj ile mnie nerwów kosztowało rozgryzienie, dlaczego płatności.pl nie wywołują połączenia online ze sklepem i aktualizacją statusu zamówienia i opłaconej kwoty... Zwłaszcza że płatności.pl są ta genialnym systemem, że otrzymanie od nich jakiegokolwiek komunikatu błedu graniczy chyba z cudem...:P Nie życzę wam tego samego :)

Komentarze

Wyjdzie kiedyś wersja pod drupala 7?
Niestety Page Speed nie chodzi pod FireFox 5
Fajne, jeszcze tu wrócę bo wszystkiego nie rozumiem, będzie aktualizacja dla drupala siedem?
Właściwie dlaczego Drupal robi tyle zapytań, aż 100 czy 200 dla podstawowej strony głównej z kilkoma teaserami? Przecież to banalne, normalnie robiłem to w max 10 zapytaniach, bez żadnego cache, tak po chłopsku, a tutaj taka niespodzianka.
Zawsze możesz wykorzystać przycisk Donate Paypal :D
Nice ... widac jak w prosty i szybki sposob mozna przyspieszyc tak uwielbianego przez nas drupala :) puscil bym Ci fav'a ale nie moge znalezc button'a "nadus to" :P
Comments closed...