O acionamento paralelo de solicitações HTTP de 1k ficaria bloqueado

10

A questão é o que realmente está acontecendo quando você aciona solicitações HTTP de saída de 1 a 2k? Vejo que resolveria todas as conexões facilmente com 500 conexões, mas subir de lá parece causar problemas, pois as conexões são deixadas abertas e o aplicativo Node fica preso lá. Testado com servidor local + exemplo Google e outros servidores simulados.

Portanto, com alguns pontos de extremidade do servidor diferentes , recebi o motivo: leia ECONNRESET, o que é bom, o servidor não pode lidar com a solicitação e gera um erro. No intervalo de solicitação de 1k a 2k, o programa seria interrompido. Ao verificar as conexões abertas, lsof -r 2 -i -avocê pode ver que há uma quantidade X de conexões que continuam penduradas lá 0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED). Quando você adiciona a configuração de tempo limite às solicitações, elas provavelmente acabam com erro de tempo limite, mas por que, de outro modo, a conexão é mantida para sempre e o programa principal terminaria em algum estado limbo?

Código de exemplo:

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
fonte
11
Você poderia postar o resultado do npx envinfoexemplo, executando o seu exemplo no meu script Win 10 / nodev10.16.0, que termina em 8432.805ms
Łukasz Szewczak 20/01
Executo o exemplo no OS X e Alpine Linux (contêiner de docker) e cheguei ao mesmo resultado.
Risto Novik 20/01
Meu mac local executa o script em 7156.797ms. Tem certeza de que não há firewalls bloqueando as solicitações?
John
Testado sem usar o firewall da máquina local, mas poderia haver um problema com meu roteador / rede local? Vou tentar executar um teste semelhante no Google Cloud ou Heroku.
Risto Novik

Respostas:

3

Para entender o que estava acontecendo com certeza, eu precisava fazer algumas modificações no seu script, mas aqui estão elas.

Primeiro, você pode saber como nodee como event loopfunciona, mas deixe-me fazer uma rápida recapitulação. Quando você executar um script, nodetempo de execução primeiro executar a parte síncrona de que, em seguida, agendar o promisese timersa ser executado nos próximos loops, e quando verificado que eles são resolvidos, execute os retornos de chamada em outro loop. Esta essência simples explica muito bem, crédito para @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

Observe que o loop de eventos nunca terminará até que haja tarefas pendentes do SO. Em outras palavras, a execução do nó nunca terminará até que haja solicitações HTTP pendentes.

No seu caso, ele executa uma asyncfunção, pois sempre retornará uma promessa, agendará para que seja executada na próxima iteração do loop. Na sua função assíncrona, você agenda outras 1000 promessas (solicitações HTTP) de uma só vez nessa mapiteração. Depois disso, você está aguardando que seja resolvido para concluir o programa. Funcionará, com certeza, a menos que sua seta anônima funcione no erromap não aconteça . Se uma das suas promessas gerar um erro e você não lidar com elas, algumas delas não terão seu retorno de chamada chamado, fazendo com que o programa termine, mas não saia , porque o loop de eventos impedirá que ele saia até que seja resolvido todas as tarefas, mesmo sem retorno de chamada. Como diz noPromise.all docs : rejeitará assim que a primeira promessa rejeitar.

Portanto, seu ECONNRESETerro on não está relacionado ao nó em si, é algo com a sua rede que fez a busca para lançar um erro e impedir que o loop de eventos termine. Com essa pequena correção, você poderá ver todos os pedidos sendo resolvidos de forma assíncrona:

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
fonte
Ei, Pedro, obrigado pelo esforço de explicar. Estou ciente de que o Promise.all seria rejeitado quando a primeira rejeição da promessa aparecer, mas na maioria dos casos não havia nenhum erro para rejeitar, de modo que a coisa toda ficaria ociosa.
Risto Novik
11
> Repara que o loop de eventos nunca terminará até que haja tarefas pendentes do SO. Em outras palavras, a execução do nó nunca terminará até que haja solicitações HTTP pendentes. Este parece um ponto interessante, as tarefas do SO são gerenciadas através do libuv?
Risto Novik 23/01
Eu acho que o libuv lida com mais coisas relacionadas às operações (coisas que realmente precisam de multi-threading). Mas eu posso estar errado, preciso ver mais em profundidade
Pedro Mutter