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
Beim reaktiven Programmieren geht es nicht nur um die Verwendung einer anderen Art von HTTP-Kommunikation. Sondern geht es auch darum, dass wir wann immer möglich auf CPU blockierende Operationen verzichten und eine alternative, nicht blockierende Operation verwenden. Daher gibt es auch reaktive Repositories für den nicht blockierenden Zugriff auf Datenbank als Teil von Spring Data. Spring Data in der klassischen Form, habe ich in diesem Blog-Artikel vorgestellt: 
mongodb-und-spring-data.html

HTTP Requests mit WebClient und RestTemplate

RestTemplate

HTTP Requests mit dem RestTemplate kennt Ihr vermutlich schon. Das RestTemplate ist eine elegante und einfache Form um HTTP REST Requests zu verschicken. Im Code sieht das z.B. so aus:

    String responseBody = new RestTemplate().getForEntity(
            "http://someurl.de/something", 
            String.class)
        .getBody();

In diesem Code Beispiel erzeuge ich eine neue Instanz des RestTemplate, um dann einen HTTP GET Request abzuschicken (getForEntity). In der Methode getForEntity definiere ich die URL und den Klassen-Typ (String.class) in den ich den Body der Response (.getBody()) automatisch geparst haben möchte. Statt String würde man hier besser fachliche Klassen verwenden, die passend zur Response sind (z.B. Employee, Customer, Car, Item etc.).
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

Um den WebClient verwenden zu können, müssen wir die Spring WebFlux Bibliothek zu unserem Projekt hinzufügen. Bei Spring Boot Projekten funktioniert das mit Gradle so:

implementation 'org.springframework.boot:spring-boot-starter-webflux'

Mit Maven funktioniert es analog:

    <dependency>
        <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

Ansonsten müssen wir nichts weiter tun, um den WebClient zu nutzen. Mit dem WebClient sieht der zuvor mit RestTemplate gezeigte GET Request so aus:

WebClient client = WebClient.create("http://someurl.de/something");
String responseBody = client.get().retrieve().toEntity(String.class)
        .block().getBody();

Der WebClient wird mittels Builder instanziiert (WebClient.create). Dann wird der abzuschickende Request spezifiziert - hier im Beispiel minimal mit der get() Methode. Ab retrieve() wird festgelegt, wie die erwartete Response aussieht. Mit toEntity(String.class) wird festgelegt, dass der Body der Response in einen String geparst werden soll. Der eigentliche HTTP Request wird hier im Beispiel mit der Methode block() blockierend verschickt. Sobald die HTTP Response angekommen ist, wird der Response-Body als String Instanz mit getBody() ausgelesen.

Wenn wir den WebClient so verwenden, haben wir keinen Vorteil gegenüber dem RestTemplate, da der HTTP Request synchron verarbeitet wird. Das Besondere am WebClient ist die nicht blockierende, Event-gesteuerte (und damit asynchrone) HTTP Request und Response Verarbeitung. Diese sieht dann so aus:

Mono<String> responseMono = client.get().retrieve()
        .bodyToMono(String.class);
responseMono.subscribe(responseBodyAsString -> {
    // Bei erfolgreichen Requests, enthält responseBodyAsString
    // 
den HTTP Response Body als String und 
    // kann hier im Lambda z.B. so verarbeitet werden:
    System.out.println(responseString);    
});
  • 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

In Firmennetzwerken müssen meist Proxies für Internet-Verbindungen eingerichtet werden. Das könnt Ihr zum Beispiel so machen:

reactor.netty.http.client.HttpClient httpClient = HttpClient.create()
    .proxy(proxy -> proxy.host("localhost").port(3128)
        .type(Proxy.HTTP).
nonProxyHosts("*.intranet.de"))
    .responseTimeout(Duration.ofSeconds(30));
WebClient proxiedClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .baseUrl("http://someurl.needs/proxy")
    .build(); 
  • 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

Mich haben die Performance-Unterschiede zwischen WebClient und RestTemplate interessiert. Dazu habe ich den WebClient einfach ausprobiert und die Zeiten gestoppt. Da beim einfachen Ausprobieren die zu erwartenden Ergebnisse schnell sichtbar wurden, habe ich keine wissenschaftlichen Benchmark-Tests durchgeführt.

Der WebClient setzt auf eine asynchrone, Event-gesteuerte Kommunikation, die eingehende Nachrichten (Responses) erst dann aktiv in Threads verarbeitet, wenn diese wirklich angekommen sind - in der Zwischenzeit wird die CPU dabei nicht blockiert bzw. nicht aufgehalten. RestTemplate kommuniziert im selben Thread synchron und blockiert damit den Thread. Da man heute fast immer mehrere CPUs und Kerne zur Verfügung hat und bei der Kommunikation mit Systemen aufgrund der Übertragungsdauer Wartezeiten entstehen, habe ich vorher schon erwartet, dass der WebClient in den meisten Tests schneller ist.
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

In meinem ersten Performance-Vergleich habe ich drei Spring Beans erstellt, die jeweils den gleichen HTTP Request gegen einen REST-Service irgendwo im Internet schicken - wie gesagt ich mache keinen wissenschaftlichen Benchmark-Test. Die drei Beans sind so aufgebaut:
  1. 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.
  2. 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)
  3. 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.
Der Performance-Vergleich ist in der JUnit 5 Testklasse ReactiveVsClassicClientTest gemacht. Das ist kein richtiger Unit-Test, da ich aber den Anwendungs-Code vom Performance-Test Code trennen wollte, habe ich mich für die Implementierung des Testablaufs und die Zeitmessung in dieser Datei entschieden: https://github.com/...
(Zum Thema JUnit 5 Tests habe ich diesen Artikel geschrieben: JUnit5andSpringBootTest.html)

In der Testklasse kann man das Attribut NUMBER_OF_CALLS anpassen, um Anzahl der Requests festzulegen, die jede Bean abschicken soll. Das Ergebnis der Zeitmessung wird dann einfach geloggt. Für die beiden Beans ThreadedClassicBackendApiClient und ReactiveBackendApiClient verwende ich Awaitility, um solange zu warten bis alle Threads eine Antwort bekommen haben. Awaitility zum Testen von Threads hatte ich in diesem Blog-Artikel erklärt: junit-testing-von-threads.html
Hier noch der Code des JUnit Tests zum Messen der Performance:

@SpringBootTest
@Slf4j
class ReactiveVsClassicClientTest {
    private static final int NUMBER_OF_CALLS = 50;
    @Test
    void useRestTemplate(@Autowired ClassicBackendApiClient client) {
log.info("RestTemplate API calls took {} ms",
                measureCalls(client));
    }
    @Test
    void useRestTemplateInThreads(
            @Autowired ThreadedClassicBackendApiClient client) {
log.info("RestTemplate in Thread API calls took {} ms",
                 measureCalls(client));
    }
    @Test
    void useWebClient(@Autowired ReactiveBackendApiClient client) {
log.info("WebClient API calls took {} ms",
                measureCalls(clientBean));
    }
    private long measureCalls(BackendApiClient clientBean) {
     long before = System.currentTimeMillis();
     for (int i = 1; i < NUMBER_OF_CALLS + 1; i++)
         clientBean.callApi(i);
Awaitility.await().until(clientBean::isQueueEmpty);
return System.currentTimeMillis() - before;
    }
}
  • 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

So kommen wir nun endlich zu den Testergebnissen 😉
  • 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 

Fazit

Die reaktive Programmierung ist ein neues Entwicklungs-Paradigma, welches seine Vorteile insbesondere bei hoher Last durch viele Anfragen oder großen Datenabfragen ausspielt. Mit meinem WebClient Performance Test habe ich gezeigt, dass der reaktive WebClient klassischen HTTP Clients bei vielen parallelen Anfragen überlegen ist. 

Auch beim Abfragen großer Datenmengen kann der WebClient seine Überlegenheit zeigen, wenn die Server-Seite mit einen Flux antwortet. Für weiterführende Infos zur reaktiven Programmierung mit Spring schaut euch gerne meinen Udemy Kurs an.

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux