Performant and optimal Spring WebClient
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 -
- 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.
- 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
- ConnectionProvider with configurable connection pool
- HttpClient with optimal configurations
- Resiliency
- Implementing secured WebClient
- WebClient recommendations
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