Я сканирую множество ссылок с модулем запроса параллельно с комбинацией модуля async.
Я замечаю множество ошибок ETIMEDOUT
и ESOCKETTIMEDOUT
, хотя ссылки доступны и довольно быстро реагируют с помощью Chrome.
Я ограничил maxSockets
до 2 и timeout
до 10000 в опциях запроса.
Я использую async.filterLimit()
с пределом 2, чтобы даже сократить параллелизм до 2 запросов каждый раз.
Таким образом, у меня есть 2 сокета, 2 запроса и тайм-аут в 10 секунд для ожидания ответа заголовков с сервера, но я получаю эти ошибки.
Здесь я использую конфигурацию запроса:
{
...
pool: {
maxSockets: 2
},
timeout: 10000
,
time: true
...
}
Вот фрагмент кода, который я использую для поиска ссылок:
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
, объект подключения является либо истинным, либо ложным, поэтому иногда это время ожидания подключения, а иногда - нет (в соответствии с документацией запроса)
UPDATE:
Я решил увеличить maxSockets
до Infinity
, чтобы соединение не зависало из-за отсутствия доступных сокетов:
pool: {
maxSockets: Infinity
}
Для контроля полосы пропускания я реализовал метод requestLoop
, который обрабатывает запрос с параметрами maxAttemps
и retryDelay
для управления запросами:
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
Хочу отметить, что я также поиграл с настройками всего здесь, чтобы избежать ошибок при сканировании, но пока попытки не увенчались успехом.
Все еще ищу помощь в решении этой проблемы.
UPDATE2:
Я решил отказаться от 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-сервер, ограничивая пользовательские запросы до максимума, выполняя ip-проверку и в результате возвращая некоторые сообщения HTTP 400 предотвратить возможную "атаку" на сервер.