Flag
在文章10 万 QPS,K6、Gatling 和 FunTester 终极对决!中,最后测试结果FunTester除了在在 CPU 方面有一丁点优势以外,内存和 QPS 均略逊一筹,特别是内存方面劣势尤为明显。当时立了一个flag
:
- 将非必要的处理改成异步
- 尝试更换测试元数据存储方式
- 逐步丢弃业务相关兼容代码
成果
周末终于有时间进行操作了。经过不懈改代码测试和临阵抱佛脚学习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 |
下面我会逐步增加一些影响因素,然后在判断各种因素对性能测试结果的影响。
优化内容
优化内容主要两方面:
- 取消所有不必要的统计、计算功能(后面会恢复),只保留运行次数和运行总时间
- 取消解析响应(这个主要体现在内存降低),后面会保留改方法,大部分都需要对响应结果进行验证
- 将响应时间统计基础数据类型由
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());// 消耗响应实体,并关闭相关资源占用
}
上面代码即是 QPS*118904* 的代码,可以看出这是很难在实际工作中采用的,因为总要做一些其他除了发送请求之外的事情。下面我就分享一下逐步增加这些功能的实测结果。
实测结果
打开监控
这里我依然用的是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)
}
测试结果如下:
所以我猜测应该是在不断记录时间戳和计算的响应时间的过程中导致这个内存上升的。总体来讲对于内存使用并不多,所以暂时先不优化了。
解析响应
这里用到的旧方法:
/**
* 简单发送请求,此处不用{@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 测试框架架构图初探
- 10 万 QPS,K6、Gatling 和 FunTester 终极对决!
- 单机 12 万 QPS——FunTester 复仇记
- 初遇 Postman,SayHi 的三种方式
- 生产环境中进行自动化测试
- JMeter 吞吐量误差分析
- IntelliJ 中基于文本的 HTTP 客户端
- 物联网测试
- Selenium4 IDE,它终于来了
- 绑定手机号性能测试
- Java 多线程编程在 JMeter 中应用
- 电子书网站爬虫实践
- Socket 接口异步验证实践
- Groovy 在 JMeter 中处理 cookie
点击阅读阅文,查看 FunTester 历史原创集合