在阻塞应用程序设计中使用Spring Webflux的WebClient是否会导致比RestTemplate更大的资源使用量

我正在开发几个具有传统线程每次请求模式的弹簧启动应用程序。我们正在使用Spring-boot-webflux来获取WebClient,以执行我们在应用程序之间的RESTful集成。因此,我们的应用程序设计要求我们在收到响应后立即阻止发布者。

最近,我们一直在讨论我们是否在原本阻塞应用程序设计中使用反应式模块不必要地花费资源。据我所知,WebClient 通过分配一个工作线程来执行事件循环中的反应式操作来利用事件循环。因此,使用webclient with会休眠原始线程,同时分配另一个线程来执行http请求。与替代的 RestTemplate 相比,WebClient 似乎会通过使用事件循环来花费额外的资源。.block()

以这种方式部分引入spring-webflux会导致额外的资源消耗,同时不会对性能产生任何积极贡献,无论是单线程还是并发,这是正确的吗?我们不希望将当前的堆栈升级为完全被动,因此逐步升级的论点不适用。


答案 1

本次演示中,团队将解释其中的一些要点。Rossen StoyanchevSpring

WebClient将使用有限数量的线程 ( 每个内核 2 个线程,在我的本地计算机上总共使用 ) 来处理应用程序中的所有请求及其响应。因此,如果您的应用程序接收并向外部服务器发出一个请求,则将以/方式处理所有这些线程的请求。12 threads100 requestsWebClientnon-blockingasynchronous

当然,正如你所提到的,一旦你调用了你的原始线程,你的原始线程就会阻塞,所以总共需要100个线程+ 12个线程来处理这些请求。但请记住,这 12 个线程的大小不会随着您发出更多请求而增加,并且它们不会执行 I/O 繁重的工作,因此这不像是生成线程来实际执行请求或使它们以每个请求线程的方式保持忙碌。block112 threadsWebClient

我不确定当线程处于其下方时,它的行为是否与通过阻塞调用时的行为相同 - 在我看来,在前者中,线程应该等待调用完成,而在后面的线程应该处理工作,所以也许那里有区别。blockRestTemplateinactiveNIOI/O

如果您开始使用这些好东西,例如处理相互依赖的请求或并行处理多个请求,这将变得有趣。然后肯定会有一个优势,因为它将使用相同的12个线程执行所有并发操作,而不是每个请求使用一个线程。reactorWebClient

例如,请考虑以下应用程序:

@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();
    }
}

如果运行此应用,可以看到所有 30 个请求都由相同的 12 个(在我的计算机中)线程立即并行处理。 如果你认为你可以从逻辑中的这种并行性中受益,那么可能值得一试。Neat!WebClient

如果没有,虽然考虑到上述原因,我实际上不会担心“额外的资源支出”,但我认为不值得为此添加整个依赖关系 - 除了额外的包袱之外,在日常操作中,推理和调试以及模型应该简单得多。reactor/webfluxRestTemplatethread-per-request

当然,正如其他人所提到的,您应该运行负载测试以获得适当的指标。


答案 2

根据 RestTemplate 的官方 Spring 文档,它处于维护模式,将来的版本可能不受支持。

从 5.0 开始,此类处于维护模式,以后只接受少量的更改请求和 bug。请考虑使用具有更现代的API并支持同步,异步和流式传输方案org.springframework.web.reactive.client.WebClient

至于系统资源,这实际上取决于您的用例,我建议运行一些性能测试,但是对于使用阻塞客户端的低工作负载,似乎每个连接拥有专用线程的性能可能更好。随着负载的增加,NIO 客户端往往表现得更好。

更新 - 反应式 API 与 Http 客户端

了解Active API(Project Reactor)和http客户端之间的区别非常重要。尽管使用反应式 API,但在我们显式使用类似 或 可以安排在不同线程池上执行的运算符之前,它不会同时添加任何其他操作。如果我们只使用WebClientflatMapdelay

webClient
  .get()
  .uri("<endpoint>")
  .retrieve()
  .bodyToMono(String.class)
  .block()

代码将在调用线程上执行,该线程与阻塞客户端相同。

如果我们为此代码启用调试日志记录,我们将看到代码在调用方线程上执行,但对于网络操作,执行将切换到线程。WebClientreactor-http-nio-...

主要区别在于内部使用基于非阻塞 IO (NIO) 的异步客户端。这些客户端使用 Reactor 模式(事件循环)来维护单独的线程池,该线程池允许您处理大量并发连接。WebClient

I/O 反应器的用途是对 I/O 事件做出反应,并将事件通知分派给各个 I/O 会话。I/O 反应器模式的主要思想是摆脱经典阻塞 I/O 模型强加的每个连接一个线程的模型。

默认情况下,使用 Reactor Netty,但如果您创建了所需的适配器(不确定它是否已存在),则可以考虑 Jetty Rective Http Client、Apache HttpComponents (async) 甚至 AWS Common Runtime (CRT) Http Client

通常,您可以看到整个行业使用异步 I/O (NIO) 的趋势,因为它们对于高负载下的应用程序具有更高的资源效率。

此外,为了有效地处理资源,整个流必须是异步的。通过使用,我们隐式地重新引入了每个连接线程的方法,这将消除NIO的大部分好处。同时,可以使用 with 作为迁移到完全反应式应用程序的第一步。block()WebClientblock()


推荐