Toon posts:

[HTTP] Caching "done right"

Pagina: 1
Acties:
  • 253 views sinds 30-01-2008
  • Reageer

Verwijderd

Topicstarter
Hoi :)

Ik ben ontwikkelaar van een CMS, wat inmiddels een leuke vorm heeft gekregen. Mijn CMS serveert alle requests zelf, dus Apache krijgt niet de kans zelf caching headers mee te sturen. Caching van plaatjes e.d. is makkelijk maar de feitelijke HTML-pagina's vereist meer aandacht. Immers: alles komt uit de database, en gezien de aard van de pagina's (forum, loginpages) kan de inhoud snel veranderen.

Nu ik op het punt sta de cache control om te gooien vraag ik jullie dan ook om advies en ideeen om HTTP caching op een elegante manier te implementeren. Caching doe ik via HTTP headers en niet in het HTML-document zelf. Ik dacht zelf aan de volgende mogelijkheden:
  • Totaal geen caching-headers meesturen, laat de client maar uitzoeken of een document outdated is of niet. Wel stuur ik Last-Modified header mee zodat de client If-Modified-Since kan sturen en een HTTP 304 Not Modified response krijgt als het document niet is veranderd.
  • Ik zet de expire datum 2 minuten in de toekomst, zodat het document gecached kan worden gedurende korte tijd.
  • Een expire datum van 30 minuten of langer.
Toch heb ik bedenkingen:
  • Als gebruikers ingelogd zijn moet een eventuele proxy geen kopie onthouden; dat kan een security issue zijn. Ik moet dus wel caching headers gebruiken dus optie 1 gaat lastig worden.
  • 2 minuten in de toekomst is leuk, maar wat als een van de twee computers (client / server) niet gesynchroniseerd is via NTP? Dan is die 2 minuten opeens in het verleden of verder in de toekomst.
  • Een oplossing zou zijn een langere expire tijd, maar in hoeverre zorgt een 'overijverige' cache voor merkbare nadelen voor de client? In de zin van: verouderde pagina's krijgen wat de bruikbaarheid vermindert.
Waar ik nu tegen aanloop is dat Firefox bij het klikken op de back button toch weer de pagina gaat opvragen, wat minder handig werkt, gezeur om POST-data die hij opnieuw moet/wil versturen en de inhoud van ingevulde formulieren verpest. Dat is mijn grootste prioriteit, minder belasting op de server is pas een tweede prioriteit.

Jullie gedachten hierover zijn van harte welkom. :)

Verwijderd

Verwijderd schreef op woensdag 11 april 2007 @ 18:40:
Hoi :)

Ik ben ontwikkelaar van een CMS, wat inmiddels een leuke vorm heeft gekregen. Mijn CMS serveert alle requests zelf, dus Apache krijgt niet de kans zelf caching headers mee te sturen.
Daar zie ik een probleem al in je ontwerp. Ga applicaties die er voor gebouwd zijn om die taken uit te voeren niet hun taken ontnemen! Dat is het domste wat je kunt doen en veroorzaakt veel problemen en onderhoud. Apache had al de problemen die jij hier nu post bovendien ookal voor je opgelost.

Verwijderd

Topicstarter
Verwijderd schreef op woensdag 11 april 2007 @ 19:13:
Daar zie ik een probleem al in je ontwerp. Ga applicaties die er voor gebouwd zijn om die taken uit te voeren niet hun taken ontnemen! Dat is het domste wat je kunt doen en veroorzaakt veel problemen en onderhoud. Apache had al de problemen die jij hier nu post bovendien ookal voor je opgelost.
Hoe kan Apache weten of mijn database-driven website is geupdate of niet? ;)
De caching van Apache werkt alleen op ruwe bestanden - .html en .jpg bijvoorbeeld. En behalve images zijn die er niet.

Daarnaast, om alles zelf te serveren biedt grote mogelijkheden en voordelen, mooie URL's om maar wat te noemen. Maar dat is een heel andere discussie.

Standaard is het zo dat bij een .php pagina alle caching wordt uitgeschakeld (door PHP module) in de HTTP headers. Ik kan dat natuurlijk overrulen en zelf een mooie cache control schrijven - en in dit topic vraag ik dan ook welke filosofie ik hierin het beste kan volgen.

  • dawuss
  • Registratie: Maart 2001
  • Laatst online: 24-11 12:28

dawuss

gadgeteer

Verwijderd schreef op woensdag 11 april 2007 @ 19:21:
Daarnaast, om alles zelf te serveren biedt grote mogelijkheden en voordelen, mooie URL's om maar wat te noemen. Maar dat is een heel andere discussie.
offtopic:
Inderdaad een andere discussie, maar Apache kan met mod_rewrite aardig mooie URL's genereren :)

micheljansen.org
Fulltime Verslaafde Commandline Fetisjist ©


Verwijderd

Topicstarter
dawuss schreef op woensdag 11 april 2007 @ 19:23:
[...]

offtopic:
Inderdaad een andere discussie, maar Apache kan met mod_rewrite aardig mooie URL's genereren :)
Ik gebruik dus mod_rewrite voor alle requests naar 1 file, die van daaruit de juiste module 'kiest' om de request af te handelen. Ik vind het zelf een mooi ontwerp, maar kost idd meer werk om alles netjes te laten verlopen.

Maar het maakt dus niet uit of je mod_rewrite gebruikt of niet m.b.t. caching van PHP-generated pagina's, dus in die zin ben ik het niet eens met het argument van Exiss.

  • CyBeR
  • Registratie: September 2001
  • Niet online

CyBeR

💩

Optie 1 is imo het best. Alleen als je ingelogd bent (en dat weet je CMS) dan moet je dus idd headers meesturen zodat spul niet gecached wordt.

All my posts are provided as-is. They come with NO WARRANTY at all.


  • BCC
  • Registratie: Juli 2000
  • Laatst online: 07:20

BCC

Typo on Rails doet html caching op een mooie manier. Kijk daar eens naar voor wat inspiratie zou ik zeggen. Je wil IMHO namelijk niet de client de pagina's laten cachen bij dingen als een forum, aangezien er nogal wat wijzigingen zijn waardoor de clients altijd de laatste versie willen zien. Ik zou dus eerder naar serverside caching gaan kijken.

Backs die posts opleveren heb je elke browser, ik denk dat je dat gewoon anders moet af vangen.

[ Voor 64% gewijzigd door BCC op 11-04-2007 19:39 ]

Na betaling van een licentievergoeding van €1.000 verkrijgen bedrijven het recht om deze post te gebruiken voor het trainen van artificiële intelligentiesystemen.


Verwijderd

Als je na een POST request de client doorstuurt via een 30x en een Location header, heb je geen last van browsers die nogmaals die POST willen uitvoeren.

  • Sjaaky
  • Registratie: Oktober 2000
  • Laatst online: 06-11 13:54
Het probleem dat de back en F5 een POST willen uitvoeren kun je voorkomen.
Hiervoor zul je elke POST actie moeten laten redirecten naar een url die vervolgens door de browser geGET wordt. Als er dan gerefresht wordt, wordt alleen de GET opnieuw gedaan. Als je back drukt wordt de vorige pagina geGET.

wat hierboven staat dus

[ Voor 6% gewijzigd door Sjaaky op 11-04-2007 20:03 ]


Verwijderd

Topicstarter
CyBeR schreef op woensdag 11 april 2007 @ 19:32:
Optie 1 is imo het best. Alleen als je ingelogd bent (en dat weet je CMS) dan moet je dus idd headers meesturen zodat spul niet gecached wordt.
Hoe reageert een client dan precies? Bij elke pageview kijken of de content gewijzigd is of hanteert de client dan zelf een soort default 'expire' timer?

Op de overige reacties: is het niet zo dat als een pagina niet 'expired' is dmv headers de browser bij back-button gewoon de pagina laat zien die hij gekregen heeft, of dat nou middels POST of GET ging? Ik meen van wel namelijk. Nog even opzoeken. :)

Ook zal ik eens spieken van Typo on Rails. :)

  • orf
  • Registratie: Augustus 2005
  • Laatst online: 07:22

orf

Ik gebruik last-modified en eTag voor caching. De client stuurt deze waarden mee zoals deze (eerder) ontvangen heeft. Krijg ik een if-modified-since header binnen met de exacte datum en komt de eTag overeen, dan geef ik een header not modified en exit ik. Op dat moment kun je er zeker van zijn dat de user agent dezelfde pagina in het geheugen heeft.

  • CyBeR
  • Registratie: September 2001
  • Niet online

CyBeR

💩

Verwijderd schreef op woensdag 11 april 2007 @ 21:46:
[...]

Hoe reageert een client dan precies? Bij elke pageview kijken of de content gewijzigd is of hanteert de client dan zelf een soort default 'expire' timer?
Uiteraard checkt 'ie bij elke pageview. Vervolgens hoef je serverside alleen maar (zo snel mogelijk) te controleren of de content gewijzigd is en hoef je niet het hele gedoe opnieuw te genereren.

All my posts are provided as-is. They come with NO WARRANTY at all.


  • djc
  • Registratie: December 2001
  • Laatst online: 08-09 23:18

djc

Ik zou vertrouwen op Last-Modified, ETag en de Vary-header. Met behulp van de Vary-header kan je waarschijnlijk ook voorkomen dat mensen elkaars gecachede pagina's te zien krijgen. Eventueel kun je inderdaad voor ingelogd gebruikers no-cache headers meesturen.

Rustacean


  • Confusion
  • Registratie: April 2001
  • Laatst online: 01-03-2024

Confusion

Fallen from grace

In dit recente topic worden heel wat dingen over caching gezegd.

Wie trösten wir uns, die Mörder aller Mörder?


Verwijderd

Topicstarter
CyBeR schreef op woensdag 11 april 2007 @ 22:14:
[...]


Uiteraard checkt 'ie bij elke pageview. Vervolgens hoef je serverside alleen maar (zo snel mogelijk) te controleren of de content gewijzigd is en hoef je niet het hele gedoe opnieuw te genereren.
Volgens mij controleert een client (browser) pas of de content gewijzigd is als de pagina 'expired' is. Dat kan of door de server opgelegd worden met de "Expires" header, soms in het verleden gezet zodat de client iedere keer controleert of de content is geupdate met een If-Modified-Since header. Zet je Expired echter 2 uur later dan controleert een client tot die tijd niet of de pagina is veranderd, tenzij de gebruiker voor refresh (F5) kiest.

De vraag is dan: wat doet een client als de server helemaal geen Expires-header stuurt, ik denk dat hij dan stilletjes een default-waarde zoals 2 uur hanteert, of 30 minuten. Dat moet ik nog even uitzoeken, waarschijnlijk verschilt deze 'standaard instelling' ook per browser.
Manuzhai schreef op donderdag 12 april 2007 @ 22:10:
Ik zou vertrouwen op Last-Modified, ETag en de Vary-header. Met behulp van de Vary-header kan je waarschijnlijk ook voorkomen dat mensen elkaars gecachede pagina's te zien krijgen. Eventueel kun je inderdaad voor ingelogd gebruikers no-cache headers meesturen.
Hm dan kies je toch gewoon voor een Private cache? Daar is die voor, dan mag alleen de 'eindgebruiker' de content cachen en niet alle proxies er tussenin. Zo kan nooit een andere gebruiker een 'ingelogde' pagina van een ander zien.


Hoe ver ben ik?
Op dit moment heb ik gekozen voor een 30-minuten Expiration met private caching header. De grootste uitdaging die ik tegenkom is om te berekenen wanneer de content is geupdate (Modified-header), zodat deze goed samenwerkt met If-Modified-Since waarop dan een HTTP 304 Not Modified respons op kan worden gegeven. Aangezien mijn site volledig database driven is kun je simpelweg de modify-datum opslaan in SQL. Maar hoe zit dat bijvoorbeeld met de 'update-tracker' aan de linkerkant, net zoals op tweakers.net. Die kan ook geupdate zijn, los van de content die op de pagina staat. Ik denk dat ik daarom een soort 'supercache' systeem moet ontwikkelen. Sommige updates zijn 'globaal' en zorgen dat alle pagina's een 'Modified'-reset krijgen. Alle pagina's zijn dan 'vernieuwd'. Bijvoorbeeld als ik een nieuw artikel plaats.

Ik als ik alles heb draaien zal ik het hier posten, hoe het bevalt. :)

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Als je een 'global' bij houdt op het moment dat je de pagina genereert en die telkens overschrijft met een recentere waarde als je die 'ergens' tegen komt (content, afbeelding, tracker, whatever) dan is op het eind van de pagina genereren die 'global' als het goed is de 'recentste' wijziging en kun je a.d.h. daarvan de juiste modify datum sturen. Lange zinnen rule! :P

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


  • Blaise
  • Registratie: Juni 2001
  • Niet online
Waarom 30 minuten? Je kan het ook een week (of zelfs 10 jaar) laten cachen, als je maar aan de browser vertelt wanneer de content gewijzigd is. Daarvoor hebben ze Etag en Modified-Since headers uitgevonden die de browser verstuurt naar je server.

Ik heb een jaar geleden deze functie gemaakt voor caching, deze werkt ongeveer zo: Als een pagina wordt gecached en de content verandert in de tussentijd komen de Etag en de modified-since niet meer overeen (de cache is dan "stale") en wordt de nieuwe content verstuurd. Als de cache nog vers is wordt er niets verstuurd. Bezoekers krijgen dus altijd de laatste content.
­
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// modified: datum gewijzigd, UNIX timestamp
// cache: true wanneer pagina moet worden gecached
function output_headers($modified = false, $cache = false){

    global $system;

    if (!$modified) $modified = $system['time']; 

    $modified_gmt = gmdate('D, d M Y H:i:s', $modified) . ' GMT';
    $expires_gmt = gmdate('D, d M Y H:i:s', $modified + (3600 * 24 * 7)) . ' GMT'; // cache een week

    // page type
    header('Content-Type: text/html;charset=utf-8');

    // pagina moet worden gecached
    if ($cache){
        $etag = md5($modified);
        $if_modified_since = getenv('HTTP_IF_MODIFIED_SINCE');
        $if_none_match = getenv('HTTP_IF_NONE_MATCH');

        header('Last-Modified: ' .  $modified_gmt);
        header('Cache-Control: store, cache, must-revalidate');
        header('Expires: ' . $expires_gmt);
        header('ETag: ' . $etag);

        // pagina is in cache van gebruiker
        // probeer zowel Etag als modified-since, soms wordt 1 van de 2 niet verstuurd
        if ($if_none_match == $etag || $if_modified_since == $modified_gmt){
            header('HTTP/1.0 304 Not Modified');
            exit; // je kan nu stoppen, pagina is gecached en de cache is vers
        }
    }

    // pagina mag niet worden gecached
    else{
        header('Last-Modified: ' .  $modified_gmt);
        header('Cache-Control: private, no-store, no-cache, must-revalidate');
    }
}

Verwijderd

Topicstarter
Hm ik denk dat je beter helemaal geen Expires-header kunt gebruiken maar juist Cache-Control: max-age, de reden hiervoor is dat als de klok van de server (of client) niet goed staat, die Expires-header dus ook niet meer klopt. Bij max-age houdt de client b.v. een kopie voor 2 uur (of langer natuurlijk).

Verder gebruik ik al Last-Modified voor images enzo, de moeilijkheid zit hem er juist in een Last-Modified-datum te genereren voor dynamische content in je CMS. Voor een bestand is dat natuurlijk pieceofcake. Etag gebruik ik niet, Etag is vooral handig als de updates voor een URL niet gebaseerd zijn op tijd maar op iets anders, of als je in een seconde meerdere updates hebt.

Op dit moment zijn de tags dus die ik verstuur:
Last-Modified: <modify time>
Cache-Control: private, max-age=<cache-tijd in seconden>

Verder is het zaak andere headers (zoals pragma en expires) te unsetten, in PHP doe je dat met:
header('Pragma:');
header('Expires:');

Tevens is het zeer verstandig ook voor je PHP documenten een Content-Length mee te sturen:
header('Accept-Ranges: bytes');
header('Content-Length: '.$bytes);

Om de Content-Length te bepalen zul je output buffering moeten gebruiken en een callback-functie. Maar makkelijker is om dit te combineren met Gzip compressie, door bijvoorbeeld de zlib-extentie te gebruiken van PHP zal deze automatisch non-binary documenten comprimeren en berekent ook de juiste Content-Length. Zlib schakel je in door in je httpd.conf bij de juiste Virtualhost de volgende wijzigingen te maken:
.
<VirtualHost *>
    ServerName dev.fluffles.net
    DocumentRoot /www/fluffles-v3
    ErrorLog /www/logs/fluffles-v3
    RewriteEngine on
    RewriteRule ^.*$ /www/dsm-dev/master.php
    php_flag magic_quotes_gpc Off
[b]    php_flag zlib.output_compression On
    php_value zlib.output_compression_level 9[/b]
</VirtualHost>


Voor mensen die ook geinteresseerd zijn in HTTP caching is dit document een absolute must om te lezen: http://www.mnot.net/cache_docs/

  • Blaise
  • Registratie: Juni 2001
  • Niet online
Hm ik denk dat je beter helemaal geen Expires-header kunt gebruiken maar juist Cache-Control: max-age, de reden hiervoor is dat als de klok van de server (of client) niet goed staat, die Expires-header dus ook niet meer klopt. Bij max-age houdt de client b.v. een kopie voor 2 uur (of langer natuurlijk).
Interessant! dat ga ik aanpassen.

Die zlib compression in je HTAccess, wordt die ook uitgevoerd als de browser geen accept headers stuurt? Momenteel compress ik de pagina nog met een output buffer, maar alleen als uit de HTTP_ACCEPT_ENCODING header blijkt dat compressie wordt ondersteund.

  • crisp
  • Registratie: Februari 2000
  • Nu online

crisp

Devver

Pixelated

Het is simpel: Expires is HTTP/1.0 en heeft inderdaad limitaties, daarvoor in de plaats is in HTTP/1.1 Cache-Control gekomen die deze limitaties grootendeels opheft. Een HTTP/1.1 client zal bij het gebruik van Cache-Control met een max-age directive de Expires negeren.

Intentionally left blank


Verwijderd

Topicstarter
Hm de problemen waar ik tegenaan loop:

1) PHP doet erg moeilijk als je sessies gebruikt, je kunt eigenlijk zelf geen Caching-headers instellen; hij overruled ze gewoon. Je kunt alleen session_cache_limiter() gebruiken en dat is beperkt, geen max-age enzo.
2) erg vervelend: bij fora enzo lukt die Cache-Control:private niet zo; omdat na een quickreply de browser een redirect krijgt naar de pagina, maar dan dus een oude versie ziet. Een stale dus. Toch nog maareens kijken, ik moet denk ik een max-age:1 gebruiken ofzo. ;(

@Blaise: zlib handelt alles af, dus stuurt ook uncompressed als de browser geen Accept-Encoding meestuurt, ook lijkt zlib te kunnen 'zien' of de output binary of textueel is, voor binary files (images) wordt geen compressie toegepast. En mogelijk ook als het bestand te klein is, dan is compressie niet lonend meer (overhead).

  • crisp
  • Registratie: Februari 2000
  • Nu online

crisp

Devver

Pixelated

Verwijderd schreef op donderdag 19 april 2007 @ 14:23:
Hm de problemen waar ik tegenaan loop:

1) PHP doet erg moeilijk als je sessies gebruikt, je kunt eigenlijk zelf geen Caching-headers instellen; hij overruled ze gewoon. Je kunt alleen session_cache_limiter() gebruiken en dat is beperkt, geen max-age enzo.
kan je niet session_cache_limiter('private, max-age=3600, must-revalidate'); doen?
En anders zou je nog na je session_start expliciet een Cache-Control header kunnen sturen, die zou die van de sessie moeten overschrijven.
2) erg vervelend: bij fora enzo lukt die Cache-Control:private niet zo; omdat na een quickreply de browser een redirect krijgt naar de pagina, maar dan dus een oude versie ziet. Een stale dus. Toch nog maareens kijken, ik moet denk ik een max-age:1 gebruiken ofzo. ;(
public/private geeft enkel aan of een proxy 'm mag cachen of niet, maar zegt verder niets over expiration zelf.

Intentionally left blank


Verwijderd

Topicstarter
crisp schreef op donderdag 19 april 2007 @ 14:31:
[...]

kan je niet session_cache_limiter('private, max-age=3600, must-revalidate'); doen?
Nee volgens deze pagina zijn de enige mogelijkheden: none, nocache, private, private_no_expire, public. Ik zou 'none' nog kunnen proberen, bedenk ik me nu. :)

En anders zou je nog na je session_start expliciet een Cache-Control header kunnen sturen, die zou die van de sessie moeten overschrijven.[/quote]
Al geprobeerd maar dat werkt niet. :(
public/private geeft enkel aan of een proxy 'm mag cachen of niet, maar zegt verder niets over expiration zelf.
Dat weet ik, maar ik gebruik dus private voor ingelogde pagina's zodat een publieke cache (proxy) nooit een 'ingelogde' pagina kan zien. Als een gebruiker niet is ingelogd zou het eventueel public kunnen worden, alhoewel het voor kan komen dat ik bijvoorbeeld een eerder gebruikte username voor niet-geregistreerde bezoekers onthoud en alvast invul. Dus misschien toch maar beter private blijven gebruiken. Jammer voor proxies dan.

[ Voor 14% gewijzigd door Verwijderd op 19-04-2007 22:43 ]


Verwijderd

ik zou zeggen duik de RFC in,
1945 is HTTP/1.0 http://tools.ietf.org/html/rfc1945
2616 is HTTP/1.1 http://tools.ietf.org/html/rfc2616

overigens is het mij nou niet helemaal duidelijk, je zegt dat je de pagina's zelf servert, dus zonder apache/iis/whatever ertussen en toch gebruik je header() en session_cache_limiter() terwijl deze functies er vanuit gaan je een webserver draait?

hoe zit je php-executable nou verbonden met je browser want als je php als command-line draait doet header() bijvoorbeeld helemaal niets. controlleer je wel of de headers ook echt worden verstuurd?
Pagina: 1