Запуск параллельной 1k HTTP-запросов застрянет

10

Вопрос в том, что на самом деле происходит, когда вы запускаете 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
Не могли бы вы опубликовать результат npx envinfo, выполнение вашего примера на моем сценарии Win 10 / nodev10.16.0 заканчивается в 8432.805ms
Łukasz Szewczak
Я запустил пример на OS X и Alpine Linux (Docker-контейнер) и достиг того же результата.
Ристо Новик
Мой локальный mac запускает скрипт в 7156.797ms. Вы уверены, что нет брандмауэров, блокирующих запросы?
Джон
Протестировано без использования брандмауэра локальной машины, но может ли это быть проблемой с моим локальным маршрутизатором / сетью? Я постараюсь запустить аналогичный тест в Google Cloud или Heroku.
Ристо Новик

Ответы:

3

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

Во-первых, вы можете знать, как nodeи как это event loopработает, но позвольте мне сделать краткое резюме. Когда вы запускаете сценарий, nodeсреда выполнения сначала запускает его синхронную часть, а затем назначает promisesи timersвыполняет следующие циклы, а когда проверяется, разрешены ли они, запускайте обратные вызовы в другом цикле. Это простое объяснение очень хорошо объясняет, спасибо @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

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

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

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

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");
  }
})();
Педро Муттер
источник
Привет, Педро, спасибо за разъяснения. Я знаю, что Promise.all будет отклонен при появлении первого отклонения обещания, но в большинстве случаев не было ошибки, чтобы отклонить, поэтому все это просто бездействовало.
Ристо Новик
1
> Исправляет, что цикл обработки событий никогда не завершится до тех пор, пока не будут выполнены задачи ОС. Другими словами, выполнение вашего узла никогда не закончится, пока не будут получены HTTP-запросы. Это кажется интересным моментом, задачи ОС управляются через libuv?
Ристо Новик
Я предполагаю, что libuv обрабатывает больше вещей, связанных с операциями (вещи, которые действительно нуждаются в многопоточности). Но я могу ошибаться, нужно посмотреть более подробно
Педро Муттер