Loading...

Performanceprobleme ade: Wie Project Loom die Asynchronität abschafft

Performance
12. Juni 2023
4 Minuten Lesezeit
Beitrag teilen:
Gefällt dir der Beitrag?
Du wirst den Newsletter lieben!

Jeder der eine Backendanwendung mit mehr als einer Hand von Nutzern entwickelt weiß, dass die meisten Performanceprobleme mit I/O zutun haben. In modernen Webanwendungen sind dies in der Regel Calls übers Netzwerk. Sei es REST-Requests zu einem anderen Service, Queries an die externe Datenbank, oder die Kommunikation mit einer Middleware. All diese Fälle bearbeiten wir - bewusst oder unbewusst - in asynchronen Threads. Und damit blockieren wir den main-thread nicht mehr. Dies korrekt zutun ist nicht trivial. Project Loom wird im September mit Java 21 released. Virtuelle Threads werden es uns ermöglichen unsere Programme komplett sequenziell zu schreiben. Und dabei nutzen wir unsere Ressourcen immer noch optimal. Doch welche Möglichkeiten eröffnen sich für das Spring-Ökosystem und wie wird uns das in Zukunft beeinflussen?

Was passiert, wenn ein REST Request an das Spring Backend geschickt wird

Der embedded web server - im default Tomcat - liest kontinuierlich den konfigurierten Port für HTTP Requests. Sobald ein neuer Request im Stream identifiziert wird, delegiert Tomcat die Verarbeitung der Daten an einen separaten Thread. In einer standard Spring Applikation sind dies die “http-nio” Threads, die du in deinem Stacktrace findest. Diese Threads werden häufig als Request-Threads bezeichnet. Innerhalb dieser Threads werden abhängig von den mitgelieferten Metadaten, wie dem path oder authentication verschiedene Filter der Servlet FilterChain ausgeführt. Im Zuge dessen landet der Request am Ende in der Controller Methode, von wo aus die Logik der Applikation beginnt. Zu diesem Zeitpunkt befinden wir uns noch immer innerhalb des Request-Thread.

Blockende I/O Operationen verhindern die Verarbeitung neuer Requests

Angenommen in unserer Businesslogik ist es notwendig einen HTTP Request an einen externen Service zu schicken. Wir brauchen das Resultat, um den eigentlichen Request des Users zu verarbeiten. Wenn wir diesen synchron, beispielsweise mit dem RestTemplate ausführen, dann blocken wir den Request-Thread. In der default Konfiguration stellt Tomcat 10 Threads zur Verarbeitung zur Verfügung. Das bedeutet, dass zu keinem Zeitpunkt mehr als 10 Requests parallel verarbeitet werden können. Das wird zum Problem, wenn wir die Threads wie oben beschrieben mit einer Operation blockieren.

Wir entblocken unsere Request-Threads durch Asynchrone Verarbeitung

Die übliche Lösung für dieses Problem ist die Einführung neuer Threads, die auf die Antwort der blockierenden Operation warten. Dazu gibt es in Spring, je nach Situation, viele unterschiedliche Mechanismen. Im Kern ist die Idee gleich. Statt die blockierende Operation, also beispielsweise der call einer externen API, auf dem Request-Thread auszuführen, initialisieren wir einen neuen Thread. In den meisten Fällen werden diese Threads in eigenen Thread-Pools verwaltet.

Es ist nicht einfach die Übersicht über sein System zu behalten

In einer typischen Backendanwendung erstreckt sich durch dieses Verhalten die Logik über mehrere Threadgrenzen. Und das ist keine triviale Aufgabe. Es muss darauf geachtet werden, dass Thread-übergreifende-Daten mitgeliefert werden. Der SecurityContext ist ein Beispiel aus dem Spring Framework. Aber auch Monitoring relevante Daten, wie eine TraceId muss an die anderen, verarbeitenden Threads gereicht werden. Hinzu kommt noch, dass manche I/O Operationen für die Logik zwingend notwendig sind. In diesen Fällen muss auf höheren Layern bereits berücksichtigt werden, dass asynchrone Operationen ausgeführt werden. Beispielsweise deklarieren wir im Controller, dass ein Future geliefert wird, damit der web server weiß, dass er in der Zwischenzeit die Request-Threads wieder frei machen kann. Damit vermischen wir die Verantwortungen durch die Layer. Das ist nicht einfach. Es macht unsere Anwendung komplexer.

Die Optimierung der Thread-Pool Konfiguration ist schwierig

Um die optimale Performance aus einem System herauszukitzeln, ist es essenziell seine Thread-Pools an die Anforderungen anzupassen. Zu viele Threads sorgen für zu viele Context-Switches im Operating System. Zu wenige können zu Leerlauf führen, wenn alle zum gleichen Zeitpunkt in einem WAITING state sind. Wie sich die Threads verhalten hängt maßgeblich von der Logik ab. Das wird noch komplizierter, wenn diese Logik unterschiedliche Ausprägungen annehmen kann. Und wie gehen wir mit kurzfristigen Spikes im System um? All diese Fragen können und müssen beantwortet werden. Aber auch diese Anforderungen machen unser System deutlich komplexer.

Project Loom lässt uns das alles vergessen - Synchron all the way

Und genau hier wird Project Loom eine Revolution. Mit Project Loom werden virtuelle Threads eingeführt. Die JVM wird selbständig die Ressourcen der echten System-Threads verwalten. Wenn ein virtueller Thread eine blockende Operation ausführt, dann wird das die JVM nicht mehr stören. Die echten Hardwareressourcen können weiterhin perfekt genutzt werden. Das hat zur Folge, dass wir unseren gesamten Businesscode synchron schreiben können. Wir schreiben ihn so wie er logisch Sinn ergibt und die JVM kümmert sich um die optimale Nutzung der Systemressourcen. Es gibt keinen Nachteil einen pausierten virtuellen Thread im System zu haben. Sobald Tomcat virtuelle Threads unterstützt ist das hier diskutierte Beispiel kein Problem mehr. Tomcat kann für jeden Request einen virtuellen Thread erstellen und die JVM kümmert sich um die sinnvolle Verteilung der Systemressourcen.

Im September 2023 werden virtuelle Threads mit Java 21 released. Und Spring, sowie das gesamte damit verbundene Ökosystem arbeitet an einem übergreifenden Support bis Ende des Jahres. Für jeden Entwickler wird diese eine massive - positive - Veränderung mit sich bringen. Ich freue mich drauf!

Kennst du schon Marcus' Backend Newsletter?

Neue Ideen. 2x pro Woche!
Top