5 Aspekte über reaktive Programmierung in Java

#JAVAPRO #ReaktiveProgrammierung ||

Niedrige Antwortzeiten sind ein zentrales nichtfunktionales Feature modernerWeb-Anwendungen. Neben Caching, effizienter Programmierung und moderner Hardware ist die Verwendung reaktiver Bibliotheken eines der vielversprechendsten Konzepte um niedrige Antwortzeiten in einer Web-Anwendung sicherzustellen. In diesem Artikel werden fünf Aspekte herausgestellt, die man bei der
Verwendung des reaktiven Patterns berücksichtigen sollte.

Das Standard-Threading-Modell bei der Verwendung von Servlet-Containern (also der nichtreaktiven Programmierung) ist recht einfach: Jeder Request gegen eine Web-Anwendung wird in einem eigenen Thread ausgeführt. Auf Standard-Tomcat-Installationen stehen 200 Threads zur Verfügung, die parallel verarbeitet werden können. Abhängig von der Ausführungsdauer eines Threads, kann somit abgeschätzt werden, wie lange ein neuer Request warten muss, bevor er abgearbeitet wird. An dieser Stelle sei darauf hingewiesen: Wenn man weniger als 200 Requests pro Sekunde und weniger als durchschnittliche 500 ms Antwortzeiten hat, ist das Threading-Modell eigentlich kein Aspekt über den man sich Gedanken machen müsste. Für eine anvisierte Antwortzeit von < 200 ms, kann man 1.000 Request pro Sekunde bearbeiten. Erst wenn man noch mehr Threads braucht, werden Requests tatsächlich blockiert und die Web-Anwendung an sich wirkt (zumindest teilweise) langsam. Das imperative Threading- und Programmiermodell ist an dieser Stelle aber noch nicht erschöpft, so kann beispielsweise der Thread-Pool vergrößert werden.

Bei der reaktiven Programmierung spielt die Anzahl der Requests hingegen eine untergeordnete Rolle. Es werden konstant zwei Threads pro logischem CPU-Kern zur Verfügung gestellt. Auf modernen Maschinen hat man also 16 Threads zur Verfügung und kann diese Zahl auch sofort wieder vergessen. Im Kern der reaktiven Programmierung werden schließlich keine Threads blockiert. Im Übrigen ist ein wichtiger Seiteneffekt des Threading-Modells, dass – zumindest in der Theorie – weniger Speicher zum Ausführen der Web-Anwendung benötigt wird.

Das Messaging-Add-on in commercetools

Um die erhofften Auswirkungen von reaktiver Programmierung auf der Leistungsfähigkeit von Web-Anwendungen in Java zu testen, wurden Lasttests geschrieben und gegen eine reaktive und eine imperative Implementierung ausgeführt. Dabei wurde jeweils Spring-Boot einmal mit dem Reactive-Stack und einmal mit dem Servlet-Stack verwendet. Als Szenario wurde ein Add-on zu commercetools erstellt, das über CustomObjects die Kommunikation zwischen Peers ermöglicht. Während der Lasttests wurden Response-Zeiten, CPU- und RAM-Nutzung untersucht und mit 500, 1.000 und 2.000 Peers gleichzeitig gearbeitet. Die Clients (Peers) sendeten und empfingen Nachrichten. Als Kommunikationsprotokoll wurden Web-Sockets verwendet.

Im Gegensatz zu den theoretisch hergeleiteten Erwartungen war die reaktive Implementierung lediglich beim Speicherverbrauch gleichauf mit der imperativen Variante. Bei CPU-Verbrauch und vor allem bei den Antwortzeiten war in unserem Szenario die imperative Version deutlich performanter (Abb. 1). Überraschenderweise war auch die Anzahl der verwendeten Threads in der reaktiven Variante nicht geringer.

Speicherverbrauch, CPU-Verbrauch und Antworten mit Web-Sockets bei 2000 Peers. (Abb. 1)

Speicherverbrauch, CPU-Verbrauch und Antworten mit Web-Sockets bei 2000 Peers. (Abb. 1)

Schlussfolgerung 1: Es gibt Szenarien, in denen eine gleichwertige reaktive Implementierung keine bessere Leistung als eine imperative Implementierung ermöglicht. Bei der Ursachenforschung stellte sich heraus, dass die Verwendung von Web-Sockets in Kombination mit dem reaktiven Stack ungünstig war. Jede Web-Socket-Verbindung wurde als eigener Thread gehalten – und auch nicht wieder geschlossen, selbst wenn die Web-Socket-Verbindung geschlossen wurde. Wenn man stattdessen eine reine HTTP-Verbindung verwendet, gibt es weiterhin signifikante Unterschiede zugunsten der imperativen Variante, diese sind aber deutlich geringer.

Schlussfolgerung 2: Stand heute sollte man den reaktiven Stack (mit Spring) nicht in Verbindung mit Web-Sockets verwenden. Ist allerdings davon auszugehen, dass das Problem in naher Zukunft gelöst wird.

Das Laborexperiment

Da die Ergebnisse trotzdem verwunderlich sind, wurde ein weiteres Szenario als Laborexperiment entwickelt, um nach weiteren Gründen für die vergleichsweise schlechte Performance der reaktiven Programmierung zu suchen. Im Laborexperiment wurde ein einfacher Verzögerungsservice als Remote-Server der Anwendung an die Stelle von commercetools gesetzt. Der Remote-Service wartete eine kurze Zeit und antwortet schließlich.

Antwortzeiten und Durchsatz im Laborexperiment bei 3000 Peers und 200 ms Latenz. (Abb. 2)

Antwortzeiten und Durchsatz im Laborexperiment bei 3000 Peers und 200 ms Latenz. (Abb. 2)

In (Abb. 2) sieht man die Ergebnisse des Laborexperiments. Aus der Abbildung wird deutlich, dass der reaktive Stack deutlich schneller antwortet als der imperative Stack und damit die Erwartungen des Experiments erfüllt. Also stellt sich die Frage, was denn nun der Unterschied zwischen den beiden Szenarien ist. Um es kurz zu machen: Die Verwendung von commercetools als API lieferte zu schnelle Antwortzeiten, sodass ein imperativer Thread nur kurz blockiert wurde. Damit konnte die erhöhte Parallelitätskapazität des reaktiven Konzepts in diesem Szenario gar nicht greifen. Der höhere Aufwand der reaktiven Bibliotheken führte schlussendlich dazu, dass die reaktive Variante langsamer war als die imperative Variante. Wird ein imperativer Thread allerdings länger von einer API, einer Persistenz oder einem Service blockiert, ist der reaktive Stack tatsächlich schneller.

Schlussfolgerung 3: Die Vorteile von reaktiver Programmierung werden dann deutlich, wenn klassische Threads länger blockiert wären und damit die Leistungsfähigkeit des Gesamtsystems nicht ausgenutzt wird.

Reaktive Programmierung in performancekritischen Teilen der Anwendung

Wie aus den drei Schlussfolgerungen hervorgeht, können die Auswirkungen reaktiver Programmierung durchaus äußerst positiv aber auch negativ sein. Positiv ist auf jeden Fall, dass man das Beste aus beiden Welten in einer Applikation nutzen kann. Dabei ist vor allem das erste Szenario zu beachten: Immer, wenn man die Web-Anwendung, eine langsame API, Persistenz oder einen langsamen Service verwendet, kann der reaktive Stack verwendet werden, um trotzdem eine hohe Anzahl von Requests zu bewältigen, ohne dass darunter die Antwortzeit leidet. Dadurch kann man eventuell sogar weniger potente Hardware wählen, da die Hardware insgesamt besser ausgenutzt wird.

Schlussfolgerung 4: Langsame, blockierende Aufrufe sollten durch reaktive, nicht-blockierende Aufrufe mit Augenmaß ersetzt werden, um die Leistungsfähigkeit einer Applikation zu steigern.

Ausblick

Ein weiterer Aspekt ist die Streaming-Eigenschaft des neueren reaktiven Java-Frameworks: Immer dann, wenn große Mengen an Daten als Stream empfangen werden, ermöglicht reaktive Programmierung das Bearbeiten des Streams schon bevor die gesamte Antwort eingetroffen ist. Das sollte einen enormen Einfluss auf die Leistungsfähigkeit einer Applikation haben. Commercetools selbst experimentiert bereits mit einer solchen API.

Schlussfolgerung 5: In Szenarien, in denen weder größere Last noch langsame APIs erwartet werden, können und sollten Stream-APIs genutzt werden, wenn diese zur Verfügung stehen, um etwa größere Datenmengen (größere Mengen von Objekten) performant zu bearbeiten.

Max Edenharter schrieb seine Masterarbeit über das vorgestellte Thema in Zusammenarbeit mit der ARITHNEA GmbH. Bereits seit 2014 war er als Werkstudent zunächst bei der adesso AG und später in deren Tochtergesellschaft ARITHNEA tätig. In seiner Wahlheimat Hamburg arbeitet er inzwischen als Softwareentwickler für den Onlinehändler OTTO.

 

 

 

Gabor Meißner ist seit fast zehn Jahren im Java- und JVM-Umfeld als Dozent und Software-Entwickler
tätig. Er wurde 2013 in Jena zum Dr. rer. nat. promoviert. Seit dem arbeitet er in den Bereichen eMarketing, IOT Recommendations und eCommerce. Gabor Meißner ist bei der adesso-Tochtergesellschaft ARITHNEA beschäftigt.

Carolyn Molski


Leave a Reply