【问题标题】:Node.js request module getting ETIMEDOUT and ESOCKETTIMEDOUTNode.js 请求模块获取 ETIMEDOUT 和 ESOCKETTIMEDOUT
【发布时间】:2016-05-25 01:24:14
【问题描述】:

我正在使用request 模块与async 模块的组合同时抓取大量链接。
我注意到很多 ETIMEDOUTESOCKETTIMEDOUT 错误,尽管链接是可访问的并且使用 chrome 可以快速响应。

我在请求选项中将 maxSockets 限制为 2,将 timeout 限制为 10000。 我使用async.filterLimit(),限制为2,甚至每次将并行度减少到2个请求。 所以我有 2 个套接字、2 个请求和 10 秒的超时时间来等待来自服务器的标头响应,但我得到了这些错误。

这是我使用的请求配置:

{
    ...
    pool: {
        maxSockets: 2
    },
    timeout: 10000
    ,
    time: true
    ...
}

这是我用来验证链接的代码的 sn-p:

var self = this;
async.filterLimit(resources, 2, function(resource, callback) {
    request({
        uri: resource.uri
    }, function (error, response, body) {
        if (!error && response.statusCode === 200) {
            ...
        } else {
            self.emit('error', resource, error);
        }
        callback(...);
    })
}, function(result) {
    callback(null, result);
});

我听了错误事件,每当错误代码为 ETIMEDOUT 时,我看到连接对象是真/假,所以有时是连接超时,有时不是(根据请求文档)

更新: 我决定将maxSockets 提升到Infinity,这样就不会因为缺少可用的套接字而挂断连接:

pool: {
    maxSockets: Infinity
}

为了控制带宽,我实现了一个requestLoop 方法,该方法使用maxAttempsretryDelay 参数来控制请求:

async.filterLimit(resources, 10, function(resource, callback) {
    self.requestLoop({
        uri: resource.uri
    }, 100, 5000, function (error, response, body) {
            var fetched = false;
            if (!error) {
                ...
            } else {
                ....
            }
            callback(...);
        });
}, function(result) {
    callback(null, result);
});

requestLoop的实现:

requestLoop = function(options, attemptsLeft, retryDelay, callback, lastError) {
    var self = this;
    if (attemptsLeft <= 0) {
        callback((lastError != null ? lastError : new Error('...')));
    } else {
        request(options, function (error, response, body) {
            var recoverableErrors = ['ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED'];
            var e;
            if ((error && _.contains(recoverableErrors, error.code)) || (response && (500 <= response.statusCode && response.statusCode < 600))) {
                e = error ? new Error('...');
                e.code = error ? error.code : response.statusCode;
                setTimeout((function () {
                    self.requestLoop(options, --attemptsLeft, retryDelay, callback, e);
                }), retryDelay);
            } else if (!error && (200 <= response.statusCode && response.statusCode < 300)) {
                callback(null, response, body);
            } else if (error) {
                e = new Error('...');
                e.code = error.code;
                callback(e);
            } else {
                e = new Error('...');
                e.code = response.statusCode;
                callback(e);
            }
        });
    }
};

所以总结一下: - 将maxSockets 提升到Infinity 以尝试克服套接字连接的超时错误 - 实现requestLoop 方法来控制失败的请求和maxAttemps 以及此类请求的retryDelay - 还有最大并发请求数由传递给async.filterLimit的数量设置

我想指出,我也尝试过这里所有的设置,以便无错误地抓取,但到目前为止尝试也失败了。

仍在寻求有关解决此问题的帮助。

更新 2: 我决定放弃 async.filterLimit 并建立自己的限制机制。 我只有 3 个变量来帮助我实现这一目标:
pendingRequests - 一个包含所有请求的请求数组(稍后会解释) activeRequests - 活动请求数 maxConcurrentRequests - 允许的最大并发请求数

到 pendingRequests 数组中,我推送一个复杂对象,其中包含对 requestLoop 函数的引用以及包含要传递给循环函数的参数的 arguments 数组:

self.pendingRequests.push({
    "arguments": [{
        uri: resource.uri.toString()
    }, self.maxAttempts, function (error, response, body) {
        if (!error) {
            if (self.policyChecker.isMimeTypeAllowed((response.headers['content-type'] || '').split(';')[0]) &&
                self.policyChecker.isFileSizeAllowed(body)) {
                self.totalBytesFetched += body.length;
                resource.content = self.decodeBuffer(body, response.headers["content-type"] || '', resource);
                callback(null, resource);
            } else {
                self.fetchedUris.splice(self.fetchedUris.indexOf(resource.uri.toString()), 1);
                callback(new Error('Fetch failed because a mime-type is not allowed or file size is bigger than permited'));
            }
        } else {
            self.fetchedUris.splice(self.fetchedUris.indexOf(resource.uri.toString()), 1);
            callback(error);
        }
        self.activeRequests--;
        self.runRequest();
    }],
    "function": self.requestLoop
});
self.runRequest();

您会注意到最后对runRequest() 的调用。 此功能的工作是管理请求并在可能的情况下触发请求,同时将最大 activeRequests 保持在 maxConcurrentRequests 的限制以下:

var self = this;
process.nextTick(function() {
    var next;
    if (!self.pendingRequests.length || self.activeRequests >= self.maxConcurrentRequests) {
        return;
    }
    self.activeRequests++;
    next = self.pendingRequests.shift();
    next["function"].apply(self, next["arguments"]);
    self.runRequest();
});

这应该可以解决任何超时错误,通过我的测试,我仍然注意到我测试过的特定网站中的一些超时。我不能 100% 确定这一点,但我认为这是由于支持 http-server 的网站的性质,通过进行 ip-checking 将用户请求限制在最大,结果返回一些 HTTP 400 消息以防止对服务器的可能“攻击”。

【问题讨论】:

  • 你有没有想过@Jorayen?
  • @DvideBy0 更新了解决方案
  • 我发现我的代码问题略有不同。就底层套接字而言,似乎没有尊重在我的代码中设置超时。我不得不等待套接字事件(见下文)
  • .on('socket', function(socket) { socket.setTimeout(30000); socket.on('timeout', function() { request.abort(); logger.error('请求在 30 秒后超时'); }); })

标签: node.js sockets asynchronous request httprequest


【解决方案1】:

编辑:https://stackoverflow.com/a/37946324/744276 的副本

默认情况下,Node 有4 workers to resolve DNS queries。如果您的 DNS 查询需要很长时间,请求将在 DNS 阶段阻塞,并且症状正好是 ESOCKETTIMEDOUTETIMEDOUT

尝试增加你的 uv 线程池大小:

export UV_THREADPOOL_SIZE=128
node ...

或在index.js(或您的入口点所在的任何地方):

#!/usr/bin/env node
process.env.UV_THREADPOOL_SIZE = 128;

function main() {
   ...
}

编辑 1:I also wrote a blog post 关于它。

编辑 2:如果查询不唯一,您可能需要使用缓存,例如 nscd

【讨论】:

  • 感谢您对本主题的有益补充,我尚未测试您的建议,但知道这一点仍然很好。一旦我有空闲时间,我也会报告结果:)
  • 我读到的关于 UV_THREADPOOL_SIZE 的内容表明,这对于阻塞 io(例如磁盘访问)最重要,但对于非阻塞 io(例如网络访问)则无关紧要。
  • 没错,只是由于getaddrinfo(3) 的工作方式,DNS 解析也会被阻止。
【解决方案2】:

我发现如果异步请求太多,那么Linux中会发生ESOCKETTIMEDOUT异常。我发现的解决方法是这样做:

将此选项设置为 request(): agent: false, pool: {maxSockets: 100} 请注意,在那之后,超时可能会撒谎,因此您可能需要增加它。

【讨论】:

  • 首先,这对我有用,因为我下载了几百个导致这个错误的小文件。其次,增加socket数量有什么坏处?它们会在某个时候自动关闭吗?
猜你喜欢
  • 2014-08-10
  • 1970-01-01
  • 2016-03-17
  • 2019-07-20
  • 1970-01-01
  • 1970-01-01
  • 2020-04-27
  • 1970-01-01
  • 2013-05-05
相关资源
最近更新 更多