Jak funguje kešování

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

Kešování mají v Nette na starost třídy z jmenného prostoru Nette\Caching. Na příklady použití se můžete podívat v tests/Caching.

$cache = new Cache; // nebo $cache = Environment::getCache()

// zápis do cache:
$cache['data'] = $myData;

// čtení z cache
$cachedData = $cache['data'];

// mazání z cache
unset($cache['data']);
// nebo
$cache['data'] = NULL;

// ověření, zda je položka v keši
if (isset($cache['data'])) ...
// nebo
$cachedData = $cache['data'];
if ($cachedData !== NULL) ...

Do keše lze ukládat jakékoliv struktury, nemusí to být jen řetězec.

Takže taková jednoduchá implementace by mohla vypadat třeba takto:

if (isset($cache['template'])) {
    $template = $cache['template'];
} else {
    $template = sloziteGenerovani();
    $cache['template'] = $template;
}

echo $template;

Ale to není zdaleka vše. Při ukládání položek lze specifikovat několik dalších parametrů a podmínek pro invalidaci. Protože v případě přiřazení $cache['key'] = $data není kde tyto podmínky specifikovat, použijeme pro ukládání obsahu funkci $cache->save($key, $data, $options). Parametr $options je pole, které může mít tyto klíče:

expire => (int) čas, kdy obsah vyexpiruje
refresh => (bool) má se expirace prodlužovat?
files => (array) seznam souborů, na kterých cache závisí
items => (array) seznam klíčů v keši, na kterých tato položka závisí
tags => (array) seznam vlastních tagů
priority => (int) priorita
consts => (array) seznam konstant, na kterých cache závisí

Příklad použití:

Cache vyexpiruje za 10 min:

$cache->save($key, $value, array(
    'expire' => time() + 60 * 10,
));

Cache vyexpiruje za 10 min. Pokud v té době bude načtena, expirace se prodlouží na dalších deset minut.

$cache->save($key, $value, array(
    'expire' => time() + 60 * 10,
    'refresh' => TRUE,
));

Cache vyexpiruje v okamžiku, kdy se změní kterýkoliv z uvedených souborů. V praxi to používám třeba pro kešování konfigurace nebo šablon – jakmile se příslušný soubor změní, Nette automaticky invaliduje i keš.

$cache->save($key, $value, array(
    'files' => array('template.phtml', 'config.ini'),
));

Cache je závislá na jiných položkách v keši – vyexpiruje v okamžiku, kdy se změní položky s klíčem ‚key1‘ nebo ‚key2‘. To lze využít tehdy, když kešujeme třeba www stránku a pod jinými klíči její části. Jakmile se část změní, invaliduje se celá stránka.

$cache->save('key3', $value, array(
    'items' => array('key1', 'key2'),
));

Položce můžeme přiřadit seznam vlastní tagů. To lze použít třeba tehdy, pokud kešujeme www stránku, kde je článek + komentáře. Jakmile se článek změní, nebo jakmile se objeví nový komentář, necháme vyexpirovat všechny položky s příslušným tagem:

$cache->save($key, $value, array(
    'tags' => array('clanek#10', 'komentare#10'),
));

// vyexpirujeme všechny položky s tagem 'komentare#10':
$cache->clean(array(
    'tags' => array('komentare#10'),
));

Nakonec je tam položka priorita, podle které lze cache čistit:

$cache->save($key, $value, array(
    'priority' => 50,
));

// smažeme všechny položky s prioritou rovnou nebo menší 100:
$cache->clean(array(
    'priority' => 100,
));

Všechny parametry lze navzájem kombinovat.

před 11 lety

veena
Člen | 98
+
0
-

To vypadá hodně chytře až báječně ;-)

před 11 lety

veena
Člen | 98
+
0
-

Co kdyby ještě cache měla metodu, která přidá do cache jen pokud tam ještě položka neexistuje.
Tj, dělala by za nás toto:

if (isset($cache['data']))

Třeba $cache->add(...) nebo tak nějak.

Editoval veena (29. 4. 2008 11:03)

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

veena napsal(a):

Co kdyby ještě cache měla metodu, která přidá do cache jen pokud tam ještě položka neexistuje.

Metoda save() má nyní třetí parameter $rewrite s výchozí hodnout TRUE.

Ale stejně – k čemu je to užitečné? ;)

před 11 lety

veena
Člen | 98
+
0
-

David Grudl napsal(a):

Metoda save() má nyní třetí parameter $rewrite s výchozí hodnout TRUE.

Ale stejně – k čemu je to užitečné? ;)

To je fakt, že takhle to k ničemu není.

Leda, že by existovala metoda, která by místo hodnoty pro keš brala callback na funkci, která by se v případě prázdné cache zavolala a hodnotu vrátila.

před 11 lety

llook
Člen | 412
+
0
-

Tohle vypadá dobře, další věc z Nette, kterou budu používat. Mám pár poznámek:

  1. Konstruktor potřebuje instanci úložiště. Například:
    $cache = new Cache(new FileStorage(dirname(__FILE__) . "/cache/"));
  2. Parametr konstruktoru FileStorage, adresář pro cache soubory, musí být včetně lomítka na konci.

    Pokud ho neuvedete, nebo uvedete cestu, do které nelze zapisovat, tak se nic hrozného neděje – jenom se prostě keš nebude ukládat a pokaždé se vám bude všechno přegenerovávat.

  3. Sice moc nechápu, jak je toho docíleno, ale zdá se, že úložiště FileStorage je thread safe.

před 11 lety

David Grudl
Nette Core | 6871
+
0
-
  1. Konstruktor potřebuje instanci úložiště. Například:
    $cache = new Cache(new FileStorage(dirname(__FILE__) . "/cache/"));

Vhodné je instanci úložiště uložit jako službu, viz implementace Environment::getCache()

  1. Parametr konstruktoru FileStorage, adresář pro cache soubory, musí být včetně lomítka na konci.

Je to tak. Nejde totiž o adresář, ale o prefix cesty. Lze použít třeba hodnotu dirname(__FILE__) . "/cache/tmp-".

  1. Sice moc nechápu, jak je toho docíleno, ale zdá se, že úložiště FileStorage je thread safe.

Ano, cache je thread safe.

před 11 lety

A.
Člen | 87
+
0
-

Chtelo by to jeste pridat primo do cache moznost vypnuti kesovani, napr. dobre pro development prostredi, kde je cache spise prekazkou.

Nemuselo by se to pak vypinat vsude mozne, kde je pouzito nejaky $useCache ci neco v tom smyslu.

před 11 lety

pmg
Člen | 372
+
0
-

3. Sice moc nechápu, jak je toho docíleno, ale zdá se, že úložiště FileStorage je thread safe.

Nejen že je thread safe, ale dokonce si také porodí při výpadku serveru během zápisu souboru. Napřed si totiž zamyká metasoubor, do kterého se pak údaje zapíšou, až když se v pořádku uloží data. Pokud se stane, že je meta nekompletní, hlavičku se nepodaří unserializovat a položka se považuje za neexistující. Říkám to dobře?

Editoval pmg (24. 5. 2008 19:49)

před 11 lety

pmg
Člen | 372
+
0
-

Návrh na zrychlení. Aby nebylo nutné pracovat se dvěma soubory pro jeden záznam keše, šla by metadata v serializované podobě zapsat na první řádek datového souboru. Jen by se přidal záznam o délce zbylých dat, podle kterého by se to při čtení zkontrolovalo. Alternativou jsou hlavičky ve stylu HTTP, které jsou pak čitelné i pro člověka.

Je to ale nepoužitelné, pokud jsou v hlavičce víceřádková data.

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

V případě TemplateStorage, což je rozšíření FileStorage, se využívá toho, že datový soubor je samostatný a neserializovaný (jde-li o řetězec) a lze jej pak „zpracovat“ pomocí require_once. Samozřejmě bylo by možné require nahradit za eval(), ale z hlediska vypisování chyb v šabloně a nejspíš i použití nějaké php opcode cache je samostatný soubor výhodnější. Spojení meta + data by mělo dopad i na závislost mezi položkami cache, ale to se dá řešit i jinak.

Na druhou stranu není úplně ideální současná závislost TemplateStorage na FileStorage. Pokud bych chtěl pro kešování šablon použít třeba MemcachedStorage (což je řádově výkonnější, ale málo dostupné na hostingu), výhoda samostatného souboru stejně zmizí.

Ale můžeme to zkusit. Udělal jsem experimentální implementaci https://files.nette.org/FileStorage.zip – Můžeš udělat nějaké benchmark testy, jak moc se liší jedno- a dvousouborová verze?

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

A. napsal(a):

Chtelo by to jeste pridat primo do cache moznost vypnuti kesovani, napr. dobre pro development prostredi, kde je cache spise prekazkou.

Nemuselo by se to pak vypinat vsude mozne, kde je pouzito nejaky $useCache ci neco v tom smyslu.

To je dobrý nápad.

před 11 lety

pmg
Člen | 372
+
0
-

Benchmark jsem poslal emailem. Byla to fuška skript odladit, aby to házelo požadované výsledky;-) Můžeš to když tak dát někam na web?

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

Pmg mi poslal benchmark (díky!) a z něj vyplývá, že jednosouborová verze ušetří něco přes 30 % času.

Napadla mě taková fitna, jak jednosouborovou verzi includovat do šablon, takže jejímu použití nic nebrání.

před 11 lety

pmg
Člen | 372
+
0
-

Napadla mě taková fitna, jak jednosouborovou verzi includovat do šablon, takže jejímu použití nic nebrání.

Tak to se těším! Nenapadá mě jiný způsob, než hlavičku zakomentovat. Nějak takhle:

<?php/* metadata */?>
Obsah souboru

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

pmg napsal(a):

Napadla mě taková fitna, jak jednosouborovou verzi includovat do šablon, takže jejímu použití nic nebrání.

Tak to se těším! Nenapadá mě jiný způsob, než hlavičku zakomentovat. Nějak takhle:

<?php/* metadata */?>
Obsah souboru

Přesně tak to funguje :-)

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

A. napsal(a):

Chtelo by to jeste pridat primo do cache moznost vypnuti kesovani, napr. dobre pro development prostredi, kde je cache spise prekazkou.

Vyřešil jsem to přes DummyStorage, což je úložiště, které ve skutečnosti nic nedělá. Pokusím se to na základě detekce prostředí zakomponovat do Environment.

před 11 lety

pmg
Člen | 372
+
0
-

Většinou se keš invaliduje při každé úpravě dat. Myslím, že kromě podmínky expire by se v praxi uplatnila minAge, která by se použila v případě častých úprav dat. Keš by se nemusela invalidovat zbytečně ani příliš často.

Ještě k jednosouborovému řešení keše. Někdy by se kromě přímého zpracování pomocí require_once mohlo hodit použít třeba parse_ini_file, dále z hlediska zachování atomicity operací by přímý přístup k souboru neměl být umožněn. Co tedy udělat takovou nástavbu pro Storages, pomocí které by šlo k souboru přistoupit pomocí vlastního streamového protokolu? Při požadavku na jméno souboru by se registroval potřebný protokol a vrátilo se třeba FileStorage://file.phtml.

Editoval pmg (29. 5. 2008 6:57)

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

pmg napsal(a):

Většinou se keš invaliduje při každé úpravě dat. Myslím, že kromě podmínky expire by se v praxi uplatnila minAge, která by se použila v případě častých úprav dat. Keš by se nemusela invalidovat zbytečně ani příliš často.

Jak by se mělo lišit expire a minAge?

Ještě k jednosouborovému řešení keše. Někdy by se kromě přímého zpracování pomocí require_once mohlo hodit použít třeba parse_ini_file, dále z hlediska zachování atomicity operací by přímý přístup k souboru neměl být umožněn. Co tedy udělat takovou nástavbu pro Storages, pomocí které by šlo k souboru přistoupit pomocí vlastního streamového protokolu? Při požadavku na jméno souboru by se registroval potřebný protokol a vrátilo se třeba FileStorage://file.phtml.

To je pěkný nápad, ovládat cache přes stream! Ale s implementací bych počkal, až to bude skutečně potřeba. V případě kešování šablon a parsování přes require je výhoda v tom, že když se v nich objeví chyba, IDE soubor přímo zobrazí (případně ho lze i krokovat). Tohle by u streamů tuším nefungovalo.

před 11 lety

pmg
Člen | 372
+
0
-

Jak by se mělo lišit expire a minAge?

Před uplynutím doby minAge (nebo jak ji nazvat) by další podmínky mohly jen nastavit příznak, že je keš neaktuální. Pokud stránku po uplynutí minimální platnosti nebude na základě příznaku nutné invalidovat, zůstane se u staré verze. Je to podobné, jako když prohlížeč po dovolenou dobu načítá obrázek z keše a po skončení platnosti pošle jen If-modified-since, místo aby data znovu přenášel.

V případě kešování šablon a parsování přes require je výhoda v tom, že když se v nich objeví chyba, IDE soubor přímo zobrazí (případně ho lze i krokovat). Tohle by u streamů tuším nefungovalo.

Pravda. Ale nemohlo by tím IDE být v tomto případě Nette, které by stream přečetlo?

Ale s implementací bych počkal, až to bude skutečně potřeba.

Jsem rád, že předchozí řešení nepřijde nazmar. Ty streamy to určitě zpomalí, takže je to (zatím) asi zbytečné.

Edit: Pokud se do souboru ukládá jen délka hlavičky, nemohlo by se stát, že obsah zůstane v bufferu a data se nepovede zapsat celá? Řešením by pak bylo fflush.

Editoval pmg (30. 5. 2008 6:26)

před 11 lety

v6ak
Člen | 206
+
0
-

To vypadá dobře. Místo minAge je IMHO skutečně lepší něco jako if-modifed-since.
Líbilo by se mi něco na způsob tohoto:

<?php
echo $cache->get(array('articles', $ar->getId()), $ar->getLastModifed(), array(new p_View_articles__generator($ar), 'getHtml'));
?>

Tedy:

  • vícerozměrné cachovací úložiště (není tak důležité)
  • možnost ‚getOrGen‘

před 11 lety

pmg
Člen | 372
+
0
-

možnost ‚getOrGen‘

V případě keše se vlastně takový přístup používá vždy, ale možná je zbytečné dělat třídu, která jen předá řízení callbacku pro generování.

Místo minAge je IMHO skutečně lepší něco jako if-modifed-since.

To přirovnání k HTTP se týkalo jen faktu, že náročná akce (přegenerování souboru v případě keše, přenos souboru v případě HTTP) se nevykoná ani po uplynutí dané doby, pokud to není potřeba. Obdobná hlavička v HTTP není, možná by se to dalo považovat za opak Max-age.

Stejně tak by se mohla hodit závislost NotAccessedWithin, která by invalidovala soubory, ke kterým se stanovenou dobu nepřistupovalo.

Doufám, že to také berete jen jako takovou diskusi, to použití je spíš okrajové;-)

Editoval pmg (2. 6. 2008 16:09)

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

Stále mi to asi nedochází. Respektive nerozumím, co nového by mohly další podmínky přinést a nešlo by to definovat pomocí

  • expire ⇒ (int) čas, kdy obsah vyexpiruje
  • refresh ⇒ (bool) má se expirace prodlužovat?
  • files ⇒ (array) seznam souborů, na kterých cache závisí
  • items ⇒ (array) seznam klíčů v keši, na kterých tato položka závisí
  • tags ⇒ (array) seznam vlastních tagů
  • priority ⇒ (int) priorita

Např.:

  • náročná akce se nevykoná ani po uplynutí dané doby = nenastavím expire
  • NotAccessedWithin, která by invalidovala soubory, ke kterým se stanovenou dobu nepřistupovalo = nastavím expire + refresh

před 11 lety

pmg
Člen | 372
+
0
-

NotAccessedWithin, která by invalidovala soubory, ke kterým se stanovenou dobu nepřistupovalo = nastavím expire + refresh

Promiňte, už jsem myslel, že to stejně nikdo nečte, tak jsem se nenamáhal to nějak popisovat. Měl jsem o této podmínce představu jako o garbage collectoru, takže jsem ji nehledal ve funkci read, ale ve funkci clean (kde jsem nic podobného nenašel).

Jako příklad bych uvedl situaci, kdy potřebujeme kešovat nějaká data vázaná k aktuálnímu přihlášení uživatele. Přihlášení vyprší po 15 minutách neaktivity a staré záznamy se smažou z databáze vždy hromadně před ověřením platnosti nějakého přihlášení. V keši proto budou zůstávat uložené informace. Metoda clean by potom zjistila, že přihlášení už stejně není aktivní a může data smazat. Dokud by k nim ale někdo přistupoval, není potřeba čas předchozího přístupu ověřovat (funkce read).

náročná akce se nevykoná ani po uplynutí dané doby = nenastavím expire

Chtěl bych kešovat data, která se mohou často měnit, ale nechci výslednou stránku sestavovat při každé úpravě: mohu si dovolit posílat pět minut starou verzi, i když už v databázi existuje nová.

Když nenastavím expire a budu keš generovat při každé úpravě dat, může se keš generovat častěji než každých pět minut, což nechci. Když nastavím expire, keš se po pěti minutách přegeneruje, což nechci, pokud se nezměnily údaje v databázi.

Takže aby se keš generovala, musí se splnit dvě podmínky: uplyne její minimální lifetime pět minut a v této době se změní údaje v databázi (označí se příznakem).

před 11 lety

David Grudl
Nette Core | 6871
+
0
-

Nejúčinnějším mechanismem invalidování položek v cache jsou asi tagy. Třeba při odhlášení uživatele je možno invalidovat 'user/456', kde 456 je identifikátor uživatele, při editaci článku je možné invalidovat post/123, kde 123 je zase id článku.

Podstatné je, že modul, který cache invaliduje, nemusí mít nic společného s modulem, který kešuje HTML stránku, nebo modulem, který zpracovává (a kešuje) RSS výstup. Tedy kromě formátu tagu.

To je základní princip kešování v Nette a do něj se musí každé rozšíření vejít.

Jestli tedy dobře chápu minAge, tak by to mělo fungovat tak, že při invalidaci prvku zkrze clean() a například tag by se nesmazal hned, ale až po uplynutí nějaké doby. Technicky vzato, nastavila by se expire bez refresh. To je řešitelné a dokonce poměrně snadno implementovatelné.

Jen místo minAge bych navrhoval asi vhodnější název timeout?

před 11 lety

pmg
Člen | 372
+
0
-

Tagy se dají dobře použít, když se uživatel odhlásí ručně. Když ale nebudu chtít zjišťovat všechna neaktuální přihlášení v databázi před jejich smazáním (náročnější operace při každém ověřování přihlášení), abych mohl vymazat položky s patřičným tagem, musím občas zavolat metodu clean a řešit to na základě stáří keše.

Já jsem o tom příkladu mluvil jako o okrajovém, v praxi by asi stačilo nastavit souboru dostatečně dlouhou dobu expire, takže by ho clean stejně smazala, možná jen později.

Taky fungovalo, kdyby šel přečíst seznam všech položek a pak jednotlivě zkontrolovat tagy (invalidační modul).

Jako expire bez refresh by se to dalo pojmout, pak by se ale patrně musela přepsat hlavička a při rozdílné délce i navazující data. Uvažoval jsem proto spíš o nějakém 1B příznaku, který by se dal upravit, aniž by se musely přepsat okolní údaje. Pokud by na smazání souboru mělo zálusk více procesů, další by si všimly, že už je soubor jako neaktuální označen a nemusely nic kontrolovat.

Jen místo minAge bych navrhoval asi vhodnější název timeout?

Samozřejmě, že mě to netrklo dřív. To je ten správný název! Do konkrétní (ne)implementace ale mluvit nechci, protože tomu nerozumím. Doufám, že se závislost uplatní v praxi, aby nezůstala jen na okrasu.

Ještě taková otázka: jak zapisujete víceslovné textové identifikátory (třeba právě klíče pole)? Možná je dobré brát to jako case insensitive a oddělit slova podtržítkem. Někdo to ale zapisuje dohromady.

před 11 lety

v6ak
Člen | 206
+
0
-

David Grudl napsal(a):

Respektive nerozumím, co nového by mohly další podmínky přinést a nešlo by to definovat pomocí …

A co if-modifed-since?


K mému návrhu vícerozměrných klíčů cache: asi je to zbytečnost, lepší by bylo mít spíš více cachí. // Omlouvám se za ten patvar ‚cachí‘, nic lepšího mě nenapadá

před 11 lety

piler
Člen | 112
+
0
-

Snazim sa do mojho projektu implementovat Triedu Nette\Cache a narazil som na problem, ze sa nevytvoria subory, ktore vytvara cache. Uz len pri

<?php
$cache = new Cache(new FileStorage(TMP_CACHE_DIR . '/'), 'cache-cms');

if (isSet($cache['name'])) {
...
}
?>

to vyhodi chybu:

fopen(/.../tmp/cache/cache-cms%00name) [<a href='function.fopen'>function.fopen</a>]: failed to open stream: No such file or directory

Pri zapisani hodnoty do cache to vyhodi tu istu chybu. Prava na zapis do adresara by mali byt v poriadku.

Editoval piler (16. 1. 2009 18:09)

před 11 lety

phx
Člen | 652
+
0
-

Proc tam mas 3 tecky v ceste? Rekl bych, ze chyba je spise, ze adresar neexistuje. Neni to ze nejsou prava. To je chyba kapku jinka. Pokud se nepletu.

před 11 lety

kravčo
Člen | 723
+
0
-

piler napsal(a):

Snazim sa do mojho projektu implementovat Triedu Nette\Cache a narazil som na problem, ze sa nevytvoria subory, ktore vytvara cache. Uz len pri

<?php
$cache = new Cache(new FileStorage(TMP_CACHE_DIR . '/'), 'cache-cms');

if (isSet($cache['name'])) {
...
}
?>

to vyhodi chybu:

fopen(/.../tmp/cache/name) [<a href='function.fopen'>function.fopen</a>]: failed to open stream: No such file or directory

Pri zapisani hodnoty do cache to vyhodi tu istu chybu. Prava na zapis do adresara by mali byt v poriadku.

Práva na zápis sa kontrolujú už v konštruktore, takže problém bude asi inde. Z toho čo si napísal ti viac pomôcť neviem…

Akurát je zvláštne, že hľadá /.../tmp/cache/name, totiž, keďže si nastavoval namespace, mal by skôr hľadať /.../tmp/cache/cache-cms%00name

Editoval kravco (16. 1. 2009 17:34)

před 11 lety

piler
Člen | 112
+
0
-

kravco napsal(a):

Akurát je zvláštne, že hľadá /.../tmp/cache/name, totiž, keďže si nastavoval namespace, mal by skôr hľadať /.../tmp/cache/cache-cms%00name

Som tam spravil chybu, uz som to editol. Tie /…/ to som len nahradil za tu celu cestu co tam je… no vsak prave, ze prava sa tam kontroluju v konstruktore ale ten subor sa mi nepodari vytvorit.

Chybu vyhodi na 340 riadku Nette\Caching\FileStorage
@version $Id: FileStorage.php 182 2008-12-31 00:28:33Z david@grudl.com $

<?php
$handle = @fopen($file, 'r+b'); // intentionally @
?>

Nenapada mi fakt, kde je problem :(

před 11 lety

kravčo
Člen | 723
+
0
-

piler napsal(a):

kravco napsal(a):

Akurát je zvláštne, že hľadá /.../tmp/cache/name, totiž, keďže si nastavoval namespace, mal by skôr hľadať /.../tmp/cache/cache-cms%00name

Som tam spravil chybu, uz som to editol. Tie /…/ to som len nahradil za tu celu cestu co tam je… no vsak prave, ze prava sa tam kontroluju v konstruktore ale ten subor sa mi nepodari vytvorit.

Chybu vyhodi na 340 riadku Nette\Caching\FileStorage
@version $Id: FileStorage.php 182 2008-12-31 00:28:33Z david@grudl.com $

<?php
$handle = @fopen($file, 'r+b'); // intentionally @
?>

Nenapada mi fakt, kde je problem :(

Určite ten riadok? Zavináč by mal vypisovanie chýb potlačiť…

Najlepšie to bude asi debugnut… prípadne napíš viac: hodnotu TMP_CACHE_DIR, config a pod. aby to šlo nasimulovať. (kľudne aj na mail)