文章来源于网易易测微信号(et163yun),欲知更多技术干货,详见微信平台。作为测试界的老司机,最近接到一项任务需要写新的性能测试代码。由于之前的测试代码风格和自己习惯的编码风格差别实在太大,因此放弃了模仿原来的测试代码继续添加测试用例的想法,自己从头开始写了一些测试代码。
结果,进行测试过程中居然出现了问题,问题,问题……
在用自己新写的测试代码测试一个文件下载接口的时候,发现了一个奇怪的现象,在并发比较小的情况下(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 测试框架的运行机理,各项配置的作用等。