NodeJSとDjangoの共同応用開発(3)――テストと最適化

8962 ワード

シリーズ
  • NodeJSとDjangoが共同で開発した(0)node.js基礎知識
  • NodeJSとDjangoが共同で開発した(1)原型構築
  • NodeJSとDjangoが連携したアプリケーション開発(2)業務フレームワーク
  • NodeJSとDjangoの共同アプリケーション開発(3)テストと最適化
  • NodeJSとDjangoの共同アプリケーション開発(4)の展開
  • テストはよく開発者に重要ではないと思われます.特に開発任務が重い時です.ですから、テストに対して文章を書くことを決めたのも大変です.でも、仕事が長くなると、テストが本当に大切だと思います.テストは保証されています.サーバーがキャンセルされるのを心配しています.
    プロジェクトの中で私たちのコードは主にsockete.ioが提供するapiを使って構築されます.これはCS構造のJavaScriptライブラリです.幸いにもsocket.ioはブラウザに依存しないclientライブラリsockete.io-clientを提供してくれました.appiとsocket.ioは同じです.これで私たちはテストコードを作成することができます.残りのjsコードは、ユニットテストとしてvowsjsを使用しています.
    まず、vowsjsについて説明しましょう.公式サイトでは、BDドライブのフレームです.設計の最初から、非同期コードをテストするために作成されています.並列テストも可能です.また、依存があるときは順番にテストしてもいいです.
    vowsjsの主な概念は、以下のようないくつかあります.Suite:0つ以上のbatchを含むオブジェクトは、実行またはエクスポートされます.Batch:複数のcontextを入れ子にしたオブジェクト.Context:一つはtopic、0個以上のvow及び0個以上のsub-contextの対象を含むことができる.Topic:1つのオブジェクトまたは非同期中に実行できる関数は、通常テスト対象を表しています.Vow:topicをパラメータとして受け入れ、その上で断言の関数を実行します.
    公式サイトには3行の偽コードがあります.
    Suite→Batch*Batch→Contect*Contect→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が帰ってくるのは1つのBatchで、addBatchの中で添加したのはContextです.
    実行順序の関係については、Batch間で順次実行され、Contect間で並列実行され、ContectとSub-Controtextの間で順次実行され、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パイプのルート協議をして、自分でチャンネルを押してメッセージを配信する必要があります.そして、ユーザーが接続を切断した時に、redisリソースを回収することを忘れないでください.
  • sockete.io-clientはテスト時の問題です.これはテスト中に発生したBUGです.元々コードはsocket.co nnectとsocket.disconnectのループで新しいユーザーとして接続されていましたが、圧力が大きい時にはサーバー側は接続ごとに正しい回収が確保されていません.これらの接続はdisconnectに接続されているかもしれません.テスト中に他の資源が急速に占有され、同時に量が高くない時に限界になります.この問題はsocketioのネットワーク構造によるものです.disconnectでは、socketioのアプリケーション層は切断されますが、リンク層は必ずしも切断されていません.つまり、サーバーから見ると、これらのユーザーは一時的に断線しているだけなので、socketは回収されません.したがって、正確な方法は、クライアントがdisconnect後にsocketのインスタンスを回収し、接続するたびに新しいインスタンスを使用することである.
  • しかし、残念なことに、今のオンラインでの圧力はまだ小さすぎて、今後のボトルネックはおそらくデータベースに表示されますが、最適化をするには時間の無駄です.もっと次の仕事に注意してください.