Запуск параллельной 1k HTTP-запросов застрянет - PullRequest
10 голосов
/ 17 января 2020

Вопрос в том, что на самом деле происходит, когда вы запускаете 1k-2k исходящих HTTP-запросов? Я вижу, что он легко разрешил бы все соединения с 500 соединениями, но движение вверх оттуда, кажется, вызывает проблемы, так как соединения остаются открытыми, и приложение Node застревает там. Протестировано на локальном сервере + пример Google и других фиктивных серверов.

Итак, с некоторыми различными серверами конечными точками я получил причину: прочитал ECONNRESET, что хорошо, сервер не смог обработать запрос и выдал ошибка. В диапазоне запросов 1k-2k программа просто зависает. Когда вы проверяете открытые соединения с помощью lsof -r 2 -i -a, вы можете увидеть, что существует некоторое количество X соединений, которые продолжают висеть там 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Когда вы добавляете настройку тайм-аута к запросам, это, вероятно, приведет к ошибке тайм-аута, но почему в противном случае соединение будет сохраняться вечно, а основная программа окажется в каком-то неопределенном состоянии?

Пример кода:

import fetch from 'node-fetch';

(async () => {
  const promises = Array(1000).fill(1).map(async (_value, index) => {
    const url = 'https://google.com';
    const response = await fetch(url, {
      // timeout: 15e3,
      // headers: { Connection: 'keep-alive' }
    });
    if (response.statusText !== 'OK') {
      console.log('No ok received', index);
    }
    return response;
  })

  try {
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  }
  console.log('Done');
})();

1 Ответ

3 голосов
/ 23 января 2020

Чтобы понять, что происходит наверняка, мне нужно было внести некоторые изменения в ваш скрипт, но здесь есть.

Во-первых, вы можете знать, как работает node и event loop, но позвольте я сделаю быстрое резюме. Когда вы запускаете сценарий, node runtime сначала запускает его синхронную часть, а затем назначает выполнение promises и timers в следующих циклах, а когда проверено, разрешены ли они, запускайте обратные вызовы в другом l * 1034. *. Это простое объяснение очень хорошо объясняет, кредит @StephenGrider:


const pendingTimers = [];
const pendingOSTasks = [];
const pendingOperations = [];

// New timers, tasks, operations are recorded from myFile running
myFile.runContents();

function shouldContinue() {
  // Check one: Any pending setTimeout, setInterval, setImmediate?
  // Check two: Any pending OS tasks? (Like server listening to port)
  // Check three: Any pending long running operations? (Like fs module)
  return (
    pendingTimers.length || pendingOSTasks.length || pendingOperations.length
  );
}

// Entire body executes in one 'tick'
while (shouldContinue()) {
  // 1) Node looks at pendingTimers and sees if any functions
  // are ready to be called.  setTimeout, setInterval
  // 2) Node looks at pendingOSTasks and pendingOperations
  // and calls relevant callbacks
  // 3) Pause execution. Continue when...
  //  - a new pendingOSTask is done
  //  - a new pendingOperation is done
  //  - a timer is about to complete
  // 4) Look at pendingTimers. Call any setImmediate
  // 5) Handle any 'close' events
}

// exit back to terminal

Обратите внимание, что событие l oop никогда не закончится, пока не будут выполнены задачи ОС. Другими словами, выполнение вашего узла никогда не закончится, пока не будут получены ожидающие HTTP-запросы.

В вашем случае он выполняет функцию async, так как он всегда будет возвращать обещание, он будет планировать его выполняется в следующей итерации l oop. В вашей функции asyn c вы запланируете еще несколько 1000 обещаний (HTTP-запросов) сразу в этой итерации map. После этого вы ждете, пока все решится на окончание sh программы. Это будет работать наверняка, если только ваша анонимная функция стрелки на map не выдаст ошибку . Если одно из ваших обещаний выдает ошибку, а вы ее не обрабатываете, некоторым обещаниям не будет вызываться обратный вызов, когда программа будет end , но не exit , потому что событие l oop будет препятствовать его выходу, пока оно не решит все задачи, даже без обратного вызова. Как сказано в Promise.all документах : он будет отклонен, как только отклоняется первое обещание.

Итак, ваша ошибка on ECONNRESET не связана с самим узлом, это нечто с вашей сетью, которая произвела выборку, чтобы выдать ошибку, а затем предотвратить завершение события l oop. С помощью этого небольшого исправления вы сможете увидеть все запросы, решаемые асинхронно:

const fetch = require("node-fetch");

(async () => {
  try {
    const promises = Array(1000)
      .fill(1)
      .map(async (_value, index) => {
        try {
          const url = "https://google.com/";
          const response = await fetch(url);
          console.log(index, response.statusText);
          return response;
        } catch (e) {
          console.error(index, e.message);
        }
      });
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  } finally {
    console.log("Done");
  }
})();
...