一次压力测试的故事

最近作了个系统,面向普通网民,设计的吞吐率是单机>1万/s,可水平扩展。我之前作的系统多是面向商业用户,很少有这么高的吞吐率要求,所以这次设计过程中对并发,资源竞争啥的也格外注意,这些暂且不表。等到开发完成,开始压力测试的时候,遇到了些故事,颇花了一些时间,此过程中也学到了不少知识。

 

系统开发完成时,运行在jetty上。当时QA作了一次压测,用5台肉鸡压一个server,得出结论是达到要求,当时我也觉得这就OK了。由于jetty用的是NIO,按照网上的说法,对大量短连接的应用,tomcat的BIO可能更适合,所以我想试试两者的差异。因为QA暂时没有时间配合,我只能自己开始鼓捣,而这一鼓捣就开始有故事了。

 

QA用jmeter作压力测试,我没用过jmeter又懒得从头学,就自己写了个肉鸡客户端,多线程以socket方式向服务器发请求,考虑到客户端本身不是重点,肉鸡就用socket直接写了:每个线程反复执行建立socket->发送->接收->关闭的过程,跑一段时间后统计下成功和各种异常的数目。

第一阶段测试的重点是BIO和NIO的差异,而不是HTTP栈的解析效率,所以直接码了个简单的多线程Socket服务器,ServerSocket.accept()一个Socket传给线程池,线程里的线程对这个socket跑接收->发送->关闭的过程。 SocketServer写好后用两个肉鸡就压上了,然后问题就来了。

肉鸡开始很快,但是一段时间后就几乎hang在那里不动,开始出现异常,包括 connection reset by peer或者connection timeout,过一会儿后吞吐量能上去一点,但很快又落下来,如此反复。这时用jvisualvm看Server,基本就没loading,那是为啥呢?

第一个反应是我的SocketServer写的有问题,于是用NIO手写了个逻辑相同的NioServer,但问题依旧,用mina框架写了个MinaServer再测也还是一样。于是觉得问题不是出现在Server的写法上,而是其它地方。在网上搜了半天,试了不少方法后,觉得可能是http://performtest163.blog.163.com/blog/static/14007696420115295119462 说的问题,看了看跑server的机器

/proc/sys/net/ipv4/tcp_tw_reuse 

/proc/sys/net/ipv4/tcp_tw_recycle 

这两个值确实都没开启,因为测试机不好随便重启,换了个机器找OP要到权限后将这两个值设为1并重启机器后,吞吐量一下子上去了,但客户端还是出现了大量的连接异常,而且这个简单的SocketServer单肉鸡的吞吐率居然比QA测的实际运行在jetty 上的Server低一半还多,这就让人很难理解了。

还是怀疑Server写法的不同。详细分析了代码后,问题应该出现在这里:

重温下以上配套的SocketServer和SocketClient逻辑

Client: 建立socket->发送->接收->关闭

Server: accept socket->接收->发送->关闭

两边都会close socket,那么到底是那边的关闭起作用了?对于这组逻辑,理论上说发送方(Server)应该先到达关闭,但由于tcp是全双工的,发送方也需要等待接收方的SYNC,所以无法确定。不过按照链接中的说法,tcp_tw_reuse 针对的是Server端主动关闭造成的TIME_WAIT,既然打开tcp_tw_reuse起了作用,所以推测这组逻辑中应该是Server端的关闭起了作用。 

另外我又去查了一下,通常HTTP SERVER除非超时,不会主动关闭socket。所以这里的差异可能是由关闭方不同造成的,顺着这个思路,我又用apache HttpClient写了个JettyClient肉鸡去压实际的jetty系统。

这个JettyClient中肉鸡中每个线程的逻辑为反复跑

new HttpClient()->用new 出来的HttpClient发一条Get->接收HttpResponse->关闭HttpClient

结果一会儿工夫肉鸡又Hang住了。由于有了前面的经验,很快发现是由于主动关闭连接,造成肉鸡的Socket耗尽的原因。但问题又来了

1) 肉鸡上确实没有打开tcp_tw_reuse ,肉鸡也不能随便打开这个选项并重启

2) 更关键的是,为什么QA的压力测试用例没有爆出这个问题呢?

 

目前为止,被测系统是同一个,区别只能在QA的测试用例和我的JettyClient中找了。看了QA的测试用例后,我得出的结论是,QA的测试用例写错了。

QA的测试用例中虽然有多线程,但并没有反复的创建和关闭HttpClient,而是在一个线程中初始化时创建了一个HttpClient并反复的通过这个HttpClient去发送Get Request并接受响应。这与线上的实际场景并不一致:线上的100W个请求,可能是100W个浏览器建立了100W个socket各进行一次http通讯,而QA测试的场景是 50个浏览器建立了50个socket,并在每个socket上进行了2W次http通讯。在此过程中,绝大多数socket的建立过程被舍弃了,这就说明了为什么QA的测试用例没有遇到socket耗尽的问题,也说明了为什么我写的简单的SocketServer吞吐量还不如QA给出的指标(这种应用下建立socket的开销可能比逻辑本身开销还要大)。

 

总结一下目前为止的结论:

1) 对于压力很大的Server,最好是打开tcp_tw_reuse,以免Server主动关闭的连接不能及时重用

2) 若要真正模拟线上的情景,还是应该用多台机器,每次都新建并关闭连接。尤其是对于这种逻辑简单的http应用,少量肉鸡大量线程客户端不关闭连接的测试方式远不如大量肉鸡少量客户端连接关闭的方式结果准确

 

你可能感兴趣的:(socket,httpclient,压力测试)