Spring WebClient实战示例

WebClient实战

本文代码地址:https://github.com/bigbirditedu/webclient

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 完美支持。

webclient的HTTP API请参考:https://github.com/bigbirditedu/webclient

服务端性能对比

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

Spring WebFlux

先看看Spring WebFlux

引入pom依赖

        
            org.springframework.boot
            spring-boot-starter-webflux
        

编写http接口

@RestController
@RequestMapping("/webflux")
public class WebFluxController {

    public static AtomicLong COUNT = new AtomicLong(0);

    @GetMapping("/hello/{latency}")
    public Mono 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 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作为服务器

Spring WebClient实战示例_第1张图片

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

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

Spring WebClient实战示例_第2张图片

设置http请求超时时间

Spring WebClient实战示例_第3张图片

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

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

Spring WebClient实战示例_第4张图片

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

Spring WebClient实战示例_第5张图片

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

Spring WebClient实战示例_第6张图片

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

Spring WebClient实战示例_第7张图片

4000用户

webflux到4000并发用户时还是很稳

Spring WebClient实战示例_第8张图片

Spring MVC

再来看看SpringMVC的性能

引入pom文件

        
            org.springframework.boot
            spring-boot-starter-web
        

编写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";
    }
}

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

Spring WebClient实战示例_第9张图片

设置请求路径

Spring WebClient实战示例_第10张图片

100用户

Spring WebClient实战示例_第11张图片

200用户

Spring WebClient实战示例_第12张图片

300用户

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

Spring WebClient实战示例_第13张图片

400用户

Spring WebClient实战示例_第14张图片

500用户

Spring WebClient实战示例_第15张图片

550用户

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

Spring WebClient实战示例_第16张图片

600用户

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

Spring WebClient实战示例_第17张图片

1000用户

Spring WebClient实战示例_第18张图片

2000用户

Spring WebClient实战示例_第19张图片

3000用户

Spring WebClient实战示例_第20张图片

4000用户

Spring WebClient实战示例_第21张图片

客户端性能比较

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

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

/**
 * Http服务提供方接口;模拟一个基准的HTTP Server接口
 */
@RestController
public 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 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 getAllProducts(String type) throws InterruptedException {
        long start = System.currentTimeMillis();
        List 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;
    }
}

Tip

其它客户端代码请访问:https://github.com/bigbirditedu/webclient

webclient

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

   
            org.springframework.boot
            spring-boot-starter-webflux
   

   
            org.springframework.boot
            spring-boot-starter-web
   

引入starter-web是为了启动Tomcat服务器,测试时统一使用Tomcat服务器跑http客户端应用程序;

引入starter-webflux是为了单独使用webclient api,而不是为了使用Netty作为Http服务器;

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

Spring WebClient实战示例_第22张图片

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

Spring WebClient实战示例_第23张图片

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

可以看到已经开始有响应超时的了

Spring WebClient实战示例_第24张图片

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

Spring WebClient实战示例_第25张图片

resttemplate(不带连接池)

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

Spring WebClient实战示例_第26张图片

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

Spring WebClient实战示例_第27张图片

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

Spring WebClient实战示例_第28张图片

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

Spring WebClient实战示例_第29张图片

resttemplate(带连接池)

        
            org.apache.httpcomponents
            httpclient
            4.5.6
        

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

Spring WebClient实战示例_第30张图片

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

Spring WebClient实战示例_第31张图片

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

和 不带连接池相比,错误率减少

Spring WebClient实战示例_第32张图片

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

Spring WebClient实战示例_第33张图片

综合来看,是否使用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秒)

Spring WebClient实战示例_第34张图片

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

带连接池的效果好些,没有出现失败的

Spring WebClient实战示例_第35张图片

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

Spring WebClient实战示例_第36张图片

webclient阻塞方式获取结果;不自定义webclient线程池配置,2000用户(JMeter不配置超时时间)

Spring WebClient实战示例_第37张图片

webclient+CompletableFuture方式获取结果;不自定义webclient线程池配置,2000用户(JMeter不配置超时时间)

Spring WebClient实战示例_第38张图片

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

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

2000用户性能比较

pooled webclient

Spring WebClient实战示例_第39张图片

rest

Spring WebClient实战示例_第40张图片

pooled rest

Spring WebClient实战示例_第41张图片

3000用户性能比较

pooled webclient

Spring WebClient实战示例_第42张图片

rest

Spring WebClient实战示例_第43张图片

pooled rest

Spring WebClient实战示例_第44张图片

webclient 的HTTP API

WebClient 作为一个 HTTP 客户端工具,其提供了标准 HTTP 请求方式,支持 Get、Post、Put、Delete、Head 等方法,可以作为替代 resttemplate 的一个强有力的工具。

API演示代码地址:https://github.com/bigbirditedu/webclient

小结

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

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

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

到此这篇关于Spring WebClient实战示例的文章就介绍到这了,更多相关Spring WebClient 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Spring WebClient实战示例)