Performant and optimal Spring WebClient

Share on:

Background

In my previous post I tried demonstrating how to implement an optimal and performant REST client using RestTemplate

In this article I will be demonstrating similar stuff but by using WebClient. But before we get started, lets try rationalizing

Why yet another REST client i.e. WebClient

IMO there are 2 compelling reasons -

  1. Maintenance mode of RestTemplate

    NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

  2. Enhanced performance with optimum resource utilization. One can refer my older article to understand performance gains reactive implementation is able to achieve.

From development standpoint, lets try understanding key aspects of implementing a performant and optimal WebClient

  1. ConnectionProvider with configurable connection pool
  2. HttpClient with optimal configurations
  3. Resiliency
  4. Implementing secured WebClient
  5. WebClient recommendations

Source Code @ Github

1. ConnectionProvider with configurable connection pool


I am pretty much sure that most of us would have encountered issues pertaining to connection pool of REST clients - e.g. Connection pool getting exhausted because of default or incorrect configurations. In order to avoid such issues in WebClient ConnectionProvider needs to be customized for broader control over :

  • maxConnections - Allows to configure maximum no. of connections per connection pool. Default value is derived based on no. of processors
  • maxIdleTime - Indicates max. amount of time for which a connection can remain idle in its pool.
  • maxLifeTime - Indicates max. life time for which a connection can remain alive. Implicitly it is nothing but max. duration after which channel will be closed

Configuring ConnectionProvider

1ConnectionProvider connProvider = ConnectionProvider
2                                    .builder("webclient-conn-pool")
3                                    .maxConnections(maxConnections)
4                                    .maxIdleTime()
5                                    .maxLifeTime()
6                                    .pendingAcquireMaxCount()
7                                    .pendingAcquireTimeout(Duration.ofMillis(acquireTimeoutMillis))
8                                    .build();

2. HttpClient with optimal configurations


Netty's HttpClient provides fluent APIs for optimally configuring itself. From WebClient's performance standpoint HttpClient should be configured as shown below -

Configuring HttpClient

 1nettyHttpClient = HttpClient
 2                    .create(connProvider)
 3                    .secure(sslContextSpec -> sslContextSpec.sslContext(webClientSslHelper.getSslContext()))
 4                    .tcpConfiguration(tcpClient -> {
 5                        LoopResources loop = LoopResources.create("webclient-event-loop",
 6                            selectorThreadCount, workerThreadCount, Boolean.TRUE);
 7
 8                        return tcpClient
 9                                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
10                                .option(ChannelOption.TCP_NODELAY, true)
11                                .doOnConnected(connection -> {
12                                    connection
13                                        .addHandlerLast(new ReadTimeoutHandler(readTimeout))
14                                        .addHandlerLast(new WriteTimeoutHandler(writeTimeout));
15                                })
16                                .runOn(loop);
17                    })
18                    .keepAlive(keepAlive)
19                    .wiretap(Boolean.TRUE);

2.1 Configuring event loop threads

1LoopResources loop = LoopResources.create("webclient-event-loop",
2                                        selectorThreadCount, workerThreadCount, Boolean.TRUE);
  • selectorThreadCount - Configures DEFAULT_IO_SELECT_COUNT of LoopResources. Defaults to -1 if not configured
  • workerThreadCount - Configures DEFAULT_IO_WORKER_COUNT of LoopResources. Defaults to number of available processors

2.2 Configuring underlying TCP configurations

  • CONNECT_TIMEOUT_MILLIS - Indicates max. duration for which channel will wait to establish connection
  • TCP_NODELAY - Indicates whether WebClient should send data packets immediately
  • readTimeout - Configures duration for which, if no data was read within this time frame, it would throw ReadTimeoutException
  • writeTimeout - Configures duration for which, if no data was written within this time frame, it would throw WriteTimeoutException
  • keepAlive - Helps to enable / disable 'Keep Alive' support for outgoing requessts

3. Resilient WebClient


While we all know the reasons for adopting Microservice Architecture, we are also cognizant of the fact that it comes with its own set of complexities and challenges. With distributed architecture, few of the major pain points for any application which consumes REST API are - 

  • Socket Exception - Caused by temporary server overload due to which it rejects incoming requests
  • Timeout Exception - Caused by temporary input / output latency. E.g. Extremely slow DB query resulting in timeout

Since failure in Distributed Systems are inevitable we need to make WebClient resilient by using some kind of Retry strategy as shown below

Resilient WebClient

 1cardAliasMono = restWebClient
 2                  .get()
 3                  .uri("/{cardNo}", cardNo)
 4                  .headers(this::populateHttpHeaders)
 5                  .retrieve()
 6                  .onStatus(HttpStatus::is4xxClientError, clientResponse -> {
 7                      log.error("Client error from downstream system");
 8                      return Mono.error(new HttpClientErrorException(HttpStatus.BAD_REQUEST));
 9                  })
10                  .bodyToMono(String.class)
11                  .retryWhen(Retry
12                              .onlyIf(this::is5xxServerError)
13                              .exponentialBackoff(Duration.ofSeconds(webClientConfig.getRetryFirstBackOff()),
14                                      Duration.ofSeconds(webClientConfig.getRetryMaxBackOff()))
15                              .retryMax(webClientConfig.getMaxRetryAttempts())
16                              .doOnRetry(this::processOnRetry)
17                  )
18                  .doOnError(this::processInvocationErrors);

Here we have configured retry for specific errors i.e. 5xxServerError and whenever its condition gets satisfied it will enforce exponential backoff strategy

4. Implementing secured WebClient


In this world of APIs, depending on the nature of data it deals, one may need to implement secured REST client. This is also demonstrated in code by using WebClientSslHelper which will be responsible for setting up the SSLContext.

Note - WebClientSslHelper should be conditionally instantiated based on what kind of security strategy (i.e. Untrusted / Trusted) needs to be in place for the corresponding environment and profile (viz test, CI, dev etc.)

5. WebClient recommendations


5.1 Determining max idle time

It should always be less than keep alive time out configured on the downstream system

5.2 Leaky exchange

While using exchangeToMono() and exchangeToFlux(), returned response i.e. Mono and Flux should ALWAYS be consumed. Faililng to do so may result in memory and connection leaks

5.3 Connection pool leasing strategy

Default leasing strategy is FIFO which means oldest connection is used from the pool. However, with keep alive timeout we may want to use LIFO, which will in turn ensure that most recent available connection is used from the pool.

5.4 Processing response without response body

If use case does not need to process response body, than one can implement it by using releaseBody() and toBodilessEntity(). This will ensure that connections are released back to connection pool

Conclusion

By knowing and understanding various aspects of WebClient along with its key configuration parameters we can now build a highly performant, resilient and secured REST client using Spring's WebClient.

comments powered by Disqus