RESTful Web APIs mit HTTP-Caching in der Praxis

von Hubert Schmid vom 2013-08-04

RESTful Web-APIs implementieren den Architekturstil des Webs, der implizit durch HTTP/1.1 beschrieben wird. Insbesondere bauen sie auf den Prinzipien auf, die sich im Web bewährt haben. Dazu gehört auch die Fähigkeit, Abfragen und Inhalte zwischenzuspeichern, um sowohl deren Anzahl als auch deren Umfang zu reduzieren.

So weit die Theorie – doch wie sieht es mit dem HTTP-Caching bei RESTful Web-APIs in der Praxis aus? Um dieser Frage nachzugehen, schaue ich mir drei verbreitete APIs genauer an, die sich selbst als RESTful beschreiben. Ich erkläre die für das Caching relevanten HTTP-Header und versuche daraus abzuleiten, wie Client und Origin-Server davon profitieren.

Apache CouchDB

CouchDB ist ein Dokumenten-orientiertes Datenbankmanagementsystem (DBMS), das eine RESTful HTTP-API für den lesenden und schreibenden Zugriff bietet. Dabei werden die gespeicherten Dokumente über URIs identifiziert. Das folgende Beispiel stammt aus der Dokumentation zu CouchDB: Bei der GET-Abfrage des Dokuments http://couchdb:5984/recipes/FishStew liefert das System die folgenden Cache-relevanten HTTP-Header:

Etag: "7-a19a1a5ecd946dad70e85233ba039ab2" Cache-Control: must-revalidate

Wichtig dabei ist, dass weder Age- noch Expires-Header enthalten sind. Der fehlende Age-Header bedeutet, dass die Antwort direkt vom Origin-Server stammt – also nicht aus einem HTTP-Cache. Der fehlende Expires-Header bedeutet in Kombination mit Cache-Control: must-revalidate, dass bei jedem folgenden Zugriff auf die gleiche URI das Dokument entweder nochmals vom Origin-Server geladen oder zumindest von ihm validiert werden muss. Darüber hinaus besagt die Direktive must-revalidate, dass die Ende-zu-Ende-Validierung in jedem Fall stattzufinden hat – selbst wenn ein HTTP-Cache so konfiguriert wäre, dass er entgegen den Angaben des Origin-Servers abgelaufene Dokumente ausliefern würde.

Für die Nutzer der API bedeuten diese Header, dass jeder zusätzliche HTTP-Cache die Laufzeit der Anfragen aufgrund des Umwegs verschlechtert. Profitieren können sie hingegen, wenn die Bandbreite zum Origin-Server der Engpass ist. Denn ein HTTP-Cache auf Seiten des Nutzers kann das übertragene Datenvolumen signifikant reduzieren, wenn hauptsächlich große Dokumente abgefragt werden.

Für den Origin-Server ergibt sich ein weiterer Nutzen: Er profitiert von HTTP-Caches, wenn die Validierung mit Etag effizienter ist als das vollständige Laden des Dokuments. Bei CouchDB ist das tatsächlich der Fall, wie ein Blick in den Quelltext belegt. Durch Datenredundanz kann das System die Bit-weise Gleichheit bestimmen, ohne auf das zu validierende Dokument zugreifen zu müssen. Dadurch verbessert sich die Effizienz des Origin-Servers, falls dadurch das Working-Set der Datenbank in den Hauptspeicher passt.

Atlassian JIRA

JIRA ist ein bekannter Issue-Tracker, der über eine RESTful HTTP-API lesenden und schreibenden Zugriff auf die Issues bietet. Für die lokale Installation benötigt man eine Lizenz, die man für Testphasen kostenlos erhält. Im folgenden verwende ich allerdings die Instanz von Atlassian, die sich anscheinend identisch zu eigenen Installationen verhält.

Der GET-Abruf des Dokuments https://jira.atlassian.com/rest/api/latest/issue/JRA-9 liefert praktisch nur einen HTTP-Header, der für das Caching relevant ist. Das macht die Sache relativ einfach.

Cache-Control: no-cache, no-store, no-transform

Die Angaben im HTTP-Header sind in gewisser Weise redundant. Entscheidend für einen HTTP/1.1-kompatiblen Cache ist nur die Direktive no-store. Sie unterbindet jegliche Art des Caching – einschließlich der Validierung. Denn sowohl gemeinsam als auch dediziert benutzte HTTP-Caches werden dadurch strikt angewiesen, das Dokument so schnell wie möglich, wirksam und dauerhaft aus dem Speicher zu entfernen.

Über die dahinter liegende Absicht von Atlassian kann ich lediglich mutmaßen. Eine plausible Erklärung ist, dass dadurch einerseits Inkonsistenz-Probleme vermieden werden, und dass man sich andererseits aus dem Caching keinen Nutzen verspricht. Denn im Gegensatz zu beispielsweise CouchDB sind in JIRA die per URI identifizierten Ressourcen nicht vollständig eigenständig, was die Bit-weise Validierung vergleichsweise aufwendig gestaltet.

Google+ API

Die dritte API stammt von Google und bietet Zugriff auf die Informationen aus ihrem sozialen Netzwerk. Im Gegensatz zu CouchDB und JIRA handelt es sich also um einen Dienst, der zentral von einem Unternehmen mehreren hundert Millionen Nutzern angeboten wird. Exemplarisch habe ich bezüglich des Cache-Verhaltens die drei folgenden URIs getestet:

https://www.googleapis.com/plus/v1/people/{userId} https://www.googleapis.com/plus/v1/people/{userId}/activities/public https://www.googleapis.com/plus/v1/activities/{activityId}

Die für das Caching relevanten HTTP-Header sind bei allen drei GET-Anfragen im Wesentlichen identisch und im Gegensatz zu CouchDB und JIRA wesentlich aussagekräftiger. Das folgende Listing zeigt die anonymisierten Antwort-Header für die letzte URI. Einen Age-Header enthält die Antwort hingegen nicht.

Expires: Sun, 04 Aug 2013 15:54:44 GMT Date: Sun, 04 Aug 2013 15:54:44 GMT Cache-Control: private, max-age=0, must-revalidate, no-transform ETag: "3Q5ljsm5uSwjkWmaDOW1Rwumb3r/hA4wLaT39GH6cOiwKsBoCWVPQqV"

Der Expires-Header enthält den gleichen Wert wie der Date-Header. Das bedeutet, dass die Antwort sofort abläuft – also nur mit Validierung wiederverwendet werden darf. Die Direktive max-age=0 hat die gleiche Bedeutung, wurde allerdings erst mit HTTP/1.1 eingeführt. Der redundante Expires-Header verhindert also, dass ältere HTTP-Caches die Antwort wiederverwenden.

Die Direktive private besagt, dass die Antwort nur für den gleichen Nutzer wiederverwendet werden darf. In der HTTP-Spezifikation ist damit der Cache des Browsers gemeint. Die Bedeutung für die Kommunikation zwischen zwei Systemen ist dagegen unklar. In diesem Fall spielt es allerdings auch keine Rolle. Denn durch max-age=0 wird bereits angewiesen, dass die Antwort sowieso nur mit einer Validierung durch den Origin-Server wiederverwendet werden darf.

Wie bei CouchDB besagt auch hier die Direktive must-revalidate, dass die Validierung selbst dann stattzufinden hat, wenn ein HTTP-Cache so konfiguriert ist, dass er die Ablaufzeit überschreibt. Schließlich verbietet die Direktive no-transform noch zwischengeschalteten Systemen, die Repräsentation der Antwort zu ändern. Zusammen mit private wäre ein solches Verhalten allerdings äußerst merkwürdig.

In Summe ergibt sich also ein ähnliches Bild wie bei CouchDB: Erstens verwendet Google selbst keinen vorgeschalteten HTTP-Cache. Zweitens ist jeder HTTP-Cache in der Kette zwischen Client und Origin-Server ein Umweg, der die Antwortzeiten gegenüber einer direkten Verbindung verschlechtert. Drittens sind die Antworten in der Regel so kurz, dass die Möglichkeit der Validierung aus Sicht des Nutzers keine Vorteile bietet. Und Viertens profitiert Google vermutlich von der Validierung mittels Etag, weil sie ihre Systeme daraufhin optimiert haben.

Fazit

Was ist also die Erkenntnis aus der Betrachtung dieser drei Beispiele? Die wichtigste Beobachtung ist, dass alle drei APIs das Caching-Verhalten relativ stark einschränken, und dafür den Cache-Control-Header von HTTP/1.1 verwenden. In allen Beispielen muss bei jedem Zugriff direkt oder indirekt auf den zugehörigen Origin-Server zugegriffen, wobei HTTP-Caches aus Sicht der Clients nur im Wege stehen. Die Origin-Server können dagegen von HTTP-Caches profitieren, wenn sie selbst einen Etag-Validierungsmechanismus implementieren, der durch Datenredundanz und einen passenden Schnitt der Ressourcen die effiziente Validierung ermöglicht. Für CouchDB tritt das auf jeden Fall zu, und bei Google+ sollte man zumindest davon ausgehen. Insgesamt also ein nüchternes Ergebnis.