在这里我想谈谈曾经在项目中遇到的有趣的事情。我们为我们的客户在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:增加线程池大小
是的,这是一个很好的解决方法,但只是解决方法!我们不能将这个值设置为几千,因为它是具有非常有限的内存的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 = <font>"/sync"</font><font>) <b>public</b> String getUserSync(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"sync: "</font><font> + x).join(); } @GetMapping(value = </font><font>"/completable-future-java-client"</font><font>) <b>public</b> CompletableFuture<String> getUserUsingWithCFAndJavaClient(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"completable-future-java-client: "</font><font> + x); } @GetMapping(value = </font><font>"/completable-future-apache-client"</font><font>) <b>public</b> CompletableFuture<String> getUserUsingWithCFAndApacheCLient(@RequestParam <b>long</b> delay) { <b>return</b> sendRequestWithApacheHttpClient(delay).thenApply(x -> </font><font>"completable-future-apache-client: "</font><font> + x); } @GetMapping(value = </font><font>"/webflux-java-http-client"</font><font>) <b>public</b> Mono<String> getUserUsingWebfluxJavaHttpClient(@RequestParam <b>long</b> delay) { CompletableFuture<String> stringCompletableFuture = sendRequestWithJavaHttpClient(delay).thenApply(x -> </font><font>"webflux-java-http-client: "</font><font> + x); <b>return</b> Mono.fromFuture(stringCompletableFuture); } @GetMapping(value = </font><font>"/webflux-webclient"</font><font>) <b>public</b> Mono<String> getUserUsingWebfluxWebclient(@RequestParam <b>long</b> delay) { <b>return</b> webClient.get().uri(</font><font>"/user/?delay={delay}"</font><font>, delay).retrieve().bodyToMono(String.<b>class</b>).map(x -> </font><font>"webflux-webclient: "</font><font> + x); } @GetMapping(value = </font><font>"/webflux-apache-client"</font><font>) <b>public</b> Mono<String> apache(@RequestParam <b>long</b> delay) { <b>return</b> Mono.fromCompletionStage(sendRequestWithApacheHttpClient(delay).thenApply(x -> </font><font>"webflux-apache-client: "</font><font> + x)); } </font>
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。
附注: