Запуск паралелі 1 к 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 (докер-контейнер) і дійшов до того ж результату.
Рісто Новік

Мій локальний 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помилка не пов’язана з самим вузлом, це щось із вашою мережею, що змусило завантажувати помилку, а потім запобігти закінченню циклу подій. За допомогою цього невеликого виправлення ви зможете побачити всі запити, які вирішуються асинхронно:

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 обробляє більше речей, пов'язаних з операціями (речі, які дійсно потребують багатопотокової передачі). Але я можу помилятися, потрібно побачити докладніше
Педро Муттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.