Reactor-netty: Slow first request of Spring's WebClient caused by Netty Reactor Http Client

Created on 28 Dec 2018  路  11Comments  路  Source: reactor/reactor-netty

The first request made with Spring WebClient takes around 6 seconds, whereas the next ones are way faster (30ms). The following Spring JIRA ticket points at Netty's initialization as the reason for this delay: https://jira.spring.io/browse/SPR-17200

I have uploaded a sample project with instructions to reproduce the problem in its readme: https://github.com/codependent/slow-webclient-sample

Reactor netty version: 0.8.3.RELEASE

JVM: Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

OS Version: MacOS Darwin user.local 18.2.0 Darwin Kernel Version 18.2.0: Fri Oct 5 19:41:49 PDT 2018; root:xnu-4903.221.2~2/RELEASE_X86_64 x86_64

areperf

Most helpful comment

Hi All,

We did several improvements for Reactor Netty version 1.0.x

In order to remove Spring Framework from the scenario, I transformed the example above to pure Reactor Netty and identified 4 different scenarios. Below you can find them and the results for this particular URL. Environment: Mac OS, JDK 8, Transport NIO, Boring SSL 2.0.34.Final:

Scenario 1: HttpClient configured with

  • Connection Pool (this is by default)
  • Ssl provider OpenSSL (this is by default)

First request ~750-850ms (includes connection establishment + TLS handshake)
Second request ~55-65ms (connection from the pool)

HttpClient client = HttpClient.create()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 2: HttpClient configured with

  • Connection Pool (this is by default)
  • Ssl provider JDK

First request ~450-550ms (includes connection establishment + TLS handshake)
Second request ~55-65ms (connection from the pool)

SslContextBuilder clientCtx = SslContextBuilder.forClient()
                           .sslProvider(SslProvider.JDK);

HttpClient client = HttpClient.create()
                  .compress(true)
                  .secure(spec -> spec.sslContext(clientCtx));

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 3: HttpClient configured with

  • No Connection Pool
  • Ssl provider OpenSSL (this is by default)

First request ~750-850ms (includes connection establishment + TLS handshake)
Second request ~155-165ms (includes connection establishment + TLS handshake)

HttpClient client = HttpClient.newConnection()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 4: HttpClient configured with

  • No Connection Pool
  • Ssl provider JDK

First request ~450-550ms (includes connection establishment + TLS handshake)
Second request ~155-165ms (includes connection establishment + TLS handshake)

SslContextBuilder clientCtx = SslContextBuilder.forClient()
                           .sslProvider(SslProvider.JDK);

HttpClient client = HttpClient.newConnection()
                  .compress(true)
                  .secure(spec -> spec.sslContext(clientCtx));

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Conclusion: In case OpenSSL is used the time difference comes from loading the native libraries. If the native libraries are preloaded then the times OpenSSL/JDK first request are equal.

For testing purposes OpenSSL can be preloaded like this:

System.out.println(OpenSsl.version());

HttpClient client = HttpClient.create()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

All 11 comments

This issue is still present in the current version 0.9.4.RELEASE taking multiple seconds for an initial request, and only milliseconds for subsequent requests.

We are using the openjdk:11-jre-slim Docker image which is based on Debian 10 and OpenJDK 11.0.6.

@violetagg @bclozel
Is it possible to eagerly trigger the slow Netty initialization as part of the application startup, so the first slow web client call wouldn't effect the actual users of the application?

We're also observing initial requests which take several seconds compared to a baseline of a few tens of milliseconds. I tried experimenting locally with latest OpenJDK 14.0.1 with OpenJ9, no noticeable improvement with the sample project shared by @codependent.

Are there any workaround for this.

Any idea if this gets done any time soonish?

Hello, I would like to know the current status of this issue too.

Hi All,

We did several improvements for Reactor Netty version 1.0.x

In order to remove Spring Framework from the scenario, I transformed the example above to pure Reactor Netty and identified 4 different scenarios. Below you can find them and the results for this particular URL. Environment: Mac OS, JDK 8, Transport NIO, Boring SSL 2.0.34.Final:

Scenario 1: HttpClient configured with

  • Connection Pool (this is by default)
  • Ssl provider OpenSSL (this is by default)

First request ~750-850ms (includes connection establishment + TLS handshake)
Second request ~55-65ms (connection from the pool)

HttpClient client = HttpClient.create()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 2: HttpClient configured with

  • Connection Pool (this is by default)
  • Ssl provider JDK

First request ~450-550ms (includes connection establishment + TLS handshake)
Second request ~55-65ms (connection from the pool)

SslContextBuilder clientCtx = SslContextBuilder.forClient()
                           .sslProvider(SslProvider.JDK);

HttpClient client = HttpClient.create()
                  .compress(true)
                  .secure(spec -> spec.sslContext(clientCtx));

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 3: HttpClient configured with

  • No Connection Pool
  • Ssl provider OpenSSL (this is by default)

First request ~750-850ms (includes connection establishment + TLS handshake)
Second request ~155-165ms (includes connection establishment + TLS handshake)

HttpClient client = HttpClient.newConnection()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Scenario 4: HttpClient configured with

  • No Connection Pool
  • Ssl provider JDK

First request ~450-550ms (includes connection establishment + TLS handshake)
Second request ~155-165ms (includes connection establishment + TLS handshake)

SslContextBuilder clientCtx = SslContextBuilder.forClient()
                           .sslProvider(SslProvider.JDK);

HttpClient client = HttpClient.newConnection()
                  .compress(true)
                  .secure(spec -> spec.sslContext(clientCtx));

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Conclusion: In case OpenSSL is used the time difference comes from loading the native libraries. If the native libraries are preloaded then the times OpenSSL/JDK first request are equal.

For testing purposes OpenSSL can be preloaded like this:

System.out.println(OpenSsl.version());

HttpClient client = HttpClient.create()
                  .compress(true);

URI uri = new URI("https://jsonplaceholder.typicode.com/todos/1");

HttpServer.create()
      .port(9090)
      .handle((req, res) -> res.sendString(client.get()
                             .uri(uri)
                             .responseContent()
                             .aggregate()
                             .asString()))
     .bindNow()
     .onDispose()
     .block();

Maybe worth a mention in the docs, e.g. under SSL and TLS.

Hi @violetagg is there a reason you say "For testing purposes OpenSSL can be preloaded like this", I would love to preload that in our production instances. Of course normally I would not have OpenSSL on my compile classpath so I would need to somehow tell netty to basically "preload your SSL engine, whatever that is", is there a way to do that?

@maczikasz We may add a configuration property for preloading the OpenSSL. Basically loading the class below will load the natives:
https://github.com/netty/netty/blob/c1ba23933cd3e7c723fe33d58173b58da80ac3bb/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java#L136

Of course normally I would not have OpenSSL on my compile classpath so I would need to somehow tell netty to basically "preload your SSL engine, whatever that is", is there a way to do that?

If you do not have OpenSSL then use -Dio.netty.handler.ssl.noOpenSsl=true thus you will use JDK provider.

We will address this issue with #1455

Was this page helpful?
0 / 5 - 0 ratings

Related issues

eugene-sadovsky picture eugene-sadovsky  路  8Comments

magx2 picture magx2  路  6Comments

hisener picture hisener  路  4Comments

vchekan picture vchekan  路  8Comments

be-hase picture be-hase  路  6Comments