wizytówka internetowa
Tworzenie wydajnych i przygotowanych na dużą popularność serwisów internetowych nie jest zadaniem łatwym. Przekonują się o tym webmasterzy, dla których sukces witryny okazuje się paradoksalnie przeszkodą w jego dalszym rozwoju - nagły wzrost transferu i generowanego obciążenia przekładają się bowiem wprost na dodatkowe koszty. Skutecznym rozwiązaniem okazuje się wówczas wykorzystanie mechanizmu cache.
Wojciech Kocjan
Idea zastosowania pamięci podręcznej - bo tak często określa się cache – znajduje swoje zastosowanie w informatyce od wielu lat: w pracy procesora, dysku twardego czy przeglądarki internetowej.
Wraz z rozwojem sieci, strony internetowe stają się co raz bardziej rozbudowanymi aplikacjami – komunikującymi się z bazami danych, korzystającymi z systemów szablonów i oferującymi bogaty interfejs, łączący w sobie technologie XHTML, CSS i JavaScript. Na każdej z tych warstw możliwe jest zastosowanie mechanizmu cache – co w artykule zostanie pokazane na przykładzie witryny napisanej w języku PHP. Zasada działania jest jednak wspólna także dla innych technologii, różnice występują jedynie w implementacji. Zastosowanie opisywanych rozwiązań pozwala na znaczne zaoszczędzenie zasobów serwera, a także zmniejszenie generowanego transferu nawet o kilkadziesiąt procent.
Najczęstszym powodem zainteresowania mechanizmem cache jest chęć ograniczenia generowanego przez witrynę transferu. Mimo, iż dostawcy usług hostingowych oferują co raz większe pakiety, prowadzenie popularnego serwisu wciąż wiąże się z koniecznością dodatkowych opłat z tytułu przekroczenia limitów transferu. Najprostszym i najłatwiejszym do wprowadzenia sposobem na ograniczenie kosztów jest wprowadzenie kompresji kodu html, css czy js.
Wszystkie popularne przeglądarki (Internet Explorer, Firefox czy Opera) obsługują kompresję w ramach protokołu HTTP, gdzie wykorzystywane są dwa algorytmy: gzip (GNU zip) oraz deflate. Oznacza to tyle, że przesyłając do przeglądarki odwiedzającego skompresowane w ten sposób pliki, zostaną one automatycznie rozpakowane, co odbywa się w tle, nie angażując w to działanie użytkownika.
Przeglądarka, wysyłając do serwera żądanie GET (pobierz), wykorzystuje nagłówek „Accept-Encoding”, w którym informuje o obsługiwanym przez siebie algorytmie kompresji. Serwer na tej podstawie może skompresować wysyłane do przeglądarki dane, co przyspiesza czas całej operacji i zmniejsza liczbę koniecznych do przesłania informacji. Jakie pliki więc warto kompresować? Doskonale nadają się do tego właśnie .html, .js i .css, których objętość można bez strat zmniejszyć nawet o 80%. Kompresowanie plików .jpg czy .zip nie ma z kolei sensu, gdyż są one już upakowane.
W przypadku silnika strony napisanego w języku PHP, aby wykorzystać gzip wystarczy dodać kilka linii kodu, które muszą się znaleźć przed wysłaniem jakichkolwiek danych do przeglądarki użytkownika:
if (!ini_get('zlib.output_compression')) {
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
ini_set('zlib.output_compression_level', 1);
ob_start('ob_gzhandler');
}
}
W pierwszej linii sprawdzamy czy kompresja nie jest już uaktywniona, gdyż można ją włączyć globalnie dla całego serwera, poprzez stosowny wpis w konfiguracji PHP. Dalszy kod wykonujemy jedynie w przypadku, gdy automatyczna kompresja jest na serwerze wyłączona. W linii nr 2. sprawdzamy, czy przeglądarka użytkownika obsługuje kompresję gzip – wychwytując, czy nazwa tego algorytmu występuje w nagłówku „Accept-Encoding” - jeśli tak, wywoływane są dwie funkcje. Za pomocą ini_set() ustawiamy wartość parametru „zlib.output_compression_level”, przypisując mu liczbę z przedziału 1-9. Im mniejsza cyfra, tym słabsza jakość kompresji – jednak co za tym idzie, także mniejsze obciążenie procesora. Różnice w objętości spakowanych danych między zastosowaniem 1 a 9 nie są spore, dlatego rozsądnym jest wybranie niskiej wartości. Wywołanie ob_start() z parametrem 'ob_gzhandler' włącza kompresję gzip.
Nieco więcej zachodu wymaga dodanie kompresji gzip w przypadku wysyłania plików z kodem CSS i JavaScript. Pojawiają się bowiem dwa problemy, które trzeba rozwiązać. Pierwszym jest fakt, że pliki .css czy .js nie są interpretowane przez PHP, co jednak można łatwo rozwiązać poprzez zmianę konfiguracji w pliku .htaccess. Wystarczą do tego 2 linie kodu.
AddType application/x-httpd-php .css .js php_value auto_prepend_file gzip.php
Pierwsza informuje serwer, że pliki .css i .js będą parsowane przez interpretator PHP. W kolejnej linijce wykorzystujemy mechanizm „auto_prepend_file”, który powoduje przy wywołaniu dołączenie pliku gzip.php, w którym zaimplementujemy mechanizm kompresji. Dzięki temu nie będziemy musieli nic zmieniać w plikach .js czy .css – pozostaną one nietknięte.
Plik .htaccess umieszczamy oczywiście w katalogu, gdzie znajdują się pliki .js oraz .css – nie w katalogu głównym konta czy np. bezpośrednio w public_html. Ważne też, aby w katalogu nie znajdowały się inne pliki .php.
Drugi problem pojawia się w momencie dodania kompresji w pliku gzip.php. Pliki .css i .js domyślnie są bowiem cacheowane przez większość przeglądarek – aby przy każdym odświeżeniu strony nie były ponownie pobierane z serwera. Zmiany dokonane w .htaccess oraz dodanie kompresji jak w przypadku plików .php, powodują modyfikację wysyłanych nagłówków, usuwając z nich m.in. „Last-modified”, przez co nie są one cacheowane. Aby nie musieć wybierać między kompresją a cacheowaniem plików w przeglądarce, należy ręcznie zapewnić przesłanie odpowiednich nagłówków. Jako że pliki .css i .js są plikami statycznymi, nie ma sensu kompresować ich przy każdym wywołaniu – plik gzip.php powinien to uwzględniać. Rozpocznijmy więc kodowanie:
<?php
$file=end(explode('/',$_SERVER['REQUEST_URI']));
$file_last_modification=filemtime($file);
$gm_file_last_modification=gmdate("D, d M Y H:i:s T", $file_last_modification);
// is file modified
if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $gm_file_last_modification) {
header("HTTP/1.0 304 Not Modified");
header('ETag: "'.md5($file.$file_last_modification).'"');
exit;
}
Na początku przygotowujemy trzy zmienne, zawierające kolejno: nazwę wywoływanego pliku oraz datę jego ostatniej modyfikacji w dwóch formatach: UNIXowym znaczniku czasu oraz GMT (Greenwich Mean Time).
Kolejnym krokiem jest sprawdzenie, czy przeglądarka użytkownika nie posiada już w swojej pamięci wywoływanego pliku – porównujemy w tym celu datę jego modyfikacji. W przypadku gdy klient ma już żądany plik, wysyłamy do przeglądarki nagłówek 304 „Not Modified” i przerywamy dalsze wywoływanie skryptu. Wcześniej wysyłany jest jeszcze nagłówek „Etag” (entity tag), który jest sumą kontrolną, pozwalającą określić czy plik został zmieniony – generujemy ją za pomocą funkcji hashującej md5, podając jako parametr nazwę pliku i datę modyfikacji.
Dalsza część kodu wykonywana będzie, jeśli przeglądarka nie ma w swoim buforze żądanego pliku CSS/JS:
(end(explode('.',$file))=='css') ? header('Content-type: text/css; charset: UTF-8') : header('Content-type: text/javascript; charset: UTF-8');
header('Last-Modified: '.$gm_file_last_modification);
header('ETag: "'.md5($file.$file_last_modification).'"');
W pierwszej linii w zależności od rozszerzenia żądanego pliku (css lub js) wysyłamy do przeglądarki nagłówek informujący o typie MIME: text/css lub text/javascript. Podane jest również zastosowane kodowanie, możemy je zmienić na np. iso8859-2, jeśli takie jest wykorzystywane, a w kodzie znajdują się polskie znaki. Następne dwa nagłówki określają datę modyfikacji pliku i entity tag – musimy je podać, aby wcześniejsze sprawdzenie modyfikacji miało sens: podana tutaj data zostanie nam przesłana przez przeglądarkę przy następnym odświeżeniu.
Czas teraz na dodanie obsługi kompresji gzip. Z racji, iż pliki css i js nie są generowane dynamicznie, ich kompresja przy każdorazowym żądaniu nie ma sensu – zastosujemy inne rozwiązanie, polegające na zapisywaniu spakowanych plików w osobnym katalogu. Kod prezentuje się następująco:
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
$filegz='gzipped/'.$file.'.gz';
$makeNewGz=false;
if (file_exists($filegz)) {
$gzip_last_modification=filemtime($filegz);
if ($gzip_last_modification+10<$file_last_modification) {
$makeNewGz=true;
}
}
else {
$makeNewGz=true;
}
if ($makeNewGz===true) {
$handle=gzopen($filegz,'w9');
gzwrite($handle,file_get_contents($file));
gzclose($handle);
chmod($filegz,0666);
}
header('Content-Encoding: gzip');
header('Vary: Accept-Encoding');
echo file_get_contents($filegz);
exit;
}
?>
Najpierw następuje sprawdzenie, czy przeglądarka obsługuje gzip – jeśli nie, żadne linie kodu nie będą wykonywane, a w dalszym kroku przesłany będzie żądany plik (gzip.php kończy swoje działanie - był on jedynie dodawany na początku wywołania .css lub .js).
Następnie przygotowujemy dwie zmienne: $filegz zawiera ścieżkę skompresowanego pliku – umieszczamy go w katalogu gzipped – musimy więc wcześniej zadbać o jego stworzenie i nadanie mu odpowiednich praw do zapisu (na serwerach UNIX najczęściej komendą: chmod 777 gzipped). Zmienną $makeNewGz ustawiamy początkowo jako false – przechowywać w niej będziemy informację, czy konieczne jest dokonanie kompresji żądnego pliku – stanie się tak tylko w dwóch przypadkach – jeśli nie ma jeszcze skompresowanego pliku lub jest on starszy niż jego nieskompresowana wersja. Dzięki takiemu rozwiązaniu, w przypadku aktualizacji np. pliku styli, serwer automatycznie wykryje zmiany, skompresuje go i poinformuje przeglądarkę, że zawartość została zmieniona.
Jeśli $makeNewGz zostało ustawione na „true” - musimy przeprowadzić kompresję, czego dokonujemy za pomocą funkcji gzopen() oraz gzwrite(). Jako jeden z argumentów tej pierwszej funkcji podajemy stopień kompresji - „w9”. Tutaj również wybieramy go z przedziału 1-9, warto jednak dać najwyższą wartość – kompresja będzie najsilniejsza, a z racji faktu że wykonywana jest on tylko jednorazowo po zmianie pliku – nie musimy się martwić o obciążenie procesora.
Pozostało nam już tylko poinformowanie przeglądarki, że wysyłana wartość jest kodowana algorytmem gzip oraz oczywiście wysłanie spakowanego pliku, do czego wykorzystywana jest funkcja file_get_contents(). Istotne jest również wywołanie exit; co zakończy wykonywanie skryptu, a co za tym idzie – nie dołączony zostanie nieskompresowany plik.
Rozwiązanie na pierwszy rzut oka wydawać się może skomplikowane, jednak jego wdrożenia w działającym serwisie internetowym można dokonać praktycznie w minutę. Wystarczy przekopiować pliki „.htaccess” oraz „gzip.php” oraz stworzyć katalog „gzipped” z prawami do zapisu przez wykonywany skrypt. Nie są konieczne modyfikacje istniejących plików .js i .css.
Przedstawione do tej pory metody dotyczyły kompresji danych przesyłanych z serwera do przeglądarki użytkownika. Pozwala to na zmniejszenie ilości wygenerowanego transferu, jednak nie wpływa na spadek obciążenia serwera – wręcz przeciwnie, jest ono nawet większe, gdyż w przypadku kompresji stron dynamicznych, procesor musi wykonać dodatkowo mechanizm upakowania danych. Jak więc oszczędzić nieco zasobów serwera?
Tworząc typową aplikację PHP, cache można zastosować na trzech płaszczyznach: buforowaniu wyników najbardziej zasobożernych operacji, systemie szablonów oraz zapytań do bazy danych. W tym pierwszym przypadku trzeba samodzielnie obsłużyć mechanizm cache, co zapewni przygotowana specjalnie do tego celu klasa. Z kolei popularne systemy szablonów oraz systemy abstrakcji baz danych mają zazwyczaj wbudowany mechanizm cache, wówczas wystarczy więc jedynie go włączyć i odpowiednio zastosować.
Załóżmy, że w naszym serwisie posiadamy kilka fragmentów kodu, które szczególnie obciążają system – mogą to być np. skomplikowane obliczenia, operacje na dysku czy pobieranie danych z zewnątrz. Aby odciążyć serwer, najbardziej zasobożerne operacje powinny być cacheowane. Ponadto, programista powinien mieć możliwość kontroli jak długo zapisywane mają być w pamięci (której funkcję będzie pełnił dysk twardy) wyniki działania konkretnych linii kodu. Aby lepiej zrozumieć zasadę działania, zacznijmy od kodu, w którym cache będzie wykorzystywany:
$cache=new cache;
$cache->setFile('sample_data');
if ($cache->isFresh(10)) {
$result=$cache->load();
}
else {
$result=pow(256,512);
$cache->save($result);
}
echo 'Liczba 256 podniesiona do potęgi 512 jest równa '.$result;
Jak widać, wprowadzenie cache do aplikacji nie jest skomplikowane. Rozpoczynamy od stworzenia instancji klasy cache, a następnie definiujemy nazwę pliku, w którym zapisywane będą dane. Kolejny krok to sprawdzenie, czy cache jest aktualny – w tym przypadku czy został wygenerowany w ciągu ostatnich 10 minut – jeśli tak, wówczas dane są ładowane do zmiennej $result. Jeśli zawartość pliku 'sample_data' jest nieaktualna, wówczas do $result przypisujemy wynik działania zasobożernej operacji. W przykładzie jest to 512-ta potęga liczby 256, co oczywiście nie ma dużego sensu (jej obliczenie przy dzisiejszej mocy procesorów nie stanowi dla komputera problemu). Kolejnym krokiem jest zapisanie wyniku działania do pliku, w którym przechowywany jest cache.
Zastosowanie tego typu kodu sprawi, iż dokonywanie obliczenia działania matematycznego będzie miało miejsce co najwyżej raz na 10 minut – niezależnie, czy skrypt zostanie wywołany w tym czasie 1, 10 czy 10 000 razy, co pokazuje możliwości tego rozwiązania. Przyjrzyjmy się teraz, jak zbudowana jest sama klasa cache:
<?php
class cache {
public $file;
public function setFile($name) {
$this->file='/home/user/cache/'.$name.'.cache';
}
public function isFresh($minutes=10) {
if (!file_exists($this->file)) return false;
if (filemtime($this->file)>=time()-($minutes*60)) {
return true;
}
else {
$this->clear($this->file);
return false;
}
}
Klasa posiada jedynie właściwość $file, w której przechowywana jest nazwa pliku, ustawiana za pomocą metody setFile(). Pliki będą posiadały rozszerzenie .cache, z kolei znajdować się będą w katalogu /home/user/cache/ - ścieżkę trzeba oczywiście dostosować do własnego przypadku.
Kolejną metodą jest isFresh(), która sprawdza aktualność cache, przyjmując jako argument liczbę minut. Jeśli plik z wynikiem działania zasobożernej operacji nie istnieje lub jest starszy niż podana wartość czasu – zwracane jest false, dzięki czemu wiemy, iż konieczne jest wykonanie operacji i zapisanie jej do cache.
Do zapisywania i ładowania cache służą odpowiednio metody save() i load():
public function save($data) {
$handle=fopen($this->file,'w');
fwrite($handle,serialize($data));
fclose($handle);
chmod($this->file,0666);
}
public function load() {
return unserialize(file_get_contents($this->file));
}
Funkcje te realizują prosty odczyt i zapis pliku, połączony z (de)serializacją danych. Serialize() zwraca wartość typu string, będącą strumieniem bajtów, mogącym odpowiadać zarówno zmiennej, tablicy czy obiektowi, dzięki czemu możemy cacheować dowolne elementy języka PHP.
Do zdefiniowana zostały jeszcze dwie metody, które również mogą okazać się niezwykle przydatne do wykorzystania w kodzie aplikacji:
public function exists() {
return file_exists($this->file);
}
public function clear($file) {
unlink($file);
}
}
Nie są to skomplikowane funkcje – pierwsza z nich sprawdza czy plik z cache istnieje, a druga – kasuje tenże plik. Gdzie to wykorzystać? Załóżmy, że serwis posiada bazę artykułów, które każdy może skomentować. Aby zaoszczędzić zasoby, chcemy wprowadzić mechanizm cacheowania tychże komentarzy – jak jednak ustawić liczbę minut, przez które będą one buforowane? Z pomocą przychodzą właśnie dwie powyższe metody, których wykorzystanie może wyglądać następująco:
$cache=new cache;
$cache->setFile('comments_'+$articleId);
if ($cache->exists()) {
$comments=$cache->load();
}
else {
$comments=get_comments($articleId);
$cache->save($comments);
}
Takie rozwiązanie ma sens wtedy, kiedy przy dodaniu komentarza wykonamy kod:
$cache=new cache
$cache->clear('comments_'+$articleId);
Jak widać, użycie exists() zamiast isFresh() pozwoliło na optymalne użycie cache – pobieranie listy komentarzy (get_comments()) będzie wykonywane jedynie wówczas, kiedy od momentu ostatniej odsłony został dopisany nowy komentarz przez użytkownika.
Napisanie klasy cache i jej wykorzystanie nie jest skomplikowane, a niesie za sobą olbrzymie możliwości, pomagając programiście na znaczną optymalizację kodu przy niedużym wysiłku - wprowadzenie tej metody nawet w działającym serwisie nie jest zadaniem czasochłonnym.
Rozwiązań pozwalających na użycie template'ów w budowie serwisu internetowego jest sporo i niemożnością jest opisanie sposobu uruchomienia cache we wszystkich z nich. Te systemy, które obsługę buforowania mają wbudowaną, posiadają również dokumentację na temat sposobu jego wykorzystania. W przypadku popularnego skryptu Smarty, włączenie cache sprowadza się do ustawienia zmiennej $caching, która przyjmuje wartość 0, 1 lub 2:
<?php
$smarty = new Smarty;
$smarty->caching = 1;
$smarty->display('page.tpl');
?>
Wartość 0 oznacza wyłączenie cache, 1 – włączenie, a 2 – włączenie z możliwością ustawienia czasu życia cache:
<?php
$smarty = new Smarty;
$smarty->caching = 2;
$smarty->cache_lifetime = 300;
$smarty->display('page.tpl');
?>
Czas aktualizacji cache podajemy w sekundach i przypisujemy do właściwości $cache_lifetime. Smarty udostępniają również szereg funkcji, których działanie jest podobne jak w opisywanej wcześniej klasie cache: is_cached() czy clear_cache(). Systemy szablonów pozwalają ponadto na cacheowanie jedynie wybranych fragmentów stron, z wyłączeniem np. formularza logowania czy sekcji, które są personalizowane (osobne ustawienia dla każdego z użytkowników).
W przypadku wykorzystania dowolnego systemu szablonów warto sprawdzić jego możliwości w zakresie cacheowania – w niektórych przypadkach można doprowadzić do takiego działania aplikacji, która będzie wykonywać się prawie tak szybko jak statyczne pliki .html.
Połączenia z bazą danych i nadmierna ilość zapytań, to często wąskie gardło aplikacji internetowych. Na szczęście i tutaj z pomocą przychodzi cache – jego wprowadzenie jest najłatwiejsze w przypadku korzystania z którejś klas, zapewniających warstwę abstrakcyjną nakładaną na natywne funkcje obsługujące połączenie z bazą danych w PHP. Podobnie jak w przypadku systemu szablonów, rozwiązań realizujących tą funkcjonalność jest wiele i nie sposób ich opisać.
Od wersji 5.1 w PHP domyślnie dostępne jest rozszerzenie PDO (PHP Data Objects), które zapewnia obiektową obsługę połączenia i dokonywania zapytań w bazie danych. Z pewnością warto z PDO korzystać, zwłaszcza że istnieją rozwiązania, rozszerzające je o możliwość cacheowania danych. Jednym z nich jest klasa PCO (PDO Cache Object queries, http://art.php.pl/Projekt/31), napisana przez Marka Pałczyńskiego na konkurs organizowany przez społeczność serwisu php.pl. Jedną z funkcji tej nakładki na PDO jest właśnie obsługa cache, która realizowana jest w bardzo prosty sposób:
$model = new DataMapper('news');
$news=$model->cache()->findAll();
Wystarczy użyć metody cache() by dane nie były pobierane z bazy danych, a z buforu, o ile taki istnieje. Co więcej, nie musimy martwić się o aktualność cache, gdyż będzie on automatycznie czyszczony w przypadku dodania, modyfikacji lub usunięcia danych w tabeli. Ponadto, aby cache zadziałał, nie może być ustawiony tryb debugowania (DEBUG=0) oraz podana musi być prawidłowa ścieżka do folderu, przechowywana w stałej CACHE_DIR.
Inną nakładką na PDO - rozszerzającą jej możliwości - jest biblioteka Open Power Driver (http://www.openpb.net/opd.php), również polskiego autorstwa: Tomasza Jędrzejewskiego. Cache realizowany jest tam w nieco inny sposób (ustawiamy jego ważność), bardzo dobrze opisany w dokumentacji.
Zastosowanie opisanych technik pozwala na znaczne odciążenie serwera, a także oszczędność generowanego przez serwis transferu. Zważywszy na niewielkie nakłady czasu, konieczne do wdrożenia rozwiązań cache i kompresji gzip, sposoby te są w zasięgu praktycznie każdego webmastera. Włożona w te działania praca z pewnością się opłaci – użytkownikom witryna załaduje się szybciej, a koszty ponoszone na opłatę dodatkowego transferu spadną.
PHP jest językiem interpretowanym, co oznacza, że przy każdym wywołaniu skryptu, jego kod jest kompilowany do kodu bajtowego, a dopiero potem wykonywany. Operacja kompilacji zajmuje oczywiście czas i zasoby serwera, dlatego i tutaj możliwe jest zastosowanie cache.
Na chwilę obecną istnieją cztery liczące się rozwiązania, które przechowują w pamięci operacyjnej lub na dysku kod bajtowy skryptów PHP, tym samym znacznie przyspieszając ich uruchamianie. Najbardziej znany jest Zend Optimizer (http://zend.com/products/zend_optimizer), będący rozwiązaniem komercyjnym, wydajnością nie ustępują mu jednak rozwiązania otwarte, takie jak eAccelerator (http://www.eaccelerator.net) czy XCache (http://xcache.lighttpd.net). Dostępne jest również rozszerzenie PHP o nazwie APC (Alternative PHP Cache), które oferuje podobną funkcjonalność.
Instalacja tego typu aplikacji wymaga praw administracyjnych na serwerze, jednak przeważnie firmy hostingowe decydują się na korzystanie z jednego z tych rozwiązań - stąd istnieje duże prawdopodobieństwo, że prowadzony przez nas serwis internetowy jest już w ten sposób cacheowany i jego kod bajtowy przechowywany jest w pamięci serwera.