cache – co ukládat a kdy invavalidovat

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip111
Člen | 244
+
0
-

Ahoj, začal jsem se zabývat trochu více cachováním v CMS a zarazil jsem se nad otázkou, co je rozumné cachovat. Cache stromové struktury menu – určitě ano, cache článků – nevím (1 query), cache ankety – nevím (2 query).
Jde mi o to, jestli je rozumné dávat do cache všechno – pokud by se na webu nezměnil obsah, web by se načítal bez jakýchkoliv SQL dotazů. Neříkám, že je to špatně, jen mi to přijde divný :)

Další otázka je invalidace cache. Například menu – invalidaci musím provést při změně v administraci. Na jak dlouho je rozumné nastavit životnost menu v cache? Mám dát na neurčito a pouze invalidovat v administraci nebo je dobré jen tak pro jistotu nastavit životnost cache třeba na 24h aby se občas menu znovu načetlo?

Stejně tak anketa – pokud ji dám do cache na neurčito, nutná invalidace by byla při hlasování a při změně v administraci. Stačí to nebo zase pro jistotu nastavit určitou životnost?

Tharos
Člen | 1030
+
0
-

Ahoj, má odpověď bude mít asi lehce filozofický přesah, ale snad pro Tebe bude alespoň trochu přínosná.

V první řadě – přístup, kdy si v aplikaci předem ukážeš, že tohle, tamto a ještě tohle budeš cachovat, není IMHO moc dobrý. Použití cache by mělo být nějak odůvodněné. Jde totiž o to, že i cache s sebou nese nějakou režii a paradoxně někdy může být například souborová cache pomalejší, než přímé získání dat z původního zdroje (přestože má nyní Nette výkonný žurnál). Jak jsi dospěl k tomu, že tu konkrétní část by bylo vhodné cachovat? A třeba u toho cachování výsledku jednoho SQL dotazu, jsi si jist, že už třeba náhodou není cachovaný? (Odkaz platí pro MySQL, ale query cache mají samozřejmě i jiné DBS.) A pokud ne, nebylo by lepší mít cache rovnou na úrovni databázového systému?

Takové logické „best practices“ v této věci je mít u každé cachované záležitosti možnost cache nějak jednoduše vypnout (ideálně na jednom místě někde v konfiguraci) a prostě experimentovat (a měřit). Ve svém CMSku jsem to měl udělané právě takhle. Vedle toho jsem měl i testovací data (odpovídající rozsahu běžné prezentace) a takový malý benchmark, který jsem měl možnost spustit v konkrétním provozním prostředí (je důležité, aby se tohle profilovalo na produkčním serveru, protože jinak mohou být měření irelevantní) a spouštěl jsem ho právě ze zapnutou a vypnutou cache a srovnával jsem (samozřejmě by to šlo celé úplně zautomatizovat). Na základě toho jsem pak u daných záležitostí cache zapnul či vypnul. Sám jsem byl překvapen, že některé věci vůbec nemělo smysl cachovat.

Ke druhé otázce, pokud je vše navrženo dobře, určitě není zapotřebí dělat, aby se cache jednou za den znovu sestavila. Nevidím v tom vůbec žádný přínos oproti přesné invalidaci v momentě, kdy je to skutečně zapotřebí.

Editoval Tharos (25. 9. 2011 22:02)

Filip111
Člen | 244
+
0
-

Úplně jsem zapomněl na cache samotné databáze. Myslel jsem, že cache v Nette je nesrovnatelně rychlejší byť i jen jediný dotaz do DB. Pokud je s ní ale režie srovnatelná s jednoduchým SQL dotazem, nemá pro tyto dotazy velký smysl. Až budu mít chvíli, zkusím si to nějak změřit.

Určitě dodělám globální vypnutí cache – uvažoval jsem spíš o jednorázovém výmazu, ale pro potřeby ladění výkonu bude užitečnější to vypnutí (příp. oboje).

Co se invalidace týče, budu věřit že sestavená cache může být platná třeba i za rok a budu invalidovat pouze při změně objektu.

Díky.

pYro
Člen | 29
+
0
-

Ja bych teda nejaky interval pro cache vzdy nastavil. Uz jen pro lepsi spanek :)
Pokud se jedna napriklad o generovani menu, tak proc ho napr. jednou denne nevygenerovat znovu? Jen pro jistotu, ze tam jsou vzdy aktualni data.

Ja osobne treba cachuju nastaveni ACL, ktere taham z dtb. a mam na nej nastaveny refresh jednou za hodinu(ano, magicka konstanta :)).

Tharos
Člen | 1030
+
0
-

Ono to, co je rychlejší – jestli vyhodnocení databázového dotazu, anebo parsování nějakého souboru na disku – se velmi těžko paušálně rozhoduje. Prví varianta obnáší provedení samotného dotazu a přenos výsledku, což vše stojí nějaký čas, druhá varianta obnáší diskové operace, parsování, hledání dat podle žurnálu, což pochopitelně také stojí nějaký čas. Nejlepší je to opravdu změřit.

V každém případě jednoduché dotazy, ve kterých se filtruje podle primárního klíče (či jiného indexu), databázové systémy vyhodnocují velmi rychle.

K tomu globálnímu vypnutí a řešení jednorázovým výmazem… Jakékoliv měření provedené jedním spuštěním je zavádějící. Chce to opravdu mít možnost spustit benchmark se třeba 1000 opakováními a výsledky zprůměrovat. Výborně se na to hodí třeba ab (Apache Benchmark).

Jinak co se globálního vypnutí týče, myslím, že udělat individuální aktivaci/deaktivaci není o tolik pracnější. K realizace cache se IMHO velmi hodí návrhový vzor dekorátor (není to můj objev, opravdu se pro cache často používá), kdy ve výsledku lze podle nějaké konfigurační direktivy rozhodovat, zda použít základní, anebo dekorovaný zdroj. Já to mám takhle udělané přímo na úrovni DI kontejneru – prostě když sestavuji službu, jejíž data je možné cachovat, zkontroluji nastavení v konfiguračním souboru, zda se skutečně cachovat mají, a podle toho službě předám standardní, anebo „odekorovaný“ zdroj. Funguje to výborně. Standardní a dekorovaný zdroj mají stejné API, takže služba samotná ve výsledku ani neví, jestli pracuje s cachovanými daty nebo normálními.

V konfiguračním souboru mám pak jednom jeden řádek s logickými hodnotami, zda danou část webu cachovat či nikoliv. Za sebe jsem zatím nenalezl hezčí řešení. :)

Editoval Tharos (19. 9. 2011 9:47)

Filip111
Člen | 244
+
0
-

@ pYro:

To je přesně to na co jsem se ptal – jestli je v Nette nutné jednou za „magickou konstantu“ vygenerovat cache znovu pro lepší spaní.

Data nikdo nezmění jinak než přes administraci, kdy dojde k invalidaci cache. Pokud to udělám ručně přes phpmyadmina, musim holt myslet na ruční výmaz cache na FTP nebo v administraci.

Na méně vytíženém webu/prezentaci tak klidně může být cache platná 5 let…proč ne. (resp. Proč by ne???…je to otázka na zkušenější :)

Pokud se nenajde rozumný důvod, proč nastavovat životnost cache na „krátký“ interval, nechám ji neomezeně. Už jen proto, že se mi nelíbí nepředvídatelnost SQL dotazů – při jednom reloadu vyprší cache tohohle objektu, při dalším reloadu pak zase jiného a každou chvíli se něco „náhodně“ načítá.

Editoval Filip111 (19. 9. 2011 9:55)

Filip111
Člen | 244
+
0
-

@ Tharos:

Jestli tomu dobře rozumim (zjednodušeně) – při nahrávání modelu si zjistíš, zda má platnou cache a podle toho vrátíš model kde jsou některé funkce přepsané tak, že vrací hodnoty z cache.
Pokud nemá platnou cache, vracíš standardní model, který při volání těchto funkcí sahá do databáze??

Já to zatím řeším tak, že si přímo v modelu zjistím zda je platná cache – pokud ano, vrátím výsledek z ní, pokud ne spustím sql dotaz.

Ot@s
Backer | 476
+
0
-

Souhlas s Tharos (většinou se oplatila důvěra v samotnou keš OS a keš RDBMS). V praxi bych aplikaci keše zvážil v případech složitějších HTML/PDF sestav.

Přidám ale ještě jeden pohled na věc. Já keš využívám takřka výhradně pouze po dobu běhu skriptu. MVC vás totiž nutí volat metody modelů (např.) $this->order->getInvoiceHeader(10) a občas se stane, že toto zavoláte během běhu skriptu více jak 1× (např. při použití dvou samostatných komponent). Strkání těchto hodnot do session a „globálních“ proměnných mě nepřijde košer, takže zde je protor pro Nette keš.

Filip Procházka
Moderator | 4668
+
0
-

Uvedu příklad, jak by to mohlo fungovat u dibi.

Vytvoříš si interface

interface IUsersRepository
{
	/**
	 * @param integer $id
	 * @return User
	 */
	function find($id);
}

Interface implementuješ

class UsersRepository extends Nette\Object implements IUsersRepository
{
	/** @var DibiConnection */
	private $db;

	public function __construct(\DibiConnection $db)
	{
		$this->db = $db;
	}

	/**
	 * @param integer $id
	 * @return User
	 */
	public function find($id)
	{
		$user = $this->db->fetch('SELECT * FROM users WHERE id =%i', $id);
		return $this->load($user);
	}

	private function load($data)
	{
		$user = new User;
		$user->username = $data['username'];
		return $user;
	}

}

Nyní nad tím postavíš kešovací dekorátor

class CachedUsersRepository extends Nette\Object implements IUsersRepository
{
	/** @var UsersRepository */
	private $repository;

	/** @var Nette\Caching\Cache */
	private $cache;

	public function __construct(UsersRepository $repository, Nette\Caching\Cache $cache)
	{
		$this->repository = $repository;
		$this->cache = $cache;
	}

	/**
	 * @param integer $id
	 * @return User
	 */
	public function find($id)
	{
		return $this->cache->call(callback($this->repository, 'find'), $id);
	}
}

A pak se budeš rozhodovat, jestli vytvořit jenom repozitář

return new UsersRepository($container->db);

Nebo ho budeš kešovat

$repository = new UsersRepository($container->db);
$cache = new Nette\Caching\Cache($storage, 'UsersRepository');
return new CachedUsersRepository($repository, $cache)

Což jde rozložit na jednotlivé služby a všechny zapsat do DI Containeru.

Editoval HosipLan (19. 9. 2011 13:07)

Filip111
Člen | 244
+
0
-

uff…díky za pěkný příklad, vypadá to, že mám zase večer o čem přemýšlet.

Myšlenky jsem pochopil, neříkám že do detailu – použití interface začíná konečně dávat smysl (zatím jsem neměl důvod ho používat).

OT: Pořád se divím, že mě Nette nutí seriózně programovat, navíc v PHP (anebo je to tou komunitou?) :)

Editoval Filip111 (19. 9. 2011 11:31)

Tharos
Člen | 1030
+
0
-

Abys měl ještě více o čem přemýšlet :), doplním k HosipLanovo implementaci (učebnicově čisté) ještě ukázku celkového použití i včetně toho řízení individuálního zapnutí/vypnutí cache.

Tohle můžeš mít ve vlastním konfigurátoru (který bude dědit z Nette\Configurator a kterým nahradíš v bootstrap.php ten výchozí):

public function createServiceUsers(Nette\DI\Container $cont)
{
	$repository = new UsersRepository($cont->db);

	if ($this->shouldWeUseCache($cont, 'users')) {
		$cache = new Nette\Caching\Cache($cont->cacheStorage, 'UsersRepository');
		$repository = new CachedUsersRepository($cache, $repository);
	}
	return new Users($repository);
}

private function shouldWeUseCache(Nette\DI\Container $cont, $serviceName)
{
	return
		isset($cont->cacheStorage) and
		array_key_exists($serviceName, $cont->params['cacheControl']) and
		$cont->params['cacheControl'][$serviceName] === true;
}

A řídit se to poté dá z config.neon následovně:

common:
	cacheControl: [webUser: true, ..., ...]

K tomuhle bych navíc doporučil… Nemít tu továrnu na službu přímo ve vlastním konfigurátoru, ale mít ji někde jinde. Úplně ideální je mít ji ve své vlastní třídě.

Btw. tohle je úplně obecný princip, ta cachovaná záležitost nemusí být žádný repositář, může to být úplně stejně i výsledek nějakého složitého výpočtu nebo tak…

Filip111
Člen | 244
+
0
-

Moc pěkný, díky.
Jen doufám, že večer dostanu k tomu to splácat dohromady a zprovoznit.