web-dev-qa-db-ja.com

シーケンシャルHTTPリクエストをノードでブロック操作していますか?

私の質問に関係のない情報は「引用」されることに注意してください

そのように(これらをスキップしてください)。

問題

複数のクライアントに代わってノードを使用して順序どおりのHTTPリクエストを作成しています。このように、元々クライアントが目的の結果を得るためにいくつかの異なるページの読み込みを行っていたものが、サーバーを介して1つのリクエストのみを受け取るようになりました。現在、フロー制御に「async」モジュールを使用し、HTTPリクエストを作成するために「request」モジュールを使用しています。 console.timeを使用すると、開始から終了まで約2秒かかる約5つのコールバックがあります(以下に含まれるスケッチコード)。

今はノードにあまり慣れていませんが、ノードのシングルスレッドの性質を認識しています。ノードがCPUにバインドされたタスク用に構築されていないことを何度も読みましたが、それが何を意味するのかを今まで本当に理解していませんでした。何が起こっているのかを正しく理解している場合、これは、現在(開発中)に持っているものが10を超えるクライアントにまで拡張されることは決してないことを意味します。

質問

私はノードの専門家ではないので、(タイトルで)この質問をして、いくつかの順次HTTP要求を行うことが実際にブロックされていることを確認します。

エピローグ

その場合、ノードでこの問題に取り組み続けることを選択した場合、さまざまな可能な解決策について議論する別のSO質問(適切な調査を行った後)を行うことを期待しています(それ自体が可能性があります)私がやろうとしていることに適していない)。

その他の締めくくりの考え

この質問が十分に詳細でなかったり、あまりにも素朴だったり、特に花言葉があったりした場合は、本当に申し訳ありません(簡潔にするようにしています)。

私の問題を手伝ってくれる人に感謝し、すべての賛成票を投じてください!

前に述べたコード:

var async = require('async');
var request = require('request');

...

async.waterfall([
    function(cb) {
        console.time('1');

        request(someUrl1, function(err, res, body) {
            // load and parse the given web page.

            // make a callback with data parsed from the web page
        });
    },
    function(someParameters, cb) {
        console.timeEnd('1');
        console.time('2');

        request({url: someUrl2, method: 'POST', form: {/* data */}}, function(err, res, body) {
            // more computation

            // make a callback with a session cookie given by the visited url
        });
    },
    function(jar, cb) {
        console.timeEnd('2');
        console.time('3');

        request({url: someUrl3, method: 'GET', jar: jar /* cookie from the previous callback */}, function(err, res, body) {
            // do more parsing + computation

            // make another callback with the results
        });
    },
    function(moreParameters, cb) {
        console.timeEnd('3');
        console.time('4');

        request({url: someUrl4, method: 'POST', jar: jar, form : {/*data*/}}, function(err, res, body) {
            // make final callback after some more computation.
            //This part takes about ~1s to complete
        });
    }
], function (err, result) {
    console.timeEnd('4'); //
    res.status(200).send();
});
8
youngrrrr

通常、node.jsのI/Oは非ブロッキングです。サーバーに同時に複数のリクエストを行うことで、これをテストできます。たとえば、各リクエストの処理に1秒かかる場合、ブロッキングサーバーは2つの同時リクエストの処理に2秒かかりますが、非ブロッキングサーバーは両方のリクエストの処理に1秒強かかります。

ただし、 request の代わりに sync-request モジュールを使用して、意図的にリクエストをブロックすることができます。明らかに、これはサーバーには推奨されません。

ブロッキングI/Oと非ブロッキングI/Oの違いを示すコードを次に示します。

var req = require('request');
var sync = require('sync-request');

// Load example.com N times (yes, it's a real website):
var N = 10;

console.log('BLOCKING test ==========');
var start = new Date().valueOf();
for (var i=0;i<N;i++) {
    var res = sync('GET','http://www.example.com')
    console.log('Downloaded ' + res.getBody().length + ' bytes');
}
var end = new Date().valueOf();
console.log('Total time: ' + (end-start) + 'ms');

console.log('NON-BLOCKING test ======');
var loaded = 0;
var start = new Date().valueOf();
for (var i=0;i<N;i++) {
    req('http://www.example.com',function( err, response, body ) {
        loaded++;
        console.log('Downloaded ' + body.length + ' bytes');
        if (loaded == N) {
            var end = new Date().valueOf();
            console.log('Total time: ' + (end-start) + 'ms');
        }
    })
}

上記のコードを実行すると、ノンブロッキングテストがすべてのリクエストを処理するのに、単一のリクエストの場合とほぼ同じ時間がかかることがわかります(たとえば、N = 10に設定した場合、ノンブロッキングコードは10回実行されますブロッキングコードよりも高速です)。これは、リクエストが非ブロッキングであることを明確に示しています。


追加の回答:

また、プロセスがCPUを集中的に使用することを心配しているとのことです。しかし、あなたのコードでは、CPUユーティリティのベンチマークを行っていません。ネットワーク要求時間(I/O、非ブロッキングであることがわかっています)とCPUプロセス時間の両方を混合しています。リクエストがブロッキングモードになっている時間を測定するには、コードを次のように変更します。

async.waterfall([
    function(cb) {
        request(someUrl1, function(err, res, body) {
            console.time('1');
            // load and parse the given web page.
            console.timeEnd('1');
            // make a callback with data parsed from the web page
        });
    },
    function(someParameters, cb) {
        request({url: someUrl2, method: 'POST', form: {/* data */}}, function(err, res, body) {
            console.time('2');
            // more computation
            console.timeEnd('2');

            // make a callback with a session cookie given by the visited url
        });
    },
    function(jar, cb) {
        request({url: someUrl3, method: 'GET', jar: jar /* cookie from the previous callback */}, function(err, res, body) {
            console.time('3');
            // do more parsing + computation
            console.timeEnd('3');
            // make another callback with the results
        });
    },
    function(moreParameters, cb) {
        request({url: someUrl4, method: 'POST', jar: jar, form : {/*data*/}}, function(err, res, body) {
            console.time('4');
            // some more computation.
            console.timeEnd('4');

            // make final callback
        });
    }
], function (err, result) {
    res.status(200).send();
});

あなたのコードは「より多くの計算」の部分でのみブロックします。したがって、他の部分が実行されるのを待つために費やされた時間を完全に無視することができます。実際、これがまさにノードが複数のリクエストを同時に処理できる方法です。他の部分がそれぞれのコールバックを呼び出すのを待っている間(最大1秒かかる場合があるとおっしゃっています)、ノードは他のjavascriptコードを実行し、他の要求を処理できます。

4
slebetman

request()関数で非ブロッキングI/Oを使用するため、コードは非ブロッキングです。これは、一連のhttpリクエストがフェッチされている間、node.jsが他のリクエストを自由に処理できることを意味します。

async.waterfall()は、リクエストをシーケンシャルに並べ、1つの結果を次のリクエストに渡すために何をしますか。リクエスト自体は非ブロッキングであり、async.waterfall()はそれを変更したり影響を与えたりしません。あなたが持っているシリーズは、あなたが連続して複数の非ブロッキングリクエストを持っていることを意味します。

あなたが持っているものは、一連のネストされたsetTimeout()呼び出しに類似しています。たとえば、この一連のコードは、内部コールバックに到達するのに5秒かかります(async.waterfall()が最後のコールバックに到達するのにn秒かかるように):

_setTimeout(function() {
    setTimeout(function() {
        setTimeout(function() {
            setTimeout(function() {
                setTimeout(function() {
                    // it takes 5 seconds to get here
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);
_

ただし、これは5つの連続した非同期操作であるため、基本的にゼロCPUを使用します。実際のnode.jsプロセスは、次のsetTimeout()をスケジュールするためにおそらく1ミリ秒以内で関与し、その後、システムが次を起動するイベントを投稿するまで、node.jsプロセスは文字通り他の多くのことを実行できます。タイマー。

Node.jsイベントキューがどのように機能するかについて詳しくは、次のリファレンスをご覧ください。

ノードでコールバックを待機している間に任意のコードを実行しますか?

非ブロッキングhttpサーバーのブロッキングコード

ユーザーコードを実行しないJavascript/Nodeの隠しスレッド:可能ですか?可能であれば、競合状態の不可解な可能性につながる可能性がありますか?

JavaScriptはバックグラウンドでAJAX応答を処理しますか? (ブラウザーについて書かれていますが、概念は同じです)

何が起こっているのかを正しく理解している場合、これは、現在(開発中)に持っているものが10を超えるクライアントにまで拡張されることは決してないことを意味します。

これは正しい理解ではありません。 node.jsプロセスでは、何千もの非ブロッキングリクエストを同時に実行することが簡単にできます。順次測定される時間は、開始から終了までの時間にすぎません。CPUリソースやその他のOSリソースの消費とは関係ありません(非ブロッキングリソースの消費に関する以下のコメントを参照してください)。

そのとき、この特定のアプリケーションにノードを使用することについてはまだ懸念があります。実行している作業が単純なI/Oではなく、計算量が多いことを考えると、どのように拡張されるかが心配です。マルチスレッドを可能にするプラットフォームに切り替えるべきだと感じています。私が尋ねていること/私が表現している懸念は意味がありますか?私はちょうど完全なBSを吐き出している可能性があり、私が何について話しているのか分かりません。

ノンブロッキングI/OはCPUをほとんど消費しませんが(要求が最初に送信されたときは少しだけ、結果が戻ってきたときは少しだけ)、コンミューターが削除結果を待っている間、CPUはまったく消費されず、 OSスレッドが消費されます。これは、コンピューターが削除サイトからの応答を待機しているときにリソースが使用されないため、node.jsが非ブロッキングI/Oに適している理由の1つです。

リクエストの処理が計算集約的である場合(たとえば、処理に測定可能な量の純粋なブロッキングCPU時間がかかる場合)、はい、計算の実行に複数のプロセスを関与させることを検討する必要があります。これを行うには複数の方法があります。 nodejsクラスタリングモジュールでクラスタリングを使用できます(つまり、複数の同一のnode.jsプロセスがそれぞれ異なるクライアントからのリクエストを処理するだけです)。または、計算量の多い作業のワークキューを作成して、計算量の多い作業を行う子プロセスのセットを作成することもできます。または、他にもいくつかのオプションがあります。これは、解決するためにnode.jsから切り替える必要があるタイプの問題ではありません。node.jsを使用して問題なく解決できます。

5
jfriend00

キューを使用して、nodeJで同時http呼び出しを処理できます https://www.npmjs.com/package/concurrent-queue

    var cq = require('concurrent-queue');
    test_queue = cq();

    // request action method
    testQueue: function(req, res) {
        // queuing each request to process sequentially
        test_queue(req.user, function (err, user) {
            console.log(user.id+' done');
            res.json(200, user)
        });
    },


    // Queue will be processed one by one.
    test_queue.limit({ concurrency: 1 }).process(function (user, cb) {
        console.log(user.id + ' started')

        // async calls will go there
        setTimeout(function () {
            // on callback of async, call cb and return response.
            cb(null, user)
        }, 1000);

    });

1人のユーザーだけが一度にリソースにアクセスしたり更新したりする必要がある機密性の高いビジネスコールに実装する必要があることに注意してください。

これにより、I/Oがブロックされ、ユーザーが待機するようになり、応答時間が遅くなります。

最適化:

リソースに依存するキューを作成することで、高速化と最適化を実現できます。そのため、共有リソースごとに個別のキューがあり、同じリソースに対する同期呼び出しは、同じリソースに対してのみ実行でき、異なるリソースに対しては呼び出しが実行されます非同期

現在のユーザーに基づいてそれを実装したいとします。そのため、同じユーザーの場合、http呼び出しは同期的にのみ実行でき、異なるユーザーの場合、https呼び出しは非同期になります。

testQueue: function(req, res) {

    // if queue not exist for current user.
    if(! (test_queue.hasOwnProperty(req.user.id)) ){
        // initialize queue for current user
        test_queue[req.user.id] = cq();
        // initialize queue processing for current user
        // Queue will be processed one by one.
        test_queue[req.user.id].limit({ concurrency: 1 }).process(function (task, cb) {
            console.log(task.id + ' started')
            // async functionality will go there
            setTimeout(function () {
                cb(null, task)
            }, 1000)
        });
    }

    // queuing each request in user specific queue to process sequentially
    test_queue[req.user.id](req.user, function (err, user) {
        if(err){
            return;
        }
        res.json(200, user)
        console.log(user.id+' done');
    });
},

これは高速で、必要なリソースのみのI/Oをブロックします。

2
Ibtesam Latif