作为测试界的老司机,最近接到一项任务需要写新的性能测试代码。由于之前的测试代码风格和自己习惯的编码风格差别实在太大,因此放弃了模仿原来的测试代码继续添加测试用例的想法,自己从头开始写了一些测试代码。
结果,进行测试过程中居然出现了问题,问题,问题……
问题发现
在用自己新写的测试代码测试一个文件下载接口的时候,发现了一个奇怪的现象,在并发比较小的情况下(50并发),TPS、响应时间什么的一切看起来都很正常。但是当并发加到100+时,TPS曲线就变得很神奇了,同时还有部分请求失败,测试结果如图1所示:
图 1 并发100+性能测试结果
被测系统的架构相对简单,业务服务前端搭个Nginx,客户端请求都是把请求发送到Nginx,然后通过Nginx转发到业务服务器。 测试环境架构如图2所示,被测服务为图2中的NefsProxy:
图 2 测试环境架构图
原因查找通过查看测试脚本的日志发现,所有的错误都是由于建立连接失败引起的,登录到Nginx所在服务器上使用netstat命令查看连接数,发现测试过程中连接数很高,高的时候能达到3000—4000。为了确认上述问题是否是由于测试脚本有问题而引起的,我马上用老的测试脚本跑了一轮测试。结果很明显,老的测试脚本跑的测试结果一切正常,TPS也比新脚本的结果高不少,并且没有失败。因此可以断定是测试脚本的问题。
接下来就要寻找原因了:
→首先怀疑是不是测试脚本在每个请求结束后没有释放连接。问题的现象是连接数高导致新连接被Nginx拒绝。但是review了代码后,发现每次请求结束后都调用了:
因此,应该不是测试客户端没有主动释放连接引起的。
→后来想起Nginx配置了将请求强制转换为长连接。
Nginx的配置如下:
问题好像有点头绪了。一般配置长连接是为了提高服务端性能,为什么在我的测试中反而起到了反作用呢?
→接下来就要回过头来好好看看自己测试代码的实现逻辑了。
其中,NosObjectOperation.getObject是java代码实现的,每次getObject方法被调用时,会new一个HttpClient对象,然后通过HttpClient发送Http请求。
到这里,问题的本质慢慢浮出水面:客户端每发送一次请求,都会new一个HttpClient,并与Nginx新建一个连接,而Nginx这边又设置了强制长连接,每个Worker的最大空闲连接数为1024(keepalive 1024)。同时,测试环境的Nginx配置了4个Worker。因此,Nginx最多会保持4096个空闲连接。所以,由于连接数过多,在空闲连接被释放前,新的连接可能就会被拒绝。
问题解决
既然问题的原因找到了,该怎么修改测试脚本呢?
当然最简单的就是每个请求处理结束后强制将连接关闭。这样虽然能解决连接数多的问题,但是也同时间接的让Nginx强制长连接的配置失效了,达不到长连接提升性能的目的。
因此,我采用了这样的解决方法:尽量模拟真实的用户场景,每个测试线程使用一个HttpClient对象(也就是在python脚本的__init__方法里new一个HttpClient对象,getObject测试方法都调用这个对象发送Http请求,同时java方法nosObjectOperation.getObject也不再每次自己创建新的HttpClient对象,而使用传入的在Python脚本方法__init__中创建的HttpClient对象):
Attention:使用grinder进行性能测试时,每次创建测试线程时会调用__init__方法,也就是说有多少个并发线程,就会被调用多少次。通过这样的修改,如果测试时是100个并发线程,那测试客户端和Nginx之间就只会有100个ESTABLISHED的连接。
问题解决了,用新的脚本跑一次测试,结果如图3所示,很让人满意:
图 3 修改测试脚本后并发100+性能测试结果
注意,总结!总结!总结!
1)做性能测试,测试脚本的编写也是一个很重要的环节,只有模拟真实用户使用情况的脚本才能跑出真实的、有参考意义的性能测试结果。
2)当性能测试结果与预期不一致时,定位问题时首先要看测试脚本是否有问题,并对测试环境的各项配置(Nginx、Tomcat等)进行梳理。
3)当使用grinder测试框架进行比较复杂的性能测试,编写测试脚本时要弄清楚grinder测试框架的运行机理,各项配置的作用等。
如果觉得《浅谈HTTP接口性能测试脚本的编写》对你有帮助,请点赞、收藏,并留下你的观点哦!