在阻塞应用程序设计中使用 Spring Webflux 的 WebClient 是否会导致比 RestTemplate 更大的资源使用
Does the use of Spring Webflux's WebClient in a blocking application design cause a larger use of resources than RestTemplate
我正在开发几个 spring-boot 应用程序,它们具有传统的每个请求一个线程的模式。我们正在使用 Spring-boot-webflux 获取 WebClient 来执行我们的 RESTful 应用程序之间的集成。因此我们的应用程序设计要求我们在收到响应后立即阻止发布者。
最近,我们一直在讨论我们是否在我们的其他阻塞应用程序设计中使用反应模块不必要地花费资源。正如我所理解的那样,WebClient 通过分配一个工作线程来执行事件循环中的反应操作来利用事件循环。因此,将 webclient 与 .block()
一起使用会使原始线程休眠,同时分配另一个线程来执行 http 请求。与备选的 RestTemplate 相比,WebClient 使用事件循环似乎会花费额外的资源。
以这种方式部分引入 spring-webflux 会导致额外的资源消耗,同时不会对性能产生任何积极贡献,无论是单线程还是并发,这是否正确?我们不希望将我们当前的堆栈升级为完全反应式,因此逐步升级的论点不适用。
根据 RestTemplate 的官方 Spring 文档,它处于维护模式,未来版本可能不支持它。
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
至于系统资源,这实际上取决于您的用例,我会建议 运行 进行一些性能测试,但似乎对于低工作负载,使用阻塞客户端可以拥有更好的性能每个连接的线程。随着负载的增加,NIO 客户端往往会表现得更好。
更新 - 响应式 API 与 Http 客户端
了解 Reactive API(Project Reactor)和 http 客户端之间的区别很重要。尽管 WebClient
使用 Reactive API 它不会添加任何额外的并发,直到我们明确使用像 flatMap
或 delay
这样的可以在不同线程池上安排执行的运算符。如果我们只使用
webClient
.get()
.uri("<endpoint>")
.retrieve()
.bodyToMono(String.class)
.block()
代码将在与阻塞客户端相同的调用线程上执行。
如果我们为此代码启用调试日志记录,我们将看到 WebClient
代码在调用者线程上执行,但对于网络操作,执行将切换到 reactor-http-nio-...
线程。
主要区别在于内部WebClient
使用基于non-blocking IO (NIO) 的异步客户端。这些客户端使用 Reactor 模式(事件循环)来维护一个单独的线程池,允许您处理大量并发连接。
The purpose of I/O reactors is to react to I/O events and to dispatch
event notifications to individual I/O sessions. The main idea of I/O
reactor pattern is to break away from the one thread per connection
model imposed by the classic blocking I/O model.
默认情况下,使用 Reactor Netty,但您可以考虑使用 Jetty Rective Http Client、Apache HttpComponents(异步),甚至 AWS Common Runtime (CRT) Http Client 如果您创建所需的适配器(不确定它是否已经存在)。
总的来说,您可以看到整个行业使用异步 I/O (NIO) 的趋势,因为它们对于高负载下的应用程序来说更节省资源。
此外,要有效地处理资源,整个流程必须是异步的。通过使用 block()
,我们隐式地重新引入了 thread-per-connection 方法,这将消除 NIO 的大部分优势。同时使用 WebClient
和 block()
可以被视为迁移到完全响应式应用程序的第一步。
好问题。
上周我们考虑从 resttemplate 迁移到 webclient。
本周,我开始测试阻塞的 webclient 和 resttemplate 之间的性能,令我惊讶的是,resttemplate 在响应负载很大的场景中表现更好。差异相当大,resttemplate 的响应时间不到一半,使用的资源也更少。
我还在进行性能测试,现在我开始测试更广泛的用户请求。
该应用程序是 mvc,正在使用 spring 5.13.19 和 spring boot 2.6.7。
我使用 jmeter 进行性能测试和健康检查 visualvm/jconsole
在 this presentation Rossen Stoyanchev
中,Spring
团队解释了其中的一些要点。
WebClient
将使用有限数量的线程 - 每个核心 2 个线程,在我的本地机器上总共 12 threads
- 来处理应用程序中的所有请求及其响应。因此,如果您的应用程序收到 100 requests
并为每个请求向外部服务器发出一个请求,WebClient
将以 non-blocking
/ asynchronous
方式处理所有使用这些线程的请求。
当然,正如您提到的,一旦您调用 block
,您的原始线程将被阻塞,因此将需要 100 个线程 + 12 个线程,总共 112 threads
来处理这些请求。但请记住,这 12 个线程的大小不会随着您发出更多请求而增加,并且它们不会 I/O 繁重的工作 ,所以它不像 WebClient
正在生成线程以实际执行请求或让它们以 thread-per-request 的方式忙碌。
我不确定当线程在 block
下时,它的行为是否与通过 RestTemplate
进行阻塞调用时的行为相同 - 在我看来,在前者中线程应该是inactive
等待 NIO
调用完成,而稍后线程应该处理 I/O
工作,因此可能存在差异。
如果您开始使用 reactor
好东西,例如处理相互依赖的请求或并行处理许多请求,这会变得很有趣。然后 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 个(在我的计算机中)线程并行处理。 Neat!
如果您认为可以从逻辑中的这种并行性中获益,那么 WebClient
可能值得一试。
如果不是,虽然鉴于上述原因我实际上不会担心“额外资源支出”,但我认为为此添加整个 reactor/webflux
依赖项是不值得的 - 此外额外的负担,在日常操作中应该更容易推理和调试 RestTemplate
和 thread-per-request
模型。
当然,正如其他人所提到的,您应该 运行 负载测试以获得适当的指标。
我正在开发几个 spring-boot 应用程序,它们具有传统的每个请求一个线程的模式。我们正在使用 Spring-boot-webflux 获取 WebClient 来执行我们的 RESTful 应用程序之间的集成。因此我们的应用程序设计要求我们在收到响应后立即阻止发布者。
最近,我们一直在讨论我们是否在我们的其他阻塞应用程序设计中使用反应模块不必要地花费资源。正如我所理解的那样,WebClient 通过分配一个工作线程来执行事件循环中的反应操作来利用事件循环。因此,将 webclient 与 .block()
一起使用会使原始线程休眠,同时分配另一个线程来执行 http 请求。与备选的 RestTemplate 相比,WebClient 使用事件循环似乎会花费额外的资源。
以这种方式部分引入 spring-webflux 会导致额外的资源消耗,同时不会对性能产生任何积极贡献,无论是单线程还是并发,这是否正确?我们不希望将我们当前的堆栈升级为完全反应式,因此逐步升级的论点不适用。
根据 RestTemplate 的官方 Spring 文档,它处于维护模式,未来版本可能不支持它。
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
至于系统资源,这实际上取决于您的用例,我会建议 运行 进行一些性能测试,但似乎对于低工作负载,使用阻塞客户端可以拥有更好的性能每个连接的线程。随着负载的增加,NIO 客户端往往会表现得更好。
更新 - 响应式 API 与 Http 客户端
了解 Reactive API(Project Reactor)和 http 客户端之间的区别很重要。尽管 WebClient
使用 Reactive API 它不会添加任何额外的并发,直到我们明确使用像 flatMap
或 delay
这样的可以在不同线程池上安排执行的运算符。如果我们只使用
webClient
.get()
.uri("<endpoint>")
.retrieve()
.bodyToMono(String.class)
.block()
代码将在与阻塞客户端相同的调用线程上执行。
如果我们为此代码启用调试日志记录,我们将看到 WebClient
代码在调用者线程上执行,但对于网络操作,执行将切换到 reactor-http-nio-...
线程。
主要区别在于内部WebClient
使用基于non-blocking IO (NIO) 的异步客户端。这些客户端使用 Reactor 模式(事件循环)来维护一个单独的线程池,允许您处理大量并发连接。
The purpose of I/O reactors is to react to I/O events and to dispatch event notifications to individual I/O sessions. The main idea of I/O reactor pattern is to break away from the one thread per connection model imposed by the classic blocking I/O model.
默认情况下,使用 Reactor Netty,但您可以考虑使用 Jetty Rective Http Client、Apache HttpComponents(异步),甚至 AWS Common Runtime (CRT) Http Client 如果您创建所需的适配器(不确定它是否已经存在)。
总的来说,您可以看到整个行业使用异步 I/O (NIO) 的趋势,因为它们对于高负载下的应用程序来说更节省资源。
此外,要有效地处理资源,整个流程必须是异步的。通过使用 block()
,我们隐式地重新引入了 thread-per-connection 方法,这将消除 NIO 的大部分优势。同时使用 WebClient
和 block()
可以被视为迁移到完全响应式应用程序的第一步。
好问题。
上周我们考虑从 resttemplate 迁移到 webclient。 本周,我开始测试阻塞的 webclient 和 resttemplate 之间的性能,令我惊讶的是,resttemplate 在响应负载很大的场景中表现更好。差异相当大,resttemplate 的响应时间不到一半,使用的资源也更少。
我还在进行性能测试,现在我开始测试更广泛的用户请求。
该应用程序是 mvc,正在使用 spring 5.13.19 和 spring boot 2.6.7。
我使用 jmeter 进行性能测试和健康检查 visualvm/jconsole
在 this presentation Rossen Stoyanchev
中,Spring
团队解释了其中的一些要点。
WebClient
将使用有限数量的线程 - 每个核心 2 个线程,在我的本地机器上总共 12 threads
- 来处理应用程序中的所有请求及其响应。因此,如果您的应用程序收到 100 requests
并为每个请求向外部服务器发出一个请求,WebClient
将以 non-blocking
/ asynchronous
方式处理所有使用这些线程的请求。
当然,正如您提到的,一旦您调用 block
,您的原始线程将被阻塞,因此将需要 100 个线程 + 12 个线程,总共 112 threads
来处理这些请求。但请记住,这 12 个线程的大小不会随着您发出更多请求而增加,并且它们不会 I/O 繁重的工作 ,所以它不像 WebClient
正在生成线程以实际执行请求或让它们以 thread-per-request 的方式忙碌。
我不确定当线程在 block
下时,它的行为是否与通过 RestTemplate
进行阻塞调用时的行为相同 - 在我看来,在前者中线程应该是inactive
等待 NIO
调用完成,而稍后线程应该处理 I/O
工作,因此可能存在差异。
如果您开始使用 reactor
好东西,例如处理相互依赖的请求或并行处理许多请求,这会变得很有趣。然后 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 个(在我的计算机中)线程并行处理。 Neat!
如果您认为可以从逻辑中的这种并行性中获益,那么 WebClient
可能值得一试。
如果不是,虽然鉴于上述原因我实际上不会担心“额外资源支出”,但我认为为此添加整个 reactor/webflux
依赖项是不值得的 - 此外额外的负担,在日常操作中应该更容易推理和调试 RestTemplate
和 thread-per-request
模型。
当然,正如其他人所提到的,您应该 运行 负载测试以获得适当的指标。