使顺序 HTTP 请求成为节点中的阻塞操作吗?

Is making sequential HTTP requests a blocking operation in node?

请注意,与我的问题无关的信息将是 'quoted'

like so (feel free to skip these).

问题

我正在使用节点代表多个客户端发出 in-order HTTP 请求。这样,最初需要客户端加载几个不同的页面才能获得所需的结果,现在只需通过我的服务器接受一个请求。我目前正在使用“async”模块进行流量控制,使用“request”模块进行 HTTP 请求。大约有 5 个回调,使用 console.time,从开始到结束大约需要 2 秒(下面包含草图代码)。

Now I am rather inexperienced with node, but I am aware of the single-threaded nature of node. While I have read many times that node isn’t built for CPU-bound tasks, I didn’t really understand what that meant until now. If I have a correct understanding of what’s going on, this means that what I currently have (in development) is in no way going to scale to even more than 10 clients.

问题

由于我不是节点方面的专家,我问这个问题(在标题中)以确认发出多个连续的 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();
});

您的代码是非阻塞的,因为它使用非阻塞 I/O 和 request() 函数。这意味着在获取您的一系列 http 请求时,node.js 可以免费为其他请求提供服务。

async.waterfall() 的作用是按顺序排列您的请求并将一个请求的结果传递给下一个请求。请求本身是非阻塞的,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);

但是,这基本上使用零 CPU 因为它只是 5 个连续的异步操作。实际的 node.js 进程可能只用了不超过 1 毫秒来安排下一个 setTimeout(),然后 node.js 进程实际上可以做很多其他事情,直到系统发布要触发的事件下一个定时器。

您可以在这些参考资料中阅读有关 node.js 事件队列如何工作的更多信息:

Run Arbitrary Code While Waiting For Callback in Node?

How does JavaScript handle AJAX responses in the background?(写的是浏览器,但是概念是一样的)

If I have a correct understanding of what’s going on, this means that what I currently have (in development) is in no way going to scale to even more than 10 clients.

这不是正确的理解。 node.js 进程可以轻松地同时处理数千个非阻塞请求。您按顺序测量的时间只是开始到结束的时间 - 它与 CPU 资源或消耗的其他 OS 资源无关(请参阅下面关于非阻塞资源消耗的评论)。

I still have concerns about using node for this particular application then. I'm worried about how it will scale considering that the work it is doing is not simple I/O but computationally intensive. I feel as though I should switch to a platform that enables multi-threading. Does what I'm asking/the concern I'm expressing make sense? I could just be spitting total BS and have no idea what I'm talking about.

非阻塞I/O几乎不消耗CPU(最初发送请求时只消耗一点点,结果返回时消耗一点点),但是当计算机等待删除结果,根本没有 CPU 被消耗,也没有 OS 线程被消耗。这是 node.js 可以很好地扩展非阻塞 I/O 的原因之一,因为当计算机等待来自删除站点的响应时不使用任何资源。

如果您对请求的处理是计算密集型的(例如,需要可测量的纯阻塞 CPU 时间来处理),那么是的,您可能想要探索让多个进程参与 运行计算。有多种方法可以做到这一点。您可以通过 nodejs 集群模块使用集群(因此您只需拥有多个相同的 node.js 进程,每个进程处理来自不同客户端的请求)。或者,您可以创建一个计算密集型工作的工作队列,并拥有一组执行计算密集型工作的子进程。或者,还有其他几种选择。这不是需要从 node.js 切换到解决的问题类型 - 它可以使用 node.js 来解决。

通常,node.js中的I/O是非阻塞的。您可以通过同时向您的服务器发出多个请求来对此进行测试。例如,如果每个请求需要 1 秒来处理,阻塞服务器将花费 2 秒来处理 2 个并发请求,但非阻塞服务器只需要 1 秒多一点来处理两个请求。

但是,您可以使用 sync-request module instead of request 故意阻止请求。显然,不建议将其用于服务器。

这里有一些代码来演示阻塞和非阻塞之间的区别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();
});

您的代码只阻塞了 "more computation" 部分。所以你可以完全忽略等待其他部分执行所花费的任何时间。事实上,这正是节点可以同时处理多个请求的方式。在等待其他部分调用各自的回调(你提到它可能需要 1 秒)节点可以执行其他 javascript 代码并处理其他请求。

在nodeJs中可以使用queue来处理并发的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);

    });

请记住,它需要为敏感的业务调用实施,其中资源需要一次仅由一个用户访问或更新。

这会阻止您的 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 您想要的资源。