WebFlux/WebClient 和 Spring MVC/RestTemplate 性能对比

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

Spring Webflux 是 Spring Framework 5.0 的新特性,是随着当下流行的 Reactive Programming 而诞生的高性能框架。传统的 Web 应用框架,比如我们…

Spring Webflux 是 Spring Framework 5.0 的新特性,是随着当下流行的 Reactive Programming 而诞生的高性能框架。传统的 Web 应用框架,比如我们所熟知的 Struts2,Spring MVC 等都是基于 Servlet API 和 Servlet 容器之上运行的,本质上都是阻塞式的。Servlet 直到 3.1 版本之后才对异步非阻塞进行了支持。而 WebFlux 是一个典型异步非阻塞框架,其核心是基于 Reactor 相关 API 实现的。相比传统的 Web 框架,WebFlux 可以运行在例如 Netty、Undertow 以及 Servlet 3.1 容器之上,其运行环境比传统 Web 框架更具灵活性。

WebFlux 的主要优势有:

  • 非阻塞性:WebFlux 提供了一种比 Servlet 3.1 更完美的异步非阻塞解决方案。非阻塞的方式可以使用较少的线程以及硬件资源来处理更多的并发。
  • 函数式编程:函数式编程是 Java 8 重要的特性,WebFlux 完美支持。

会讲解到如下类容:

  • Spring MVC 与 Spring WebFlux 服务端性能对比
  • RestTemplate 和 WebClient 客户端性能对比
  • 带连接池的 RestTemplate
  • WebClient 的阻塞式用法性能分析
  • CompleteFuture 方式使用 WebClient 性能分析
  • 最佳实践:性能对比结果分析与总结

适合人群:在实际开发中需要使用 HTTP 客户端访问外部资源的场景,接口调用频繁,对性能有严格的要求的相关开发、测试、运维人员。



服务端性能对比

比较的是 Spring MVC 与 Spring WebFlux 作为 HTTP 应用框架谁的性能更好。

Spring WebFlux

先看看 Spring WebFlux。

引入 pom 依赖:

        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-webflux</artifactId>        </dependency>

编写 http 接口:

@RestController@RequestMapping(\"/webflux\")public class WebFluxController {    public static AtomicLong COUNT = new AtomicLong(0);    @GetMapping(\"/hello/{latency}\")    public Mono<String> hello(@PathVariable long latency) {        System.out.println(\"Start:\" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        System.out.println(\"Page count:\" + COUNT.incrementAndGet());        Mono<String> res = Mono.just(\"welcome to Spring Webflux\").delayElement(Duration.ofSeconds(latency));//阻塞 latency 秒,模拟处理耗时        System.out.println(\"End:  \" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        return res;    }}

启动服务器,可以看到 WebFlux 默认选择 Netty 作为服务器:

在这里插入图片描述

使用 JMeter 进行压测:File -> 新建测试计划 -> 添加用户线程组 -> 在线程组上添加一个取样器,选择 Http Request。

配置 Http 请求,并在 HTTP Request 上添加监听器;这里不做复杂的压测分析,选择结果树聚合报告即可。

在这里插入图片描述

设置 http 请求超时时间(这一步也可以忽略,真实环境下 http 访问外部接口都会设置超时时间):

在这里插入图片描述

设置并发用户数,60 秒内全部启起来。

不断调整进行测试,每次开始前先 Clear All 清理一下旧数据,再点 save 保存一下,再点 Start 开始:

在这里插入图片描述

1000 用户,99 线大约 24 毫秒的延迟:

在这里插入图片描述

2000 用户,99 线大约 59 毫秒的延迟:

在这里插入图片描述

3000 用户,99 线大约 89 毫秒的延迟:

在这里插入图片描述

4000 用户,WebFlux 到 4000 并发用户时还是很稳:

在这里插入图片描述

Spring MVC

再来看看 Spring MVC 的性能。

引入 pom 文件:

        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>

编写 http 接口:

@RestController@RequestMapping(\"/springmvc\")public class SpringMvcController {    public static AtomicLong COUNT = new AtomicLong(0);    @GetMapping(\"/hello/{latency}\")    public String hello(@PathVariable long latency) {        System.out.println(\"Start:\" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        System.out.println(\"Page count:\" + COUNT.incrementAndGet());        try {            //阻塞 latency 秒,模拟处理耗时            TimeUnit.SECONDS.sleep(latency);        } catch (InterruptedException e) {            e.printStackTrace();            return \"Exception during thread sleep\";        }        System.out.println(\"End:\" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        return \"welcome to Spring MVC\";    }}

启动服务器。可以看到 Spring MVC 默认选择 Tomcat 作为服务器:

在这里插入图片描述

设置请求路径:

在这里插入图片描述

100 用户:

在这里插入图片描述

200 用户:

在这里插入图片描述

300 用户,从 300 用户开始,响应时间就开始增加:

在这里插入图片描述

400 用户:

在这里插入图片描述

500 用户:

在这里插入图片描述

550 用户,本例中,传统 Web 技术(Tomcat+Spring MVC)在处理 550 用户并发时,就开始有超时失败的:

在这里插入图片描述

600 用户,在处理 600 用户并发时,失败率就已经很高;用户并发数更高时几乎都会处理不过来,接近 100% 的请求超时。

在这里插入图片描述

1000 用户:

在这里插入图片描述

2000 用户:

在这里插入图片描述

3000 用户:

在这里插入图片描述

4000 用户:

在这里插入图片描述

由此可见传统 Web 技术和基于 Reactor 的 Spring WebFlux 的服务端性能差距还是挺大的,质的区别。

客户端性能比较

我们来比较一下 HTTP 客户端的性能。

先建一个单独的基于 Spring Boot 的 Http Server 工程提供标准的 http 接口供客户端调用。

/** * Http 服务提供方接口;模拟一个基准的 HTTP Server 接口 */@RestControllerpublic class HttpServerController {    @RequestMapping(\"product\")    public Product getAllProduct(String type, HttpServletRequest request, HttpServletResponse response) throws InterruptedException {        long start = System.currentTimeMillis();        System.out.println(\"Start:\" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        //输出请求头        Enumeration<String> headerNames = request.getHeaderNames();        while (headerNames.hasMoreElements()) {            String head = headerNames.nextElement();            System.out.println(head + \":\" + request.getHeader(head));        }        System.out.println(\"cookies=\" + request.getCookies());        Product product = new Product(type + \"A\", \"1\", 56.67);        Thread.sleep(1000);        //设置响应头和 cookie        response.addHeader(\"X-appId\", \"android01\");        response.addCookie(new Cookie(\"sid\", \"1000101111\"));        System.out.println(\"End:\" + LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS\")));        System.out.println(\"cost:\" + (System.currentTimeMillis() - start) + product);        return product;    }    @RequestMapping(\"products\")    public List<Product> getAllProducts(String type) throws InterruptedException {        long start = System.currentTimeMillis();        List<Product> products = new ArrayList<>();        products.add(new Product(type + \"A\", \"1\", 56.67));        products.add(new Product(type + \"B\", \"2\", 66.66));        products.add(new Product(type + \"C\", \"3\", 88.88));        Thread.sleep(1000);        System.out.println(\"cost:\" + (System.currentTimeMillis() - start) + products);        return products;    }    @RequestMapping(\"product/{pid}\")    public Product getProductById(@PathVariable String pid, @RequestParam String name, @RequestParam double price) throws InterruptedException {        long start = System.currentTimeMillis();        Product product = new Product(name, pid, price);        Thread.sleep(1000);        System.out.println(\"cost:\" + (System.currentTimeMillis() - start) + product);        return product;    }    @RequestMapping(\"postProduct\")    public Product postProduct(@RequestParam String id, @RequestParam String name, @RequestParam double price) throws InterruptedException {        long start = System.currentTimeMillis();        Product product = new Product(name, id, price);        Thread.sleep(1000);        System.out.println(\"cost:\" + (System.currentTimeMillis() - start) + product);        return product;    }    @RequestMapping(\"postProduct2\")    public Product postProduct(@RequestBody Product product) throws InterruptedException {        long start = System.currentTimeMillis();        Thread.sleep(1000);        System.out.println(\"cost:\" + (System.currentTimeMillis() - start) + product);        return product;    }    @RequestMapping(\"uploadFile\")    public String uploadFile(MultipartFile file, int age) throws InterruptedException {        long start = System.currentTimeMillis();        System.out.println(\"age=\" + age);        String filePath = \"\";        try {            String filename = file.getOriginalFilename();            //String extension = FilenameUtils.getExtension(file.getOriginalFilename());            String dir = \"D:\\\\files\";            filePath = dir + File.separator + filename;            System.out.println(filePath);            if (!Files.exists(Paths.get(dir))) {                new File(dir).mkdirs();            }            file.transferTo(Paths.get(filePath));        } catch (IOException e) {            e.printStackTrace();        }        Thread.sleep(1000);        System.out.println(\"cost:\" + (System.currentTimeMillis() - start));        return filePath;    }}

再来比较下各个客户端的性能差异。

下列客户端 WebClient/RestTemplate 代码请访问:

https://gitee.com/mnb1103/webclient.git

WebClient

和测试服务端时单独依赖不同的服务器相比,这次同时引入两个依赖。

   <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-webflux</artifactId>   </dependency>   <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>   </dependency>

引入 starter-web 是为了启动 Tomcat 服务器,测试时统一使用 Tomcat 服务器跑 http 客户端应用程序;引入 starter-webflux 是为了单独使用 WebClient API,而不是为了使用 Netty 作为 Http 服务器。

500 用户(超时时间设置 6 秒):

在这里插入图片描述

1000 用户(超时时间设置 6 秒):

在这里插入图片描述

1100 用户(超时时间设置 6 秒),可以看到已经开始有响应超时的了:

在这里插入图片描述

1200 用户(超时时间设置 10 秒):

在这里插入图片描述

resttemplate(不带连接池)

500 用户(超时时间设置 6 秒):

在这里插入图片描述

1000 用户并发(超时时间设置 6 秒):

在这里插入图片描述

1100 用户并发(超时时间设置 6 秒):

在这里插入图片描述

1200 用户(超时时间设置 10 秒),有少量响应超时:

在这里插入图片描述

resttemplate(带连接池)

        <dependency>            <groupId>org.apache.httpcomponents</groupId>            <artifactId>httpclient</artifactId>            <version>4.5.6</version>        </dependency>

500 用户(超时时间设置 6 秒):

在这里插入图片描述

1000 用户(超时时间设置 6 秒):

在这里插入图片描述

1100 用户(超时时间设置 6 秒),和不带连接池相比,错误率减少:

在这里插入图片描述

1200 用户(超时时间设置 10 秒),效果比不带连接池的 resttemplate 好点,但是响应耗时普遍还是比带连接池的 WebClient 高。

在这里插入图片描述

综合来看,是否使用 http 连接池对于单个接口影响有限,池的效果不明显;在多 http 地址、多接口路由时连接池的效果可能更好。

WebClient 连接池

默认情况下,WebClient 使用连接池运行。池的默认设置是最大 500 个连接和最大 1000 个等待请求。如果超过此配置,就会抛异常。

reactor.netty.internal.shaded.reactor.pool.PoolAcquirePendingLimitException: Pending acquire queue has reached its maximum size of 1000

报错日志显示已经达到了默认的挂起队列长度限制 1000,因此我们可以自定义线程池配置,以获得更高的性能。

关于 Reactor Netty 连接池请参考 Netty 官方和 Spring 官方的文档:

  • https://projectreactor.io/docs/netty/snapshot/reference/index.html#_connection_pool_2
  • https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client-builder-reactor-resources

1000 用户(超时时间设置 6 秒):

在这里插入图片描述

1100 用户(超时时间设置 6 秒),带连接池的效果好些,没有出现失败的:

在这里插入图片描述

1200 用户(超时时间设置 10 秒),响应延迟比默认配置的 WebClient 好些:

在这里插入图片描述

下面看下 WebClient 以阻塞方式获取结果的性能。

不自定义 WebClient 线程池配置,2000 用户(JMeter 不配置超时时间):

在这里插入图片描述

下面看下 WebClient + CompletableFuture 方式获取结果的性能。

不自定义 WebClient 线程池配置,2000 用户(JMeter 不配置超时时间):

在这里插入图片描述

虽然测试效果几乎没有差别,但是我们要清楚地知道调用 block 方法是会引发实时阻塞的,会一定程度上增加对 CPU 的消耗。

实际开发中通常是为了使用异步特性才用 WebClient,如果用 block 方式就白瞎了 WebClient 了,还不如直接用 restTemplate。

继续提高并发用户数,看看结果。

2000 用户性能比较

pooled webclient

在这里插入图片描述

rest

在这里插入图片描述

pooled rest

在这里插入图片描述

3000 用户性能比较

pooled webclient

在这里插入图片描述 rest

在这里插入图片描述

pooled rest

在这里插入图片描述

比较下来还是池化的 RestTemplate 和池化的 WebClient 效果好,实际生产环境推荐使用。如果应用的上下游需要异步支持可以使用 WebClient,保证全链路异步化,而不至于上游都异步化了,却卡(阻塞)在了 Http 出口处。

小结

使用 WebClient 在等待远程响应的同时不会阻塞本地正在执行的线程;本地线程处理完一个请求紧接着可以处理下一个,能够提高系统的吞吐量;而 restTemplate 这种方式是阻塞的,会一直占用当前线程资源,直到 HTTP 返回响应。如果等待的请求发生了堆积,应用程序将创建大量线程,直至耗尽线程池所有可用线程,甚至出现 OOM。另外频繁的 CPU 上下文切换,也会导致性能下降。

但是作为上述两种方式的调用方(消费者)而言,其最终获得 http 响应结果的耗时并未减少。比如文章案例中,通过浏览器访问后端的的两个接口(Spring MVC、Spring WebFlux)时,返回数据的耗时相同。即最终获取(消费)数据的地方还会等待。

使用 WebClient 替代 restTemplate 的好处是可以异步等待 http 响应,使得线程不需要阻塞;单位时间内有限资源下支持更高的并发量。但是建议 WebClient 和 WebFlux 配合使用,使整个流程全异步化;如果单独使用 WebClient,笔者实测,和 resttemplate差别不大!欢迎留言指教

代码地址:https://gitee.com/mnb1103/webclient.git

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

免责声明: 1、本站信息来自网络,版权争议与本站无关 2、本站所有主题由该帖子作者发表,该帖子作者与本站享有帖子相关版权 3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和本站的同意 4、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责 5、用户所发布的一切软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。 6、您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 7、请支持正版软件、得到更好的正版服务。 8、如有侵权请立即告知本站(邮箱:1099252741@qq.com,备用微信:1099252741),本站将及时予与删除 9、本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章和视频仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。