Home > front end >  Does the use of Spring Webflux's WebClient in a blocking application design cause a larger use
Does the use of Spring Webflux's WebClient in a blocking application design cause a larger use

Time:05-20

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.

CodePudding user response:

According to official Spring documentation for RestTemplate it's in the maintenance mode and probably will not be supported in future versions.

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

As for system resources, it really depends on your use case and I would recommend to run some performance tests, but it seems that for low workloads using blocking client could have a better performance owning to a dedicated thread per connection. As load increases, the NIO clients tend to perform better.

CodePudding user response:

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.

CodePudding user response:

Great question.

Last week we considered migrating from resttemplate to webclient. This week, I start testing the performance between the blocking webclient and the resttemplate, to my surprise, the resttemplate performed better in scenarios where the response payloads were large. The difference was considerably large, with the resttemplate taking less than half the time to respond and using fewer resources.

I'm still carrying out the performance tests, now I started the tests with a wider range of users for request.

The application is mvc and is using spring 5.13.19 and spring boot 2.6.7.

For perfomance testing I'm using jmeter and for health check visualvm/jconsole

  • Related