Memcached, z czym to się je?
Na memcached natknęliśmy się podczas projektowania systemu wielojęzyczności (i18n) jednego z naszych serwisów. Problemem była kwestia przechowywania tłumaczeń tekstów. Pomysłów było kilka:
- tłumaczenia przechowywane w plikach PHP, odpowiedni plik ze stałymi będzie includowany podczas obsługi żądania strony,
- tłumaczenia przechowywane w plikach XML, pliki będą doczytywane w miarę potrzeby,
- tłumaczenia przechowywane w bazie danych, i na bieżąco wczytywane.
Wszystkie te podejścia mają jednak słaby punkt, otóż za każdym żądaniem strony, tłumaczenia muszą być wczytywane od nowa. Faktem jest, że wszystkie trzy metody zawierają w sobie pewien rodzaj cache’owania (wczytywane pliki są cache’owane przez system operacyjny, zapytania bazodanowe cache’owane są przez bazę danych), ale tak na prawdę żaden z nich nie oferuje optymalnego rozwiązania. Najgorszym spośród wyżej wymienionych rozwiązań jest chyba czytanie tłumaczeń z plików XML. Wyobraźmy sobie, że za każdym razem gdy skrypt napotyka napis, musi wczytać odpowiedni plik i przeparsować XML w celu znalezienia tłumaczenia. Dodajmy do tego średnio 100 użytkowników aktualnie korzystających z aplikacji webowej i mamy murowaną porażkę. Można oczywiście spróbować cache’ować napisy na czas żądania strony, ale trzeba wziąć pod uwagę, że tłumaczenia raz wczytane, nie będą się często zmieniać. Większość z nich jest również wspólna dla wszystkich użytkowników. Tutaj do akcji spokojnie można zaprząc memcached.
Co to jest?
Memcached jest to system cache’owania obiektów zbudowany przez firmę Danga Interactive, na potrzeby serwisu LiveJournal. Jego zadaniem było zwiększenie prędkości serwisu przez cache’owanie obiektów, wyników zapytań itd. i tym samym zdjęcie z bazy danych obciążenia generowanego przez serwis i użytkowników. Memcached działa jako aplikacja rozproszona, typu klient-serwer.
Jak ugryźć?
Zaczynamy od uruchomienia demona memcached na naszym serwerze. Demon nie posiada pliku konfiguracyjnego, zamiast niego będziemy używać kilku opcji w linii poleceń. Spróbujmy tak:
# ./memcached -d -m 2048 -l 10.0.0.40 -p 11211Polecenie to spowoduje uruchomienie aplikacji memcached jako usługi. Demon będzie zużywał 2GB pamięci RAM oraz nasłuchiwał na IP 10.0.0.40, na porcie 11211.
Możemy oczywiście zmniejszyć ilość ramu zużywanego przez demona, 2GB to zdecydowanie za dużo na przechowywanie samych tłumaczeń.
Teraz możemy przejść do implementacji. Zacznijmy od stworzenia managera cache’u, czyli klasy ‘CacheManager’. Klasa ta powinna:
- posiadać dane służące do połączenia się z demonem memcached (czyli adres IP, numer portu)
- pozwalać na nawiązanie połączenia,
- pozwalać na dodawanie i usuwanie obiektów z cache’a,
Dodatkowo zaimplementujemy również metody, które pozwolą na wyczyszczenie całego cache’u i sprawdzenie stanu serwera.
Zacznijmy od pól klasy. Dodajmy następujące zmienne:
/** * Nazwa serwera, na którym memcache czeka na połączenia. */ protected $host; /** * Numer portu, na którym memcache czeka na połączenia. */ protected $port; /** * Wartość true oznacza, że połączenie jest nawiązane */ protected $is_connected = false;
oraz utwórzmy klasę wyjątku, której będzie używać CacheManager.
class MemCacheException extends Exception { }
Teraz czas na samego CacheManager, nie będę wnikał w jego budowę, nie ma co ukrywać jest prosta;) Poniżej znajduje się kompletny kod tej klasy, a tutaj plik skryptu.
/** * Narzędzie opakowujące klasę MemCache. Udostępnia metody do obsługi i * zarządzania Cachem. * @author Maciek Rogoziński * @version 1.0 * @updated 09-May-2007 14:52:38 */ class CCacheManager { /** * Nazwa serwera, na którym memcache czeka na połączenia. */ protected $host; /** * Numer portu, na którym memcache czeka na połączenia. */ protected $port; /** * True oznacza, że połączenie jest nawiązane. */ protected $is_connected = false; protected $memcache = null; /** * * @param host Nazwa serwera, na którym memcache czeka na połączenia. * @param port Numer portu, na którym memcache czeka na połączenia. */ public function __construct($host = 'localhost', $port = 11211) { $this->host = $host; $this->port = $port; } /** * Metoda pobiera obiekt Memcache. * @return Metoda zwraca obiekt Memcache. */ protected function GetMemcache() { if($this->memcache == null) { throw new MemCacheException('Połączenie z Memcache nie zostało nawiązane, użyj metody Connect.'); } return $this->memcache; } /** * Tworzy połączenie z serwerem memcache. */ public function Connect() { if($this->is_connected) { return true; } $this->is_connected = true; $this->memcache = new Memcache(); if(!$this->memcache->connect($this->host, $this->port)) { throw new MemCacheException('Memcache nie może nawiązać połączenia z serwerem.'); } } /** * Usuwa wartość z serwera. * * @param key Nazwa klucza, pod którym składowana była wartość. * @param timeout Czas po którym wartość straci ważność. * @return Zwraca TRUE w przypadku sukcesu, FALSE w przypadku porażki. */ public function Delete($key, $timeout) { return $this->GetMemcache()->delete($key, $timeout); } /** * Odczytuje wartość skojarzoną z kluczem, z serwera. * * @param key Nazwa klucza, pod którym sk≥składowana była≥a wartość. * @return Zwraca string powiązany z kluczem key lub FALSE gdy klucz nie został * znaleziony. */ public function Get($key) { return $this->GetMemcache()->get($key); } /** * Sprawdza status serwera. * @return Zwraca FALSE jeśli serwer nie działa, inaczej TRUE. */ public function GetIsServerOk() { return $this->GetMemcache()->getServerStatus($this->host, $this->port); } /** * Składuje dane na serwerze. Jeśli dany klucz istnieje, zostanie nadpisany. * @return Zwraca TRUE w przypadku sukcesu, FALSE w przypadku porażki. * * @param key Nazwa klucza, z którym będzie powiązana wartość. * @param value Wartość, która ma być składowana. Stringi i inty są składowane * 'as is', reszta jest serializowana. * @param flag Użyj MEMCACHE_COMPRESSED aby składowane wartości były * kompresowane (używa biblioteki zlib). * @param expire Czas po którym wartość traci ważność. Jeśli jest równe 0, * wartość nigdy nie straci ważności. */ public function Set($key, $value, $flag = null, $expire = 0) { return $this->GetMemcache()->set($key, $value, $flag, $expire); } /** * Nadaje wszystkim wartościom po stronie serwera status brak ważności. Wartości * te mogą zostać zastąpione przez serwer nowymi wartościami. * @return Zwraca TRUE w przypadku sukcesu, FALSE w przypadku porażki. */ public function ExpireAll() { return $this->GetMemcache()->flush(); } }
Z czym to się je?
Gdy już mamy nasze narzędzie przejdźmy do przykładów użycia. Najpierw tworzymy obiekt i łączymy się z demonem memcached. Do połączenia użyjemy domyślnych ustawień.
$cache = new CacheManager(); $cache->Connect();
Teraz, zgodnie z ideą memcached, spróbujemy przechować napis w cache’u.
$cache->Set('napis', 'napis do przechowania');
Tym sposobem zapisaliśmy string ‘napis do przechowania’ w cache’u. Aby go odczytać należy wywołać następującą metodę:
$cache->Get('napis');
Zwróci ona napis, który poprzednio zapisaliśmy pod tym kluczem. Oczywiście przechowanie napisu w ramach jednego żądania strony to nic wielkiego, jednak jeśli spróbujesz, okaże się że napis jest stale dostępny w całej aplikacji. Aby skasować, wartość pod kluczem ‘napis’ użyjemy metody:
$cache->Delete('napis');
W klasie CacheManager zaimplementowaliśmy jeszcze dwie dodatkowe metody:
- ExpireAll - powoduje utratę ważności wszystkich kluczy, które były zapisane w memcached (jednym słowem tracimy możliwość odwoływania się do obiektów, które były zapisane pod kluczami),
- GetIsServerOk - zwraca wartość true jeśli demon memcached działa prawidłowo, inaczej false.
Zachowanie przy stole
Ze względu na kiepską dokumentację, ciężko jest zorientować się jak naprawdę działa nasze cache’owanie. Otóż całość przypomina swoją budową dużą tablicę asocjacyjną tzn. korzystamy z niej posługując się kluczem pod którym możemy zapisać jakiekolwiek dane. Typy podstawowe tj. int, string składowane są ‘as is’, natomiast cała reszta ulega serializacji. Można stąd wywnioskować, że skoro następuje serializacja, nasze obiekty są przechowywane jako kopie, a nie jako referencje tzn. zmieniając obiekt w memcached nie spowodujemy zmian w oryginalnym obiekcie.
Podsumowanie
Memcached jest to bardzo ciekawe rozwiązanie pozwalające korzystać z ogólnie dostępnego cache’u aplikacji. Szczególnie warto się nim zainteresować, gdy posiadamy zasoby wielokrotnego użytku powodujące obciążanie serwera www i/lub serwera bazodanowego. Prostota budowy powoduje, że użycie jest bardzo intuicyjne i nie powinno powodować problemów. Dużym minusem memcached jest natomiast manual, tak jak duża część projektów open-source’owych (patrz także “Flyspray, Pligg, Facebook… dokumentacja i programowanie obiektowe”) posiada bardzo mizerną dokumentację.
Memcached, z czym to się je? is licensed under a
Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Na tych samych warunkach 2.5 Polska License.


28. October 2008, 20:43
Ciekawy post, dodalem twoj blog do ulubionych, bede tu teraz wpadal czesciej, pozdrawiam