亲宝软件园·资讯

展开

Spring WebClient

程猿薇茑 人气:0

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 的主要优势有:

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

服务端性能对比

比较的是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请求超时时间

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

4000用户

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

在这里插入图片描述

Spring MVC

再来看看SpringMVC的性能

引入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";
    }
}

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

在这里插入图片描述

设置请求路径

在这里插入图片描述

100用户

在这里插入图片描述

200用户

在这里插入图片描述

300用户

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

在这里插入图片描述

400用户

在这里插入图片描述

500用户

在这里插入图片描述

550用户

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

在这里插入图片描述

600用户

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

在这里插入图片描述

1000用户

在这里插入图片描述

2000用户

在这里插入图片描述

3000用户

在这里插入图片描述

4000用户

在这里插入图片描述

客户端性能比较

我们来比较一下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<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;
    }
}

Tip

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

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官方的文档:

http://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

在这里插入图片描述

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差别不大!欢迎留言指教!

加载全部内容

相关教程
猜你喜欢
用户评论