L'attivazione del parallelo delle richieste HTTP 1k si bloccherebbe


10

La domanda è cosa sta realmente accadendo quando si attivano richieste HTTP in uscita 1k-2k? Vedo che risolverebbe facilmente tutte le connessioni con 500 connessioni, ma spostarsi verso l'alto da lì sembra causare problemi in quanto le connessioni vengono lasciate aperte e l'app Node rimarrà bloccata lì. Testato con server locale + esempio Google e altri server simulati.

Quindi, con alcuni endpoint server diversi, ho ricevuto un motivo: leggere ECONNRESET che va bene che il server non è in grado di gestire la richiesta e genera un errore. Nell'intervallo di richieste 1k-2k il programma si bloccherebbe e basta. Quando controlli le connessioni aperte con lsof -r 2 -i -apuoi vedere che ci sono alcune quantità X di connessioni che rimangono sospese lì 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Quando aggiungi un'impostazione di timeout alle richieste, ciò probabilmente finirebbe con un errore di timeout, ma perché altrimenti la connessione verrà mantenuta per sempre e il programma principale finirà in qualche stato di limbo?

Codice di esempio:

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
Potresti pubblicare il risultato del npx envinfo, eseguendo il tuo esempio sul mio script Win 10 / nodev10.16.0 termina in 8432.805ms
Łukasz Szewczak

Ho eseguito l'esempio su OS X e Alpine Linux (contenitore docker) e ho raggiunto lo stesso risultato.
Risto Novik,

Il mio mac locale esegue lo script in 7156.797ms. Sei sicuro che non ci siano firewall che bloccano le richieste?
Giovanni

Testato senza utilizzare il firewall del computer locale, ma potrebbe essere un problema con il mio router / rete locale? Proverò ad eseguire un test simile in Google Cloud o Heroku.
Risto Novik,

Risposte:


3

Per capire cosa stava succedendo di sicuro, avevo bisogno di apportare alcune modifiche al tuo script, ma qui ci sono.

Innanzitutto, potresti sapere come nodee come event loopfunziona, ma fammi fare un breve riepilogo. Quando si esegue uno script, noderuntime prima eseguire la parte sincrona di esso quindi pianificare l' promisese timersda eseguire sulle prossime loop, e quando selezionato si risolvono, eseguire i callback in un altro ciclo. Questo semplice riassunto lo spiega molto bene, merito a @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

Si noti che il ciclo degli eventi non finirà mai fino a quando non saranno presenti attività del sistema operativo in sospeso. In altre parole, l'esecuzione del nodo non finirà mai fino a quando non saranno presenti richieste HTTP in sospeso.

Nel tuo caso, esegue una asyncfunzione, poiché restituirà sempre una promessa, pianificherà che venga eseguita nella successiva iterazione del ciclo. Sulla tua funzione asincrona, pianifichi altre 1000 promesse (richieste HTTP) contemporaneamente in quella mapiterazione. Dopodiché, stai aspettando che tutto sia risolto per terminare il programma. Funzionerà sicuramente, a meno che la tua funzione di freccia anonima sul mapnon generi alcun errore . Se una delle tue promesse genera un errore e non le gestisci, alcune delle promesse non avranno il loro callback chiamato mai facendo terminare il programma ma non uscire , perché il loop degli eventi gli impedirà di uscire fino a quando non si risolve tutte le attività, anche senza richiamata. Come dice ilPromise.all documenti : verrà rifiutato non appena la prima promessa verrà rifiutata.

Quindi, il tuo ECONNRESETerrore on non è correlato al nodo stesso, è qualcosa con la tua rete che ha fatto il recupero per generare un errore e quindi impedire la fine del ciclo degli eventi. Con questa piccola correzione, sarai in grado di vedere tutte le richieste che vengono risolte in modo asincrono:

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");
  }
})();

Ehi, Pedro grazie per lo sforzo di spiegare. Sono consapevole del fatto che Promise.all si rifiuta quando appare il primo rifiuto di promessa, ma nella maggior parte dei casi non vi è stato alcun errore da rifiutare, quindi l'intera cosa sarebbe semplicemente inattiva.
Risto Novik,

1
> Ripara che il loop degli eventi non finirà mai fino a quando non ci saranno attività del SO in sospeso. In altre parole, l'esecuzione del nodo non finirà mai fino a quando non saranno presenti richieste HTTP in sospeso. Questo sembra un punto interessante, le attività del sistema operativo sono gestite attraverso il libuv?
Risto Novik,

Immagino che libuv gestisca più cose legate alle operazioni (cose che hanno davvero bisogno del multi-threading). Ma potrei sbagliarmi, ho bisogno di vedere più in profondità
Pedro Mutter il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.