我正在开发几个具有传统模式的spring-boot应用程序,这些应用程序具有每个请求线程的模式。我们正在使用Spring-boot-webflow来获取WebClient来执行应用程序之间的RESTful集成。因此,我们的应用程序设计要求我们在收到响应后立即阻止发布者。
最近,我们一直在讨论在我们的其他阻塞应用程序设计中使用响应模块是否不必要地花费资源。据我所知,WebClient通过分配一个工作线程来使用事件循环来执行事件循环中的响应操作。因此,使用带有. block()
的webclient将Hibernate原始线程,同时分配另一个线程来执行超文本传输协议-Request。与替代RestTemplate相比,WebClient似乎会通过使用事件循环花费额外的资源。
以这种方式部分引入spring-webflow会导致额外的资源消耗,同时不会对性能产生任何积极的贡献,无论是单线程还是并发的,这是否正确?我们不期望将当前堆栈升级为完全反应性的,因此逐步升级的论点不适用。
在本演示文稿中,来自Spring
团队的Rossen Stoyanchev解释了其中的一些要点。
WebClient
将使用有限数量的线程-每个内核2个,总共12个线程在我的本地机器上-来处理应用程序中的所有请求及其响应。因此,如果您的应用程序收到100个请求
并为每个请求向外部服务器发出一个请求,WebClient
将以非阻塞
/异步
的方式处理所有使用这些线程的线程。
当然,正如您所提到的,一旦您调用block
,您的原始线程就会阻塞,因此需要100个线程12个线程,总共112个线程来处理这些请求。但请记住,这12个线程不会随着您发出更多请求而增长,并且它们不会进行I/O繁重的工作,因此WebClient
不会生成线程来实际执行请求或让它们在每个请求的线程中保持忙碌。
我不确定当线程在block
下时,它的行为是否与通过RestTemplate
进行阻塞调用时相同-在我看来,在前者中,线程应该是非活动
等待NIO
调用完成,而在后者中,线程应该处理I/O
工作,所以可能有区别。
如果你开始使用反应器
好东西,例如处理相互依赖的请求,或者并行处理许多请求。那么WebClient
肯定会有优势,因为它将使用相同的12个线程执行所有并发操作,而不是每个请求使用一个线程。
例如,考虑此应用程序:
@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个线程(在我的计算机中)立即并行处理。整洁!
如果你认为你可以从逻辑中的这种并行性中受益,那么尝试一下WebClient
可能是值得的。
如果没有,虽然我实际上不会担心上述原因的“额外资源支出”,但我认为不值得为此添加整个反应器/网络流量
依赖项——除了额外的包袱,在日常操作中,推理和调试RestTemplate
和每请求线程
模型应该简单得多。
当然,正如其他人提到的,您应该运行负载测试以获得适当的指标。
根据RestTemplate的官方Spring留档,它处于维护模式,未来的版本可能不支持。
从5.0开始,该类处于维护模式,只接受较小的更改请求和错误。请考虑使用org. springframe.web.reactive.client.WebClient
,它具有更现代的API并支持同步、异步和流式传输场景
至于系统资源,这真的取决于你的用例,我建议运行一些性能测试,但是对于低工作负载,使用阻塞客户端似乎可以有更好的性能,因为每个连接都有一个专用线程。随着负载的增加,NIO客户端往往表现得更好。
更新-反应式APIvs Http客户端
了解ReactiveAPI(Project Retor)和超文本传输协议客户端之间的区别很重要。尽管WebClient
使用ReactiveAPI但它不会同时添加任何额外的运算符,直到我们显式使用planMap
或delay
之类的运算符来调度不同线程池上的执行。如果我们只是使用
webClient
.get()
.uri("<endpoint>")
.retrieve()
.bodyToMono(String.class)
.block()
代码将在与阻塞客户端相同的调用者线程上执行。
如果我们启用此代码的调试日志记录,我们将看到WebClient
代码在调用者线程上执行,但对于网络操作,执行将切换到reactor-超文本传输协议-nio-…
线程。
主要区别在于,在内部WebClient
使用基于非阻塞IO(NIO)的异步客户端。这些客户端使用Reactor模式(事件循环)来维护一个单独的线程池,允许您处理大量并发连接。
I/O反应器的目的是对I/O事件做出反应并将事件通知分派给各个I/O会话。I/O反应器模式的主要思想是摆脱经典阻塞I/O模型强加的每个连接一个线程的模型。
默认情况下,使用ReactorNetty,但如果您创建所需的适配器(不确定它是否已经存在),您可以考虑Jetty RtivicHttp Client、ApacheHttpComponents(异步)甚至AWSCommon Runtime(CRT)Http Client。
一般来说,您可以看到整个行业使用异步I/O(NIO)的趋势,因为它们对于高负载下的应用程序更节省资源。
此外,为了有效地处理资源,整个流程必须是异步的。通过使用block()
,我们隐含地重新引入了每连接线程的方法,这将消除NIO的大部分好处。同时,使用WebClient
和block()
可以被认为是迁移到完全反应性应用程序的第一步。
好问题。
上周我们考虑了从restplate迁移到webclient。本周,我开始测试阻塞的webclient和rest模板之间的性能,令我惊讶的是,在响应有效负载很大的场景下,resttem的性能更好。差异相当大,resttem响应时间不到一半,资源使用更少。
我还在进行性能测试,现在我开始与更广泛的用户进行测试。
该应用程序是mvc,使用Spring 5.13.19和Spring boot 2.6.7。
对于性能测试,我正在使用jmeter和健康检查Visualvm/jConsole