Cake PHP. Buforowanie stron
Transkrypt
Cake PHP. Buforowanie stron
Technika CakePHP Buforowanie stron Cache jest mechanizmem umożliwiającym zredukowanie opóźnienia w dostarczaniu danych do użytkownika oraz zmniejszenia obciążenia serwera. W aplikacjach internetowych często zachodzi konieczność wyświetlania tych samych informacji wielokrotnie. Np. sklep internetowy wyświetla listę dostępnych produktów w odpowiedzi. na każde żądanie potencjalnych klientów. Dowiesz się... Powinieneś wiedzieć... • Poznasz różne techniki buforowania stron WWW oraz możliwość ich praktycznego zastosowania w CakePHP. • Wymagana jest znajomość framework CakePHP. Przydatna będzie znajomość języka SQL i umiejętność administrowania bazą danych MySQL. Poziom trudności W ielokrotne generowanie stron, których zawartość nie zmienia się, powoduje zbędne obciążenie serwera WWW i bazy danych. Każde żądanie od użytkownika musi zostać przetworzone, a wynik skierowany do przeglądarki internetowej, jednak za każdym razem serwer pobiera te same dane. Nie stanowi to problemu, gdy strony przegląda kilku użytkowników, ale kłopoty mogą się pojawić wraz ze wzrostem popularności serwisu. Im więcej użytkowników, tym bardziej serwer będzie obciążony wykonywaniem zadań, które prowadzą do wyświetlenia stron nieróżniących się od siebie wcale lub różniących się tylko w niewielkim stopniu. Naturalnym rozwiązaniem problemu spadku wydajności jest zapisywanie „na boku” generowanych stron i udostępnianie ich innym użytkownikom. Zamiast za każdym razem generować tą samą zawartość strony wystarczy przy pierwszym wyświetleniu zapamiętać ją i używać przy kolejnych wywołaniach. Technika ta nazywa się page caching (pol. buforowanie stron) i jest najprostszą metodą przyspieszania działania serwisu. Przy pierwszym wyświetleniu strona zostaje zapisana do pliku HTML. Każde następne odwołanie do tej samej strony powoduje, iż do przeglądarki użytkownika zostanie wysłana zawartość bezpośrednio z ca50 che z pominięciem wszystkich etapów, dzięki którym strona została pierwotnie wygenerowana. W praktyce odbywa się to prawie tak szybko jak udostępnianie statycznych stron HTML. Cache przeglądarki internetowej Zamiast za każdym razem generować tę samą zawartość strony wystarczy przy pierwszym wyświetleniu zapamiętać ją i używać przy kolejnych wywołaniach – Rysunek 1. Właściwie w każdej nowoczesnej przeglądarce internetowej znajdują się ustawienia związane z cache. Najczęściej jest to parametr określający miejsce zarezerwowane na pliki tymczasowe przeglądanych stron WWW. Zasady aktualizacji cache są proste. Przeglądarka za pomocą znaczników HTTP sprawdza, czy przechowywane dane są aktualne – Listing 1. i w miarę możliwości korzysta z nich zamiast pobierać dane z Internetu. Dzięki temu powrót do poprzednio wyświetlanej strony WWW w przeglądarce (przycisk „wstecz”) powoduje szybkie wyświetlenie informacji, najczęściej bez konieczności pobierania danych z sieci. Kontrola aktualności cache odbywa się za pomocą znaczników: Cache - Control, Expires, Last-Modified oraz ETag przesyłanych w nagłówku stron WWW: Expires: DATA_GMT – Znacznik określa termin możliwej zmiany lub wygaśnięcia ważności dokumentu. Po upływie określonego czasu (liczonego względem GMT) dokument może ulec zmianie lub zostać usunięty. Najprostszym sposobem wymuszenia, by plik nie zo- stał umieszczony w cache, jest przypisanie daty, która minęła. Znacznik idealnie nadaje się do określania polityki tworzenia cache plików graficznych, które bardzo rzadko ulegają zmianie – Listing 1. Cache-Control: DYREKTYWA – Znacznik określa sposób zachowania mechanizmu buforowania plików (zarówno serwera WWW, jak i serwerów proxy) mówiącego, co powinno być buforowane i co może być przechowywane w cache. Najbardziej przydatne są poniższe dyrektywy: • max-age=[sekundy] – określa maksymalny czas, w którym dane są traktowane jako aktualne (liczony względem daty określonej przez znacznik Last-Modified); • public – wymusza buforowane (przydatne dla stron, które w normalnych warunkach nie są umieszczane w cache); Listing 1. Przykładowe żądanie i odpowiedź serwera WWW HEAD / HTTP/1.1 HOST: localhost HTTP/1.1 200 OK Date: Mon, 02 Jul 2007 21:24:12 GMT Server: Apache Accept-Ranges: bytes X-Powered-By: PHP/4.2.2 Set-Cookie: PHPSESSID= ac35d9b842215f4fb23ca419337af4b8; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Connection: close Content-Type: text/html Content-Language: pl 05/2007 CakePHP • no-cache – wymusza pominięcie cache (przeglądarki i serwerów proxy) i pobieranie danych bezpośrednio z Internetu; • no-store – wymusza usunięcie danych z cache zaraz po przesłaniu ich do użytkownika; • must-revalidate – wymusza sprawdzanie stanu przedawnionych dokumentów znajdujących się w cache. Last-Modified: DATA _ GMT– Znacznik określa datę ostatniej modyfikacji dokumentu – Listing 1. ETag: ZNACZNIK – Znacznik jest unikalnym identyfikatorem strony generowanym przez serwer WWW, który zmienia się za każdym razem, gdy przesyłane dane ulegną modyfikacji. Znaczniki możemy wysyłać do przeglądarki za pomocą funkcji header() – Listing 2. nerowany, co przy częstych zmianach stawia pod znakiem zapytania sens używania tej technologii. W praktyce jednak strony WWW można podzielić na fragmenty, które są statyczne, oraz na takie, które zmieniają się często, i poddać buforowaniu tylko te ostatnie. Wynikowa strona jest wtedy „składana” z mniejszych bloków, z których część będzie znajdowała się w cache. Jest to bardzo intuicyjne – w przypadku sklepu internetowego lista produktów jest fragmentem, który najrzadziej ulega zmianom, a z kolei koszyk zakupów klienta może zmieniać się dynamicznie. Wykorzystanie page cache w CakePHP zaczniemy od zapoznania się konfiguracją serwera. Domyślnie cache widoków jest zablokowane. By je aktywować, musimy zmie- nić na TRUE wartość stałej CACHE_CHECK w pliku /app/config/core.php. define ('CACHE_ CHECK', true); W kontrolerze powiązanym z widokiem, dla którego włączamy cache, musimy dodać CacheHelper przez umieszczenie kodu: var $helpers = array('Cache'); Następnie należy określić, co chcemy umieścić w cache. Do zmiennej $cacheAction musimy przypisać tablicę zawierającą akcje, które mają być buforowane, oraz czas (w sekundach), przez który cache ma być aktualny (można używać liczb lub zapisów „1 day” czy „60 seconds”) – Listing 3. W praktyce zdarza się, że pewne fragmenty strony są wypełniane dynamicznymi danymi i nie mogą znaleźć się w cache ze względu na wyświetlane dane. Wystarczy wówczas, że Listing 2. Przykładowe znaczniki wysyłane do przeglądarki za pomocą funkcji header() Page cache // strona nie będzie umieszczona w cache przeglądarki Mechanizm page cache jest powiązany z adresem URL. To na jego podstawie w kontrolerze zapada decyzja, czy strona ma być wygenerowana dynamicznie, czy też ma być użyty cache. Jeżeli użytkownik już wcześniej odwoływał się do określonego adresu WWW, to został wygenerowany plik cache – o ile prezentowane informacje nie uległy zmianie, to ponowne odwołanie do tego samego adresu spowoduje udostępnienie danych wcześniej wygenerowanych. Z tego faktu wynikają pewne ograniczenia. Strony zależące od parametrów przekazywanych w URL lub od informacji przechowywanych w sesji nie będą mogły skorzystać z page cache, podobnie jak strony, których zawartość jest uzależniona od czasu. Adres WWW takich witryn może być za każdym razem inny i w ten sposób ciągle byłyby tworzone kolejne pliki cache. Sposób działania page cache (buforowanie całej strony) doskonale sprawdza się w przypadku stron, których zawartość nie zmienia się często. Jest to trudne do osiągnięcia, jeżeli prezentowanych jest wiele danych z różnych źródeł. Jeżeli zajdą zmiany w dowolnym źródle danych, plik cache będzie musiał zostać ponownie wyge- // wymaga obsługi protokołu HTTP/1.1 header("Cache-Control: no-cache, must-revalidate"); // data z przeszłości header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); Lisgin 3. Definiowanie akcji, które mają zostać buforowane przez CakePHP // Możemy zdefiniować cache dla wszystkich parametrów akcji var $cacheAction = array('view/' => 86400); // lub dla każdego parametru oddzielnie. var $cacheAction = array( 'view/23/' ); 'view/48/' => 21600, => 21600 ����������������������������������� ����������� ���������� QUERY-CACHE Nowoczesne bazy danych są najczęściej wyposażone w mechanizm QUERY-CACHE powodujący zapamiętanie zapytań kierowanych do bazy oraz skojarzonych z nimi wyników. Takie samo zapytanie SQL do bazy danych spowoduje przekazanie wyników przechowanych w QUERY-CACHE bez konieczności odczytu informacji z plików bazodanowych. Cache jest czyszczony w przypadku zmian w tabelach do których zapytania się odwołują. MySQL4 posiada błąd, który powoduje czyszczenie całego QUERY-CACHE w przypadku zmian w dowolnej tablicy, co niekorzystnie wpływa na czas odpowiedzi serwera na te same zapytania. ����������������������������������������� ���������������������������������� ����������� ���������� ����� Rysunek 1. Przesyłanie do użytkowników stron WWW www.phpsolmag.org 51 Technika fragment kodu, który nie powinien być buforowany, określimy za pomocą znaczników <cake:nocache> oraz </cake:nocache> – Listing 4. Czyszczenie cache Przy korzystaniu z cache kluczowym momentem w funkcjonowaniu serwisu jest sposób reakcji na zmiany w prezentowanych danych. Jeżeli nie będzie sprawnego mechanizmu informowania aplikacji, które pliki cache należy usunąć, to użytkownikowi zostaną zapre- zentowane błędne nieaktualne dane. W najprostszym przypadku w reakcji na zmiany możemy usunąć wszystkie pliki cache. Rozwiązanie to ma jednak tę wadę, że nawet niewielka zmiana spowoduje konieczność czasochłonnego generowania cache na nowo. Zdecydowanie lepiej jest usuwać tylko te pliki cache, które prezentują dane, które uległy zmianie. CakePHP wykorzystuje fakt, że prezentowane na stronach WWW dane są powiązane z modelem danych, a cache jest związany z widokiem (który z kolei może wyświe- Listing 4. Przykład wyłączenia buforowania fragmentu kodu widoku <h1> Ostatnie 10 wiadomości! </h1> <cake:nocache> <ul> <?php foreach($recentMessages as $message): ?> <li>$message['title']</li> <?endforeach;?> </ul> </cake:nocache> Listing 5. Funkcja clearCache() usuwająca nieaktualne pliki cache // Usuń wszystkie strony z cache, bazując na nazwie kontrolera clearCache('controller'); // Usuń wszystkie strony z cache, bazując na nazwie kontrolera i nazwie akcji clearCache('controller_action/'); // Usuń wszystkie strony z cache, bazując na nazwie kontrolera, nazwie akcji // i parametrze // Do funkcji clearCache() można przekazywać wiele parametrów clearCache(array('controller_action_params','controller2_action_params)); Memcached System cache przechowujący dane w pamięci RAM, umożliwiający zapisywanie danych i obiektów. Wysoce wydajny i skalowalny umożliwia łączenie serwerów działających w oparciu o różne architektury systemowe. Stosowany m.in. w serwisach LiveJournal i Wikipedia. System memcached jest dostępny w repozytoriach wielu dystrybucji Linuksa (kod źródłowy można pobrać z adresu http://www.danga.com/memcached). Obsługę memcached w PHP zapewnia binarne rozszerzenie, dostępne na pecl.php.net. Krótki przegląd metod API: • • • • • • • • • • • • • • • • • 52 Memcache::add – dodaje element do serwera. Memcache::addServer – dDodaje serwer memcached do listy wykorzystywanych serwerów. Memcache::close – zamyka połączenie. Memcache::decrement – zmniejsza wartość elementu. Memcache::delete – usuwa element z serwera. Memcache::flush – usuwa wszystkie elementy z serwera. Memcache::get – zwraca element z serwera. Memcache::getExtendedStats – statystyki wszystkich serwerów memcached. Memcache::getServerStatus – zwraca stan serwerów memcached. Memcache::getStats – statystyki serwerów. Memcache::getVersion – zwraca wersję serwera memcached. Memcache::increment – inkrementuje wartość elementu. Memcache::pconnect – otwiera stałe połączenie. Memcache::replace – zmienia wartość podanego elementu. Memcache::set – zapisuje dane na serwerze. Memcache::setCompressThreshold – włącza automatyczną kompresję dużych wartości. Memcache::setServerParams – zmienia parametry i stan serwera. tlać dane z różnych modeli danych). Zmiana danych dowolnego modelu spowoduje więc automatyczne usunięcie cache dla całego widoku. CakePHP automatycznie usuwa cache, gdy nastąpi zmiana w stanie aplikacji. Jeżeli użytkownik spowoduje działanie, które będzie skutkowało zmianami w bazie danych (INSERT, UPDATE, DELETE), zostanie usunięte cache dla widoku powiązanego z kontrolerem odwołującym się do modeli danych, które uległy zmianie. Istnieje możliwość ręcznego sterowania cache i usuwania nieaktualnych danych za pomocą funkcji clearCache(). Jako parametr przekazujemy nazwę kontrolera, kontrolera i akcji lub kontrolera, akcji i parametrów identyfikujących plik cache – Listing 5. W prosty sposób można zaimplementować dodatkowy scenariusz czyszczenia cache sprawdzający się w sytuacjach, gdy nie możemy zapobiec cyklicznemu tworzeniu się nowych plików cache, ale jednocześnie chcemy uniknąć ciągłego usuwania plików. Zamiast spowalniać działanie aplikacji ciągłym testowaniem aktualności cache możemy uruchomić dodatkowy proces działający w tle. Proces ten będzie odpowiedzialny za cykliczne czyszczenie cache co określony (konfigurowalny) przedział czasowy niezależnie od obciążenia aplikacji. Składowanie danych Najprostszym sposobem przechowywania cache są pliki umieszczone bezpośrednio w systemie operacyjnym (jest to jedyny sposób przechowywania cache obsługiwany automatycznie przez CakePHP w wersji 1.1.xx). Wygenerowana strona HTML zostanie umieszczona w publicznie dostępnym obszarze serwera (w miejscu określonym ścieżką dostępu wskazywaną przez stałą $CACHE) – każde odwołanie do tej samej strony będzie wówczas przekierowywane na plik cache. Rozwiązanie takie oprócz oczywistych zalet, takich jak prostota implementacji czy dobra wydajność, ma podstawową wadę: nie jest rozwiązaniem skalowalnym. Jeżeli nasz serwis będzie się rozwijał i zajdzie konieczność uruchomienia dodatkowego serwera WWW, to staniemy przed problemem związanym z dystrybucją plików cache i ich synchronizacją. Zapytania od użytkowników mogą być kierowane do dowolnego serwera, każdy będzie więc z nich tworzył pliki cache niezależnie. Poza tym usunięcie cache z jednego serwera nie spowoduje usunięcia plików z pozostałych serwerów. Można co prawda utworzyć jeden wspólny sieciowy filesystem na potrzeby cache wszystkich serwerów, ale rozwiązanie traci wtedy swoją prostotę. Innym rozwiązaniem jest umieszczanie danych cache w bazie danych. Jest to elastyczniejsze od plików cache w systemie operacyjnym – głównie kosztem dodatkowego obciążenia zasobów serwe05/2007 CakePHP Listing 6. Struktury danych wykorzystane do przechowywania cache mysql> desc dbcache; // metoda usuwająca z bazy danych wszystkie dane, mysql> desc dbcache; // których termin ważności minął +------------+------------------+------+-----+---------+-----| Field | Type function purge() ----------+ Extra | Null | Key | Default | { | +------------+------------------+------+-----+---------+---------------+ return $this->query("delete * from cache where expires_at < NOW()"); } | key | int(10) unsigned | NO | PRI | NULL | } | value | text | YES | | NULL | // przykładowy kontroler wykorzystujących cache | YES | | NULL | // internetowego forum | | | expires_at | datetime | +------------+------------------+------+-----+---------+---------------+ // przykładowy model danych obsługujący buforowanie stron // w bazie danych klasa Cache class MyCache extends AppModel // jeżeli używamy PHP4, musimy zdefiniować zmienną // zawierającą nazwę klasy // klasa MessagesControler wyświetlająca wiadomości class MessagesController extends AppController{ var $name = 'Messages'; var $helpers = array('Html','Time'); var $layout = 'default'; var $uses = array('Forum','Topic','Comment','Message', 'MyCache'); function view($id) { var $name = 'MyCache'; // pobierz informacje o temacie dyskusji i powiązaną // z nim listę komentarzy jeżeli temat nie istnieje, // korzystamy z bazy danych – tabeli o nazwie cache // ponownie wyświetl listę działów var $useTable = ‘cache’; $topic = $this->MyCache->find('topic'.$id); if (empty($topic)) { $topic = $this->Topic->find(array('Topic.id'=> // metoda wyszukująca wartość skojarzoną z kluczem $id, 'Topic.topic_id'=>0)); // i zwracająca dane jako zmienną PHP if (! empty($topic)) { function find($key) { // przechowuj przez tydzień $this->MyCache->store('topic'.$id, $topic, time() $rc = $this->query("select * from cache where key = '$key' and expires_at < NOW() limit 1"); } return unserialize($rc['cache'][0]['value']); else { // metoda przechowująca w bazie danych klucz razem // z wartością function store($key, $value, $expires=null) { } // jutro $value = serialize($value); return $this->query("replace cache set value = '$value', } $expires_at = '$expires' where key = '$key'); } $this->redirect('/forums/index'); exit(); // przekaż zmienną $topic do widoku if (is_null($expires)) $expires = time()+(24 * 60 * 60)); $expires = date('Y-m-d H:i:s', $expires); + (7 * 24 * 60 * 60)); } } $this->set('topic', $topic); function remove($id) { // metoda usuwająca przechowywane dane na podstawie klucza // usuń temat dyskusji wraz ze wszystkimi wiadomościami // wyszukiwania // usuń zbędne pliki z cache function delete($key) if ($this->Topic->drop($id, true)) $this-> { } MyCache->delete('topic'.$id); return $this->query("delete * from cache where key = '$key'"); www.phpsolmag.org $this->redirect('/forums/index'); } exit(); 53 Technika Listing 7. Wykorzystanie memcached z poziomu PHP // utworzenie puli dostępnych serwerów memcached // każdy z nich będzie wykorzystywany proporcjonalnie // do listy wag przekazanych jako prametr do metody Mamcache::addServer() $memcache = new Memcache; $memcache->addServer('memcache_host1', 11211, 50); $memcache->addServer('memcache_host2', 11211, 25); $memcache->addServer('memcache_host3', 11211, 25); $memcache = new Memcache; $memcache->connect('localhost', 11211) or die ('Nie mogę się połączyć'); if ($get_result = $memcache->get('key')) { // jeżeli obiekt jest w cache, to skorzystaj z niego echo '<b>Dane z serwera</b>:<br/>'; echo $get_result->str_attr.'<br />'; echo $get_result->int_attr; } else { // w cache nie ma żądanych danych, zapisz dane $tmp_object = new stdClass; $tmp_object->str_attr = 'test'; $tmp_object->int_attr = time(); $memcache->set('key', $tmp_object, false, 10) or die ( 'Nie udało się zapisać elementu'); echo 'Zapisane dane zostaną usunięte po 10 sekundach<br/>'; echo 'Odśwież stronę, by zobaczyć dane zapisane na serwerze memcached'; } ra mechanizmami zapisu i odczytu danych (zobacz ramka QUERY-CACHE). Oczywiście istnieje możliwość skalowania takiego rozwiązania, jednak nie jest ono proste ponieważ wymaga żmudnej konfiguracji (replikacja baz danych) i nie jest standardowo obsługiwane przez CakePHP 1.1.xx. Przykładowy model danych obsługujący przechowywanie cache w bazie danych został przedstawiony w Listingu 6. Najbardziej elastycznym sposobem przechowywania danych jest mechanizm memcached przechowujący dane w pamięci RAM serwera (zobacz ramka). W miarę, jak dane będą zapełniały przydzieloną pamięć, system będzie automatycznie usuwał te, które były najrzadziej używane (można też usuwać dane za pomocą odpowiednich wywołań systemowych). Memcached został także wyposażony w mechanizmy równoważące obciążenie, gdy wykorzystywanych jest kilka serwerów, które można w prosty sposób przyłączać (lub 54 odłączać) bez konieczności przerywania działania serwisu (obsługa memcached zostanie włączona do CakePHP począwszy od wersji 1.2.xx). Typowa sesja połączenia z memcached z poziomu PHP została przedstawiona na Listingu 7. W pierwszej kolejności następuje zdefiniowanie puli dostępnych serwerów pracujących pod kontrolą memcached. Każdy z nich będzie wykorzystywany proporcjonalnie do wagi określonej podczas inicjalizacji połączenia i w razie awarii zastąpiony przez kolejny serwer z listy. Każdy dostęp do danych poprzedzony jest testem, czy informacje są dostępne w cache. Tylko w przypadku negatywnym nastąpi konieczność wykonania czasochłonnych obliczeń, po których wynik zostanie zapisany w cache (kolejne zapytania będą więc korzystały z bufora). Memcached sam zatroszczy się o usunięcie zbędnych (przestarzałych) danych z pamięci – w naszym przykładzie po 10 sekundach. Memcached ma jeszcze jedną zaletę – można go użyć jako mechanizm przechowujący dane nie tylko na potrzeby buforowania całych stron (page cache). Możemy w cache umieszczać dowolne dane (wyniki obliczeń, dynamicznie zmieniające się relacje między danymi) i na ich podstawie generować strony WWW. Podobnie jak w przypadku page cache problemem jest określenie, czy strony na których były prezentowane dane, które uległy modyfikacji, znajdują się w cache. Memcached umożliwia przechowywanie wyników obliczeń, więc nie możemy polegać na prostej zależności mówiącej, że w przypadku zmian w modelu danych należy usunąć cache związany z widokiem prezentującym dane.Zamiast tego można posłużyć się sztuczką polegającą na powiązaniu przechowywanych danych przechowywanych w cache ze znacznikiem sygnalizującym zmiany. Wystarczy, że utworzymy model danych, który będzie dostarczał informacje o stanie aplikacji, np. unikalny numer, niezmienny tak długo, jak długo nie nastąpiły zmiany w bazie danych lub innych obliczeniach. Jeżeli tym numerem będziemy posługiwali się przy obsłudze cache, to jego zmiana wymusi ponowne przeliczenie zmienionych danych. Jeżeli przestrzeń przeznaczona na bufor będzie się zmniejszać, memcached automatycznie usunie dane, które przez określony czas nie były aktywne. Rozwiązanie takie sprawdzi się przede wszystkim przy skomplikowanych stronach WWW opartych o przechowywane w cache dane pochodzące z czasochłonnych obliczeń. Koszt zaangażowania zasobów serwera w wykrycie zmian w stanie aplikacji będzie wówczas zrównoważony przez oszczędności wynikające z braku potrzeby wykonywania czasochłonnej generacji strony. Podsumowanie Mechanizm page cache w CakePHP w połączeniu z umiejętnym wykorzystaniem cache przeglądarki internetowej może zredukować opóźnienia w dostarczaniu danych do użytkownika oraz zmniejszyć obciążenie serwera WWW i bazy danych. Warto zwrócić szczególną uwagę na technologię memcached, która zapewnia szybki dostęp do danych zawartych w cache, jest elastyczna i skalowalna, dzięki czemu może „rosnąć” razem z naszym serwisem. PIOTR GAPIŃSKI Autor w wolnych chwilach zajmuje się programowaniem w różnych językach (głównie Rebol, Ruby, PHP i AmigaE). Kontakt z autorem: [email protected] 05/2007