Node.js - http.request() 连接池问题

10

考虑以下简单的Node.js应用程序:

var http = require('http');
http.createServer(function() { }).listen(8124); // Prevent process shutting down

var requestNo = 1;
var maxRequests = 2000;

function requestTest() {
    http.request({ host: 'www.google.com', method: 'GET' }, function(res) {
        console.log('Completed ' + (requestNo++));

        if (requestNo <= maxRequests) {
            requestTest();
        }
    }).end();
}

requestTest();
它向google.com发出2000个HTTP请求,一个接一个地进行。问题在于它在第5个请求时暂停约3分钟,然后继续处理第6到第10个请求,再暂停3分钟,然后请求11到15,暂停,以此类推。编辑:我尝试将www.google.com更改为localhost,这是在我的机器上运行的极其基本的Node.js应用程序,返回“Hello world”,但我仍然遇到了3分钟的暂停。

现在我读到可以增加连接池限制:

http.globalAgent.maxSockets = 20;
现在,如果我运行它,它会处理请求1-20,然后暂停3分钟,接着请求21-40,再次暂停,如此循环。最后,经过一番研究,我发现可以通过在请求选项中设置agent: false来完全禁用连接池。
http.request({ host: 'www.google.com', method: 'GET', agent: false }, function(res) {
    ...snip....

...然后它可以很好地处理所有2000个请求。

我的问题是,这样做是否明智?会不会出现过多的HTTP连接的危险?为什么会暂停3分钟?如果我已经完成了连接,它应该立即将其添加回池中,以便下一个请求使用,那为什么要等待3分钟?请原谅我的无知。

如果行不通,那么在不锁定或崩溃的情况下,Node.js应用程序进行大量HTTP请求的最佳策略是什么?

我正在运行Mac OSX 10.8.2上的Node.js版本0.10。


编辑:我发现如果我将上面的代码转换成for循环,并尝试同时建立一堆连接,那么大约在242个连接后就开始出现错误。错误是:

Error was thrown: connect EMFILE
(libuv) Failed to create kqueue (24)

...和代码...

for (var i = 1; i <= 2000; i++) {
    (function(requestNo) {
        var request = http.request({ host: 'www.google.com', method: 'GET', agent: false }, function(res) {
            console.log('Completed ' + requestNo);
        });

        request.on('error', function(e) {
            console.log(e.name + ' was thrown: ' + e.message);
        });

        request.end();
    })(i);
}

我不知道一个负载很重的Node.js应用程序是否能够达到那么多的同时连接。


1
您的文件描述符不足,在OSX上默认限制为相当低的256个。您可以使用 ulimit -n 2048 增加该数字,这将允许从同一 shell 运行的后续 Node 进程同时打开这些2000个与 Google 的连接,但我认为这并不是您想要的。我不确定3分钟是从哪里来的,听起来像是连接池中的限流问题(或者Google正在对您进行限流?)。 - robertklep
感谢您提供有关OSX文件描述符的信息,现在更加清晰了。我猜在Linux上运行的现场网站不会有问题。但是,如果我在本地计算机上访问正在运行的Node.js Web应用程序,则需要等待3分钟,我明白这一点。 - Sunday Ironfoot
1
阅读这个后,我想知道三分钟超时是否是Google服务器的保活超时(虽然如果我正确理解文档,只要您不断发出请求,它就不应该等待这些保活超时过期才开始新请求...) - robertklep
robertklep - 请参见上面的编辑,但我尝试将www.google.com更改为localhost,这是在我的机器上运行的极其基本的Node.js应用程序,返回“Hello world”,但我仍然遇到了3分钟的暂停。 - Sunday Ironfoot
我的猜测是3分钟来自内存分配。 - Shimon Doodkin
1个回答

19
你需要消耗响应。
请记住,在v0.10中,我们实现了streams2。这意味着直到你开始查找数据事件时,数据事件才会发生。所以你可以像这样做:
http.createServer(function(req, res) {
  // this does some I/O, async
  // in 0.8, you'd lose data chunks, or even the 'end' event!
  lookUpSessionInDb(req, function(er, session) {
    if (er) {
      res.statusCode = 500;
      res.end("oopsie");
    } else {
      // no data lost
      req.on('data', handleUpload);
      // end event didn't fire while we were looking it up
      req.on('end', function() {
        res.end('ok, got your stuff');
      });
    }
  });
});

然而,当你没有读取数据时,流不会丢失数据的另一面是,实际上如果你不读取它们,它们就不会丢失数据!也就是说,它们开始暂停,你必须读取它们才能得到任何输出。
所以,在你的测试中发生的情况是,你正在发出大量请求并且没有消耗响应,最终谷歌会关闭套接字,因为没有任何操作,它会认为你已经死了。
有些情况下,无法消耗传入的消息:也就是说,如果你在请求上没有添加response事件处理程序,或者在服务器上完全编写并完成响应消息而没有读取请求,则我们只是将数据丢弃。
但是,如果你正在监听“response”事件,那么你有责任处理对象。在你的第一个示例中添加response.resume(),你将看到它以合理的速度进行处理。

2
谢谢你!没错,"response.resume()" 可以用。而且,就像你说的,只需使用 "response.on('data', function() { })" 消耗响应也可以。此外,在回调函数中调用 "this.destroy()" 似乎也可以。 - Sunday Ironfoot
我还要补充一点,这在文档http://nodejs.org/api/http.html#http_http_request_options_callback中并没有很清楚地说明,但如果这是由streams2带来的新行为,那么0.10版本刚刚发布也就可以理解了。 - Sunday Ironfoot
在这个代码解决方案中,http.request 发生在哪里?换句话说,完整的代码是什么样子的? - TetraDev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接