Skip to content
Advertisement

Does the use of Spring Webflux’s WebClient in a blocking application design cause a larger use of resources than RestTemplate

I am working on several spring-boot applications which have the traditional pattern of thread-per-request. We are using Spring-boot-webflux to acquire WebClient to perform our RESTful integration between the applications. Hence our application design requires that we block the publisher right after receiving a response.

Recently, we’ve been discussing whether we are unnecessarily spending resources using a reactive module in our otherwise blocking application design. As I’ve understood it, WebClient makes use of the event loop by assigning a worker thread to perform the reactive actions in the event loop. So using webclient with .block() would sleep the original thread while assigning another thread to perform the http-request. Compared to the alternative RestTemplate, it seems like WebClient would spend additional resources by using the event loop.

Is it correct that partially introducing spring-webflux in this way leads to additional spent resources while not yielding any positive contribution to performance, neither single threaded and concurrent? We are not expecting to ever upgrade our current stack to be fully reactive, so the argument of gradually upgrading does not apply.

Advertisement

Answer

In this presentation Rossen Stoyanchev from the Spring team explains some of these points.

WebClient will use a limited number of threads – 2 per core for a total of 12 threads on my local machine – to handle all requests and their responses in the application. So if your application receives 100 requests and makes one request to an external server for each, WebClient will handle all of those using those threads in a non-blocking / asynchronous manner.

Of course, as you mention, once you call block your original thread will block, so it would be 100 threads + 12 threads for a total of 112 threads to handle those requests. But keep in mind that these 12 threads do not grow in size as you make more requests, and that they don’t do I/O heavy lifting, so it’s not like WebClient is spawning threads to actually perform the requests or keeping them busy on a thread-per-request fashion.

I’m not sure if when the thread is under block it behaves the same as when making a blocking call through RestTemplate – it seems to me that in the former the thread should be inactive waiting for the NIO call to complete, while in the later the thread should be handling I/O work, so maybe there’s a difference there.

It gets interesting if you begin using the reactor goodies, for example handling requests that depend on one another, or many requests in parallel. Then WebClient definitely gets an edge as it’ll perform all concurrent actions using the same 12 threads, instead of using a thread per request.

As an example, consider this application:

@SpringBootApplication
public class SO72300024 {

    private static final Logger logger = LoggerFactory.getLogger(SO72300024.class);

    public static void main(String[] args) {
        SpringApplication.run(SO72300024.class, args);
    }

    @RestController
    @RequestMapping("/blocking")
    static class BlockingController {

        @GetMapping("/{id}")
        String blockingEndpoint(@PathVariable String id) throws Exception {
            logger.info("Got request for {}", id);
            Thread.sleep(1000);
            return "This is the response for " + id;
        }

        @GetMapping("/{id}/nested")
        String nestedBlockingEndpoint(@PathVariable String id) throws Exception {
            logger.info("Got nested request for {}", id);
            Thread.sleep(1000);
            return "This is the nested response for " + id;
        }

    }

    @Bean
    ApplicationRunner run() {
        return args -> {
            Flux.just(callApi(), callApi(), callApi())
                    .flatMap(responseMono -> responseMono)
                    .collectList()
                    .block()
                    .stream()
                    .flatMap(Collection::stream)
                    .forEach(logger::info);
            logger.info("Finished");
        };
    }

    private Mono<List<String>> callApi() {
        WebClient webClient = WebClient.create("http://localhost:8080");
        logger.info("Starting");
        return Flux.range(1, 10).flatMap(i ->
                        webClient
                                .get().uri("/blocking/{id}", i)
                                .retrieve()
                                .bodyToMono(String.class)
                                .doOnNext(resp -> logger.info("Received response {} - {}", I, resp))
                                .flatMap(resp -> webClient.get().uri("/blocking/{id}/nested", i)
                                        .retrieve()
                                        .bodyToMono(String.class)
                                        .doOnNext(nestedResp -> logger.info("Received nested response {} - {}", I, nestedResp))))
                .collectList();
    }
}

If you run this app, you can see that all 30 requests are handled immediately in parallel by the same 12 (in my computer) threads. Neat! If you think you can benefit from such kind of parallelism in your logic, it’s probably worth it giving WebClient a shot.

If not, while I wouldn’t actually worry about the “extra resource spending” given the reasons above, I don’t think it would be worth it adding the whole reactor/webflux dependency for this – besides the extra baggage, in day to day operations it should be a lot simpler to reason about and debug RestTemplate and the thread-per-request model.

Of course, as others have mentioned, you ought to run load tests to have proper metrics.

Advertisement