Spring WebClient der reaktive HTTP Client im Performance-Vergleich zum RestTemplate
Spring Reactive ist der moderne, reaktive Technologie Stack von Spring. Es ist die skalierbare, resiliente, responsive und Event-basierte Alternative zum klassischen Servlet Stack - dem bisherigen Standard in jedem Spring Web Projekt. Teil des reaktiven Stacks ist Spring WebFlux und dessen WebClient zum Verschicken von HTTP Requests. In diesem Artikel zeige ich, dass der WebClient unter Last deutlich schneller als der klassische Spring RestTemplate ist.
Spring WebFlux
Das Reactor Projekt bildet die Grundlage des reaktiven Stacks in Spring. Es bietet eine Event-basierte, nicht blockierende Architektur, so dass darauf aufbauende Anwendungen mehr Leistung aus ihren CPU-Ressourcen herausholen.
Spring WebFlux ist Teil des reaktiven Stacks und damit das reaktive Gegenstück zu Spring MVC im klassischen Servlet Stack.
Weitere Infos zu Spring MVC findet ihr in meinem Blog: spring-mvc-thymeleaf.html
Weitere Details zum Spring Reactor Projekt findet ihr hier: https://spring.io/reactive
Von dort stammt auch die Gegenüberstellung von Servlet und reaktivem Stack. Diese hilft die einzelnen Komponenten besser einzuordnen.
Gegenüberstellung Reactive und Servlet Stack |
Hier konzentriere ich mich auf den Vergleich der HTTP Clients aus beiden Stacks:
- WebClient: Aus dem reaktiven Stack als Teil der Spring WebFlux Bibliothek
- RestTemplate: Aus dem Servlet Stack als Teil der Spring Web Bibliothek
mongodb-und-spring-data.html
HTTP Requests mit WebClient und RestTemplate
RestTemplate
Das RestTemplate kann noch viel mehr, z.B. Header mitschicken, in komplexere Typen als String parsen und unterstützt natürlich auch alle anderen HTTP Methoden (POST, PUT etc.). Weitere Infos zum RestTemplate findet ihr z.B. hier:
RestTemplate verschickt 3 synchrone Requests - Haupt-Thread blockiert bis Response ankommt. |
WebClient
// den HTTP Response Body als String und
- client ist hier dieselbe WebClient Instanz wie im vorherigen Code-Beispiel.
- Mit der Methode bodyToMono legen wir fest, dass als Antwort auf diesen Request ein Mono erwartet wird. Ein Mono ist ein Datenstrom, der höchstens eine Instanz vom definierten Datentyp (hier String.class) übertragen wird. Die Alternative zum Mono ist der Flux für Datenströme, die beliebig viele Instanzen des definierten Datentyps enthalten. Würden wir statt String, einen Datentyp wie Customer, Employee, Item, Car etc. verwenden, wird klar, dass man besser Flux bei Antworten in Listen-Form verwendet.
In diesem Blog-Artikel werde ich Flux und Mono nicht weiter betrachten, obwohl diese Typen sehr wichtig für die Performance in der reaktiven Programmierung sind! - Statt der Methode block verwenden wir beim reaktiven Programmieren die Methode subscribe. subscribe schickt den Request ab und meldet einen Lambda-Ausdruck an, der die Antwort verarbeitet, wenn diese ankommt. Damit ist subscribe eine nicht blockierende Methode und der Thread, der mit der WebClient Instanz den Request abgeschickt hat, läuft einfach weiter. Im Lambda Ausdruck findet eine asynchrone Verarbeitung der Response statt. Hier im Beispiel wird der Response Body einfach mit System.out in die Konsole geschrieben. Die Response könnte aber auch in einer Datenbank gespeichert werden oder auf andere sinnvolle Weise verarbeitet werden.
WebClient verschickt 3 asynchrone Requests - Haupt-Thread wartet nicht auf die Responses. |
Web-Client mit Proxy
.type(Proxy.HTTP).nonProxyHosts("*.intranet.de"))
- Statt des WebClient Default ClientConnector erzeugt ihr einen eigenen basierend auf dem Netty HttpClient.
- Im Netty HttpClient wird der Proxy mit type, host, port und optional nonProxyHosts definiert. Im HttpClient können noch weitere Einstellungen, wie z. B. Timeouts konfiguriert werden.
- Ansonsten wurde hier nur der WebClient-Builder anstelle der zuvor gezeigten create Methode verwendet.
Performance-Vergleich WebClient und RestTemplate
Das zeigt dann auch der Test-Code, den ich zu diesem Artikel geschrieben habe.
Ihr findet den Code hier und könnt damit eigene Performance-Tests machen:
Requests gegen normale REST Webservices
- ClassicBackendApiClient Bean schickt einen GET Request mittels einer RestTemplate Instanz an einen REST-Service im Internet. Das funktioniert genau so wie im vorherigen Abschnitt RestTemplate gezeigt. Die Response wird dann zusammen mit einer Request-Id geloggt. Der Test-Ablauf ist im ersten Sequenz-Diagramm zum RestTemplate gezeigt.
- ThreadedClassicBackendApiClient Bean erstellt zum Verschicken jedes einzelnen REST-Requests einen eigenen Thread mittels CompletableFuture.runAsync. Im Thread wird die ClassicBackendApiClient Bean zum Verschicken und Loggen des Requests wiederverwendet. Im Code sieht das so aus:
@Component
public class ThreadedClassicBackendApiClient {
@Autowired ClassicBackendApiClient client;
public void callApi(final int requestId) {
CompletableFuture.runAsync(() -> client.callApi(requestId));
}
}
Weitere Infos zum CompletableFuture aus Java 8, findet ihr zum Beispiel in diesem Buch:
Buch: Java ist auch eine Insel (zu Java 14) - ReactiveBackendApiClient Bean verschickt den gleichen REST-Request wie die anderen 2 Beans nur diesmal mit einer WebClient Instanz. Der Code dazu sieht aus wie im vorherigen Abschnitt WebClient gezeigt - dort zeigt auch das zweite Sequenz-Diagramm den Test-Ablauf.
Hier noch der Code des JUnit Tests zum Messen der Performance:
measureCalls(client));
@Autowired ThreadedClassicBackendApiClient client) {
measureCalls(client));
measureCalls(clientBean));
- Alle Beans implementieren das Interface BackendApiClient mit der Methode callApi. Daher konnte ich die Logik zum Messen der Zeit und das Abschicken aller Requests in einer for-Schleife in die Hilfs-Methode measureCalls auslagern.
- @Slf4j ist eine Annotation von Lombok, die den Logger im Attribut log bereitstellt. Schaut euch für weitere Details die Webseite von Lombok an: https://projectlombok.org/
- Wie @Autowired funktioniert habe ich hier erklärt: kernkonzepte-von-spring.html
Testergebnisse
- NUMBER_OF_CALLS = 2
- Auf meinem Rechner schwanken die Zeiten zwischen 0,8 und 3 Sekunden für alle Beans.
Meist waren asynchron arbeitenden Beans minimal schneller. Wenn man allerdings mit so wenigen Requests arbeitet, kann man keine ernsthaften Aussagen zu Performance-Unterschieden machen. - NUMBER_OF_CALLS = 15
- ClassicBackendApiClient Bean Zeiten zwischen 4 und 5,2 Sekunden
- ThreadedClassicBackendApiClient Bean Zeiten zwischen 1,2 und 2,4 Sekunden
- ReactiveBackendApiClient Bean Zeiten zwischen 2 und 3 Sekunden
- Es fällt also schon bei 15 Requests auf, dass die asynchrone Verarbeitung durch ThreadedClassicBackendApiClient oder ReactiveBackendApiClient Bean schneller ist als die synchrone Verarbeitung durch die ClassicBackendApiClient Bean.
- NUMBER_OF_CALLS = 150
- ClassicBackendApiClient Bean braucht nun circa 30 Sekunden. Wenn man das mit den vorherigen Zeiten vergleicht, erkennt man einen linearen Zusammenhang. Jeder weitere Request verlängert die Gesamtdauer, da alle Request nacheinander ausgeführt werden.
- ThreadedClassicBackendApiClient Bean Zeiten zwischen 7 und 10 Sekunden
- ReactiveBackendApiClient Bean Zeiten zwischen 3 und 5,5 Sekunden
- Die asynchrone Verarbeitung wird nun im Vergleich zur synchronen erheblich schneller.
- Außerdem fällt auf, das sich bei dieser Request-Anzahl auch messbare Unterschiede zwischen der nicht blockierenden, reaktiven Programmierung (ReactiveBackendApiClient ) und der im jeweiligen Thread blockierenden, klassischen Programmierung (ThreadedClassicBackendApiClient) zeigen.
- NUMBER_OF_CALLS = 500
- Weitere Messungen mit noch größeren Request-Anzahlen zeigen, dass die Unterschiede immer größer werden. Das RestTemplate ist auch beim Einsatz in Threads durch die Anzahl der verfügbaren Threads und Kerne limitiert, während die reaktive Programmierung durch das nicht blockierende Warten auf eingehende Nachrichten (subscribe) seine Vorteile voll ausspielen kann. Bei Tests mit 500 Requests war der WebClient um circa Faktor 5 schneller als das RestTemplate in Threads.
Performance-Testergebnisse als Liniendiagramm |
Kommentare