在上期的文章插上 NIO 翅膀,FunTester 飞上天中,我学习了Java NIO的相关基础,今天我来分享一下自己实践的结果。

本来我的想法是在性能测试中应用这个异步请求客户端,毕竟这个义务的出现就是为了解决一些性能问题。但是在我自己在本地实际测试之后发现如果在发压端使用异步请求这种策略。的确能够提升请求的效率,但是这种效率是创建了更多的线程去处理响应。在测试服务 QPS 比较大的情况下,会极大的占用客户端的 cpu,这和增加并发线程串行处理类似效果。

在之前的文章单机 12 万 QPS——FunTester 复仇记同等硬件条件和软件条件下,异步请求到 qps 并没有达到理想中的效果,反而占用了更多的 cpu。Cpu 占用能达到 900%。这远远大于相同 q ps 处理能力情况下的。串行请求占用。对于响应时间稍微长一些的接口异步请求的效果等同于增加线程,而且给客户端统计响应时间,造成一些困扰。所以我就放弃了在性能测试中应用异步请求的这个想法。

抛开性能不谈,我们用到异步请求最多的场景应该是在自动化接口测试。异步请求可以极大的提升请求的频率。如果我们有成百上千个接口,用例需要执行,也就是说我们可能至少得执行上千次请求。才能完成一次用力的检查。如果使用多线程去执行这些任务,显得有些累赘繁重。而且技术要求又比较高。稍有不慎,就有可能会导致用例的非正常原因失败。在这种场景下,http client 异步请求就有了展示的天地。

下面我分享一下异步请求在下面我分享一下异步请求在 HTTP 接口自动化测试中的效率。

moco 服务

服务端我利用FunTester moco server 框架架构图测试框架在局域网环境起了一个测试服务,服务只有一个默认延迟接口,用来模拟固定响应时间的 HTTP 接口。。Groovy脚本如下:

import com.mocofun.moco.MocoServer

class TestDemo extends MocoServer{

    static void main(String[] args) {
        def log = getServerNoLog(12345)
        server.response(delay(jsonRes(getJson("Have=Fun ~ Tester !")), 10))
        def run = run(log)
        waitForKey("FunTester")
        run.stop()
    }
}

在后面的测试中,我会更改。延迟的时间已达到控制接口响应时间的目的来验证异步请求在不同响应时间的情况下的效率对比。整个测试过程我会采用加法语言作为基础,而不是之前用到的 Groovy。

基础测试

在 FunTester 测试框架中,我封装了很多个用于发起 http 请求的方法。具体的方法如下,但是在实际的测试过程中,因为是单线程去做的测试,所以差别不是很大。

getHttpResponse(httpGet);
executeOnly(httpGet);
executeSimlple(httpGet);
executeSync(httpGet);
executeSyncWithLog(httpGet);
executeSync(httpGet);

脚本

基础设施脚本的思路非常简单,就是创建一个请求,然后进行多次循环,然后统计总时间。这里之所以要先请求一次,为了防止第一次请求时间过长会对整个测试结果造成的影响。

public static void main(String[] args) {
    String url = "http://localhost:12345/FunTester";
    HttpGet httpGet = getHttpGet(url);
    LOG_KEY = false;
    getHttpResponse(httpGet);
    long start = Time.getTimeStamp();
    for (int i = 0; i < 200; i++) {
        getHttpResponse(httpGet);
    }
    long end = Time.getTimeStamp();
    output(end - start);


    testOver();
}

测试数据

测试结果基本在 1250ms-1400ms 之间。

异步请求的效率

不处理响应

脚本如下:

public static void main(String[] args) {
    String url = "http://localhost:12345/FunTester";
    HttpGet httpGet = getHttpGet(url);
    LOG_KEY = false;
    ClientManage.startAsync();
    getHttpResponse(httpGet);
    long start = Time.getTimeStamp();
    for (int i = 0; i < 100; i++) {
        executeSync(httpGet);
    }
    long end = Time.getTimeStamp();
    output(end - start);
    testOver();
}

测试结果18ms ~ 25ms之间。效率提升有几十倍之多。一开始我也是这么觉得的。但是其实这个数据并不是他真正的效率。因为这个方法只是把请求发出去了,而并没有接收到请求,在大多数的时候,我们需要接收到请求,然后再去做一些。别的操作,所以说我们一般都是需要把请求接收处理之后,然后才算是整个请求的完成。

处理响应

处理响应会用到一个类org.apache.http.concurrent.FutureCallback,我们可以自定义去实现这个类。也可以在我们执行所有请求之后。然后去阻塞获取响应。

首先我演示一个简单的日志打印的实现类。

/**
 * 异步请求打印日志的callback
 */
public static final FutureCallback<HttpResponse> logCallback = new FutureCallback<HttpResponse>() {
    @Override
    public void completed(HttpResponse httpResponse) {
        HttpEntity entity = httpResponse.getEntity();
        String content = getContent(entity);
        logger.info("响应结果:{}", content);
    }

    @Override
    public void failed(Exception e) {
        logger.warn("响应失败", e);
    }

    @Override
    public void cancelled() {
        logger.warn("取消执行");
    }
};

这里就不给大家展示这个实现类测试数据了。因为没有多大的差别,在实际工作中,我暂时也想不到啊应用场景。

下面分享一种异步请求之后,对响应数据结果的收集的这实现类。

public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
    String url = "http://localhost:12345/FunTester";
    HttpGet httpGet = getHttpGet(url);
    LOG_KEY = false;
    ClientManage.startAsync();
    getHttpResponse(httpGet);
    List<Future<HttpResponse>> fs = new ArrayList<>();
    long start = Time.getTimeStamp();
    for (int i = 0; i < 100; i++) {
        Future<HttpResponse> httpResponseFuture = executeSync(httpGet);
        fs.add(httpResponseFuture);
    }
    for (int i = 0; i < fs.size(); i++) {
        Future<HttpResponse> httpResponseFuture = fs.get(i);
        HttpResponse httpResponse = httpResponseFuture.get();//阻塞获取响应结果
        EntityUtils.consume(httpResponse.getEntity());//消耗响应试题,释放连接资源
    }
    long end = Time.getTimeStamp();
    output(end - start);
    testOver();
}

这个实现方式是官方的一个实现方式,其中 get 方法是阻塞获取响应响应结果。呃,用一个 for 循环遍历,就是为了拿到所有的响应结果之后再统计时间。实测的结果总的响应时间是110ms ~ 130ms之间。对比串行请求,这个效率提升了大概十倍。

上面这种方式就是异步把所有的请求发出去之后,然后通过阻塞的方法。获取所有的响应结果,然后再去做进一步的处理。下面给大家介绍一种异步处理响应结果的方式。这里用到了一个类java.util.concurrent.CountDownLatch。有兴趣的小伙伴可以看一下我以前写的应用文章:Java 线程同步三剑客

这里要模拟的就是对一类请求的响应进行统一的结果验证。主要的目标就是验证一下响应的 code 以及简单验证响应的内容。

测试代码:

public static void main(String[] args)  {
    String url = "http://localhost:12345/FunTester";
    HttpGet httpGet = getHttpGet(url);
    LOG_KEY = false;
    ClientManage.startAsync();
    getHttpResponse(httpGet);
    CountDownLatch countDownLatch = new CountDownLatch(100);
    long start = Time.getTimeStamp();
    for (int i = 0; i < 100; i++) {
        executeSync(httpGet, new FunTester(countDownLatch));
    }
    long end = Time.getTimeStamp();
    output(end - start);
    testOver();
}

实现类代码:


private static class FunTester implements FutureCallback<HttpResponse> {

    CountDownLatch countDownLatch;

    public FunTester(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void completed(HttpResponse result) {
        try {
            HttpEntity entity = result.getEntity();
            String content = getContent(entity);
            Assert.assertTrue(content.contains("FunTes1ter"));
        } finally {
            countDownLatch.countDown();
        }
    }

    @Override
    public void failed(Exception ex) {
        logger.error(ex);
    }

    @Override
    public void cancelled() {
        logger.error("取消了!");
    }

}

Have Fun ~ Tester !


↙↙↙阅读原文可查看相关链接,并与作者交流