memcached, mała darmowa aplikacja, która odpowiednio wykorzystana pozwoli nam bardzo szybko i skutecznie podnieść wydajność aplikacji lub serwisów WWW. Słowo memcache na łamach serwisu pojawiało się już kilka razy (opis, charakterystyka memcache, zastosowanie memcache jako storage + nginx) i na pewno jeszcze się pojawi wielokrotnie, ale tym razem wystąpi w roli głównej ;-) pokażę jak można praktycznie wykorzystać memcache w celu zwiększenia wydajności aplikacji PHP poprzez odciążenie np. bazy danych. Za przykład posłuży nam prosty skrypt, ale najpierw instalacja...
Do prawidłowego działania potrzebujemy daemona memcached oraz modułu PHP php5-memcache.
Instalacja memcached (Ubuntu)
root@katha:/home/jamzed# apt-get install memcached php5-memcache
memcached do prawidłowego działania wymaga libevent (biblioteki służącej do obsługi zdarzeń, w skrócie, obsługa kolejki połączeń). Warto pamiętać o tym, żeby po instalacji php5-memcache wykonać restart Apache.
Jeśli apt-get install powiódł się, powinniśmy mieć działający serwer memcached:
root@katha:/home/jamzed# /etc/init.d/memcached start
Starting memcached: memcached disabled in /etc/default/memcached.
Komunikat ten oznacza, że do wystartowania memcached, musimy dokonać małej modyfikacji w pliku /etc/default/memcached:
ENABLE_MEMCACHED=no
zmieniamy na
ENABLE_MEMCACHED=yes
startujemy jeszcze raz:
root@katha:/home/jamzed# /etc/init.d/memcached start
Starting memcached: memcached.
Plik konfiguracyjny memcached znajduje się w lokalizacji /etc/memcached.conf i zawiera takie opcje jak:
- -d - tryb pracy jako daemon
- logfile - plik z logami naszego daemona
- -v - verbose mode (dostępne: -v oraz -vv)
- -m 64 - ilość pamięci jaką przeznaczamy memcache'owi (tyle MB danych pomieści)
- -p 11211 - port na którym będzie słuchał
- -u nobody - uprawnienia użytkownika z jakimi się uruchomi
- -l 127.0.0.1 - adres IP na którym będzie słuchał
- -c 1024 - limit połączeń
Mając działający daemon, możemy spróbować napisać prosty kod w PHP do sprawdzenia działania naszego serwera:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php $memcache = new Memcache; $memcache->connect('127.0.0.1','11211'); echo "Pobieram wartosc z memcachedn"; $val = $memcache->get('key'); if ( $val != FALSE ) { echo "Wartosc jest w memcached: $valn"; } else { echo "Brak danych w memcached, wpisuje wartosc do memcachedn"; $memcache->set('key','text value',0,10); echo "Pobieram wartosc z memcachedn"; $wartosc = $memcache->get('key'); echo "Pobrana wartosc: $valn"; } ?> |
Co robi powyższy kod:
- tworzy nowy obiekt memcached typu Memcache,
- wykonuje połączenie do serwera memcache,
- sprawdza czy może pobrać klucz "key",
- jeśli wartość została pobrana, jest wyświetlana i skrypt końcy działanie,
- jeśli nie ma wpisanej wartości, jest dodawana na 10 sekund, następnie pobierana oraz wyświetlana,
Wyjaśnienie argumentów $memcache->set ('nazwa_klucza', 'wartość_która_będzie_pod_kluczem', kompresja, czas_życia_obiektu);
Dlaczego tak? Ponieważ przekładając to na implementację gdzie będziemy mieli bazę danych, najpierw powinniśmy sprawdzić czy mamy w memcache'u wynik kwerendy, w przypadku kiedy będzie to pobierzemy go z memcached i zostawimy bazę w spokoju, natomiast jeśli nie będzie to pobierzemy go z bazy danych i wstawimy do memcache'a na przyszłość.
Kod pobierający 10000 razy dane z bazy może wyglądać następująco:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php mysql_connect(‘localhost’,'memcache’,'memcache’); mysql_select_db(‘memcache’); for($a=1; $a<=10000; $a++) { $rand_id = rand(1,100); $result = mysql_query(“SELECT data FROM memcache WHERE id = $rand_id”); $row = mysql_fetch_array($result); echo “$rand_id: “.$row['data'].”n”; } mysql_close(); ?> |
mamy tutaj:
- połączenie do MySQL'a,
- wybranie bazy danych,
- pętlę wykonującą się 10,000 razy,
- losowanie $rand_id z przediału 1,100,
- pobranie z bazy danych dla ID = $rand_id,
- wyświetlenie informacji na ekran,
- zamknięcie połączenia z bazą danych
wszystko jest ok, zapytanie jest bardzo proste i prawdopodobnie baza i tak wrzuci to wszystko do pamięci, więc po zastosowaniu memcached w tym konkretnym przypadku raczej nie skróci się nam czas wykonywania skryptu, ale chodzi mi o przedstawienie samego sposobu, dlatego nie zwracajmy na to uwagi ;-) przepiszmy powyższy kod tak by przed pobraniem informacji, sprawdził czy jest ona w memcached i dopiero w przypadku kiedy jej nie ma, pobierał ją z bazy.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?php mysql_connect(‘localhost’,'memcache’,'memcache’); mysql_select_db(‘memcache’); $memcache = new Memcache; $memcache->connect(‘127.0.0.1′,’11211′); for($a=1; $a<=10000; $a++) { $rand_id = rand(1,100); $val = memQuery(“SELECT data FROM memcache WHERE id = $rand_id”, $memcache); echo “$rand_id: $valn”; } mysql_close(); $memcache->close(); function memQuery($sql,$memObject) { $key = md5($sql); $val = $memObject->get($key); if ( $val != FALSE ) { echo “T: “; return $val; } else { $querySql = mysql_query($sql); $row = mysql_fetch_array($querySql); $val = $row['data']; $memObject->set($key, $val, 0, 10); echo “F: “; return $val; } } ?> |
Skrypcik trochę nam się powiększył, ale nic skomplikowanego nie doszło ;-) pojawiła się funkcja memQuery(), która pobiera w argumencie zapytanie SQL, wylicza z niego sumę kontrolną md5, która tak naprawdę będzie naszym kluczem dla wpisu w memcached, pod którym zostanie umieszczona właściwa wartość (pobrana z bazy danych), jeśli taki klucz będzie w memcached to funkcja zwróci wartość, jeśli nie, to odpyta bazę danych, umieści wynik w memcached na 10 sekund i dopiero zwróci wartość. Taka implementacje daje nam możliwość szybkiego umieszczania w memcached wyników zapytań SQL, to jest tylko PoC, nie obsługiwane są żadne wyjątki ani skomplikowane struktury danych, podpowiem tylko, że jeśli wartości nie będą pojedyncze tak jak w tym przypadku, a np. zapytanie będzie takie SELECT a,b,c,d FROM d, to warto skusić się o serializację danych przed umieszczeniem ich w memcached ;-)
Kiedy używać memcached? Zawsze! ;-) o ile oczywiście jest to możliwe, kiedy np. pobierasz dane z bazy, który przez pewien okres czasu są stałe. Natomiast w przypadku danych które szybko się zmieniają, wrzucanie tego dodatkowo do warstwy cache'ującej nie ma sensu, bo dochodzi Ci dodatkowe połączenie TCP i obsługa, takie zapytania najczęściej charakteryzują się tym, że zawierają jakiś przedział czasowy (WHERE data_dodania < aktulny_czas)...
Warto też wspomnieć, że samo tworzenie zapytań SQL jest bardzo istotną kwestią przy wydajności, często już od samej kolejności użycia klauzuli WHERE zależy szybkość wykonania zapytania, a o kwerendach zawierających timestamp nawet nie będę wspominał. ;-) tak więc nie traktujcie memcached jako lekarstwa na wszystkie problemy, zawsze w pierwszej kolejności optymalizujcie zapytania SQL, optymalizujcie i jeszcze raz optymalizujcie ;-)