Nette Framework: Cache

Cache (čtěte „keš“) je označení pro rychlou mezipaměť, do které se duplikují často používaná a přitom těžko dostupná data, aby se přístup k nim urychlil. Ukážeme si, co v této oblasti nabízí Nette Framework
Seriál: Začínáme s Nette Framework (17 dílů)
- Nette Framework: zvyšte svoji produktivitu 10. 3. 2009
- Nette Framework: Odvšivujeme 17. 3. 2009
- Nette Framework: MVC & MVP 24. 3. 2009
- Nette Framework: Refactoring 31. 3. 2009
- Nette Framework: Chytré šablony 7. 4. 2009
- Nette Framework: adresářová struktura aplikace 14. 4. 2009
- Nette Framework: AJAX 21. 4. 2009
- Nette Framework: AJAX (pokračování) 28. 4. 2009
- Nette Framework: AJAX (dokončení) 5. 5. 2009
- Nette Framework: Sessions 12. 5. 2009
- Nette Framework: Přihlašování uživatelů 19. 5. 2009
- Nette Framework: Ověřování oprávnění a role 26. 5. 2009
- Nette Framework: Neprůstřelné formuláře 2. 6. 2009
- Nette Framework: Neprůstřelné formuláře II 9. 6. 2009
- Nette Framework: Neprůstřelné formuláře III 16. 6. 2009
- Nette Framework: Cache 23. 6. 2009
- Nette Framework: Co se do seriálu nevešlo? 30. 6. 2009
Nálepky:
Mezi strojovým časem a paměťovým prostorem existuje jakási nepřímá úměra. Kešování je jedním ze způsobů, jak čas transformovat do prostoru. Idea je prostá: mějme časově náročnou operaci, která pro vstupní parametry vygeneruje výstup. Ten si uložíme do mezipaměti a příště budeme moci pro stejné vstupní parametry získat výsledek bez volání oné náročné operace.
Kešování je tedy další vrstva, kterou přidáme do aplikace.
Jenže celé to má řadu úskalí. Je potřeba si zodpovědět otázky:
- má na výsledek operace vliv ještě něco jiného kromě vstupních parametrů? (a pokud ano, jak tyto vlivy pojmout?)
- bývá vůbec operace volána se stejnými vstupními parametry často?
- jakou má kešovací vrstva režii?
Při špatném nastavení nebo odhadu situace se zcela klidně může stát, že implementace cache způsobí znatelný pokles výkonu aplikace a přitom zvýší její náročnost na paměťové zdroje. Jinými slovy, cache není samospasitelná a měla by být implementována s rozmyslem a erudovanou osobou.
Existuje celá řada úrovní, na které lze data kešovat. Je třeba dobré vědět, jestli např. databázový server disponuje keší (pravděpodobně ano) a dokázat ji správně využít. Přičemž duplikovat tuto cache na straně aplikace, tj. ukládat do paměti PHP skriptu všechny SQL dotazy a jejich výsledky, může být potom kontraproduktivní. Daleko většího efektu se dosáhne při kešování finální podoby dat, tedy vygenerovaného HTML fragmentu.
Téma optimalizace výkonnosti a kešování by vystačilo na samostatný seriál, nebudu proto zabíhat do podrobností a vrátím se k oblasti bezprostředně související s Nette Framework.
Opcode cache
PHP je interpretovaný jazyk, což znamená, že při každém HTTP požadavku musí server všechny skripty naparsovat a zkompilovat do binární podoby nazývané opcode. Pod termínem opcode cache se pak rozumí mechanismus, který zkompilovaný kód udržuje v mezipaměti, takže není potřeba jej generovat pokaždé znovu.
Existuje celá řada opcode cache implementací, např. eAccelerator, Zend Optimizer, ionCube, APC. Co s nimi má společného Nette Framework? Celkem důležitou věc. Framework totiž používá takové konstrukce a postupy, které lze pomocí opcode cache dobře akcelerovat a zároveň se vyhýbá všemu, co je z pohledu opcode cache překážkou. Příkladem spolupráce jsou šablony, které se kompilují do PHP skriptů a ukládají do dočasného adresáře jako soubory s příponou .php
. Naopak typickou překážkou je příkaz eval(), na kterém framework nestaví.
V souvislosti s výkonem musím zmínit, že Nette Framework na rychlosti hodně lpí (v nezávislém testu na mateřském serveru Root.cz byl vyhodnocen jako jeden z nejrychlejších frameworků vůbec) a proto za běhu kešuje i některé kritické interní struktury. Sem patří například správa hierarchie komponent.
Cache do vaší aplikace
Konečně se dostáváme k tomu, co je z pohledu programátora nejzajímavější. Tedy jaké nástroje dává framework k tomu, aby mohl do cache ukládat vlastní data. Platí zde totéž, co pro ostatní části frameworku. Na straně API je kladen důraz na přehlednost a jednoduchost syntaxe, na straně backendu se používají rozhraní (interfaces) a s nimi spojená možnost chování zcela přizpůsobit specifickým potřebám.
Pokud již víte, jak se pracuje se sessions, bude vám použití cache připadat důvěrně známé:
require 'Nette/loader.php';
// pokud používáte verzi pro PHP 5.3, odkomentujte následující řádek:
// use NetteEnvironment;
// získáme přístup do jmenného prostoru cache 'myData'
$cache = Environment::getCache('myData');
// zápis do cache; $value může obsahovat jakoukoliv strukturu
$cache[$key] = $value;
// čtení z cache
$cachedData = $cache[$key];
// mazání cache
unset($cache[$key]);
Cache mají na starost třídy a rozhraní ze jmenného prostoru NetteCaching
.
Ukažme si ještě kešovací obal nad časově náročnou funkcí:
// výpočetně náročná funkce
function veryExpensiveFunction($input)
{
return ...;
}
// obal, který výpočet kešuje
function cachedFunction($input)
{
$cache = Environment::getCache(__FUNCTION__);
if (isset($cache[$input])) {
return $cache[$input]; // buď vrátíme výsledek rovnou z cache
} else { // nebo jej spočítáme a uložíme do cache
return $cache[$input] = veryExpensiveFunction($input);
}
}
Metoda Environment::getCache()
vrací objekt NetteCachingCache
, který slouží k pohodlné manipulaci s daty, nicméně sám nikam nic neukládá. Kam se tedy data uloží? To záleží na konkrétním úložišti, tj. objektu implementujícím rozhraní NetteCachingICacheStorage
. Výchozím úložištěm je NetteCachingFileStorage
, které, jak je z názvu patrné, ukládá data do souborů. Výhoda rozdělení mezi Cache
a úložiště spočívá v tom, že když se nyní rozhodnete úložiště změnit (například nahradit diskové soubory za Memcached), není potřeba v kódu nic měnit.
NetteCachingFileStorage
U výchozího úložiště NetteCachingFileStorage
se ještě na chvíli zastavme. Jelikož výkonný Memcached byste na sdíleném hostingu hledali marně, budou soubory asi tím nejčastějším místem pro mezipaměť ve vašich aplikacích. Dobrou zprávou je, že toto úložiště je v Nette Framework velmi dobře optimalizované pro výkon. Ale především: zajišťuje plnou atomicitu operací.
Co to znamená? Že při použití cache se vám nemůže stát, že přečtete soubor, který ještě není (jiným vláknem) kompletně zapsaný, nebo že by vám jej někdo „pod rukama“ smazal. Použití cache je tedy zcela bezpečné.
Invalidace dat
S ukládáním dat do mezipaměti vznikají dva problémy. Jednak je tu pochopitelně hrozba, že se úložiště zcela zaplní a nebude možné další data zapisovat. A také se může stát, že některá dříve uložená data se stanou v průběhu času neplatná. Nette Framework proto nabízí mechanismus, jak omezit platnost dat nebo je řízeně mazat (v terminologii frameworku „invalidovat“).
Platnost dat je třeba nastavit už v okamžiku ukládání. Syntaxe přiřazení $cache[$key] = $value
k tomu prostor nenabízí, proto pro uložení použijeme metodu save
a platnost budeme specifikovat třetím parametrem:
// pokud používáte verzi pro PHP 5.3, odkomentujte následující řádek:
// use NetteCachingCache;
$cache->save($key, $value, array(
Cache::EXPIRE => '+ 20 minutes', // lze zadat v sekundách nebo jako UNIX timestamp
));
Z kódu je patrné, že hodnotu jsme uložili s platností 20 minut. Po uplynutí této doby bude cache hlásit, že pod klíčem $key
žádný záznam nemá. Pokud bychom chtěli obnovit dobu platnosti s každým přístupem (tj. čtením hodnoty), lze toho docílit takto:
$cache->save($key, $value, array(
Cache::EXPIRE => '+ 20 minutes',
Cache::SLIDING => TRUE,
));
Šikovná je možnost nechat data vyexpirovat v okamžiku, kdy byl změněn určitý soubor:
$cache->save($key, $value, array(
Cache::FILES => 'soubor.php', // lze uvést i pole více souborů
));
Kritérium Cache::FILES
je samozřejmě možné kombinovat i s časovou expirací Cache::EXPIRE
.
Velmi užitečným invalidačním nástrojem je tzv. tagování. Mějme třeba HTML stránku s článkem a komentáři, kterou uložíme do cache, abychom ji nemuseli pokaždé renderovat znovu.
$id = (string) $_GET['id'];
$cache = Environment::getCache();
if (isset($cache[$id])) {
// buď vykresli stránku z cache
echo $cache[$id];
} else {
// nebo ji vyrenderuj
ob_start();
...
... // kreslíme stránku
...
$cache->save($id, ob_get_flush(), array( // a ulož do cache
Cache::TAGS = array("article/$id", "comment/$id"),
));
}
Tedy HTML stránce článku s ID např. 10 jsme přiřadili tagy article/10
a comment/10
. Přesuňme se do administrace. Tady najdeme formulář pro editaci článku. Společně s uložením článku do databáze zavoláme příkaz:
// smažeme z cache položky s určitým tagem
Environment::getCache()->clean(array(
Cache::TAGS = array("article/$id"),
));
}
Stejně tak v místě přidání nového komentáře (nebo editace komentáře) neopomeneme invalidovat příslušný tag:
Environment::getCache()->clean(array(
Cache::TAGS = array("comment/$id"),
));
}
Čeho jsme tím dosáhli? Že se nám HTML (nebo i jiná) cache bude invalidovat sama. Kdykoliv uživatel změní článek s ID 10, dojde k vynucené invalidaci tagu article/10
a HTML stránka, která uvedený tag nese, se z cache smaže. Totéž nastane při vložení nového komentáře pod příslušný článek.
Pokračování příště
V příštím díle, posledním před prázdninami, si projdeme několik užitečných tříd, na které dosud nezbyl čas.
Autor článku je vývojář na volné noze, specializuje se na návrh a programování moderních webových aplikací. Vyvíjí open-source knihovny Texy, dibi a Nette Framework a pravidelně pořádá školení pro tvůrce webových aplikací, které od podzimu 2009 nabídne kurz vývoje AJAXových aplikací.
Šťoural obecný Mastodontus albensis má tentokrát dvě připomínky:
Kešovací obal nad časově náročnou funkcí jsem moc nepochopil, stačí
jedna funkce namísto dvou (kešování přímo v tělu dané funkce).
Tagování je prima věcička, dalo by se ovšem zabudovat přímo do storage
vrstvy a starat se jen o přiřazování tagů. Ale to je opravdu jen
drobnost :)
Spájanie do jednej funkcie nemusí byť vždy najlepším riešením. Čo ak
by si po čase chcel pridať ešte nejakú inú funkcionalitu pre prácu
s cache (šifrovanie, komprimovanie, …)? Myslím, že cache je dobrý adept
pre vzor Dekorátor
http://www.smallbulb.net/…-memoization
ad Mastodont: snažím se v článku nabádát k tomu, že spojení do
jedné funkce je obvykle návrhovou chybou. Proto ten příklad a proto zmínka,
že kešování je „další vrstva“.
ad memoization: Nette má pro něco takového integrovanou podporu, která
dojela na to, že na řadě hostingů beží eAccelerator s bugem, který tohle
znemožňuje :-( Velmi nepříjemná věc, takže se o použití anotací ani
nezmiňuju. Dokud nevznikne workaround, nebo inkriminovaná verze eAcceleratoru
nevymizí.
ktera posledni verze na to trpi?
Cache jako dekorátor je samozřejmě možná, ale psát extra wrapper pro
každou kešovanou funkci?
Jsem rád že se v nějakém frameworku objevila tagovaná cache – už
dlouho přemýšlím, že by se velmi hodila do Drupalu, jenže tam by
přidání tagů do cache vyžadovalo přepsání tak 90% jádra a modulů (aby
svůj kešovaný obsah správně tagovaly).
Ale zpět k tématu – Davide, přemýšlel jsi i nad tím, že by
klíčem do cache nebyl jen string, ale množina stringů (tagů)? Umožnilo by
to třeba cachovat obsah bloků které jsou závislé na uživateli, nějak
takhle:
I když, asi by to šlo řešit i nějakým wrapperem nad cache co by tu
array nějak jednoznačně kanonizoval do stringu (hmm, serialize()?)…
A jinak, těším se na lambdy v PHP 5.3, to wrapování funkcí mi přijde
jako děsná duplikace kódu:
V drupalu:
Pri ukladani dat:
cache_set(‚kafka:article:‘ . $node->nid,
$data);
Pri udalosti, kdy musim invalidovat cache:
cache_clear_all(‚kafka:article:‘ . $node->nid)
Pripadne, kdyz chci smazat vse s article:
cache_clear_all(‚kafka:article:‘, ‚cache‘, TRUE);
Jak se to efektivne lisi od tagu?
V těch array na konci by mělo být ⇒ místo =, ne?
Jde nějak cache vypnout? Nemůžu najít kde. Laděnka při chybách za běhu vyhazuje chyby v cache souborech, kde už jsou některé věci (Latte) přeložené, a nesedí číslo řádku. Většinou to není problém, ale občas přemýšlím, kde co se stalo – třeba když píšu víc kódu a testuji až pak.
Cache zkompilovaných šablon? Je možné ji smazat. Bývá uložena v tempu.
Čísla řádků trošku problém jsou, chce to trošku hledat podle okolí. Ono by to asi šlo udělat líp, shrnul jsem to zde: http://forum.nettephp.com/cs/3682-generovane-prekladane-zdrojaky-a-cisla-radku-typicky-sablony?pid=27015#p27015