系列目录
- NodeJS与Django协同应用开发(0) node.js基础知识
- NodeJS与Django协同应用开发(1)原型搭建
- NodeJS与Django协同应用开发(2)业务框架
- NodeJS与Django协同应用开发(3)测试与优化
- NodeJS与Django协同应用开发(4)部署
测试往往被开发人员认为是不那么重要的环节,尤其是在开发任务特别重的时候。所以决定针对测试写一篇文章也是有些不容易,不过工作时间长了就觉得测试真的很重要。测试就是一份担保,免于我们整日提心吊胆怕服务器挂掉。
在项目里我们的代码主要是使用的socket.io所提供的api来构建的。这是一个CS结构的JavaScript库,所幸的是socket.io为我们提供了不依赖浏览器的client库socket.io-client。api和socket.io是一样的,这样我们就可以用来编写我们的测试代码了。其余的js代码使用了vowsjs作为单元测试。
先说说vowsjs吧,在它的官网上讲得还是很清楚的,这是一套BDD驱动的(behavior driven development)框架,从设计之初就是为了测试异步代码而编写的,它可以并行测试也可以在有依赖的时候按顺序测试。
vowsjs的主要概念有如下几个:
Suite:一个包含0个或多个batch的对象,可以被执行或导出。
Batch:一个包含了嵌套多个context的对象。
Context:一个可以包含topic,0个或多个vow以及0个或多个sub-context的对象。
Topic:一个对象或是可以在异步过程中被执行的函数,通常代表了测试对象。
Vow:接受topic作为参数并在其之上执行断言(assertion)的函数。
官网上有三行伪代码解释了之间的关系:
Suite → Batch*
Batch → Context*
Context → Topic? Vow* Context*
我们来看一下具体是怎么做的吧,这里拿了一个链表来作为测试对象。
var target = require('../target');
vows.describe('target list test').addBatch({
'target list add remove get': {
topic: new target.target_list(1),
'group id should be 1': function(list) {
assert.equal(list.group_id, 1);
},
'add first element': {
topic: function(list) {
list.add(new target.target('testusername1', 0, 0, 0));
return list;
},
'list first element not null': function(list) {
assert.isNotNull(list.first);
},
'add second element': {
topic: function(list) {
list.add(new target.target('testusername2', 0, 0, 0));
return list;
},
'list length should be 2': function(list) {
assert.equal(list.length, 2);
},
'list first and last are not same': function(list) {
assert.notStrictEqual(list.first, list.last);
},
'add third element and remove second': {
topic: function(list) {
list.add(new target.target('testusername3', 0, 0, 0));
list.remove('testusername2');
return list;
},
'list length should be 2': function(list) {
assert.equal(list.length, 2);
},
'list get username2 should be undefined': function(list) {
assert.isUndefined(list.get('username2'));
},
'list last element should be username3': function(list) {
assert.strictEqual(list.last.username, 'testusername3');
},
'remove first element': {
topic: function(list) {
list.remove('testusername1');
return list;
},
'list length should be 1': function(list) {
assert.equal(list.length, 1);
},
'list first element should be username3': function(list) {
assert.strictEqual(list.first.username, 'testusername3');
},
'list last element should be username3': function(list) {
assert.strictEqual(list.last.username, 'testusername3');
},
'list get username1 should be undefined': function(list) {
assert.isUndefined(list.get('testusername1'));
},
},
}
}
},
},
'target list trigger': {
topic: function(){
var list = new target.target_list(1);
for (var i = 0; i < 10; i++){
list.add(new target.target('testusername'+i, 0,0,0));
}
return list;
},
'list length should be 10': function(list){
assert.equal(list.length, 10);
},
'do trigger': {
topic: function(list){
list.trigger();
return list;
},
'watch log': function(list){
assert.equal(true, true);
}
}
}
}).export(module);
代码里具体的函数代表了什么功能这个并不重要,重要的是我们要知道vowsjs提供的函数都有什么用。
vowsjs里每一个describe返回的都是一个Suite,addBatch返回的是一个Batch,在addBatch中添加的是Context。
关于执行顺序的关系:
Batch之间是顺序执行的,Context之间是并行执行的,Context和Sub-Context之间是顺序执行的,Vow之间是顺序执行的。
了解了这个关系我们在测试模块的时候就非常方便了。我们的socketio也可以这么测试,不过并不推荐这么做,这是因为和服务器交互频繁,事件的触发往往不是由客户端决定的。
因此我们在测试socketio相关的功能时是这么做的,这里以连接到断开举例。
var io = require('socket.io-client');
var process = require('process');
var cluster = require('cluster');
var concurrency = process.argv[2];
var requests = process.argv[3];
var rperc = requests / concurrency;
function time(prev_time) {
var diff;
if (prev_time) {
diff = process.hrtime(prev_time);
return (diff[0] * 1e9 + diff[1]) / 1e6; // nano second -> ms
} else {
diff = process.hrtime();
return diff
}
};
if (cluster.isMaster) {
for (var i = 0; i < concurrency; i++) {
cluster.fork();
}
var totalAvgTime = 0;
var totalConnAvgTime = 0;
var finishedWorker = 0;
cluster.on('exit', function(worker, code, signal) {
console.log('worker %d exited',worker.process.pid);
if (++finishedWorker == concurrency) {
console.log('concurrency: ' + concurrency);
console.log('connAvgTime: ' + (totalConnAvgTime / concurrency));
console.log('avgTime: ' + (totalAvgTime / concurrency));
}
});
for (var id in cluster.workers) {
cluster.workers[id].on('message', function(msg) {
totalAvgTime += msg.avgTime;
totalConnAvgTime += msg.connAvgTime;
});
}
} else {
var pid = process.pid
var count = 0;
var avgTime = 0; //ms
var avgCount = 0;
var connAvgTime = 0; //ms
var connCount = 0;
function newConnect(){
var startTime = time();
var socket = io.connect('http://localhost:9000', {
query: "type=test&device=web&identify=" + pid + count + i
});
socket.on('connect', function() {
socket.emit('setup', {
'id': '10',
'username': '' + pid + count + i,
}, function() {
var diffTime = time(startTime);
connAvgTime = ((connAvgTime * connCount) + diffTime) / (++connCount);
socket.disconnect();
});
});
socket.on('disconnect', function() {
var diffTime = time(startTime);
avgTime = ((avgTime * avgCount) + diffTime) / (++avgCount);
if (count < rperc) {
count++;
delete socket;
newConnect();
} else {
process.send({ 'avgTime': avgTime, 'connAvgTime': connAvgTime });
process.exit();
}
});
}
newConnect();
}
这是一个压力测试的例子,目的是为了测试服务器在多用户频繁连接断开的情况下新连接的性能表现。
这里我们使用了nodejs的cluster模块来模拟多并发。服务器端在收到新连接时会查询一次数据库,与redis建立一次连接。
这边有一些优化的tips:
首先是关于redis
开发中很容易会把于redis的连接写成与socket连接一一对应,也就是每个socket有自己独立的redis连接。这会导致潜在的性能问题。数个空闲的连接很快能把redis资源耗尽。如果能够预料到大量的业务请求的话,需要做一个redis管道的路由协议,自己按channel分发一下消息。以及不要忘了在用户断开连接时回收redis资源。socket.io-client在测试时的问题
这其实算是测试过程中遇到的一个BUG。原本代码里是用socket.connect与socket.disconnect的循环来作为新用户连接,但是在压力大的时候服务器端不会确保每一个连接都被正确的回收,哪怕这些连接可能已经被disconnect了。测试中导致了其他资源被迅速占用,并发量在不高的时候就到了极限。
这个问题是由socketio的网络结构导致的。disconnect时socketio的应用层会断开,但是链路层不一定断开,也就是说在服务器看来这些用户只是暂时断线,因此才不回收socket。所以正确的做法是客户端在disconnect后回收socket实例,并且每次连接时都使用新的实例。
然而可惜的是现在线上的压力还是太小了,以后的瓶颈恐怕会出现在数据库上,不过太早做优化只是浪费时间。不如更加注意一下后续的工作。