使用nGrinder执行socket.io应用负载测试

原文 :  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应用中,每个用户跟服务器之间是一直保持连接的。
    然后,在这个init函数中,一个包含“user”事件的消息被发送到服务器,相当于在服务器端做用户登陆之类的初始化。 再然后,在每一个测试函数中,我们都会发送一个包含““user message””事件的消息。
    接下来,我们需要一个支持这个客户端脚本的服务器端的应用程序。服务器端使用node.js,安装socket.io模块。
    然后再写一个名为server.js脚本,内容如下:
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/。
    然后,在nGrinder的脚本编辑页面中验证这个脚本以确认它能够正常运行。验证结果应该是这样的:
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测试。

    下面是最终的报告:

使用nGrinder执行socket.io应用负载测试_第1张图片

最后,别忘了检查服务器端日志:

......
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









你可能感兴趣的:(使用nGrinder执行socket.io应用负载测试)