单机12万QPS——FunTester复仇记

Flag

在文章10万QPS,K6、Gatling和FunTester终极对决!中,最后测试结果FunTester除了在在CPU方面有一丁点优势以外,内存和QPS均略逊一筹,特别是内存方面劣势尤为明显。当时立了一个flag

  1. 将非必要的处理改成异步
  2. 尝试更换测试元数据存储方式
  3. 逐步丢弃业务相关兼容代码

成果

周末终于有时间进行操作了。经过不懈改代码测试和临阵抱佛脚学习Java基础,终于得到了一个满意的结果。硬件方面:在关掉监控软件,关掉了其他无关要紧的程序,软件方面停掉了所有无用的有用的代码(注释得亲妈都不认识),最终的测试结果停留在118904,交于之前的数据104375提升13.92%。内存也有大大地降低,在请求量300万 ~ 500万规模情况下,占用内存在700M ~ 800M区间,相较于之前的1770,减少了57.62

这里放上上次测试结果:

框架 CPU 内存 QPS RT
K6 718.74 370.0 75980 1
Gatling 585.97 350.0 113355 1
FunTester 528.03 1770 104375 1

下面我会逐步增加一些影响因素,然后在判断各种因素对性能测试结果的影响。

优化内容

优化内容主要两方面:

  1. 取消所有不必要的统计、计算功能(后面会恢复),只保留运行次数和运行总时间
  2. 取消解析响应(这个主要体现在内存降低),后面会保留改方法,大部分都需要对响应结果进行验证
  3. 将响应时间统计基础数据类型由int改成了short

测试代码

我用了最新的com.funtester.base.constaint.FixedThread模板,这个方法进行了多处简化,用来作为一个基础模板使用,替代旧的模板com.funtester.base.constaint.ThreadLimitTimesCount等。


import com.funtester.base.constaint.FixedThread
import com.funtester.base.constaint.ThreadBase
import com.funtester.config.Constant
import com.funtester.frame.execute.Concurrent
import com.funtester.httpclient.ClientManage
import com.funtester.httpclient.FunLibrary
import org.apache.http.client.methods.HttpRequestBase

class Share2 extends FunLibrary {

    public static void main(String[] args) {
        ClientManage.init(10, 5, 0, "", 0)
        String url1 = "http://localhost:12345/tps"
        Constant.RUNUP_TIME = 0;
        def get = getHttpGet(url1)
        def task = new FunTester(get)
        new Concurrent(task,30, "本地固定QPS测试").start()
        testOver()
    }
    private static class FunTester extends FixedThread<HttpRequestBase> {

        FunTester(HttpRequestBase httpRequestBase) {
            super(httpRequestBase, 50000, true)
        }

        @Override
        protected void doing() throws Exception {
            FunLibrary.executeOnly(f)
        }

        @Override
        ThreadBase clone() {
            return new FunTester(f)
        }
    }

}

新的压测模板

com.funtester.base.constaint.FixedThread#run方法中被注释掉的代码即为不必要的统计和计算内容。但是针对实际测试场景来说是必须要的,所以测试完成我会重新恢复这些代码。

/**
 * 为了适应OK项目,新增类,后续{@link ThreadLimitTimeCount}和{@link ThreadLimitTimesCount}将会在某个时刻被弃用,会一直保留兼容旧用例
 */
public abstract class FixedThread<F> extends ThreadBase<F> {

    private static final long serialVersionUID = -4617192188292407063L;

    private static final Logger logger = LogManager.getLogger(ThreadLimitTimesCount.class);

    public FixedThread(F f, int limit, boolean isTimesMode) {
        this.isTimesMode = isTimesMode;
        this.limit = limit;
        this.f = f;
    }

    protected FixedThread() {
        super();
    }

    @Override
    public void run() {
        try {
            before();
            long ss = Time.getTimeStamp();
            while (true) {
//                try {
                executeNum++;
//                    long s = Time.getTimeStamp();
                doing();
//                    long et = Time.getTimeStamp();
//                    short diff = (short) (et - s);
//                    costs.add(diff);
//                } catch (Exception e) {
//                    logger.warn("执行任务失败!", e);
//                    errorNum++;
//                } finally {
//                    if ((isTimesMode ? executeNum >= limit : (Time.getTimeStamp() - ss) >= limit) || ThreadBase.needAbort() || status())
                if (executeNum >= limit)
                    break;
//                }
            }
            long ee = Time.getTimeStamp();
            if ((ee - ss) / 1000 > RUNUP_TIME + 3)
                logger.info("线程:{},执行次数:{},错误次数: {},总耗时:{} s", threadName, executeNum, errorNum, (ee - ss) / 1000.0);
            Concurrent.allTimes.addAll(costs);
            Concurrent.requestMark.addAll(marks);
        } catch (Exception e) {
            logger.warn("执行任务失败!", e);
        } finally {
            after();
        }
    }

    @Override
    public boolean status() {
        boolean b = errorNum > executeNum * 2 && errorNum > 10;
        if (b) {
            ThreadBase.stop();
            logger.error("错误率过高,停止测试!");
        }
        return b;
    }

    @Override
    protected void after() {
        super.after();
        GCThread.stop();
    }


}

这个是相当精简的方法,真的注释得亲妈都不认识,但是在我后面逐步恢复代码的实测中,影响不大。

执行方法

旧的方法如下:

    /**
     * 简单发送请求,此处不用{@link CloseableHttpResponse#close()}也能释放连接
     *
     * @param request
     */
    public static String executeSimlple(HttpRequestBase request) throws IOException {
        CloseableHttpResponse response = ClientManage.httpsClient.execute(request);
        return getContent(response.getEntity());
    }

其中com.funtester.httpclient.FunLibrary#getContent方法内容如下:


    /**
     * 解析{@link HttpEntity},不区分请求还是响应
     *
     * @param entity
     * @return
     */
    public static String getContent(HttpEntity entity) {
        String content = EMPTY;
        try {
            content = EntityUtils.toString(entity, DEFAULT_CHARSET);// 用string接收响应实体
            EntityUtils.consume(entity);// 消耗响应实体,并关闭相关资源占用
        } catch (Exception e) {
            logger.warn("解析响应实体异常!", e);
            fail();
        }
        return content;
    }

这里解析响应为java.lang.String类型,是在太费内存了。但是如果想验证响应结果,又是必需的。

新的请求方法:

    /**只发送要求,不解析响应
     * 此处不用{@link CloseableHttpResponse#close()}也能释放连接
     * @param request
     * @throws IOException
     */
    public static void executeOnly(HttpRequestBase request) throws IOException {
        CloseableHttpResponse response = ClientManage.httpsClient.execute(request);
        EntityUtils.consume(response.getEntity());// 消耗响应实体,并关闭相关资源占用
    }

上面代码即是QPS118904的代码,可以看出这是很难在实际工作中采用的,因为总要做一些其他除了发送请求之外的事情。下面我就分享一下逐步增加这些功能的实测结果。

实测结果

打开监控

这里我依然用的是Mac自带的活动管理器监控,在优化过程中我也用到了jvisualvm监控,但是不如系统自带来得直观,所以这里就不展示。在实际观测中,会发现堆内存大于1G的,实际使用很低(300M以下),挺有趣,有机会再研究研究Groovy

框架 CPU 内存 QPS RT
FunTester 558.71 741.9 MB 117123 1
  • 性能变化不大,说明监控资源消耗不怎么影响测试结果。

异常处理

这里我把com.funtester.base.constaint.FixedThread#run方法中try-catch代码放开。

 @Override
    public void run() {
        try {
            before();
            long ss = Time.getTimeStamp();
            while (true) {
                try {
                    executeNum++;
//                    long s = Time.getTimeStamp();
                    doing();
//                    long et = Time.getTimeStamp();
//                    short diff = (short) (et - s);
//                    costs.add(diff);
                } catch (Exception e) {
                    logger.warn("执行任务失败!", e);
                    errorNum++;
                } finally {
                    if ((isTimesMode ? executeNum >= limit : (Time.getTimeStamp() - ss) >= limit) || ThreadBase.needAbort() || status())
                        break;
                }
            }
            long ee = Time.getTimeStamp();
            if ((ee - ss) / 1000 > RUNUP_TIME + 3)
                logger.info("线程:{},执行次数:{},错误次数: {},总耗时:{} s", threadName, executeNum, errorNum, (ee - ss) / 1000.0);
            Concurrent.allTimes.addAll(costs);
            Concurrent.requestMark.addAll(marks);
        } catch (Exception e) {
            logger.warn("执行任务失败!", e);
        } finally {
            after();
        }
    }

实测结果如下:

框架 CPU 内存 QPS RT
FunTester 532.26 684.8 MB 118400 1
  • 实测发现,QPS没有变化。(误差范围内),说明在没有抛出异常的情况下,try-catch代码对性能影响可忽略。在抛出异常的情况下,可以参考文章性能测试误差对比研究(四),当然在QPS超过10万的情况下,影响应该会非常大,毕竟响应速度太快了。

统计计算

这次我把统计代码放开,看看对性能的影响。com.funtester.base.constaint.FixedThread#run如下:

 @Override
    public void run() {
        try {
            before();
            long ss = Time.getTimeStamp();
            while (true) {
                try {
                    executeNum++;
                    long s = Time.getTimeStamp();
                    doing();
                    long et = Time.getTimeStamp();
                    short diff = (short) (et - s);
                    costs.add(diff);
                } catch (Exception e) {
                    logger.warn("执行任务失败!", e);
                    errorNum++;
                } finally {
                    if ((isTimesMode ? executeNum >= limit : (Time.getTimeStamp() - ss) >= limit) || ThreadBase.needAbort() || status())
                        break;
                }
            }
            long ee = Time.getTimeStamp();
            if ((ee - ss) / 1000 > RUNUP_TIME + 3)
                logger.info("线程:{},执行次数:{},错误次数: {},总耗时:{} s", threadName, executeNum, errorNum, (ee - ss) / 1000.0);
            Concurrent.allTimes.addAll(costs);
            Concurrent.requestMark.addAll(marks);
        } catch (Exception e) {
            logger.warn("执行任务失败!", e);
        } finally {
            after();
        }
    }

实测结果:

框架 CPU 内存 QPS RT
FunTester 558.71 961.6 MB 117054 1

QPS依然没有太多变化,内存显然升高了很多。这里我们借助jvisualvm工具查看内存占用情况。

有数据收集内存监控

起初我猜测FunTester框架中不断重建统计对象导致的内存占用如此之多,所以特意在同样的情况下对比了一个没有统计每次请求次数的内存监控图。看来两种情况下堆内存大小最大差值在120M左右。在使用堆内存上差不多也是这个差距。数据量250万,

无数据收集内存监控

为此我多做了一个实验,100万的数字存储占用的空间。

测试代码:

        for (i in 0..<1000000) {
            def i1 = i % 5
            list.add(i1)
        }

测试结果如下:

100万数字占用内存

所以我猜测应该是在不断记录时间戳和计算的响应时间的过程中导致这个内存上升的。总体来讲对于内存使用并不多,所以暂时先不优化了。

解析响应

这里用到的旧方法:

    /**
     * 简单发送请求,此处不用{@link CloseableHttpResponse#close()}也能释放连接
     *
     * @param request
     */
    public static String executeSimlple(HttpRequestBase request) throws IOException {
        CloseableHttpResponse response = ClientManage.httpsClient.execute(request);
        return getContent(response.getEntity());
    }

测试结果如下:

框架 CPU 内存 QPS RT
FunTester 562.79 1.25 GB 110501 1

性能稍微有一些下降,不过影响也不大。但是内存很高,这个符合预期。

我又进行了一个小实验,测试结束之后,sleep(100000),然后观察堆内存使用,发现在达到极值之后会慢慢下降。

总结

经过一些优化之后,FunTester指标QPS、CPU和内存均有提升。对于之前担心过的统计代码会导致性能下降的疑惑也不存在了。加上有些代码依然可以保持在11万+的QPS,加上解析响应也能达到11万QPS,虽然内存有所增加,单还是在可接受范围内。本次的学习和优化告一段落。

Have Fun ~ Tester !

FunTester,一群有趣的灵魂,腾讯云&Boss认证作者,GDevOps官方合作媒体。


点击阅读阅文,查看FunTester历史原创集合

posted @ 2021-07-27 17:43  FunTester  阅读(143)  评论(0编辑  收藏  举报