Le déclenchement parallèle de requêtes HTTP 1k resterait bloqué

10

La question est de savoir ce qui se passe réellement lorsque vous déclenchez des requêtes HTTP sortantes 1k-2k? Je vois que cela résoudrait toutes les connexions facilement avec 500 connexions mais le déplacement vers le haut à partir de là semble causer des problèmes car les connexions sont laissées ouvertes et l'application Node y serait bloquée. Testé avec serveur local + exemple Google et autres serveurs fictifs.

Donc, avec certains points de terminaison de serveur différents, j'ai reçu une raison: lisez ECONNRESET, ce qui est bien, le serveur n'a pas pu gérer la demande et générer une erreur. Dans la plage de requêtes 1k-2k, le programme se bloque simplement. Lorsque vous vérifiez les connexions ouvertes avec, lsof -r 2 -i -avous pouvez voir qu'il y a une quantité X de connexions qui y restent 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Lorsque vous ajoutez un paramètre de délai d'attente aux demandes, cela aboutirait probablement à une erreur de délai d'attente, mais pourquoi sinon la connexion est maintenue indéfiniment et le programme principal se retrouverait dans un état limbo?

Exemple de code:

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
la source
1
Pourriez-vous publier le résultat de l' npx envinfoexécution de votre exemple sur mon script Win 10 / nodev10.16.0 se termine en 8432.805ms
Łukasz Szewczak
J'exécute l'exemple sur OS X et Alpine Linux (Docker Container) et atteint le même résultat.
Risto Novik
Mon Mac local exécute le script en 7156,797 ms. Êtes-vous sûr qu'aucun pare-feu ne bloque les demandes?
John
Testé sans utiliser le pare-feu de la machine locale, mais cela pourrait-il être un problème avec mon routeur / réseau local? J'essaierai d'exécuter un test similaire dans Google Cloud ou Heroku.
Risto Novik

Réponses:

3

Pour comprendre ce qui se passait à coup sûr, j'ai dû apporter quelques modifications à votre script, mais les voici.

Tout d'abord, vous savez peut-être comment nodeet son event loopfonctionnement, mais permettez-moi de faire un bref résumé. Lorsque vous exécutez un script, le noderuntime exécute d'abord la partie synchrone de celui-ci, puis planifie le promiseset timersà exécuter sur les boucles suivantes et, une fois vérifiés, ils sont résolus, exécutez les rappels dans une autre boucle. Ce simple résumé l'explique très bien, crédit à @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

Notez que la boucle d'événements ne se terminera jamais tant qu'il n'y aura pas de tâches OS en attente. En d'autres termes, l'exécution de votre nœud ne se terminera jamais tant qu'il n'y aura pas de requêtes HTTP en attente.

Dans votre cas, il exécute une asyncfonction, car il retournera toujours une promesse, il planifiera son exécution dans la prochaine itération de la boucle. Sur votre fonction asynchrone, vous planifiez 1000 autres promesses (requêtes HTTP) à la fois dans cette mapitération. Après cela, vous attendez tout, puis être résolu pour terminer le programme. Cela fonctionnera, bien sûr, à moins que votre fonction de flèche anonyme sur le mapne génère aucune erreur . Si l' un de vos promesses renvoie une erreur et vous ne manipulez pas, certaines des promesses ne génèreront pas de rappel appelé jamais rendre le programme à la fin , mais pas à la sortie , car la boucle d'événements qui l' empêchera de sortie jusqu'à ce qu'il décide toutes les tâches, même sans rappel. Comme il est dit sur lePromise.all docs : il sera rejeté dès que la première promesse sera rejetée.

Ainsi, votre ECONNRESETerreur d' erreur n'est pas liée au nœud lui-même, c'est quelque chose avec votre réseau qui a effectué la récupération pour générer une erreur, puis empêcher la boucle d'événement de se terminer. Avec ce petit correctif, vous seriez en mesure de voir toutes les demandes résolues de manière asynchrone:

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
la source
Hé, Pedro, merci pour l'effort expliquant. Je suis conscient que Promise.all rejetterait lorsque le premier rejet de promesse apparaît, mais dans la plupart des cas, il n'y avait aucune erreur à rejeter, le tout serait donc inactif.
Risto Novik
1
> Répare que la boucle d'événements ne se terminera jamais tant qu'il n'y aura pas de tâches OS en attente. En d'autres termes, l'exécution de votre nœud ne se terminera jamais tant qu'il n'y aura pas de requêtes HTTP en attente. Cela semble un point intéressant, les tâches du système d'exploitation sont gérées via le libuv?
Risto Novik
Je suppose que libuv gère plus de choses liées aux opérations (choses qui ont vraiment besoin de multi-threading). Mais je peux me tromper, j'ai besoin de voir plus en profondeur
Pedro Mutter