原文 : Using nGrinder to perform load test for a socket.io app by Mavlarn
nGrinder不仅可以用来测试通常的Web应用程序,也可以用于JDBC,Web服务或者像socket.io所提供的这样的实时应用。
socket.io旨在帮助我们在各种浏览器与移动设备上实现实时app功能。现在,基于浏览器和移动设备的应用越来越多,而我们也可以使用nGrinder对这些应用进行性能测试。因为我们可以使用扩展的java包来扩展测试脚本,所以,我们借助socket.io的java客户端,就可以实现对于基于socket.io的应用的性能测试。关于socket.io的java客户端,我们使用socket.io-java-client,有关这个包的使用,请参考github上的说明,其代码中也有实例。
但是,这个java客户端使用了异步方式来发送请求和接收响应。而我们在进行测试的时候,需要记录每一个请求的处理时间,所以我们需要将对SocketIO类做一些修改,来实现同步的目的。
其主要思想是,使用SocketIO对象创建一个与应用服务器的连接,借助java的同步机制,在发送请求之后,就等待返回值。在这个例子中,我使用了Java Lock和Condition来达到这一目标。我们创建一个SocketIO类的子类BlockingSocketIO,添加一个发送消息并等待返回结果的方法。下面是BlockingSocketIO类的源代码:
package my; import io.socket.IOAcknowledge; import io.socket.IOCallback; import io.socket.SocketIO; import io.socket.SocketIOException; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.json.JSONObject; /** * Class description. * * @author Mavlarn * @since */ public class BlockingSocketIO implements IOCallback { private SocketIO socketIO; private ReentrantLock transportLock; private Condition responseCondition; private String respMsg; public BlockingSocketIO (String url) { try { transportLock = new ReentrantLock(); responseCondition = transportLock.newCondition(); socketIO = new SocketIO(url, this); } catch (Exception e) { e.printStackTrace(); } } public String sendAndRcv (final String message) { try { transportLock.lock(); socketIO.send(message); respMsg = null; responseCondition.await(); return respMsg; } catch (Exception e) { e.printStackTrace(); } finally { transportLock.unlock(); } return respMsg; } public String sendAndRcv(final JSONObject json) { try { transportLock.lock(); socketIO.send(json); respMsg = null; responseCondition.await(); return respMsg; } catch (Exception e) { e.printStackTrace(); } finally { transportLock.unlock(); } return respMsg; } public String emitAndRcv(String event, final Object args) { try { transportLock.lock(); socketIO.emit(event, args); respMsg = null; responseCondition.await(); return respMsg; } catch (Exception e) { e.printStackTrace(); } finally { transportLock.unlock(); } return respMsg; } @Override public void onMessage(JSONObject json, IOAcknowledge ack) { setResponse(json.toString()); } @Override public void onMessage(String data, IOAcknowledge ack) { setResponse(data); } private void setResponse(String data) { try { transportLock.lock(); respMsg = data; responseCondition.signal(); System.out.println("Server said:" + data); } finally { transportLock.unlock(); } } @Override public void onError(SocketIOException socketIOException) { System.out.println("an Error occured"); socketIOException.printStackTrace(); } @Override public void onDisconnect() { System.out.println("Connection terminated."); } @Override public void onConnect() { System.out.println("Connection established"); } @Override public void on(String event, IOAcknowledge ack, Object... args) { System.out.println("Server triggered event '" + event + "'"); setResponse(args<a href="/wiki_ngrinder/entry/0" class="notexist">0</a>.toString()); } }
我们需要把这个类打成jar包并上传到nGrinder的lib文件夹中。同时,也要上传socketio.jar及它所依赖的库WebSocket.jar和Json-org.jar。
接下来,我们需要在nGrinder中执行测试场景的Python脚本。如下所示:
from net.grinder.script.Grinder import grinder from net.grinder.script import Test from org.json import JSONObject from my import BlockingSocketIO test1 = Test(1, "Test1") class TestRunner: def testSocketIO(self): json = JSONObject() user = "Thread-%s" % grinder.threadNumber json.putOpt("user", user) msg = "test message<%s>." % user json.putOpt("message", msg) grinder.logger.info("msg:" + json.toString()) respMsg = self.socketIO.emitAndRcv("user message", json) return respMsg def __init__(self): grinder.statistics.delayReports=True #init socket io #create socket io object in thread init function. Then every thread will use its own socket.io connection. self.socketIO = BlockingSocketIO("http://127.0.0.1:3000") #send socket.io server to init user json = JSONObject() user = "Thread-%s" % grinder.threadNumber json.putOpt("username", user) self.socketIO.emitAndRcv("user", json) # test method def __call__(self): resp = self.testSocketIO() if "test message" in resp : grinder.statistics.forLastTest.success = 1 else : grinder.statistics.forLastTest.success = 0 test1.record(TestRunner.testSocketIO)在这个脚本中,在测试对象TestRunner的init函数中,我们创建了一个socket.io连接对象,然后这个线程的所有测试将使用相同的连接。这对基于socket.io的长连接池的应用是非常重要的。因为基于socket.io应用中,每个用户跟服务器之间是一直保持连接的。
var http = require('http'), io = require('socket.io'); var app = http.createServer(); app.listen(3000); console.log('Server running at http://127.0.0.1:3000/'); // Socket.IO server var io = io.listen(app); io.sockets.on('connection', function (socket) { console.log("new connection from" + socket); get and log connection socket.on('user message', function (msg) { //accept a request with “user message” event socket.emit('user message processed', {user: msg.user, message: msg.message}); }); socket.on('user', function (userMsg) { //accept a request with “user” event, like user login. socket.user = userMsg.username; socket.emit('user processed', {user: userMsg.user, message: "New user come in."}); }); socket.on('disconnect', function () { if (!socket.user) return; socket.emit('announcement', {user: socket.user, action: 'disconected'}); }); });用下面的语句运行这个模拟服务器:
node server.js你应该能够看到一条日志说这个服务器正运行在http://127.0.0.1:3000/。
2013-03-11 13:13:08,844 INFO elapsed time is 17 ms 2013-03-11 13:13:08,844 INFO Final statistics for this process: 2013-03-11 13:13:08,854 INFO Tests Errors Mean Test Test Time TPS Time (ms) Standard Deviation (ms) Test 1 1 0 3.00 0.00 58.82 "Test1" Totals 1 0 3.00 0.00 58.82 Tests resulting in error only contribute to the Errors column. Statistics for individual tests can be found in the data file, including (possibly incomplete) statistics for erroneous tests. Composite tests are marked with () and not included in the totals. …… 2013-03-11 13:13:08,750 INFO validation-0: starting threads Mar 11, 2013 1:13:08 PM io.socket.IOConnection sendPlain INFO: > 5:::{"args":<a href="/wiki_ngrinder/entry/usernamethread-0" class="notexist">{"username":"Thread-0"}</a>,"name":"user"} Mar 11, 2013 1:13:08 PM io.socket.IOConnection transportMessage INFO: < 1:: Connection established Mar 11, 2013 1:13:08 PM io.socket.IOConnection transportMessage INFO: < 5:::{"name":"user processed","args":<a href="/wiki_ngrinder/entry/messagenew-user-come-in" class="notexist">{"message":"New user come in."}</a>} Server triggered event 'user processed' Server said:{"message":"New user come in."} Mar 11, 2013 1:13:08 PM io.socket.IOConnection sendPlain INFO: > 5:::{"args":<a href="/wiki_ngrinder/entry/messagetest-messagethread-0-userthread-0" class="notexist">{"message":"test message<Thread-0>.","user":"Thread-0"}</a>,"name":"user message"} Mar 11, 2013 1:13:08 PM io.socket.IOConnection transportMessage INFO: < 5:::{"name":"user message processed","args":<a href="/wiki_ngrinder/entry/userthread-0messagetest-messagethread-0" class="notexist">{"user":"Thread-0","message":"test message<Thread-0>."}</a>} Server triggered event 'user message processed' Server said:{"message":"test message<Thread-0>.","user":"Thread-0"} 2013-03-11 13:13:08,855 INFO validation-0: finished
从结果的信息我们可以看出这个测试是成功的,而且服务器处理了2个请求,一个是“user”,另一个是“user message”。而在用户的名字方面,我使用“线程-<线程号>”。 如果我们想要使用多个Vuser(虚拟用户)来进行测试,就要使用不同的名字。
服务器端日志应与下面的内容类似:
debug - client authorized info - handshake authorized gr0AYzAn7sAKTE_XsORt debug - setting request GET /socket.io/1/websocket/gr0AYzAn7sAKTE_XsORt debug - set heartbeat interval for client gr0AYzAn7sAKTE_XsORt debug - client authorized for debug - websocket writing 1:: new connection from<a href="/wiki_ngrinder/entry/object-object" class="notexist">object Object</a> debug - websocket writing 5:::{"name":"user processed","args":<a href="/wiki_ngrinder/entry/messagenew-user-come-in" class="notexist">{"message":"New user come in."}</a>} debug - websocket writing 5:::{"name":"user message processed","args":<a href="/wiki_ngrinder/entry/userthread-0messagetest-messagethread-0" class="notexist">{"user":"Thread-0","message":"test message<Thread-0>."}</a>} info - transport end (socket end) debug - set close timeout for client gr0AYzAn7sAKTE_XsORt debug - cleared close timeout for client gr0AYzAn7sAKTE_XsORt debug - cleared heartbeat interval for client gr0AYzAn7sAKTE_XsORt debug - discarding transport
这些服务器日志说的是,它收到了一个客户端连接并握手成功,然后处理了2个请求。最后,客户端断开。 接下来,我们可以用这个测试脚本创建一个nGrinder测试。
下面是最终的报告:最后,别忘了检查服务器端日志:
...... info - transport end (socket end) debug - set close timeout for client JFrRHYoO3__jN4pdsOSi debug - cleared close timeout for client JFrRHYoO3__jN4pdsOSi debug - cleared heartbeat interval for client JFrRHYoO3__jN4pdsOSi debug - discarding transport info - transport end (socket end) debug - set close timeout for client pDHSiLJhTXaqVR5osOSk debug - cleared close timeout for client pDHSiLJhTXaqVR5osOSk debug - cleared heartbeat interval for client pDHSiLJhTXaqVR5osOSk debug - discarding transport info - transport end (socket end) debug - set close timeout for client 7u_rypQFSZ2vcTGKsOSj debug - cleared close timeout for client 7u_rypQFSZ2vcTGKsOSj debug - cleared heartbeat interval for client 7u_rypQFSZ2vcTGKsOSj debug - discarding transport info - transport end (socket end) debug - set close timeout for client fmxnHFQ_U-wsCmdMsOSg debug - cleared close timeout for client fmxnHFQ_U-wsCmdMsOSg debug - cleared heartbeat interval for client fmxnHFQ_U-wsCmdMsOSg debug - discarding transport
在服务器的日志中,应该会有一些连接被丢弃(discarded)的日志。在这个测试中,Vuser是10,所以在日志中应该出现10次“discarding transport”。这意味着,一个是Vuser模拟一个创建了一个连接的真实用户。如果你不想用一个线程对应一个连接,你可以将下面的代码移动到TestRunner之前:
socketIO = BlockingSocketIO("http://127.0.0.1:3000")那么在这个处理过程中的所有线程将使用同一连接。
顺便说一下,node服务器是行在我个人的笔记本上。从TPS和平均时间,我们可以看到socket.io服务器的性能是非常不错的。请在附件中查看这个test所使用的需要上传到lib目录的Java包。
附件1: report.png 附件2: socket.io-test-libs.zip