Kolumne

EnterpriseTales: Die sieben, äh, acht Irrtümer verteilter Systeme

Lars Röwekamp

Das waren noch Zeiten, als unsere heißgeliebten monolithischen Enterprise-Anwendungen robust und sicher vor sich hinliefen. Heute dagegen baut man stark verteilte Systeme auf Basis von niedlichen, kleinen Services, die scheinbar permanent wegzubrechen drohen. Wie soll man das bitte noch im Griff behalten?

Beschäftigt man sich mit dem Thema Microservices und den zugehörigen Architekturansätzen, stößt man schnell auf das Thema Resilience. Reichte es früher aus, ein Softwaresystem stabil (reliable) zu gestalten, muss es heute belastbar und flexibel (resilient) sein. Warum ist das eigentlich so und was genau ist der Unterschied? Enterprise Tales auf Ursachenforschung …

Für eine monolithische Anwendung ist es essenziell, dass sie das macht, was man von ihr erwartet: Nämlich ohne Fehler und ohne Ausfälle zu funktionieren. Denn fällt ein solches System aus, fällt per Definition alles aus. Entsprechend viel Energie wird darauf verwandt, ein solches System fehlerfrei (Development) und hoch verfügbar (Deployment) aufzubauen. Während der Entwicklung sorgen ausgefeilte QA-Szenarien dafür, die Software möglichst Bug-frei zu bekommen. Ausfallsicherheit der Server erkauft man sich über Load Balancing und komplexe Cluster, oder man schiebt die Anwendung gleich mit entsprechenden SLAs in die Cloud.

Bei einem Microservice-basierten System ist die Ausgangslage ein wenig anders. Fällt einer der Services aus, können die anderen Services theoretisch weiterarbeiten, vorausgesetzt das System ist auf die Ausfälle bzw. das Fehlverhalten einzelner Services vorbereitet. Die Idee von Resilience ist also, mit temporären Problemen sinnvoll umgehen zu können. Was genau in diesem Kontext sinnvoll ist, klären wir später. Zusätzlich sollte ein resilientes System in der Lage sein, sich schnell und wenn möglich automatisch von einem aufgetretenen Problem zu erholen. Dabei sollte es wieder zur normalen Ausgangsposition zurückkehren, nachdem es in einen Ausnahmezustand gelaufen ist. Die Verwendung von Microservices löst dabei nicht automatisch das Problem. Ganz im Gegenteil. Es ist viel Energie notwendig, ein stark verteiltes System so aufzubauen, dass temporäre Ausfälle oder fehlerhafte Responses einzelner Services sauber abgefangen werden können, ohne dass dabei die Ergonomie der Anwendung leidet. Gleiches gilt für die automatische Wiederherstellung des Systems.

Die sieben, äh, acht Irrtümer verteilter Systeme

Dass verteilte Systeme, und dazu gehören nun einmal auch unsere Microservice-basierten Anwendungen, nicht immer so agieren, wie gewünscht, ist spätestens seit Peter Deutsch Auflistung der sieben größten Fehlannahmen bei verteilten Systemen bekannt. Diese hat James Gosling einige Jahre später um einen achten Punkt ergänzt:

  1. Das Netzwerk ist immer verfügbar.
  2. Die Latenz ist null.
  3. Die Bandbreite ist unendlich.
  4. Das Netzwerk ist sicher.
  5. Die Topologie ändert sich nicht.
  6. Es gibt einen Administrator.
  7. Die Transportkosten sind null.
  8. Das Netzwerk ist homogen.

Ein System wird niemals zu 100 Prozent fehlerfrei sein, insbesondere dann nicht, wenn es aus einer Menge von lose gekoppelten Services besteht. Ein System, das sich die Eigenschaft Resilience auf die Fahne schreibt, sollte also flexibel mit diesen Problemen umgehen können und nicht nur resistent gegen Fehler im Programmcode sein. Denn das reicht in der Regel nicht aus. Gleichzeitig sollte es in der Lage sein, sich schnell – wenn möglich automatisch – von einem Ausnahmezustand zu erholen.

Ein Plan B muss her

Unser Ziel ist es also, ein System aufzubauen, bei dem jeder Service Call potenziell fehlschlagen kann, ohne dass dies negative Implikationen auf das Gesamtsystem hat. Anders formuliert sollte das System auch dann mehr oder minder sinnvoll agieren, wenn ein Service nicht erreichbar ist oder fehlerhafte Daten und Formate liefert. Toleranz ist gefragt.

Wichtig dabei ist, dass wir von vornhinein den Fehlerfall und sinnvolle Alternativen als Szenarien mit einplanen. Das gilt insbesondere auch für die Daten und Informationen, die am UI angezeigt werden sollen oder für die Art, wie ein Fehler die User Experience beeinflusst. Wir brauchen einen fachlichen Plan B! Zu theoretisch? Ok, hier ein Beispiel: Nehmen wir einmal an, einem Nutzer eines Webshops sollen via Recommendation-Service Kaufvorschläge präsentiert werden. Leider ist der Service temporär nicht erreichbar. In diesem Fall wäre es sinnvoll, einen alternativen Service anzusprechen, der weniger qualifizierte, in diesem Fall also weniger personalisierte, Ergebnisse liefert oder auf Seiten des Clients mit gecachten und somit eventuell veralteten Daten arbeitet. In beiden Fällen erreichen wir wahrscheinlich nicht das optimale Ergebnis. Wahrscheinlich fällt das dem Nutzer aber gar nicht auf. Die Wahrscheinlichkeit, dass er ein für ihn passendes Produkt findet, ist zwar geringer, aber trotzdem haben wir dank unseres Fallback-Szenarios eine vertretbare User Experience erreicht.

Es stellt sich nun natürlich die Frage nach dem Aufwand. Ist dieser überhaupt noch vertretbar? Würde man alle Ausnahmebehandlungen (Timeouts oder Reaktion auf HTTP-Error-Codes) per Hand behandeln, sicherlich nicht. Aber zum Glück gibt es Tools, die sich genau auf diesen Aufgabenbereich spezialisiert haben. Mit ihrer Hilfe lassen sich Wenn-Dann-Szenarien für Ausnahmen konfigurieren. Der bekannteste Vertreter ist sicherlich Hystrix aus dem Netflix OSS Stack.

Früherkennung ist Pflicht, nicht Kür

Unabhängig von den implementierten oder konfigurierten Alternativszenarien ist es essenziell, dass sich das System nach einem Ausnahmefall schnell wieder erholt. Fehler oder Ausfälle müssen also schnellstmöglich erkannt werden. Das System muss sich automatisch wiederherstellen können, z. B. durch den Neustart eines Service. Noch besser wäre es natürlich, wenn es gar nicht erst in einen Ausnahmezustand gerät. Ein verdichtetes Echtzeitmonitoring ist hier der Schlüssel zum Erfolg. Und dies nicht nur, um Fehlsituation zu erkennen, sondern vor allem auch, um sie im Vorfeld zu vermeiden. Ziel des Monitorings ist es, negative Tendenzen zu erkennen und gezielt darauf zu reagieren. Dies gilt sowohl auf Ebene der Architektur, indem z. B. auf Änderungen der Antwortzeiten, des Durchsatzes oder der Circuit-Breaker-Status reagiert wird, als auch auf Ebene von Businessmetriken (weniger Anmeldungen, weniger Bestellungen oder Umsatzrückgang). Wie geht man nun aber vor, wenn es trotz Frühwarnsystemen doch zu einem Problem kommt? Auf jeden Fall gilt es, das Rad nicht neu zu erfinden, sondern auf etablierten Patterns aufzusetzen.

Resilience Patterns

Wie so häufig in der Wunderwelt der IT haben sich auch im Umfeld von resilienten Systemen etliche Patterns etabliert, von denen ich einige an dieser Stelle nennen möchte: Da wäre zunächst einmal Promises und Fallbacks aka „der fachliche Plan B“. Betrachtet man den Vertrag zwischen Consumer und Supplier als eine Art Versprechen, sollte man überlegen, wie man das Versprechen auch dann halten kann, wenn der ursprünglich angedachte Supplier nicht verfügbar ist. Im einfachsten Fall könnte man den Call wiederholen. Hierbei gilt es zu beachten, dass der Call eventuell von Seiten des Service schon bearbeitet wurde und nur die Antwort aussteht. Natürlich könnte man auch ein gecachtes Ergebnis statt der tatsächlichen Antwort nutzen oder einen alternativen Service gleicher oder ähnlicher fachlicher Qualität aufrufen. Auch die Verlagerung der Ergebnisberechnung in den Consumer oder ein zwischengeschaltetes System, das statischen Content ausliefert, ist ein mögliches Szenario. Bei allen genannten Fallback-Lösungen bekommen wir wahrscheinlich ein suboptimales Ergebnis. Dies ist für den Consumer in der Regel aber deutlich besser als kein Ergebnis, da das System so mit korrekten fachlichen Daten weiterarbeiten kann.

Wie bereits erwähnt, muss bei der Wiederholung einzelner Calls sichergestellt werden, dass sich das System weiterhin korrekt verhält. Seiteneffekte müssen ausgeschlossen werden. Man spricht in diesem Fall auch von Idempotenz. Was sich zunächst einfach anhört, ist in der Praxis ein nicht zu unterschätzendes Problem. Wiederhole ich zum Beispiel im Falle eines Timeouts einen Service-Call, der dafür sorgt, dass 50 Euro von meinem Konto auf ein anderes Konto transferiert werden, dann möchte ich natürlich nicht, dass die 50 Euro mehrfach von meinem Konto abgebucht werden. Eine Möglichkeit, die gewünschte Idempotenz sicherzustellen, liegt im Verzicht auf Delta-Messages. Anstatt also als Message „erhöhe um 50 Euro“ zu senden, würde ein „setze Kontostand auf 1 000 Euro“ helfen. Leider ist dies nicht immer so einfach möglich. Eine Alternative ist eine Infrastruktur, z. B. ein Messaging Bus, der in der Lage ist, Dubletten auszufiltern. Natürlich helfen auch eindeutige UUIDs dabei, Wiederholungstäter bei Requests oder Messages zu identifizieren.

Ein weiteres Mittel, ein System möglichst flexibel zu halten, sind Consumer Contracts. Sie helfen, nicht valide Responses des Suppliers zu vermeiden. Bei diesem Verfahren senden die verschiedenen Consumer ihre Erwartungen an die Schnittstelle an den Supplier. Es werden also die Teile einer Schnittstelle gesammelt, die tatsächlich von den verschiedenen Consumern genutzt werden. Der Supplier kann dabei solange ungestört Änderungen an der Schnittstelle vornehmen, solange keines der relevanten Attribute geändert wird. Anders formuliert ist eine Änderung der Schnittstelle nur dann ein Breaking Change, wenn sie mindestens einen der Consumer direkt betrifft. Wie nicht anders zu erwarten, gibt es das eine oder andere Tool, mit dem das Einhalten des Consumer-driven Contracts automatisch getestet werden kann.

Damit das Pattern Consumer Contracts funktioniert, muss noch eine weitere Voraussetzung erfüllt sein. Der Consumer darf nur die Teile validieren, die er tatsächlich benötig. Alles andere wird ignoriert. Das gilt z. B. auch für die Struktur der auszutauschenden Daten. Es gilt also das Credo: „Be conservative in what you send and be liberal in what you expect.“.

Fazit

Microservices ermöglichen es uns dank ihrer losen Kopplung, Systeme aufzubauen, die auch dann fachlich sinnvoll funktionieren, wenn einzelne Services nicht verfügbar sind oder fehlerhafte Responses oder Daten liefern. Leider bekommt man dieses Verhalten nicht geschenkt. Man muss schon einiges an Energie investieren, um am Ende ein flexibles System zu erhalten, das fachlich korrekt auf temporäre Ausfälle reagiert. Angefangen bei einfachen Load-Balance-Szenarien, über Timeouts, Circuit Breaker, Bulk Heads und Fallback-Services bis hin zu automatischer Service Discovery und Configuration Injection gibt es unzählige Ansatzpunkte zur Flexibilisierung eines verteilten Systems. Die Verwendung von Patterns, wie Promises und Fallbacks, Idempotenz oder Consumer Contracts, kann dabei ebenfalls sehr nützlich sein.

Der Schlüssel zum Erfolg ist neben einem Plan B das schnelle Erkennen und die automatische Regeneration des Systems. Wichtig ist dabei, dass Ausnahmezustände die Ausnahme bleiben und nicht zur Regel werden. Hier hilft umfangreiches Echtzeitmonitoring von technischen und fachlichen Kennzahlen. In diesem Sinne: Stay tuned …

Geschrieben von
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: