Das parallele Auslösen von 1k HTTP-Anforderungen würde hängen bleiben

10

Die Frage ist, was tatsächlich passiert, wenn Sie 1k-2k ausgehende HTTP-Anforderungen auslösen. Ich sehe, dass es alle Verbindungen mit 500 Verbindungen leicht lösen würde, aber das Aufsteigen von dort scheint Probleme zu verursachen, da die Verbindungen offen bleiben und die Node-App dort stecken bleibt. Getestet mit lokalem Server + Beispiel Google und anderen Mock-Servern.

Bei einigen verschiedenen Serverendpunkten erhielt ich einen Grund: Lesen Sie ECONNRESET, was in Ordnung ist. Der Server konnte die Anforderung nicht verarbeiten und einen Fehler auslösen. Im 1k-2k-Anforderungsbereich würde das Programm einfach hängen bleiben. Wenn Sie die offenen Verbindungen mit überprüfen, können lsof -r 2 -i -aSie feststellen, dass dort immer wieder X Verbindungen hängen 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Wenn Sie Anforderungen eine Timeout-Einstellung hinzufügen, wird dies wahrscheinlich zu einem Timeout-Fehler führen. Warum wird die Verbindung sonst für immer aufrechterhalten und das Hauptprogramm befindet sich in einem Schwebezustand?

Beispielcode:

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
quelle
1
Könnten Sie das Ergebnis des npx envinfoBeispiels veröffentlichen, das auf meinem Win 10 / nodev10.16.0-Skript ausgeführt wird und am
Łukasz Szewczak
Ich habe das Beispiel unter OS X und Alpine Linux (Docker-Container) ausgeführt und das gleiche Ergebnis erzielt.
Risto Novik
Mein lokaler Mac führt das Skript in 7156.797 ms aus. Sind Sie sicher, dass keine Firewalls die Anforderungen blockieren?
John
Getestet ohne lokale Computer-Firewall, aber könnte es ein Problem mit meinem lokalen Router / Netzwerk sein? Ich werde versuchen, einen ähnlichen Test in Google Cloud oder Heroku durchzuführen.
Risto Novik

Antworten:

3

Um zu verstehen, was sicher geschah, musste ich einige Änderungen an Ihrem Skript vornehmen, aber hier sind einige.

Zunächst wissen Sie vielleicht, wie nodeund wie es event loopfunktioniert, aber lassen Sie mich kurz zusammenfassen. Wenn Sie ein Skript ausführen, führen Sie zur nodeLaufzeit zuerst den synchronen Teil davon aus und planen Sie dann die promisesund timers, die in den nächsten Schleifen ausgeführt werden sollen. Wenn diese aktiviert sind, führen Sie die Rückrufe in einer anderen Schleife aus. Dieser einfache Kern erklärt es sehr gut, danke an @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

Beachten Sie, dass die Ereignisschleife niemals endet, bis anstehende Betriebssystemaufgaben vorliegen. Mit anderen Worten, Ihre Knotenausführung wird niemals beendet, bis ausstehende HTTP-Anforderungen vorliegen.

In Ihrem Fall wird eine asyncFunktion ausgeführt, da immer ein Versprechen zurückgegeben wird und die Ausführung in der nächsten Schleifeniteration geplant wird. In Ihrer asynchronen Funktion planen Sie weitere 1000 Versprechen (HTTP-Anforderungen) gleichzeitig in dieser mapIteration. Danach warten Sie darauf, dass alle aufgelöst werden, um das Programm zu beenden. Es wird sicher funktionieren, es sei denn, Ihre anonyme Pfeilfunktion auf dem maplöst keinen Fehler aus . Wenn einer Ihrer verspricht einen Fehler wirft und Sie nicht damit umgehen, wird ein Teil der Versprechungen nicht ihre Rückruf jemals das Programm machen namens müssen am Ende aber nicht zu Ausfahrt , weil die Ereignisschleife es zu verlassen , bis es Entschlüssen verhindern alle Aufgaben, auch ohne Rückruf. Wie es auf dem stehtPromise.all docs : Es wird abgelehnt, sobald das erste Versprechen abgelehnt wird.

Ihr On- ECONNRESETFehler hängt also nicht mit dem Knoten selbst zusammen, sondern mit Ihrem Netzwerk, das den Abruf veranlasst hat, einen Fehler auszulösen und dann zu verhindern, dass die Ereignisschleife endet. Mit diesem kleinen Fix können Sie sehen, dass alle Anforderungen asynchron aufgelöst werden:

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
quelle
Hey, Pedro, danke für die Mühe zu erklären. Ich bin mir bewusst, dass das Versprechen abgelehnt wird, wenn das erste Versprechen abgelehnt wird, aber in den meisten Fällen gab es keinen Fehler beim Ablehnen, sodass das Ganze nur im Leerlauf lief.
Risto Novik
1
> Repariert, dass die Ereignisschleife niemals endet, bis anstehende Betriebssystemaufgaben vorliegen. Mit anderen Worten, Ihre Knotenausführung wird niemals beendet, bis ausstehende HTTP-Anforderungen vorliegen. Dies scheint ein interessanter Punkt zu sein, OS-Aufgaben werden über die libuv verwaltet?
Risto Novik
Ich denke, libuv behandelt mehr Dinge im Zusammenhang mit den Operationen (Dinge, die wirklich Multithreading benötigen). Aber ich könnte mich irren, muss genauer sehen
Pedro Mutter