Start - Publikationen - Wissen - TOGAF - Impressum -

Grundlagen


Die HTTP Spezifikation definiert, wie HTTP Header Einträge Clients und Proxys mit Caching-Anweisungen versorgen. Seit HTTP1.1 stehen umfangreiche Möglichkeiten zur Steuerung des Cachings bereit. Ein unbedachter oder vernachlässigter Einsatz dieser Einträge kann folgenschwer sein:

  • die Anwendung verschwendet Netz- und CPU Ressourcen, da zu wenig gecached wird
  • wird zu viel oder zu lange gecached, werden dem Anwender veraltete Informationen präsentiert
  • werden proxyseitig vertrauliche Informationen gecached, so können diese einem falschen Nutzer präsentiert werden

Die HTTP Header Date, Expires, Pragma und Cache-Control spielen dabei die wichtigste Rolle. Eine explizite Angabe dieser Werte ist außerdem wichtig, da das Verhalten von Clients und Proxys bei fehlenden Angaben nicht standardisiert ist. Vernünftige Werte für diese Attribute erläutern sich am besten aus der Sicht der drei wichtigen Anwendungsfälle beim Cachen:

1. Caching verhindern


Die Inhalte dieser Seite ändern sich potenziell und sind nie aktuell:

Date: <aktuelle Serverzeit>
Expires: Sat, 6 May 1995 12:00:00 GMT  // Hauptsache in der Vergangenheit
Cache-Control: no-cache
Cache-Control: post-check=0,pre-check=0 // für die Freunde des IE, proprietär
Pragma: no-cache // für die Freunde von HTTP1.0
Aber Achtung! Verwirrenderweise bedeutet no-cache NICHT, dass ein Response niemals gecached wird. Insbesondere im Zusammenhang mit dem Conditional GET werden auch als no-cache gekennzeichnete Antworten regulär gecached. Eine zusätzliche Angabe von no-store in Cache-Control weist den Browser an, unter keinen Umständen den Inhalt dieser Seite clientseitig permanent zu speichern. Aber auch hier gilt große Vorsicht, dieser Eintrag garantiert NICHT die Geheimhaltung der Informationen. So mag ein Client dennoch die Inhalte, zum Beispiel als History-Eintrag, lokal vorhalten. Das ganze ist so herrlich verwirrend, dass ich es nochmal als Tabelle festhalte:
Cache-Control Was man vermutet Was die Spezifikation sagt
no-cache Clientseitig erfolgt kein Caching: FALSCH! der Client KANN cachen, zum Beispiel im Rahmen des Conditional GET
must-revalidate Die Daten im Cache werden validiert: FALSCH! Die Validierung erfolgt sowieso, da no-cache angegeben wurde. Aber wenn die Validierung nicht durchgeführt werden kann (sprich: kein Netzwerk), dann muss ein Fehler angezeigt werden.
no-store Die Daten werden niemals clientseitig gespeichert: FALSCH! Speichern im Clientcache erfolgt trotzdem, aber es wird VERSUCHT, die Inhalte andersweitig 'sicher' zu behandeln. Sie werden beispielsweise nicht dauerhaft gespeichert.

Wie erwähnt ist das Verhalten von Clients und Proxys ohne irgendwelche Angaben zum Cachen nicht spezifiziert. In den alten Tagen des Internets mit gringer Bandbreite war es ein durchaus plausibles Verhalten, solche Inhalte für eine willkürliche Zeit vorzuhalten. So gesehen ist die no-cache Direktive eine Aufforderung an Clients, dies nicht zu tun.

2. Cachen nur im HTTP Client


Die Daten sind nutzerspezifisch und bleiben x Sekunden aktuell:

Date: <aktuelle Serverzeit>
Expires: <aktuelle Serverzeit + x Sekunden>
Cache-Control: private
Die Angabe in Expires kann mit der max-age Direktive überschrieben werden. Existiert die Angabe, dann wird diese berücksichtigt, selbst wenn die Angaben bei Expires restriktiver sind.
Date: <aktuelle Serverzeit>
Expires: <..> // Angabe wird ignoriert, da max-age gesetzt
Cache-Control: private, max-age=<x Sekunden>

3. Cachen im HTTP Client und im HTTP Proxy


Die Daten sind für alle Nutzer gleich (nicht nutzerspezifisch) und bleiben x Sekunden aktuell:

Date: <aktuelle Serverzeit>
Expires: <aktuelle Serverzeit + x Sekunden>
Cache-Control: public
Wieder kann mit der Angabe von max-age die Angabe von Expires überschrieben werden. Die Angabe von public ist dann überflüssig, da max-age diese impliziert:
Date: <aktuelle Serverzeit>
Expires: <..> // Angabe wird ignoriert, da max-age gesetzt 
Cache-Control: max-age=<x Sekunden>  // impliziert 'public' 
Alle Zeitangaben sind entsprechend der HTTP Spezifikation zu formatieren (Beispiel: Fri, 30 Oct 2010 14:19:41 GMT), am besten man benutzt Servicemethoden wie setDateHeader in HttpServletResponse.

Conditional GET


Server können jederzeit GET Anfragen mit einem Statuscode HTTP 304 Not Modified beantworten. In diesem Fall signalisiert der Server dem anfragenden Client, dass die Ressource in clientseitigen Cache aktuell ist und wiederverwendet werden kann. Offenbar sind zwei Dinge für diesen Mechanismus nötig: a) Beim Versenden einer Ressource muss der Server dem Client die Zeit der Erstellung mitgeben und b) der Server muss wissen, ob sich die Ressource inzwischen geändert hat. Technisch wird dies über den Last-Modified Eintrag im Header des Responses erreicht - beim nächsten Request wird dieser Wert als If-Modified-Since wieder zum Server geschickt und kann ausgewertet werden.

Für Servlets ist die praktische Umsetzung ganz einfach. Man überschreibt getLastModified im Servlet, der Rest - also das Setzen des Last-Modified Eintrag und die Auswertung des If-Modified-Since geschieht automatisch über die Implementierung in HttpServlet:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response)
  throws ServletException, IOException {
  // setzte relevante Header und erlaube Caching, der Date Header wird automatisch gesetzt
  response.setContentType("text/html;charset=UTF-8");
  // Seiteninhalte ab hier, die Logik für Conditional GET ist im HttpServlet, basierend
  // auf getLastModified implementiert
  :
}
:
@Override
protected long getLastModified(HttpServletRequest req) {
  // Testweise: simuliert neuen Content alle 10 Sekunden
  long lastModified = System.currentTimeMillis();
  lastModified /= 10000;
  lastModified *= 10000;
  return lastModified;
}
Zum Test lädt man die Seite dauernd, sie wird nur alle 10 Sekunden neu erzeugt und geladen (Status 200 OK), dazwischen wird ein Status 304 gesendet und der Client nutzt die Version im Cache.

Für JSPs und darauf basierenden Technologien ist das Vorgehen nicht so einfach, da der generierte JSP Code nicht von HttpServlet erbt (getLastModified wird nie gerufen!). Die Lösung besteht darin, den Last-Modified Eintrag im Response zu setzen und den If-Modified-Since im Request auszuwerten - mit anderen Worten Conditional GET selber zu implementieren:

<%
  // simuliert neue Inhalte alle 10 Sekunden
  long lastModified = System.currentTimeMillis();
  lastModified /= 10000;
  lastModified *= 10000;
  //
  long ifModifiedSince = request.getDateHeader("If-Modified-Since");
  if (ifModifiedSince != -1 && !response.isCommitted() && ifModifiedSince >= lastModified) {
    // schicke 304 Not Modified - der Client soll seinen Cache nutzen
    response.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED);
    return;
  }
  // ab hier Seiteninhalte
  response.setDateHeader("Last-Modified", lastModified);
  :
%>
Es ist mir nicht klar, warum es in der JSP Spezifikation dafür keine Unterstützung gibt.

Kann nicht anhand einer Zeitangabe die Aktualität einer Ressource validiert werden, dann muss der entity tag einer Ressource zur Anwendung kommen. Dazu sendet der Server im Header einen Wert für ETag - der ist technisch gesehen ein möglichst einfach zu ermittelnder Hash der betreffenden Ressource. Diese Angabe wird vom Client als If-None-Match in der nächsten Anfrage zum Server zurückgeschickt. Beim ETag gibt es keine Methode analog getLastModified im Servlet die man überschreiben könnte. Hier ist wieder Handarbeit gefordert:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response)
  throws ServletException, IOException {
  //
  response.setContentType("text/html;charset=UTF-8");
  // ermittle den aktuelle ETag
  String currentETag = getETag();
  // lese den ETag aus dem Client-Request bei Gleichheit 
  // wird 304 Not Modified zum Client geschickt
  if(currentETag != null && !response.isCommitted() && 
      currentETag.equals(request.getHeader("If-None-Match"))) {
    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    return;
  }
  //
  response.setHeader("ETag", currentETag);
  // ab hier Seitenausgabe
  :
}
:
private String getETag() {
  //
  // Der ETag der Ressource ist typischerweise ein Hash der
  // Ressource. Hier wird beispielhaft ein ETag simuliert der sich alle 10 Sekunden ändert
  long currentTime = System.currentTimeMillis();
  // Auf diese Weise ändert sich current Time nur alle 10 Sekunden:
  currentTime /= 10000;
  currentTime *= 10000;
  //
  // man beachte die Quotierung, ist von der Spec so gefordert
  return "/"" + currentTime + "/"";
}
Für JSP verfährt man analog Last-Modified im letzten Abschnitt. Auch hier ist nicht klar, warum es in der Servlet Spezifikation für den ETag keine Unterstützung ähnlich getLastModified gibt.

Ermittlung von ETag und Last-Modified


Die gerade gegebenen Beispiele für die Verwendung des ETag- und Last-Modified Header funktionieren, verschleiern aber etwas: in der Praxis lassen sich die Werte erst ermitteln, nachdem alle fachlichen Informationen zur Erstellung der Seite bekannt sind. Fachliche Inhalte werden demnach doppelt abgefragt, einmal für die Berechnung von ETag/Last-Modified und ein weiteres Mal, wenn nötig, für das Erstellen der View. Das kommt natürlich nur dann in Frage, wenn fachliche Inhalte billig zu ermitteln sind - was in den seltendsten Fällen so sein wird. Welche anderen Lösungen bieten sich an:

  • Manche Systeme verwalten selber Hash- und Last-Modified-Angaben ihrer Ressourcen (Archivsysteme, Filesysteme). In diesem Idealszenario nutzt man natürlich einfach diese Angaben. HTTP Server verhalten sich übrigens genauso und benutzen standardmäßig die Meta-Informationen des Quellsystems.
  • Fachliche Inhalte werden einmalig ermittelt und dann für die Dauer eines Requests vorgehalten. So ließe sich das Ergebnis eines JDBC-SELECT in eine Ergebnisliste konvertieren oder man verwendet CachedRowSet-Implementierungen. Die Daten werden als Attribut im HttpServletRequest hinterlegt.
  • Wie bereits erwähnt können die meisten Anwendungsfälle mit sekunden- oder minutenaktuellen Daten leben. Ein Server kann vor diesem Hintergrund selber Inhalte cachen, deren ETag/Last-Modified ermitteln und in periodischen Abständen neu generieren. Als Ablageort eignen sich der ServletContext (RAM), das File-System oder die Blob-Spalte einer Datenbanktabelle.
  • Servlets und JSP arbeiten intern mit einem Puffer, dessen Größe bestimmt werden kann. Solange dieser Puffer nicht voll ist und kein response.flush gerufen wurde, sind noch keine Inhalte zu Clients gesendet. Es bietet sich also an, die Antwort ganz normal zu generieren und am Ende der Abarbeitung ein Conditional GET zu triggern:
    // Pseudocode: //
    // hole fachliche Daten
    ResultSet = SELECT x FROM y 
    // generiere die Inhalte und ermittle gleichzeitig ETag/Last-Modified
    while (ResultSet.hasNext()) { 
      writer.write(nextResult);
      etag = etag + hash(nextResult);
    }
    :
    // Conditional GET wenn noch nichts committed
    if (!response.isCommitted() && etag.equals(request.getHeader("If-None-Match"))) {
      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
      return;
    }
    // setze den ETag im Header, Quotes nicht vergessen
    response.addHeader("ETag", '"' + etag + '"');
    // Inhalte werden zum Client gesendet
    return;
    
    Wie gesagt: das kann nur funktionieren, wenn der Response-Puffer groß genug eingestellt wurde. Außerdem muss man sich klar machen, dass die Response zwar vielleicht nicht zum Client geschickt wird, sie wird aber komplett generiert und liegt im Speicher des Servers bis sie verschickt oder verworfen werden kann.

Werden POST Anfragen gecached?


Für eine Erklärung muss man etwas ausholen: ein POST Request hat formal keinen Inhalt als Response, da ein POST Request nicht die URI einer Ressource adressiert, sondern eine Handlungsaufforderung an den Server darstellt. Erst die Antwort des Servers gibt neben dem Status des POST Requests auch einen Hinweis, wie der Client weiterarbeiten sollte. Der korrekte Ablauf sieht typischerweise so aus:

  1. Der Client sendet ein POST Request.
  2. Server antwortet mit einem Statuscode wie HTTP 201 Created, HTTP 202 Accepted, HTTP 302 Moved Temporarly oder HTTP 303 See Other - der Status bezieht sich auf die Ausführung des POST Requests. Die URI im Location Header gibt dem Client einen Hinweis darauf, wie es weitergehen kann.
  3. Die weitaus häufigste Art wie ein Server korrekt auf ein POST antwortet ist dabei der Status HTTP 302 Moved Temporarly (zur Begründung siehe Redirect Status Code). Der Client wird die URI des Location Header auslesen und mit dieser URL ein GET Request absenden. Für diesen GET Request gelten nun die gleichen Regeln zum Cachen wie sonst auch - insbesondere Conditional GET wird unterstützt.
  4. Der Server antwortet mit Inhalten dieses GET Requests in denen auch normalerweise der Effekt des POST-Requests sichtbar wird.

Dieser Ablauf wird als PRG-Pattern bezeichnet und wenn man es implementiert stellt sich die Frage nach dem Caching eines POST Requests nicht. In der Praxis wird jedoch der POST Request des Clients häufig mit Status HTTP 200 OK und Inhalten beantwortet und bedeutet eine Verletzung der POST-Semantik: der Request wird gleichzeitig als Handlungsaufforderung und als Adresse zu einer URI, deren Content als Response geliefert wird, gehandhabt. Eine solche Response kann laut HTTP Spezifikation, Kapitel 9 auch für POST Requests gecached werden, wenn die Header entsprechend gesetzt sind - gemeint ist damit: für zukünftige GET Requests derselben URI. Der POST Request wird aber immer zum Server geschickt - alles andere macht konzeptionell auch kein Sinn.

GET POST
Semantik fordert eine Ressource mit einer URI an, HTTP safe und idempotent Handlungsanweisung an den Server, ändert Ressource, nicht HTTP safe, nicht idempotent
Caching ja, wenn die Header Date, Content-Cache und Expires gesetzt sind. Die Inhalte von POST Anfragen werden zwar gecached, können aber nur bei GET Anfragen derselben URL genutzt werden ebenso
Conditional GET ja, mit Last-Modified oder ETag, Browser versuchen hier POST und GET Anfragen gleich zu behandeln ebenso

Zusammenfassung - Best Practices für das Cachen bei Webanwendungen


  • Clientseitiges Cachen ist die billigste, effektivste und gleichzeitig am häufigsten vernachlässigte Optimierungsmaßnahme in Webanwendungen. Die wichtigste Regel lautet deshalb: Cachen Sie immer wenn möglich und so lange wie möglich, indem Sie die HTTP Header Expires, Date und Cache-Control entsprechend setzen. Die meisten Anwendungsfälle kommen mit Daten zurecht, die ein paar Sekunden oder Minuten alt sind. Erst recht, wenn der Nutzer darüber informiert ist, dass Daten nur in gewissen Abständen aktualisiert werden.
  • Cachen Sie möglichst öffentlich, indem Sie die public Directive benutzen. Cachen erfolgt nun potenziell auch in HTTP Proxys, diese versorgen hunderte Clients mit Inhalten und entlasten Ihren Server enorm. Die private Directive nur dann einsetzen, wenn es sich wirklich um Nutzer-spezifische Daten handelt.
  • Implementieren Sie das Conditonal GET mittels der Angabe des ETag (Hash-basiert) oder Last-Modified (Zeitstempel-basiert). Beim nächsten Request können Sie dann If-None-Match repsektive If-Modified-Since auswerten und gegebenenfalls ein 304 Not Modified zum Client schicken. Allerdings hilft Ihnen diese Strategie nur dann, wenn das Ermitteln des Hashes beziehungsweise der Aktualisierungszeit aus Sicht des Servers im Vergleich zum Neuerstellen der Inhalte Ressourcen spart.
  • Kombinieren Sie zeitbasiertes Cachen und Conditonal GET.
  • Implementieren Sie das PRG-Pattern, indem Sie nach jedem POST ein Redirect auf eine View senden. Auf diese Weise müssen Sie sich nicht mit der Frage auseinandersetzen, wie und ob überhaupt Ihre Clients die POST-Anfragen cachen. Außerdem vermeiden Sie so das Double Submit Problem. Der zusätzliche Roundtrip beim PRG Pattern ist übrigens seit HTTP1.1 netzwerktechnisch dank der keep alive Direktive vollkommen unproblematisch.

Und wie immer gilt auch hier: Testen Sie mit der Hilfe geeigneter Werkzeuge die Ergebnisse Ihrer Bemühungen und unterlassen Sie Optimierungen, wenn kein Effekt festgestellt werden kann.

Referenzen


Hypertext Transfer Protocol -- HTTP/1.1
Caching Tutorial for Webmasters
Google Articel HttpCaching
HTTP caching
Eine Seite, die mit XMLHttpRequests das Cacheverhalten des Browsers prüft
Web Caching Article von Brian D. Davison

copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved