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

Posted

技术标签:

【中文标题】使顺序 HTTP 请求成为节点中的阻塞操作吗?【英文标题】:Is making sequential HTTP requests a blocking operation in node? 【发布时间】:2016-01-02 19:45:57 【问题描述】:

请注意,与我的问题无关的信息将被“引用”

像这样(随意跳过这些)。

问题

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

现在我对节点相当缺乏经验,但我知道 节点的单线程性质。虽然我已经多次阅读该节点 不是为 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();
);

【问题讨论】:

不,它不是阻塞的——因此为什么需要回调来处理异步结果。自然,如果存在所需的序列化关系,则必须完成先前的请求才能使用结果.. 阻塞意味​​着在请求完成之前不能运行其他代码 - 在同一执行上下文中。在此示例中,request 调用将立即返回,并且可以进行后续操作;因此非阻塞。等待请求(可能需要)不会使 request 呼叫阻塞。 “瀑布”方法有效地导致请求序列化,但这只是对底层异步模型和 Node.js 完成的非阻塞 IO 施加的排序。微不足道的反例包括并行运行多个瀑布,甚至处理多个并发 Node.js 请求。 Node.js 在执行 IO Web 请求本身时将使用“0 CPU”(该过程是如此可笑的 IO 绑定)。如果 实际上 是“重要处理”(即在回调中),则需要单独关注获取外部资源需要多少时间。 Node.js 计算琐碎是“仅限于一个核心/线程”,这通常足够快,除非使用额外的并发策略,例如多处理。 如果请求回调中的计算需要 1 秒是一个问题(这确实是“阻塞节点的很长时间”),那么请努力将其作为问题/探索的重点. 【参考方案1】:

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

但是,您可以通过使用sync-request 模块而不是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();
);

您的代码只阻塞在“更多计算”部分。因此,您可以完全忽略等待其他部分执行所花费的任何时间。事实上,这正是节点可以同时服务多个请求的方式。在等待其他部分调用相应的回调(您提到它可能需要长达 1 秒)时,节点可以执行其他 javascript 代码并处理其他请求。

【讨论】:

感谢您的回复和您提供的示例。虽然我收到的两个答案都非常具有描述性和帮助性,但我会接受你的答案,因为你能够与我的 console.log 语句相关联。您的附加答案更清楚地说明了哪些操作是/未阻塞。我还对其进行了测试,结果我的每个“计算”都不到 100 毫秒!感谢您的贡献!【参考方案2】:

您的代码是非阻塞的,因为它使用带有request() 函数的非阻塞I/O。这意味着在获取您的一系列 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?

blocking code in non-blocking http server

Hidden threads in Javascript/Node that never execute user code: is it possible, and if so could it lead to an arcane possibility for a race condition?

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

如果我对正在发生的事情有正确的理解,这意味着 我目前拥有的(正在开发中)绝不会扩展到 甚至超过 10 个客户。

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

我仍然担心将节点用于这个特定的 然后申请。考虑到这一点,我担心它会如何扩展 它所做的工作不是简单的 I/O,而是计算密集型。 我觉得我应该切换到一个能够启用的平台 多线程。我所问的/我所表达的担忧是否使 感觉?我可能只是随地吐痰,不知道我是什么 正在谈论。

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

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

【讨论】:

嗨!非常感谢您花时间回复。我读了你写的所有东西(减去你链接到的其他一些 SO 帖子——但我会写的,我保证!)。我接受了另一个答案,因为他能够更具体地将他介绍的概念与我的特定代码联系起来。然而,这并不是说您的回答并不出色!诚然,您也确实回答了我提出的确切问题(以及更多问题),但由于我只能接受一个答案,我将选择一个帮助所有部分点击在一起的答案。不过,感谢您非常周到且写得很好的回答! 另外,感谢您解决了我评论过的问题,并感谢 +1 提到集群作为一种可能的解决方案,同时仍在使用节点!【参考方案3】:

你可以在nodeJs中使用队列来处理并发的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。

【讨论】:

以上是关于使顺序 HTTP 请求成为节点中的阻塞操作吗?的主要内容,如果未能解决你的问题,请参考以下文章

使响应顺序与 Netty 中的请求顺序匹配

队头阻塞

发送多个 mpi 非阻塞发送 - 它会保留发送的顺序吗

http.get 请求闭环问题(节点)

ZooKeeper客户端源码——向服务端发起请求(顺序响应+同步阻塞+异步回调)

子程序必须按字母顺序排列。真的吗?