SpringBoot性能比较:Spring MVC与WebFlux


在这里我想谈谈曾经在项目中遇到的有趣的事情。我们为我们的客户在AWS中编写了一些轻量级微服务,它只是通过HTTP代理对某些底层服务的请求,并将其返回给客户端。
乍一看,什么可能比编写REST代理服务更简单?
所以,当然,我们从Spring Boot开始编写简单的RestControllers。我们做了POC,结果很好。第三方服务具有符合要求的服务响应时间SLA,我们使用此值进行性能测试,第三方服务的响应时间非常好〜大约10-100ms。我们还决定利用CPU作为我们的微服务的扩展策略,这个服务是在Docker中作为AWS ECS服务运行。我们在AWS中配置了自动扩展并上线了。

事实上,并非一切顺利。运行经常超时,我们经常重启AWS ECS任务。我们只是运行很少的任务,另外,看到CPU和内存消耗很低但我们的服务还是太慢,有时甚至有超时错误。
问题在于第三方服务。第三方服务响应时间变为500-1000ms。但它从来没有超时问题,能够处理更多的客户端。
所以关键问题还是在于我们的服务。我们没有在需要时扩展我们的应用程序。我们进行了500-1000毫秒的性能测试,并感到震惊。
CPU很低,内存很好,但我们只能处理200个请求/秒。
这是Servlet线程的连接问题,默认线程池是200,这就是为什么我们在1000毫秒响应时间内有200个请求/秒的原因。
但我们需要一个弹性服务:我们应该处理与底层服务一样多的请求。响应时间应与基础服务几乎相同。
我们研究它并找到了几个选项:

  1. 增加线程池大小
  2. 使用Servlet的DeferredResult或CompletableFuture
  3. Spring与WebFlux反应

选项1:增加线程池大小
是的,这是一个很好的解决方法,但只是解决方法!我们不能将这个值设置为几千,因为它是具有非常有限的内存的Docker。每个线程都需要堆栈内存。
另一个问题是,如果某些第三方服务的响应时间很长,例如,5秒,我们仍会遇到同样的问题。吞吐量等于=线程池大小/响应时间。如果我们有1000个线程和5秒延迟,则吞吐量是200个请求/秒。CPU再次很低,服务有足够的资源进行处理。

选项2:带Servlet的DeferredResult或CompletableFurure(非阻塞)
Servlet 3.1支持异步处理。为了使它工作,我们需要返回一些Promise,Servlet将以异步方式处理它。
我们将DeferredResult与CompletableFurure进行了比较,结果相同。因此,我们同意测试CompletableFurure。

选项3:Spring与WebFlux反应
这是现在最热门的话题。从Spring 文档
“使用少量线程处理并发性并使用更少的硬件资源进行扩展的非阻塞Web堆栈”

测试
测试环境:
Spring Boot:2.1.2.RELEASE(最新)
Java:11 OpenJDK
节点:t2.micro(亚马逊Linux)
代码:https //github.com/Aleksandr-Filichkin/spring-mvc-vs-webflux
Http客户端: Java 11 Http客户端,Apache Http客户端,Spring WebClient

Test-Service(我们的代理服务)公开了几个GET端点进行测试。所有端点都有一个延迟(以毫秒为单位)参数,用于模拟第三方服务延迟。

@GetMapping(value = "/sync")
public String getUserSync(@RequestParam long delay) {
    return sendRequestWithJavaHttpClient(delay).thenApply(x ->
"sync: " + x).join();
}
@GetMapping(value =
"/completable-future-java-client")
public CompletableFuture<String> getUserUsingWithCFAndJavaClient(@RequestParam long delay) {
    return sendRequestWithJavaHttpClient(delay).thenApply(x ->
"completable-future-java-client: " + x);
}
@GetMapping(value =
"/completable-future-apache-client")
public CompletableFuture<String> getUserUsingWithCFAndApacheCLient(@RequestParam long delay) {
    return sendRequestWithApacheHttpClient(delay).thenApply(x ->
"completable-future-apache-client: " + x);
}
@GetMapping(value =
"/webflux-java-http-client")
public Mono<String> getUserUsingWebfluxJavaHttpClient(@RequestParam long delay) {
    CompletableFuture<String> stringCompletableFuture = sendRequestWithJavaHttpClient(delay).thenApply(x ->
"webflux-java-http-client: " + x);
    return Mono.fromFuture(stringCompletableFuture);
}
@GetMapping(value =
"/webflux-webclient")
public Mono<String> getUserUsingWebfluxWebclient(@RequestParam long delay) {
    return webClient.get().uri(
"/user/?delay={delay}", delay).retrieve().bodyToMono(String.class).map(x -> "webflux-webclient: " + x);
}
@GetMapping(value =
"/webflux-apache-client")
public Mono<String> apache(@RequestParam long delay) {
    return Mono.fromCompletionStage(sendRequestWithApacheHttpClient(delay).thenApply(x ->
"webflux-apache-client: " + x));
}

User-Service(第三方服务)公开单个端点GET“/ user?delay = {delay}”。延迟(ms)参数用于延迟仿真。如果我们发送/ user?delay = 10,则响应时间将为10 ms +网络延迟(AWS内部最小);
这个用户服务是我们的第三方服务(用户服务),它非常快,可以处理超过4000个请求/秒

对于性能测试,我们将使用Jmeter。我们将测试100,200,400,800个并发请求的服务,延迟10,100,500毫秒。每个实施总共12个测试。
重要的提示:
我们仅针对热服务器测量性能:在每次测试之前,我们的服务处理了100万个请求(用于JIT编译器和JVM优化)

测试代码https://github.com/Aleksandr-Filichkin/spring-mvc-vs-webflux

测试结果点击标题见原文

结论(在单核,1GB RAM服务器实例上):
Spring Webflux在所有测试情况下都获胜,包括使用WebClient和Apache clients情况! 
当底层服务很慢(500ms)时,有最显着的差异(比阻塞Servlet快4倍);它比使用CompetableFuture的非阻塞Servlet快15-20%;此外,与Servlet(20 vs 220)相比,它不会创建大量线程。

不幸的是,我们无法在任何地方使用WebFlux,因为我们需要异步驱动程序/客户端。否则,我们必须创建自定义线程池/包装器。

Servlet阻塞方式仅适用于底层服务快速(10ms)的情况。
Servlet非阻塞方式是一个非常好的解决方案,对于底层服务很慢(500毫秒)的情况。只有在有大量请求的情况下,它才会输给Webflux。

附注:

  • 对于单核,1GB RAM服务器实例,Java 11 Http Client比Apache Http客户端慢(性能降低约30%)
  • Spring WebClient与Apache Http Client(都使用netty)在单核,1GB RAM服务器实例上具有相同的性能
  • 当你只有一个核心和一个小内存时,WebFlux和Java 11 Http Client的组合运行时模型不能很好地工作(https://github.com/spring-projects/spring-framework/issues/22333


 

辛苦 banq 大叔,最近也正做这方面的性能对比,正好能做参考。