Spowodowałoby to zablokowanie równoległego uruchamiania 1 000 żądań HTTP

10

Pytanie brzmi, co się dzieje, gdy uruchamiasz wychodzące żądania HTTP 1k-2k? Widzę, że rozwiązałoby to wszystkie połączenia z łatwością z 500 połączeniami, ale przejście w górę stamtąd wydaje się powodować problemy, ponieważ połączenia pozostają otwarte, a aplikacja Node utknie w tym miejscu. Testowane na serwerze lokalnym + przykład Google i innych fałszywych serwerach.

Więc z kilkoma różnymi punktami końcowymi serwera otrzymałem powód: przeczytaj ECONNRESET, co jest w porządku, serwer nie mógł obsłużyć żądania i zgłosić błąd. W zakresie żądań 1k-2k program po prostu się zawiesił. Gdy sprawdzisz otwarte połączenia lsof -r 2 -i -a, zobaczysz, że istnieje pewna liczba połączeń X, które się tam zawieszają 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Gdy dodasz ustawienie limitu czasu do żądań, prawdopodobnie skończy się to błędem przekroczenia limitu czasu, ale dlaczego inaczej połączenie będzie utrzymywane na zawsze, a główny program skończy w stanie zawieszenia?

Przykładowy kod:

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');
})();
Risto Novik
źródło
1
Czy mógłbyś opublikować wynik npx envinfodziałania twojego przykładu na moim skrypcie Win 10 / nodev10.16.0 kończy się na 8432.805ms
Łukasz Szewczak
Uruchomiłem przykład na OS X i Alpine Linux (kontener dokerów) i osiągnąłem ten sam wynik.
Risto Novik
Mój lokalny komputer Mac uruchamia skrypt w wersji 7156.797ms. Czy na pewno nie ma żadnych zapór blokujących żądania?
John
Przetestowano bez użycia zapory lokalnej maszyny, ale czy może to być problem z moim lokalnym routerem / siecią? Spróbuję przeprowadzić podobny test w Google Cloud lub Heroku.
Risto Novik

Odpowiedzi:

3

Aby na pewno zrozumieć, co się dzieje, musiałem wprowadzić pewne zmiany w skrypcie, ale oto są.

Po pierwsze, możesz wiedzieć, jak nodei jak to event loopdziała, ale pozwól mi szybko podsumować. Po uruchomieniu skryptu nodeśrodowisko wykonawcze najpierw uruchamia jego synchroniczną część, a następnie planuje wykonanie promisesi timersdo wykonania w następnych pętlach, a po zaznaczeniu, że zostały one rozwiązane, uruchom wywołania zwrotne w innej pętli. Ta prosta treść wyjaśnia to bardzo dobrze, podziękowania dla @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

Pamiętaj, że pętla zdarzeń nigdy się nie skończy, dopóki nie zostaną wykonane zadania systemu operacyjnego. Innymi słowy, wykonanie węzła nigdy się nie skończy, dopóki nie zostaną oczekujące żądania HTTP.

W twoim przypadku uruchamia asyncfunkcję, ponieważ zawsze zwróci obietnicę, zaplanuje wykonanie w następnej iteracji pętli. W funkcji asynchronicznej planujesz kolejne 1000 obietnic (żądań HTTP) jednocześnie w tej mapiteracji. Następnie czekasz na wszystko, a następnie zdecydujesz się zakończyć program. Na pewno zadziała, chyba że twoja anonimowa funkcja strzałki mapnie zgłasza żadnego błędu . Jeśli jeden z twoich obietnic zgłasza błąd i nie poradzić, niektóre z obietnic nie będzie miał ich zwrotna nazywa kiedykolwiek dzięki czemu program do zakończenia , ale nie do wyjścia , bo pętla zdarzenie uniemożliwi jej wyjściu aż rozwiązuje wszystkie zadania, nawet bez oddzwaniania. Jak napisano wPromise.all dokumenty : odrzuci, gdy tylko pierwsza obietnica odrzuci.

Twój ECONNRESETbłąd nie jest związany z samym węzłem, jest czymś w twojej sieci, która spowodowała, że ​​pobieranie zwróciło błąd, a następnie uniemożliwiło zakończenie pętli zdarzeń. Dzięki tej małej poprawce można zobaczyć, że wszystkie żądania są rozwiązywane asynchronicznie:

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");
  }
})();
Pedro Mutter
źródło
Hej, Pedro dziękuję za wytłumaczenie. Wiem, że Promise.all odrzuci, gdy pojawi się pierwsze odrzucenie obietnicy, ale w większości przypadków nie było błędu do odrzucenia, więc cała sprawa byłaby po prostu bezczynna.
Risto Novik
1
> Naprawia pętlę zdarzeń, która nigdy się nie skończy, dopóki nie zostaną wykonane zadania systemu operacyjnego. Innymi słowy, wykonanie węzła nigdy się nie skończy, dopóki nie zostaną oczekujące żądania HTTP. To wydaje się interesujący punkt, zadania systemu operacyjnego są zarządzane przez libuv?
Risto Novik
Wydaje mi się, że libuv obsługuje więcej rzeczy związanych z operacjami (rzeczy, które naprawdę wymagają wielowątkowości). Ale mogę się mylić, muszę zobaczyć bardziej dogłębnie
Pedro Mutter