Why Reactive: Reaktive Architekturen und ihre Geschichte

Seite 5: Nicht blockierendes Netzwerk als neuer Ansatz

Inhaltsverzeichnis

Auch auf der Netzwerkebene gibt es noch Optimierungspotenzial. Schließlich ist das auf „Request and Response“ ausgerichtete HTTP-Protokoll nicht besonders gut für eine asynchrone Übertragung kontinuierlicher Datenströme zwischen Client und Server geeignet. Vor allem dann nicht, wenn der Server dem Client asynchron Daten senden möchte. Dieses Problem löst das Anfang der 2010er-Jahre eingeführte WebSockets-Protokoll. Im Gegensatz zum HTTP-Protokoll lassen sich Daten asynchron und bidirektional zwischen Client und Server übertragen. Damit wird der Client nach einer Anfrage nicht blockiert, sondern erhält das Ergebnis vom Server asynchron, sobald es bereitsteht.

Allerdings gibt es noch mehr Luft nach oben, etwa beim Umgang des Servers mit den Threads. Der Server benötigt für jeden Client genau einen Thread. Dieser erhält erst dann die Freigabe für einen anderen Client, wenn die Verbindung zwischen Server und Client geschlossen ist. Da nun in der Regel viele Clients um nur wenige Threads auf dem Server konkurrieren, kann es schnell zu einer Blockade kommen. Vor allem dann, wenn besonders viele Clients zugleich Operationen auf dem Server durchführen.

Dieses Problem griff Java SE unter dem Namen Non-blocking IO (NIO) erstmals mit der Version 1.4 auf, Java 9 führte es dann weiter. Der Server besitzt nur noch einen Main Thread, der die Verbindungen entgegennimmt und die Operationen, die über das Netzwerk eintreffen, über einen Event-Loop verarbeitet. Eine bestimmte Anzahl von Worker Threads, die über Thread-Pools bereitgestellt werden, kümmert sich dann um eine effiziente Bearbeitung der Operationen.

Die Begrenzung der Geschwindigkeit des Datenstroms zwischen Client und Server ist ein weiteres Thema: Server-Produkte wie Netty oder Undertow implementieren NIO und erweitern es um Features wie das Backpressure-Protokoll. Es ermöglicht, dass Client und Server die Geschwindigkeit des Datenstroms aushandeln können, sodass es zu keiner Überlastung des Servers kommt.

Die Reactive Streams lassen sich nicht nur auf die Thread-Ebene, sondern auch auf die Netzwerk-Ebene übertragen. Beispielsweise kann dann ein Server die Rolle eines Publishers einnehmen und ein Client die Rolle des Subscribers. Der Server bearbeitet fortan die Datenströme und sendet die Ergebnisse über das Netzwerk zurück an den Client. Dieser Ansatz steckt beispielsweise in WebFlux, einem neuen Framework aus dem Spring-Ökosystem. Weitergedacht lässt er sich auch auf Datenbanken, Message-Broker oder andere Middleware-Komponenten ausweiten. Mit R2DBC gibt es bereits eine Spezifikation zu reaktiven Treibern für SQL-Datenbanken, die die klassischen blockierenden JDBC-Datenbanktreiber im reaktiven Umfeld ablösen sollen.

Eine reaktive Architektur ist nur so gut wie das schwächste Glied in der Kette von Architekturkomponenten. Wenn eine Datenbank nicht reaktiv ist, sondern blockiert, dann bleibt auch der Rest der Anwendung stecken. Folglich ist das Übertragen des reaktiven Konzepts auf alle Architekturkomponenten ein wichtiger und notwendiger Schritt.

Die Streaming-Architektur erfüllt die Forderungen aus dem eingangs erwähnten Reactive Manifesto schon weitgehend, und zwar deutlich besser als die bisher betrachteten Architekturstile. Der Fokus dieser Architektur liegt dabei allerdings nicht auf der Server- und Anwendungsebene, sondern vielmehr auf der Daten-, Thread-, und Netzwerkebene. Auf der Datenebene wird ein kontinuierlicher unendlicher Datenstrom vorausgesetzt, so wie das in den Anwendungsfällen vorkommt. Die Kleinteiligkeit der Daten ermöglicht es erst, die Anforderungen aus dem Manifesto zu erfüllen.

Auf der Thread-Ebene wird die Elastizität durch eine Parallelisierung der Operationen erreicht. Dabei spielt ein effizienter Einsatz der Threads die entscheidende Rolle. Effiziente Push-Verfahren ersetzen konventionelle Synchronisierungsansätze der Threads über Polling-Verfahren. Auf der Thread-Ebene spielt Resilienz allerdings eine untergeordnete Rolle, denn die Threads stehen nicht unter Beobachtung und man muss sie im Fehlerfall auch nicht neu starten.

Auf der Netzwerkebene wird die Elastizität durch den Einsatz von NIO erreicht. Damit lässt sich eine große Anzahl von Clients gleichzeitig über das Netzwerk vom Server bedienen, und es lassen sich lange Operationen ausführen, ohne andere Clients zu blockieren. Das Backpressure-Protokoll, über das Clients mit dem Server den Datendurchsatz verhandeln, erhöht die Resilienz, da es kritische Lastsituationen vermeidet.