Kešování HTML výstupu

před 11 lety

David Grudl
Nette Core | 6849
+
0
-

Aneb jak ušetřit trafic serveru, uživatelům a snížit vytížení serveru.

Cache na straně klienta

Nejprve si nastudujte výborný Kešovací návod pro autory webu a webmastery.

Jak vidíte, kešování na straně klienta je závislé především na HTTP hlavičkách Cache-Control, Expires, Last-Modified a ETag. Nette má zabudovanou podporu pro tyto hlavičky. Stačí zavolat metodu $presenter->lastModified($modified, [$etag, [, $expire]]), kde $modified nese čas poslední změny stránky (jako UNIX timestamp) a $etag libovolný validační řetězec. Metoda jednak nastaví hlavičky Last-Modified nebo ETag, ale především zkontroluje, zda se klient neptá hlavičkou If-Modified-Since, jestli se stránka od určitého času nezměnila, nebo nekontroluje hlavičkou If-None-Match hodnotu validačního identifikátoru. Pokud se nezměnila, je presenter ukončen metodou terminate(). Ušetří se čas generování stránky a přenesený objem dat na straně serveru i klienta.

Volitelný třetí parametr $expire nastaví HTTP hlavičky Cache-Control a Expires (hodnotou expire je buď UNIX timestamp, nebo počet sekund od „včil“, nebo FALSE, což znamená: nekešuj!).

Metoda lastModified pracuje jen na produkčním serveru, tedy když Environment::isProduction() vrátí FALSE. To lze ovlivnit přes Environment::setMode('production', TRUE).

Cache na straně serveru

Na straně serveru máme daleko víc možností, co a jak kešovat. Můžeme kešovat výstup jednotlivých komponent, dílčí procesy, nebo celé HTML stránky. Jako doporučenou literaturu bych uvedl zdejší návod na Nette\Caching.

Jak kešovat třeba HTML výstup z celého presenteru? Přepíšeme si například metodu startup() presenteru, aby zjistila, jestli stránka už není v cache a pokud ne, zapne output buffering:

private $cacheKey;

/**
 * @return void
 */
protected function startup()
{
    // vytvoříme cache ve jmenném prostoru 'application/output'
    // (jméno prostoru je libovolný řetezec)
    $cache = Environment::getCache('application/output');

    // klíčem bude třeba jméno presenteru a view + obsah parametru id
    $key = $this->getName() . ':' . $this->getAction() . '#' . $this->params['id'];
    // ověření, zda je položka v keši
    if (isset($cache[$key])) {
        echo $cache[$key]; // vypsat a finíto
        $this->terminate();
    } else {
        ob_start();
        $this->cacheKey = $key;
    }
}



/**
 * @param  Exception  optional catched exception
 * @return void
 */
protected function shutdown($exception)
{
    if ($this->cacheKey) {
        $content = ob_get_clean();
        if ($content) {
            echo $content;
            $cache = Environment::getCache('application/output');
            $cache->save($this->cacheKey, $content, array(
                'expire' => 300, // 300 s = 5 minut
            ));
        }
    }
}

Při ukládání do keše metodou save() můžeme nastavit další parametry, viz zmíněný návod Jak funguje kešování v Nette. Šikovné jsou zejména tagy. Například stránky blogu můžeme do cache uložit s dlouhou (cca nekonečnou) expirací a otagovat je tagem např. ‚item/123‘, kde 123 je ID článku. Když pak někdo napíše na blog komentář, nebo když uživatel zedituje článek, tak se invaliduje příslušný tag ‚item/xyz‘ a stránka se smaže s cache.

před 11 lety

veena
Člen | 98
+
0
-

Díky Davide za návod.

To kešování výstupu na straně serveru by mohl zjednodušeně vykonávat nějaký decorator, což?

před 11 lety

veena
Člen | 98
+
0
-

Furt se to snažím pochopit a moc mi to nejde.

Kešováním pomocí: $presenter->lastModified($modified[, $etag, [, $expire]])

bych chtěl dosáhnout toho, aby když uživatel přijde na stránku, tak se pro něho zakešovala na xx sekund, asi že?

Tzn expires bych nastavil na těch xx sekund, ale na co mám nastavit $modified? Na now?

Neni mi totiž jasná tahle část:

Metoda jednak nastaví hlavičku Last-Modified, ale především zkontroluje, zda se klient neptá hlavičkou If-Modified-Since, jestli se stránka od určitého času nezměnila … Pokud se nezměnila, je presenter ukončen metodou terminate(). Ušetří se čas generování stránky a přenesený objem dat na straně serveru i klienta.

Konkrétně se píše „jestli se stránka od určitého času nezměnila“ Od jakého času? Od toho co byl prohlížeči nastaven v hlavičce last modified?

Tu cache na straně serveru bych použil při nakechování celé stránky třeba po pěti minutách pro všechny uživatele, to chápu.

Editoval veena (14. 8. 2008 9:34)

před 11 lety

phx
Člen | 652
+
0
-

Vtip je v tom, ze prohlizec aby nemusel tahat celou stranku posle info „mam doma v cache stranku z …“ A server but rekne ta je OK a nice se neprenasi. A nebo nee tady mas novou.

Aby toto fungovalo musi to podporovat jak prolizec tak i webapp.

Na uzivateli je tedy aby spravne rekl metode $presenter->lastModified() kdy byla stranka naposled zmenena. Dle toho se vyhodnoti co jsem jiz psal.

před 11 lety

Panda
Člen | 570
+
0
-

Jinými slovy: $modified by se měl nastavit na čas modifikace nejnovějšího prvku na stránce.

Pokud mám tedy na stránce třeba prvky: článek, blok s novinkami a anketu, článek byl naposledy modifikován včera, do bloku s novinkami přišla poslední zpráva před půl hodinou a počet hlasů v anketě se změnil před minutou, tak to nastavím na čas modifikace (tzn. posledního hlasu) té ankety. Pokud bych pak upravil článek, ale nevytvořil bych žádnou novinku a nikdo by mi nezahlasoval do ankety, tak to musím nastavit na datum modifikace článku.

Konkrétní řešení by vypadalo třeba definicí nového rozhraní (a k tomu rovnou třeba i abstraktní komponentu, která by část práce udělala za programátora), které by nakazovalo implementovat metodu třeba getLastModificationTime. Ta by vracela čas poslední modifikace. Všechny prvky na stránce, které by se mohly změnit, by implementovaly toto rozhraní/dědily od komponenty. Ve vhodný okamžik (tzn. v moment, kdy už všechny komponenty budou znát datum své poslední modifikce – záleží na konkrétní aplikaci, pravděpodobně by to však mělo být před renderováním, nebo na konci metody render{view}()) by se prošly a našel by se čas poslední modifikace. Pokud by stránka s nějakou komponentou raději neměla být kešována, tak stačí, aby její LastModified() vždy vracelo now.

před 11 lety

David Grudl
Nette Core | 6849
+
0
-

V revizi č. 55 jsem implementoval podporu pro ETag, jakožto silný validační identifikátor. (čas poslední změny se obvykle považuje za slabý identifikátor, protože není schopen podchytit změny v rámci 1 sekundy). ETag je vlastně libovolný řetězec, obvykle se generuje jako MD5() nějakého klíče.

BC break: změnilo se pořadí parameterů metody lastModified(), úvodní post jsem aktualizoval.

před 11 lety

veena
Člen | 98
+
0
-

Panda napsal(a):

Pokud by stránka s nějakou komponentou raději neměla být kešována, tak stačí, aby její LastModified() vždy vracelo now.

Co se stane, když sem nastavil LastModified(time()-120) ?

Zakešuje mi to po prvním přístupu na 2 minuty v prohlížeči?

Mě to mozek prostě nějak furt nebere :-(

Editoval veena (21. 8. 2008 23:18)

před 11 lety

David Grudl
Nette Core | 6849
+
0
-

Hele, četl jsi pozorně ten Kešovací návod pro autory webu a webmastery? Ať tu neřešíme něco, co je lépe popsané jinde.

před 11 lety

romansklenar
Člen | 657
+
0
-

David Grudl napsal(a):

Metoda lastModified pracuje jen na produkčním serveru, tedy když Environment::isProduction() vrátí FALSE. To lze ovlivnit přes Environment::setMode('production', TRUE).

Sice to je takhle napsáno i v dokumentaci, ale není to překlep? Já měl za to, že na produkčním serveru by mělo Environment::isProduction() být TRUE

FALSE by vrátilo jen na localhostu, pokud nastavím prostředí PRODUCTION a dojde na detekci módu

Environment::setName(Environment::PRODUCTION);
Environment::isProduction(); // FALSE

protože mód production se určuje podle adresy serveru.

Proto radši používám takovéto nastavení v bootstrapu

// Environment::setName(Environment::DEVELOPMENT);
Environment::setName(Environment::PRODUCTION);

$config = Environment::loadConfig();

Environment::setMode('production', Environment::getName() == Environment::PRODUCTION  ? TRUE : FALSE);
Environment::setMode('debug', Environment::getName() == Environment::DEVELOPMENT ? TRUE : FALSE);

Debug::enable(NULL, NULL, $emailHeaders);
// parametr logování chyb je NULL, takže se rozhodne podle autodetekce z Environment podle řežimu production, tzn: Environment::isProduction() ? 'logovat' : 'zobrazovat'

if (Environment::getName() == Environment::DEVELOPMENT) { Debug::enableProfiler(); }

// a možná se bude hodit i automatické zachytávání výjimek
$application = Environment::getApplication();
$application->catchExceptions = Environment::getName() == Environment::PRODUCTION ? TRUE : FALSE;

// nebo klidně takto, protože Environment::isProduction() již vrací TRUE
$application->catchExceptions = Environment::isProduction() ? TRUE : FALSE;

kde výběr prostředí a tím i chování aplikace (zachytávání/zobrazování výjimek a logování) provádím jen zakomentováním jednoho ze dvou řádků pro nastavení jména prostředí, vše ostatní se už pak odvíjí od toho, takže pokud teď chci na localhostu nasimulovat chování v produkčním prostředí, správně se mi nastvaví i mód production.

Pokud to uznáte za přínosné, může se to doplnit jako tip do dokumentace.

před 11 lety

David Grudl
Nette Core | 6849
+
0
-

romansklenar napsal(a):

Sice to je takhle napsáno i v dokumentaci, ale není to překlep?

jasně, je to překlep.

Proto radši používám takovéto nastavení v bootstrapu

Environment::setMode('production', Environment::getName() == Environment::PRODUCTION  ? TRUE : FALSE);
Environment::setMode('debug', Environment::getName() == Environment::DEVELOPMENT ? TRUE : FALSE);

Můžeš si pro každé prostředí dát do config.ini

[production]
mode.production = yes

[development]
mode.production = false

a mód se ti bude nastavovat už při Environment::loadConfig().

if (Environment::getName() == Environment::DEVELOPMENT) { Debug::enableProfiler(); }

To bych nedělal. To bude fungovat jen v případě, že používáš právě tyto dvě prostředí. Jakmile budeš potřebovat více prostředí, narazíš na problém.

před 11 lety

romansklenar
Člen | 657
+
0
-

David Grudl napsal(a):

Můžeš si pro každé prostředí dát do config.ini

[production]
mode.production = yes

[development]
mode.production = false

a mód se ti bude nastavovat už při Environment::loadConfig().

Tím se to hezky zjednodušuje, mělo mě trknout podívat se do examples :)

To bych nedělal. To bude fungovat jen v případě, že používáš právě tyto dvě prostředí. Jakmile budeš potřebovat více prostředí, narazíš na problém.

Na tom je to založené, zatím mi stačila tyto dvě prostředí. Jde spíš jen o takovou automatizaci, kdy hodně často měním tyto dva režimy a testuju, ať nemusím přepisovat nebo komentovat na více místech.

Možná bude lepší ale použití

if (!Environment::isProduction()) { Debug::enableProfiler(); }

Editoval romansklenar (14. 9. 2008 20:59)

před 11 lety

romansklenar
Člen | 657
+
0
-

Nevíte někdo jak vyhazovat/zpracovávat vyjímky při zaplém output bufferingu z úvodního postu tak, aby k ukončení aplikace a nezůstala viset na prázdné bílé obrazovce a zároveň aby se logovaly? (logování mám vyřešeno v ErrorPresenteru, tzn stačilo by mi to předat nějak na něj, s vyplým output bufferingem to vše funguje, vyjímka se předá a zpracuje v ErrorPresenteru). Any ideas?

před 11 lety

xificurk
Člen | 119
+
0
-

romansklenar napsal(a):

Nevíte někdo jak vyhazovat/zpracovávat vyjímky při zaplém output bufferingu z úvodního postu tak, aby k ukončení aplikace a nezůstala viset na prázdné bílé obrazovce a zároveň aby se logovaly? (logování mám vyřešeno v ErrorPresenteru, tzn stačilo by mi to předat nějak na něj, s vyplým output bufferingem to vše funguje, vyjímka se předá a zpracuje v ErrorPresenteru). Any ideas?

Imho to nebude chyba output bufferingu. Mně to funguje naprosto v pohodě. Nejčastěji jsem na bílou obrazovku smrti narazil, když se mi vyskytla chyba, která spustila přesměrování na ErrorPresenter a při tomto přesměrování nastala další chyba.

před 11 lety

romansklenar
Člen | 657
+
0
-

Fakt že jo, chyba byla někde jinde… Kde to ale netuším, protože jsem si s tím hoďku hrál tak dlouho až jsem to celé překopal, že nezůstal stát na kameni kámen a nepodařilo se mi postřehnout co byla ta klíčová změna.

před 10 lety

phx
Člen | 652
+
0
-

Zdravim vas…

Mam problem. Ve startup() zapnu ob_start(); dale v shutdown() si stahnu data z bufferu $content = ob_get_flush(); a nejak s nima pracuji.

Vse je OK do chvile kdy dojde na strance k presnerovani. V tu chvili mi to vyhodi vyjimku, ze nemuze modifikovat hlavicky, protoze radek s $content = ob_get_flush(); pry odeslal nejaky vystup.

Kde je problem?

Dalo by se to obejit, kdyby slo nejak detekovat zda nedoslo k presmerovani, ale jak na to jsem neprisel.

před 10 lety

romansklenar
Člen | 657
+
0
-

A kde dojde k volání toho přesměrování?

Pokud přesměrování pořešíš před zapnutím OB (pokud to tvoje situace umožňuje) tak by ses mohl vyhnout problému.

Už si nepamatuju jak, ale tohle jsem taky řešil, a pro práci s OB používám potomka presenteru, který je na to určený (řešení defakto stejné jako publikoval xificurk).

před 10 lety

phx
Člen | 652
+
0
-

Dle doporuceni jsem zacal kesovat ve startup(), driv to snad krome konstruktoru nejde. A resit logiku pohledu ve startup se mi nechce.

Nebo je vhodne OB zapnout az na konci prepareXzy() nebo beforeRender()? Logicke by mi prislo kesovat od beforeRender() po afterRender(), ale pokud se nepletu tak v dobe volani afterRender jeste neni vyrenderovan cely layout, ale jen scena.

Pokud myslis https://forum.nette.org/…iewtopic.php?… tak bohuzel uz to nejde stahnout:(( Nemas to nekde?

před 10 lety

romansklenar
Člen | 657
+
0
-

Jj tady to máš:

před 10 lety

phx
Člen | 652
+
0
-

Nedari se… Vpodstate to mam stejne. Zapnu OB, vypnu OB ziskam data a vypisu. Pri redirectu zadna data nejsou. Overeno:

$content = ob_get_flush();
echo "'".$content."'";  // vypise ''
die();

Tak at mi to netvrdi, ze byl nejaky vystup vygenerovan!!! Uz me napada jedine BUG v PHP.

před 10 lety

kravčo
Člen | 723
+
0
-

phx napsal(a):

Nedari se… Vpodstate to mam stejne. Zapnu OB, vypnu OB ziskam data a vypisu. Pri redirectu zadna data nejsou. Overeno:

$content = ob_get_flush();
echo "'".$content."'";    // vypise ''
die();

Tak at mi to netvrdi, ze byl nejaky vystup vygenerovan!!! Uz me napada jedine BUG v PHP.

prečo ob_get_flush()? nechcelo by to skôr ob_get_clean()?

Podľa PHP dokumentácie ob_get_flush() okrem toho, že vráti buffer, tak ho aj pošle prehliadaču… Tipujem, že aj keď posiela prázdny reťazec, posiela pred ním hlavičky…

před 10 lety

phx
Člen | 652
+
0
-

To je opsane od Davida viz nahore. Navic potrebuji vysledek pro sebe (cache) a zaroven poslat uzivateli:)

Tak jo asi bug v php protoze:

// nejde
$content = ob_get_flush();

// jde
$content = ob_get_clean();
echo $content;

I kdyz principelne by to melo byt totozne ne?

před 10 lety

kravčo
Člen | 723
+
0
-

phx napsal(a):

Vse je OK do chvile kdy dojde na strance k presnerovani. V tu chvili mi to vyhodi vyjimku, ze nemuze modifikovat hlavicky, protoze radek s $content = ob_get_flush(); pry odeslal nejaky vystup.

Narážal som tým najmä na toto, predtým som neúplne citoval…
Totiž ob_get_flush() výstup odosiela, po jeho volaní sú už hlavičky odoslané.

// nejde
$content = ob_get_flush();

// jde
$content = ob_get_clean();
echo $content;

Tu súhlasím, totožné by to malo byť, ale len v prípade, že medzi získaním z ob_get_clean() a vypisovaním sa nič iné nevykonáva…

před 10 lety

phx
Člen | 652
+
0
-

kravco napsal(a):

Narážal som tým najmä na toto, predtým som neúplne citoval…
Totiž ob_get_flush() výstup odosiela, po jeho volaní sú už hlavičky odoslané.

Takze to odesle i nic? To bych povazoval za BUG:)

před 10 lety

_Martin_
Generous Backer | 680
+
0
-

Těžko říct takhle, co se tam přesně děje, ale doporučuji používat fci ob_get_clean() – jinak by jsi – pokud dojde k přesměrování na další kešovaný presenter – spustil v metodě startup nové bufferování, aniž by jsi předchozí ukončil. Navíc se to může hodit, pokud chceš používat kompresi přenášených dat (třeba metodou enableCompression() třídy HttpRespons).

před 10 lety

phx
Člen | 652
+
0
-

_Martin_ napsal(a):

Těžko říct takhle, co se tam přesně děje, ale doporučuji používat fci ob_get_clean() – jinak by jsi – pokud dojde k přesměrování na další kešovaný presenter – spustil v metodě startup nové bufferování, aniž by jsi předchozí ukončil.

Novy request po presmerovani by nemel ovlivnit predchozi, ohledne OB. Kazdy reqest zacina s cistou pameti, krome session:) Leda ze myslis vnitrni Nette predani.

před 10 lety

_Martin_
Generous Backer | 680
+
0
-

phx napsal(a):

Novy request po presmerovani by nemel ovlivnit predchozi, ohledne OB. Kazdy reqest zacina s cistou pameti, krome session:) Leda ze myslis vnitrni Nette predani.

Přesně, špatně jsem se vyjádřil, myslel jsem „přesměrování“ pomocí ForwardingException.

před 10 lety

David Grudl
Nette Core | 6849
+
0
-

Mezi ob_get_flush() a ob_get_clean() je zásadní rozdíl – clean obsahu bufferu jen vrátí, flush do vypíše a vrátí.

Chybou je, že jsem v uvedeném řešení s přesměrováním nepočítal. Nenapadlo mě to. Šlo by to upravit takto:

protected function shutdown()
{
    if ($this->cacheKey) {
        $content = ob_get_clean();
        if ($content) {
            echo $content;
            ...
        }
    }
}

Ale ani to není úplně ideální – cache by totiž neměla v případě chyby nic ukládat. Takže jsem upravil Presenter, aby metodě shutdown() předával případnou zachycenou výjimku.