NodeJS与Django协同应用开发(3)—— 测试与优化


系列目录


测试往往被开发人员认为是不那么重要的环节,尤其是在开发任务特别重的时候。所以决定针对测试写一篇文章也是有些不容易,不过工作时间长了就觉得测试真的很重要。测试就是一份担保,免于我们整日提心吊胆怕服务器挂掉。

在项目里我们的代码主要是使用的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:

  1. 首先是关于redis
    开发中很容易会把于redis的连接写成与socket连接一一对应,也就是每个socket有自己独立的redis连接。这会导致潜在的性能问题。数个空闲的连接很快能把redis资源耗尽。如果能够预料到大量的业务请求的话,需要做一个redis管道的路由协议,自己按channel分发一下消息。以及不要忘了在用户断开连接时回收redis资源。

  2. socket.io-client在测试时的问题
    这其实算是测试过程中遇到的一个BUG。原本代码里是用socket.connect与socket.disconnect的循环来作为新用户连接,但是在压力大的时候服务器端不会确保每一个连接都被正确的回收,哪怕这些连接可能已经被disconnect了。测试中导致了其他资源被迅速占用,并发量在不高的时候就到了极限。
    这个问题是由socketio的网络结构导致的。disconnect时socketio的应用层会断开,但是链路层不一定断开,也就是说在服务器看来这些用户只是暂时断线,因此才不回收socket。所以正确的做法是客户端在disconnect后回收socket实例,并且每次连接时都使用新的实例。

然而可惜的是现在线上的压力还是太小了,以后的瓶颈恐怕会出现在数据库上,不过太早做优化只是浪费时间。不如更加注意一下后续的工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容