In der Heroku Dokumentation und in vielen anderen Quellen wird oft darauf hingewiesen, dass eine Heroku Applikation zustandslos sein sollte. Viele Anwendungen – vor allem im Java EE Bereich – verwalten aber Zustände im Arbeitsspeicher und/oder Dateisystem serverseitig. Hier möchte ich deshalb darauf eingehen, wieso sich Heroku so strikt gegen Zustände wehrt und wie man auch mit Heroku eine zustandsbehaftete Applikation erstellen oder migrieren kann.

Die Heroku Architektur

Um den Grund für die geforderte Zustandslosigkeit zu erklären, zunächst mal ein Blick auf die Heroku Architektur:

Heroku ist eine Cloud basierte Application Delivery Platform und zählt zu den so genannten PaaS (Platform as a Service) Providern. Anwendungen müssen so nicht mehr auf den eigenen Servern laufen, sondern werden von Heroku in der Cloud gehosted, administriert und gewartet. Aber auch Heroku selbst verwendet keine eigenen Server, sondern realisiert sein Cloud-Angebot über virtuelle Maschinen in Amazons Elastic Compute Cloud (Amazon EC2). Innerhalb der virtuellen Maschinen wird dem Entwickler ein Stack zur Verfügung gestellt.

Heroku StacksDer Stack (aktuell Celadon Cedar-14) liefert:

  • ein Betriebssystem (Ubuntu 10.x)
  • eine Ausführungsumgebung (je nach verwendeter Sprache)
  • Applikationsunterstützende Software (z.B. Data Stores, Monitoring, Logging) über Add-ons.

Der polyglotte Stack unterstützt und managed aktuell viele Sprachen bzw. Technologien (Ruby, Node.js, Java, Scala, PHP, Go etc.) und ermöglicht es, eine Applikation in der Cloud auszuführen, ohne sich Gedanken über die darunterliegende Infrastruktur machen zu müssen. Wird eine Anwendung nach Heroku gepushed (z.B. über Git), wird daraus ein Slug erstellt. Ein Slug ist nichts anderes, als eine komprimierte und paketierte lauffähige Version der Anwendung. Dieser Slug wird dann auf einen so genannten Dyno ausgeführt. Dynos sind isolierte, virtualisierte Unix Container, welche mit Hilfe des Stacks alles nötige bereitstellen, um die Anwendung auszuführen.

Warum sollte die Anwendung nicht zustandsbehaftet sein?

Eine Webanwendung läuft in Heroku auf einem Dyno. Requests werden über die Heroku Router an den Dyno (bzw. das Interface der darin enthaltenen Anwendung) weitergeleitet. Nimmt der Traffic zu und ist ein einzelner Dyno nicht mehr performant genug, können weitere Dynos hinzugeschaltet werden. Dabei wird auf den weiteren Dynos eine Kopie des Slugs erstellt und die Lastverteilung (Load Balancing) auf die Dynos wird transparent über den Heroku Dyno Manager geregelt.

Keine Sticky Sessions

Wichtig dabei ist, dass man keinen Einfluss auf die Arbeit des Load Balancers hat. Es kann nicht garantiert werden, dass zwei aufeinanderfolgende Requests eines bestimmten Clients auch beim selben Dyno landen. Somit ist auch nicht sicher, dass im Arbeitsspeicher gehaltene Session Information von einem zum nächsten Request noch zu Verfügung steht. Prinzipiell ist es möglich einen Load Balancer so zu realisieren, dass ein Client an eine bestimmte Gegenstelle gebunden wird. Dieses Konzept, auch „Sticky Sessions“ oder „Session Affinity“ genannt, wird aber von Heroku nicht unterstützt. Warum nicht? Bei Verwendung von Sticky Sessions kann nicht garantiert werden, dass das System sauber skaliert. Man stelle sich vor drei Clients werden durch Sticky Sessions an einen bestimmten Dyno gebunden. Plötzlich geht die Anfragerate dieser Clients explosionsartig in die Höhe. Schon kann Heroku nicht einfach neue Dynos hinzuschalten und die Anfragen verteilen – das System skaliert nicht sauber.

Share Nothing Architecture

Heroku ArchitectureHeroku verwendet eine Share-Nothing Architektur, die dafür sorgt, dass Speicher- und Systemressourcen gerecht verteilt werden. Jeder Dyno hat eine bestimmte Menge RAM allokiert – isoliert von anderen Dynos und Prozessen. So wird z.B. sichergestellt, dass eventuelle Memory Leaks einer Anwendung nicht andere, auf derselben Hardware laufende Anwendungen, beeinflussen. Dieses bietet somit auch Schutz gegen Buffer Overflows, Security Risiken und sonstiges – den Speicher betreffendes – Schadenspotential.

Dyno Isolation hat den Effekt, dass keine App eine andere, auf derselben virtuellen Instanz oder demselben Server kompromittieren kann. Über LXC (Linux Containers) und chroot wird diese Prozessisolierung erreicht, was die Prozesse sogar vom Basisbetriebssystem des Rechners isoliert. Zudem stellt chroot sicher, dass jeder Dyno sein eigenes, unabhängiges Dateisystem bekommt. Dies enthält ein Betriebssystem, den Stack, der benötigt wird, um den Code der Applikation auszuführen, und natürlich den deployten Code selbst. Das Dateisystem, wie auch die Umgebung, in der ein Dyno läuft, ist als „flüchtig“ zu betrachten. Der Load Balancer könnte weiter Requests desselben Clients an andere Dynos schicken, welche dann auch entsprechend in einer anderen Umgebung laufen. Daher ist auch das Dateisystem ungeeignet, um Session Information abzulegen, und sollte als Read-Only betrachtet werden.

Self Healing

Aber auch eine Anwendung, die nur auf einem Dyno läuft, sollte nicht zustandsbehaftet sein:

Heroku verwendet ein Self-Healing Konzept, um „Software Erosion“ zu vermeiden. Eine Applikation, die lange läuft, crashed früher oder später… sei es durch Patches, die nicht eingespielt wurden, Speicher, der vollläuft, Logfiles, die die Festplatte füllen, oder Sonstigem. Das Dyno Konzept in Heroku verhindert solche Crashes, indem Dynos mindestens einmal pro Tag „gecycled“ werden. Hierbei wird im Hintergrund ein neuer Dyno mit der aktuellsten Code Basis und der aktuellsten Version des Managed Stack eingespielt. Der alte Dyno wird heruntergefahren und durch den neuen – vielleicht auf einer anderen Hardware laufenden – ersetzt. Zudem behält sich Heroku vor, dieses Verfahren je nach Bedarf (Hardware, Speicher etc.) beliebig anzuwenden. Der Übergang, in dem der Load Balancer von dem alten auf den neuen Dyno verweist, erfolgt nahtlos – der Client bekommt davon nichts mit. Dieses Verfahren hat unter Anderem auch den Vorteil, dass Stack Updates automatisch mit eingespielt werden. Es bedeutet aber auch, dass alle zustandsbehaftete Information aus dem Arbeitsspeicher und/oder Dateisystem weg ist.

Was bedeutet das für die Heroku Applikation?

Welche Folgen hat die Heroku Architektur für die Applikation:

Die hohe Skalierbarkeit über hinzu- und weggeschaltete Dynos ist der Heroku Architektur geschuldet. Diese fordert ein, dass Dynos beliebig ausgetauscht werden können. Das bedeutet aber auch, dass sich die Anwendungen entsprechend verhalten sollten. Heroku ist kein guter Kandidat für Applikationen, die Zustände in Memory halten, oder lokal auf dem Server ablegen.

Eine Heroku Anwendung selbst sollte zustandslos und autark sein. Der Zustand des Dateisystems sollte so betrachtet werden als existiere nicht länger ein Request – Response Zyklus. Wenn man sich nicht daran hält, kann es zu unvorhergesehenen Konsequenzen kommen.

Beispiel: Wenn man dem User erlaubt Files hochzuladen und diese im Dateisystem der Dyno Umgebung ablegt, kann es zwar schon sein, dass das File in dem nächsten Request noch zu Verfügung steht. Wird der Dyno jedoch neu gestartet und/oder woanders ausgeführt, sind alle im Filesystem abgelegten Sachen weg. Immer, wenn ein Dyno neu gestartet oder hochskaliert wird, wird eine Kopie des letzten Slugs auf einen neuen Dyno deployed – dies inkludiert auch eine neue Kopie des Dateisystems – womit alles was zuvor abgelegt wurde weg ist.

Erstellte oder hochgeladene Files, die vorher auf der Festplatte abgelegt wurden, können in Heroku in einer Datenbank z.B. Postgres Datenbank abgelegt werden. Alternativ kann man sich eines Persistent Storage, wie z.B. Amazon Simple Storage Service (Amazon S3) bedienen. Zur Verwaltung und Handling von Logfiles gibt es einige Heroku Addons, die eine zentrale Speicherung ermöglichen und das Handling erleichtern.

Migration von Anwendungen nach Heroku

Manche Programme und Libraries speichern temporäre Dateien im Dateisystem. Beim Portieren einer solchen Applikation sollte der Code refactored und/oder auf ein anderes Programm bzw. Library ausgewichen werden.

Die meisten embedded Datenbanken speichern ihre Daten im lokalen Dateisystem. Für solche DBs ist Heroku nicht geeignet. Hier sollte auf eine Data Store Addon ausgewichen werden, welches sich nicht auf dieses Verfahren stützt.

Multiple Data Stores

Es kann auch Sinn machen, eine Kombination aus verschiedenen Data Stores zu benutzen. Man kann z.B. transaktionale read/write Operationen auf den eigentlichen Anwendungsdaten über eine relationale SQL Datenbank realisieren. Oft sind aber relationale Datenbanken, wie z.B. Postgres SQL, zu schwergewichtig für einfache Dinge, wie das Ablegen von Session Informationen. Hier bietet sich ein NoSQL Speicher an. Mögliche Kandidaten aus der Addon Liste sind hier z.B. Redis, CouchDB, Memcachier oder Mongo DB. Aber auch einfache Key-Value Stores, wie IronCache, sind auch eine Überlegung wert.

Session Management

Web Applikationen können mit Session Informationen auf unterschiedliche Art und Weise umgehen – nur ein paar Spielarten sind für Heroku geeignet. Noncentralized Session Information – in den Dynos verteilt – sollte vermieden werden. Herokus Share-Nothing Architektur unterstützt keine „Statefull Sessions“. Auch „Distributed Memory Sharing“ und „Memory Virtualisierung“ werden von Heroku nicht unterstützt. Auch der Ansatz aufeinanderfolgende Requests desselben Clients immer an gleiche Ziel weiterzuleiten – „Sticky Sessions“ – ist in Heroku durch das transparente Load Balancing ungeeignet.

Aber zum Glück gibt es Abhilfe

1te Möglichkeit: Die Session Information wird auf Client Seite gehalten. Ist der Client ein Web Browser, bieten sich hier in Cookies oder der Local Storage an. Aber wenn die Info clientseitig abgelegt wird, braucht es auch ein geeignetes Verschlüsselungsverfahren, damit die Daten vertraulich und integer behandelt werden. Dieser Ansatz bietet sich an, wenn das Volumen der Session Information gering ist. Im Falle eines Web Browsers gibt es definierte Limits und zudem müssen die notwendigen Informationen in jedem Request mit versandt werden.

2te Möglichkeit: Die Session Information zugänglich für alle Dynos in einem zentralen Repository ablegen. Hier kann eine relationale Datenbank verwendet werden, aber ein Key Value Store wie Redis oder Memcached sind angebrachter. Wenn man also eine bestehende Anwendung migrieren will, sollte man darauf achten wie und ob aktuell Session Information verwaltet wird. Dabei muss diese nicht vom Entwicklerteam explizit erstellt worden sein. Oft ist Memory basiertes Session Handling – transparent – über das genutzte Framework realisiert. Im Idealfall lässt sich das Framework so um-konfigurieren, dass ein Data Store aus dem Addon Umfeld verwendet wird. Wenn nicht, hat man hier schlechte Karten.

Heroku PlatformAber auch Frameworks, die einen clientseitigen Session Speicher realisieren, können Probleme bereiten. Oft wird auf Serverseite ein zufälliger Encryption Key generiert und im Dateisystem abgelegt – falls noch nicht vorhanden. Auf einem anderen Dyno ist das natürlich ein neu generierter anderer Key … schon können die Daten nicht mehr entschlüsselt werden. Hier bietet es sich an den Key selbst zu generieren und in einer Heroku Konfigurationsvariable zu speichern. Diese sind zentral abgelegt und für alle Dynos gleichermaßen zugänglich.

Auch zu beachten ist, dass manche Sprachen Build in Caching Mechanismen haben (page, data, http). Http Caching erfordert nicht viel oder gar keinen Aufwand, wenn es über Browser Caching realisiert ist. Aber Caching auf Serverseite funktioniert natürlich nicht optimal über mehrere Dynos verteilt.
Manche Java Applikationen stützen sich auf javax.servlet.http.HttpSesion, um den Zustand zu speichern. Wenn man für seine Anwendung z.B. Tomcat über den Webapp Runner verwendet, kann man das Session Handling an das Memcache Addon auslagern. Man stellt den Memchached-Session-Manager über eine Heroku Konfigurationsvariable zur Verfügung, verlinkt den Webapp Runner über ein Flag im Procfile, und schon wird Memcache transparent für die Anwendungslogik als Session Store verwendet.

Zustandsbehaftete Anwendungen in Heroku

Die Share Nothing Architektur von Heroku und die Flüchtigkeit der Umgebung, in der eine Anwendung läuft, machen ein Umdenken nötig. Für zustandsbezogene Daten gibt es drei Ansätze:

  • Verwaltung im Client
  • Verwaltung über ein externes Data Store Addon, welches allen Dynos zugänglich ist
  • Heroku Konfigurationsvariablen (sind immer allen Dynos zugänglich)

Verwendet man einen/mehrere dieser Ansätze für seine Anwendung, steht auch in Heroku einer zustandsbehafteten Anwendung nichts im Wege.

Weitere Infos zu Heroku gibt’s direkt auf der Heroku-Homepage: https://www.heroku.com