Přejít k navigační liště

Zdroják » PHP » Nette Framework: Cache

Nette Framework: Cache

Články PHP, Různé

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

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 NetteCachingFi­leStorage, 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.

NetteCachingFi­leStorage

U výchozího úložiště NetteCachingFi­leStorage 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í.

Komentáře

Subscribe
Upozornit na
guest
11 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Mastodont

Šť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 :)

plistiak

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

hasan
michal

ktera posledni verze na to trpi?

Mastodont

Cache jako dekorátor je samozřejmě možná, ale psát extra wrapper pro
každou kešovanou funkci?

keff

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:

$cache->put(
        array(
                'module' => 'MyModule',
                'blockId' => $blockId,
                'user-id' => $currentUserId
        ),
        $cachedContent,
        $cacheTimeout
        );

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:

function GetArticles($from, $to) {

        $cache->get(
                'myKey',
                function ($from, $to) {
                        ...
                        return $articles;
                }
        );
}
JD

V drupalu:

Pri ukladani dat: cache_set(‚kaf­ka:article:‘ . $node->nid,
$data); 

Pri udalosti, kdy musim invalidovat cache:
cache_clear_a­ll(‚kafka:arti­cle:‘ . $node->nid) 

Pripadne, kdyz chci smazat vse s article:
cache_clear_a­ll(‚kafka:arti­cle:‘, ‚cache‘, TRUE); 

Jak se to efektivne lisi od tagu?

blizzy

V těch array na konci by mělo být ⇒ místo =, ne?

Karl-von-bahnhof

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.

v6ak

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

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.