Cookies, Sitzungen und Persistenz

Einleitung

HTTP (Hypertext Transfer Protocol) wurde für ein zustandsloses Anfrage-Antwort-Modell zur Datenübertragung von einem Server an einen Client entwickelt. Die erste Version, 1.0, unterstützte ein reines 1:1-Verhältnis zwischen Anfragen und Verbindungen (das heißt, pro Verbindung wurde genau ein Anfrage-Antwort-Paar unterstützt).

Version 1.1 erweiterte dieses Verhältnis auf N:1, das heißt, mehrere Anfragen pro Verbindung. Damit wurde der zunehmenden Komplexität von Webseiten Rechnung getragen, einschließlich der vielen Objekte und Elemente, die vom Server an den Client übertragen werden müssen.

Mit der Einführung von HTTP/2 unterstützte HTTP weiterhin ein Modell mit mehreren Anfragen pro Verbindung. Die grundlegendsten Änderungen betreffen den Austausch von Headern und den Wechsel von einer textbasierten zu einer binären Übertragung.

Im Laufe der Zeit wurde HTTP mehr als nur ein einfacher Mechanismus zur Übertragung von Texten und Bildern von einem Server an einen Client; es entwickelte sich zu einer Plattform für Anwendungen. Die weite Verbreitung des Browsers, seine Plattformunabhängigkeit und die Möglichkeit, Anwendungen unkompliziert bereitzustellen, ohne die hohen Kosten für die Unterstützung mehrerer Betriebssysteme und Umgebungen tragen zu müssen, waren zweifellos attraktiv. Leider wurde HTTP nicht als Transportprotokoll für Anwendungen konzipiert, sondern für die Übertragung von Dokumenten. Selbst moderne Einsatzbereiche von HTTP, etwa für APIs, gehen von einer dokumentähnlichen Nutzlast aus. Ein gutes Beispiel dafür ist JSON, ein als Text übertragenes Datenformat aus Schlüssel-Wert-Paaren. Dokumente und Anwendungsprotokolle sind zwar in der Regel textbasiert, doch damit enden die Gemeinsamkeiten auch schon.

Herkömmliche Anwendungen benötigen eine Möglichkeit, ihren Zustand beizubehalten, Dokumente hingegen nicht. Anwendungen basieren auf logischen Abläufen und Prozessen. Für beides muss die Anwendung jederzeit wissen, an welcher Stelle sich der Benutzer gerade befindet, und dafür ist ein Zustand erforderlich. Obwohl HTTP von Natur aus zustandslos ist, hat es sich de facto zum Transportprotokoll für Webanwendungen entwickelt. Durch einen der wohl am weitesten verbreiteten und nützlichsten Hacks der Technikgeschichte erhielt HTTP eine Möglichkeit, den Zustand während der gesamten Nutzung einer Anwendung nachzuverfolgen. Bei diesem „Hack“ kommen Sitzungen und Cookies ins Spiel.

Sitzungen

Zustandsloses in Zustandsbehaftetes umwandeln

Mithilfe von Sitzungen können Web- und Anwendungsserver Zustände aufrechterhalten. Diese einfachen Speicherbereiche werden jeder TCP-Verbindung zugeordnet, die mit einem Web- oder Anwendungsserver hergestellt wird, und dienen als In-Memory-Speicher für Informationen in HTTP-basierten Anwendungen.

Wenn ein Benutzer zum ersten Mal eine Verbindung zu einem Server herstellt, wird eine Sitzung erstellt und dieser Verbindung zugeordnet. Entwickler nutzen diese Sitzung dann, um darin anwendungsrelevante Daten zu speichern. Dabei kann es sich sowohl um wichtige Informationen wie eine Kunden-ID als auch um weniger wichtige Daten handeln, beispielsweise darum, wie die Startseite der Website angezeigt werden soll.

Das beste Beispiel für den Nutzen von Sitzungen sind Warenkörbe, da fast jeder von uns schon einmal online eingekauft hat. Die Artikel in einem Warenkorb bleiben während einer „Sitzung“ erhalten, da jeder Artikel in Ihrem Warenkorb in irgendeiner Form in der Sitzung auf dem Server hinterlegt ist. Ein weiteres gutes Beispiel sind assistentengestützte Anwendungen zur Produktkonfiguration oder -anpassung. Mit diesen „Mini“-Anwendungen können Sie verschiedene Optionen durchsehen und auswählen. Am Ende sind Sie meist überrascht, wie hoch die geschätzten Kosten für all die Extras sind, die Sie hinzugefügt haben. Während Sie sich durch die einzelnen „Optionsseiten“ klicken, werden Ihre zuvor ausgewählten Optionen in der Sitzung gespeichert, sodass sie problemlos abgerufen, ergänzt oder gelöscht werden können.

Moderne Anwendungen sind so konzipiert, dass sie zustandslos sind, doch ihre Architekturen entsprechen diesem Prinzip möglicherweise nicht. Moderne Skalierungsmethoden basieren häufig auf Architekturmustern wie Sharding, bei dem Anfragen anhand indexierbarer Daten wie Benutzername oder Kontonummer weitergeleitet werden müssen. Dies erfordert gewissermaßen einen zustandsbehafteten Ansatz, da die indexierbaren Daten mit jeder Anfrage übermittelt werden, um eine korrekte Weiterleitung und das ordnungsgemäße Verhalten der Anwendung sicherzustellen. In dieser Hinsicht erfordern moderne „zustandslose“ Anwendungen und APIs häufig eine ähnliche Betreuung und Pflege wie ihre zustandsbehafteten Vorgänger.

Das Problem besteht darin, dass Sitzungen an Verbindungen gebunden sind und Verbindungen, die zu lange inaktiv bleiben, wegen Zeitüberschreitung beendet werden. Zudem unterscheidet sich die Definition von „zu lange“ bei Verbindungen erheblich von der bei Sitzungen. In der Standardkonfiguration einiger Webserver wird eine Verbindung beispielsweise geschlossen, sobald sie 15 Sekunden lang inaktiv war, also keine weiteren Anfragen gestellt wurden. Eine Sitzung bleibt auf diesen Webservern hingegen standardmäßig 300 Sekunden beziehungsweise 5 Minuten lang im Arbeitsspeicher. Offensichtlich stehen diese beiden Einstellungen im Widerspruch zueinander, denn welchen Nutzen hat eine Sitzung, die einer Verbindung zugeordnet ist, sobald diese wegen Zeitüberschreitung beendet wurde?

Man könnte meinen, man könne einfach den Zeitüberschreitungswert für die Verbindung erhöhen, um ihn an den der Sitzung anzugleichen und so diese Diskrepanz zu beheben. Eine Erhöhung dieses Werts bedeutet jedoch, dass möglicherweise Arbeitsspeicher für eine Verbindung belegt wird, die unter Umständen gar nicht genutzt wird. Dadurch kann sich sowohl die maximale Anzahl gleichzeitiger Benutzer Ihres Servers verringern als auch letztlich dessen Leistung beeinträchtigt werden. Und Sie möchten die Zeitüberschreitung der Sitzung sicher nicht an die der Verbindung anpassen, denn die meisten Menschen brauchen mehr als fünf Minuten, um sich umzusehen oder ihr neues Spielzeug individuell anzupassen.

Wichtiger Hinweis: HTTP/2 behebt zwar einige dieser Probleme, bringt jedoch andere mit sich, die mit der Aufrechterhaltung des Zustands zusammenhängen. Obwohl dies laut Spezifikation nicht vorgeschrieben ist, unterstützen die gängigen Browser HTTP/2 nur über TLS/SSL. Beide Protokolle erfordern persistente Verbindungen, um die Leistungseinbußen durch eine erneute Aushandlung zu vermeiden. Dies wiederum setzt Sitzungsinformationen voraus, also zustandsbehaftetes Verhalten.

Das Ergebnis sind also Sitzungen, die weiterhin Speicherplatz auf dem Server belegen, selbst nachdem die zugehörigen Verbindungen aufgrund von Inaktivität beendet wurden. Dadurch werden wertvolle Ressourcen verbraucht und möglicherweise Benutzer verärgert, bei denen Ihre Anwendung schlichtweg nicht funktioniert.

Glücklicherweise lässt sich dieses Problem mithilfe von Cookies lösen.

Cookies

Die Krümelspur führt nach Hause

Cookies sind kleine Datenpakete, die vom Browser auf dem Client gespeichert werden. Sie können – und tun dies auch – allerlei interessante Informationen über Sie, Ihre Anwendungen und die von Ihnen besuchten Websites speichern. Der Begriff „Cookie“ leitet sich von „Magic Cookie“ ab, einem bekannten Konzept aus der UNIX-Welt, das sowohl die Idee als auch die Bezeichnung inspirierte. Cookies werden über den HTTP-Header „Cookie“ erstellt und zwischen Browser und Server ausgetauscht.

Cookie: JSESSIONID=9597856473431 Cache-Control: no-cache Host: 127.0.0.2:8080 Connection: Keep-Alive

Der Browser erkennt automatisch, dass er das Cookie aus dem HTTP-Header in einer Datei auf Ihrem Computer speichern soll, und verwaltet Cookies für jede Domain separat. Die Cookies einer bestimmten Domain werden vom Browser stets in den HTTP-Headern an den Server übermittelt, sodass Entwickler von Webanwendungen diese Werte einfach serverseitig in der Anwendung abrufen können.

Das Problem der Sitzungs-/Verbindungsdauer wird mithilfe eines Cookies gelöst. Fast alle modernen Webanwendungen erzeugen eine „Sitzungs-ID“ und übermitteln sie als Cookie. Dadurch kann die Anwendung die Sitzung auf dem Server auch dann wiederfinden, wenn die Verbindung, über die die Sitzung erstellt wurde, bereits geschlossen ist. Durch diesen Austausch von Sitzungs-IDs bleibt der Zustand selbst bei einem zustandslosen Protokoll wie HTTP erhalten. Doch was geschieht, wenn die Nutzung einer Webanwendung die Kapazität eines einzelnen Web- oder Anwendungsservers übersteigt? In der Regel wird ein Load Balancer oder – in heutigen Architekturen – ein Application Delivery Controller (ADC) eingesetzt, um die Anwendung so zu skalieren, dass Verfügbarkeit und Leistung den Anforderungen aller Benutzer gerecht werden.

In modernen Anwendungen kommen Cookies möglicherweise weiterhin zum Einsatz, doch andere HTTP-Header gewinnen zunehmend an Bedeutung. API-Schlüssel für die Authentifizierung und Autorisierung werden häufig über einen HTTP-Header übertragen. Gleiches gilt für andere benutzerdefinierte Header, die Daten enthalten, die für das Routing und die ordnungsgemäße Skalierung der Backend-Dienste erforderlich sind. Ob diese Daten über das herkömmliche „Cookie“ oder einen anderen HTTP-Header übertragen werden, ist weniger wichtig als die Erkenntnis, welche Bedeutung sie für die Gesamtarchitektur haben.

Das Problem dabei ist, dass Lastausgleichsalgorithmen im Allgemeinen lediglich darauf ausgelegt sind, Anfragen auf verschiedene Server zu verteilen. Lastausgleichsverfahren basieren auf branchenüblichen Algorithmen wie Round Robin, der geringsten Anzahl an Verbindungen oder der schnellsten Antwortzeit. Keiner dieser Algorithmen berücksichtigt den Sitzungszustand, sodass die einzelnen Anfragen desselben Benutzers an eine Anwendung jeweils auf unterschiedliche Server verteilt werden können. Dadurch ist der gesamte Aufwand zur Implementierung eines Zustands für HTTP nutzlos, da die in der Sitzung eines Servers gespeicherten Daten nur selten mit anderen Servern im „Pool“ geteilt werden.

Hier kommt das Konzept der Persistenz ins Spiel.

Persistenz

Das Band, das verbindet

Persistenz – auch als „Stickiness“ bezeichnet – ist eine von ADCs eingesetzte Technik, die sicherstellt, dass die Anfragen eines einzelnen Benutzers immer an den Server weitergeleitet werden, auf dem seine Sitzung begonnen hat. Einige Load-Balancing-Produkte und -Dienste bezeichnen diese Technik als „Sticky Sessions“, was eine durchaus treffende Bezeichnung ist.

Persistenz wird seit Langem beim Load Balancing von SSL/TLS-fähigen Websites eingesetzt, da es die Leistung erheblich beeinträchtigen würde, den rechenintensiven Aushandlungsprozess nach dessen Abschluss und dem Austausch der Schlüssel erneut zu starten. Daher implementierten ADCs eine SSL-Sitzungspersistenz, um sicherzustellen, dass Benutzer stets an denselben Server weitergeleitet wurden, mit dem sie sich ursprünglich verbunden hatten.

Im Laufe der Jahre machten Browserimplementierungen die Entwicklung einer Technik erforderlich, mit der sich die aufwendige Neuaushandlung dieser Sitzungen vermeiden lässt. Diese Technik wird als Cookie-basierte Persistenz bezeichnet.

Anstatt sich auf die SSL/TLS-Sitzungs-ID zu verlassen, setzt der Load Balancer beim ersten Zugriff eines Clients auf die Website ein Cookie, um die Sitzung eindeutig zu identifizieren. Bei nachfolgenden Anfragen greift er dann auf dieses Cookie zurück, um die Verbindung zum entsprechenden Server aufrechtzuerhalten.

Das Konzept der Cookie-basierten Persistenz wurde inzwischen auch auf Anwendungssitzungen übertragen. Dabei werden von Web- und Anwendungsservern generierte Sitzungs-ID-Informationen verwendet, um sicherzustellen, dass Benutzeranfragen während derselben Sitzung stets an denselben Server weitergeleitet werden. Ohne diese Funktion müssten Anwendungen, die einen Lastenausgleich erfordern, eine andere Möglichkeit zur gemeinsamen Nutzung von Sitzungsinformationen finden oder die Zeitüberschreitungen für Sitzungen und Verbindungen so weit erhöhen, dass die Anzahl der zur Unterstützung ihrer Benutzerbasis benötigten Server schnell nicht mehr zu bewältigen wäre.

Obwohl die gängigste Form der Persistenz mithilfe von Sitzungs-IDs realisiert wird, die im HTTP-Header übertragen werden, können ADCs heute auch andere Daten zur Aufrechterhaltung der Persistenz verwenden. Alle Daten, die in einem Cookie gespeichert oder aus den IP-, TCP- oder HTTP-Headern abgeleitet werden können, lassen sich zur persistenten Zuordnung einer Sitzung nutzen. Tatsächlich kann ein intelligenter ADC alle Daten in einer Anwendungsnachricht, die den Benutzer eindeutig identifizieren, verwenden, um eine persistente Verbindung zwischen dem Browser und einem Server aufrechtzuerhalten.

Fazit

HTTP mag zwar ein zustandsloses Protokoll sein, doch es ist uns gelungen, diesem allgegenwärtigen Protokoll dennoch einen Zustand aufzuzwingen. Mithilfe von Persistenz und Application Delivery Controllern lassen sich hochverfügbare, leistungsfähige Webanwendungen konzipieren, ohne die etwas fragile Integration von Cookies und Sitzungen zu beeinträchtigen, die für die Aufrechterhaltung des Zustands erforderlich ist.

Diese Funktionen verleihen HTTP einen Zustand, obwohl seine Implementierung und Ausführung weiterhin zustandslos bleiben. Ohne Cookies, Sitzungen und Persistenz hätten wir sicherlich ein zustandsbehaftetes Protokoll gefunden, auf dem wir unsere Anwendungen aufbauen könnten. Stattdessen vermitteln die Funktionen und Leistungsmerkmale von Application Delivery Controllern zwischen Browsern (Clients) und Servern, um diese Funktionalität bereitzustellen. Dadurch wird der Nutzen von HTTP über statische Webseiten und herkömmliche Anwendungen hinaus auf moderne, auf Microservices basierende Architekturen und den Liebling der digitalen Wirtschaft – die API – ausgeweitet.

VERÖFFENTLICHT AM 19. JANUAR 2018