ti-enxame.com

Como o Node.js é inerentemente mais rápido quando ainda confia em Threads internamente?

Acabei de ver o seguinte vídeo: Introdução ao Node.js e ainda não entendi como você obtém os benefícios da velocidade.

Principalmente, em um ponto, Ryan Dahl (criador do Node.js) diz que o Node.js é baseado em loop de eventos, em vez de baseado em threads. Os threads são caros e só devem ser deixados para os especialistas de programação simultânea a serem utilizados.

Posteriormente, ele mostra a pilha de arquitetura do Node.js que possui uma implementação C subjacente que possui seu próprio conjunto de threads internamente. Então, obviamente, os desenvolvedores do Node.js nunca iniciariam seus próprios threads ou usariam o pool de threads diretamente ... eles usam callbacks assíncronos. Isso eu entendo.

O que eu não entendo é o ponto que o Node.js ainda está usando threads ... está apenas escondendo a implementação, então como isso é mais rápido se 50 pessoas requisitarem 50 arquivos (não atualmente na memória) bem, então não são necessários 50 threads ?

A única diferença é que, como ele é gerenciado internamente, o desenvolvedor do Node.js não precisa codificar os detalhes do thread, mas, por baixo, ele ainda está usando os threads para processar as solicitações de arquivos IO _ (bloqueio).

Então você não está apenas pegando um problema (encadeando) e escondendo-o enquanto o problema ainda existe: principalmente múltiplos encadeamentos, comutação de contexto, dead-locks ... etc?

Deve haver algum detalhe que ainda não entendi aqui.

276
Ralph Caraveo

Na verdade, existem algumas coisas diferentes sendo confundidas aqui. Mas começa com o meme de que os threads são realmente difíceis. Então, se eles são difíceis, você é mais provável, ao usar threads para 1) quebrar devido a bugs e 2) não usá-los da forma mais eficiente possível. (2) é o que você está perguntando.

Pense em um dos exemplos que ele dá, onde uma requisição chega e você executa alguma consulta, e então faz algo com os resultados disso. Se você escrevê-lo de uma maneira processual padrão, o código pode ser assim:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Se a requisição chegar, fazendo com que você crie um novo thread que tenha o código acima, você terá um thread lá, não fazendo nada enquanto query() estiver rodando. (O Apache, de acordo com Ryan, está usando um único thread para satisfazer o pedido original, enquanto o nginx o supera nos casos dos quais ele está falando, porque não está.)

Agora, se você fosse realmente inteligente, você expressaria o código acima de uma maneira em que o ambiente poderia sair e fazer outra coisa enquanto estiver executando a consulta:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

Isso é basicamente o que o node.js está fazendo. Você está basicamente decorando - de uma maneira que seja conveniente por causa da linguagem e do ambiente, daí os pontos sobre fechamentos - seu código de tal forma que o ambiente possa ser inteligente sobre o que roda e quando. Dessa forma, o node.js não é novo no sentido de que ele inventou a E/S assíncrona (não que alguém tenha reivindicado algo assim), mas é novidade em que a forma como é expressa é um pouco diferente.

Nota: quando digo que o ambiente pode ser inteligente sobre o que é executado e quando, especificamente, o que quero dizer é que o encadeamento que ele usou para iniciar alguma E/S agora pode ser usado para manipular alguma outra solicitação ou algum cálculo que pode ser feito em paralelo, ou inicie alguma outra E/S paralela. (Não estou certo de que o nó seja sofisticado o suficiente para iniciar mais trabalho para o mesmo pedido, mas você entende a ideia.)

137
jrtipton

Nota! Esta é uma resposta antiga. Embora ainda seja verdade no esboço, alguns detalhes podem ter mudado devido ao rápido desenvolvimento do Node nos últimos anos.

Está usando encadeamentos porque:

  1. A opção O_NONBLOCK do open () não funciona nos arquivos .
  2. Existem bibliotecas de terceiros que não oferecem IO sem bloqueio.

Para falsificar o IO sem bloqueio, os threads são necessários: fazer o bloqueio IO em um thread separado. É uma solução feia e causa muita sobrecarga.

É ainda pior no nível do hardware:

  • Com DMA a CPU descarrega de maneira assíncrona IO.
  • Os dados são transferidos diretamente entre o dispositivo IO e a memória.
  • O kernel envolve isso em uma chamada de sistema de bloqueio síncrona.
  • O Node.js envolve a chamada do sistema de bloqueio em um thread.

Isso é simplesmente estúpido e ineficiente. Mas funciona pelo menos! Podemos aproveitar o Node.js porque ele esconde os detalhes feios e complicados por trás de uma arquitetura assíncrona orientada a eventos.

Talvez alguém irá implementar O_NONBLOCK para arquivos no futuro? ...

Edit: Eu discuti isso com um amigo e ele me disse que uma alternativa aos tópicos é polling com select : especifica um timeout de 0 e do IO nos descritores de arquivos retornados (agora que eles estão garantidos para não bloquear).

32
nalply

Eu temo que estou "fazendo a coisa errada" aqui, se assim for, me exclua e peço desculpas. Em particular, não consigo ver como eu crio as pequenas anotações que algumas pessoas criaram. No entanto, tenho muitas preocupações/observações para fazer neste segmento.

1) O elemento comentado no pseudo-código em uma das respostas populares

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

é essencialmente falso. Se o encadeamento estiver computando, então não está dando voltas, está fazendo o trabalho necessário. Se, por outro lado, ele estiver simplesmente aguardando a conclusão do IO, então não usando o tempo de CPU, o ponto inteiro da infra-estrutura de controle de thread no kernel é que a CPU encontrará algo útil para Faz. A única maneira de "mexer os polegares", como sugerido aqui, seria criar um loop de pesquisa, e ninguém que tenha codificado um servidor da web real é inepto o suficiente para fazer isso.

2) "Threads são difíceis", só faz sentido no contexto do compartilhamento de dados. Se você tem essencialmente threads independentes, como é o caso ao lidar com requisições web independentes, então o threading é trivialmente simples, você apenas codifica o fluxo linear de como manipular um job, e fica bastante sabendo que ele manipulará múltiplos pedidos, e cada um será efetivamente independente. Pessoalmente, eu me arrisco que para a maioria dos programadores, aprender o mecanismo de fechamento/retorno de chamada é mais complexo do que simplesmente codificar a versão de thread de cima para baixo. (Mas sim, se você tiver que se comunicar entre os threads, a vida fica muito difícil muito rápido, mas eu não estou convencido que o mecanismo de fechamento/callback realmente mude isso, apenas restringe suas opções, porque essa abordagem ainda é possível com threads De qualquer forma, essa é outra discussão que não é relevante aqui).

3) Até agora, ninguém apresentou nenhuma evidência real de por que um tipo específico de mudança de contexto consumiria mais ou menos tempo do que qualquer outro tipo. Minha experiência na criação de kernels multitarefa (em pequena escala para controladores embutidos, nada tão chique como um sistema operacional "real") sugere que esse não seria o caso.

4) Todas as ilustrações que eu vi até hoje que pretendem mostrar o quanto mais rápidoNode é do que outros servidores da Web são terrivelmente falhos, no entanto, elas são defeituosas de uma forma que indiretamente ilustra uma vantagem que eu definitivamente aceitar para Node (e não é de modo algum insignificante). Node não parece precisar (nem mesmo permitir, na verdade) ajuste. Se você tiver um modelo encadeado, será necessário criar encadeamentos suficientes para manipular a carga esperada. Faça isso mal e você terá um desempenho ruim. Se houver poucos encadeamentos, então a CPU está inativa, mas não é possível aceitar mais solicitações, criar muitos encadeamentos e você desperdiçará memória do kernel e, no caso de um ambiente Java, você também estar desperdiçando memória heap principal. Agora, para Java, desperdiçar heap é a primeira, melhor maneira de estragar o desempenho do sistema, porque coleta de lixo eficiente (atualmente, isso pode mudar com o G1, mas parece que o júri ainda está fora nesse ponto a partir do início de 2013 pelo menos) depende de ter muito monte de reposição. Então, há o problema, ajustá-lo com poucos threads, você tem CPUs ociosas e uma taxa de transferência ruim, ajusta-a com muitas, e se atém de outras maneiras.

5) Existe uma outra maneira pela qual eu aceito a lógica da afirmação de que a abordagem do Node "é mais rápida por design", e é isso. A maioria dos modelos de thread usa um modelo de switch de contexto fatiado em tempo, sobreposto ao mais apropriado (alerta de julgamento de valor :) e modelo preemptivo mais eficiente (sem julgamento de valor). Isso acontece por dois motivos: primeiro, a maioria dos programadores parece não entender a preempção de prioridade e, segundo, se você aprender a segmentar em um ambiente Windows, o tempo limite está lá, goste ou não (claro, isso reforça o primeiro ponto). notadamente, as primeiras versões de Java usaram preempção de prioridade nas implementações do Solaris e timeslicing no Windows. Como a maioria dos programadores não entendeu e reclamou que o "threading não funciona no Solaris" eles mudaram o modelo para timeslice em todos os lugares). De qualquer forma, o resultado é que o timeslicing cria switches de contexto adicionais (e potencialmente desnecessários). Cada switch de contexto leva o tempo de CPU e esse tempo é efetivamente removido do trabalho que pode ser feito no trabalho real em questão. No entanto, a quantidade de tempo investido na troca de contexto por causa do agendamento de tempo não deve ser mais do que uma porcentagem muito pequena do tempo total, a menos que algo muito estranho esteja acontecendo, e não há motivo para esperar que seja esse o caso. servidor web simples). Então, sim, as mudanças de contexto em excesso envolvidas no timeslicing são ineficientes (e estas não acontecem em kernel threads como regra, btw) mas a diferença será de alguns por cento do throughput, não do tipo de fatores de números inteiros que estão implícitos nas declarações de desempenho que são frequentemente implícitas para o Node.

De qualquer forma, desculpas por tudo isso ser longo e rambly, mas eu realmente sinto que até agora, a discussão não provou nada, e eu ficaria feliz em ouvir alguém em uma dessas situações:

a) uma explicação real do porquê Node deve ser melhor (além dos dois cenários que descrevi acima, o primeiro dos quais (má sintonia) eu acredito ser a verdadeira explicação para todos os testes que eu vi até agora. ([edit], na verdade, quanto mais eu penso sobre isso, mais eu estou querendo saber se a memória usada por um grande número de stacks pode ser significante aqui. Os tamanhos de pilha padrão para threads modernos tendem a ser bem grandes, mas a memória alocada por um sistema de eventos baseado em fechamento seria apenas o que é necessário)

b) um benchmark real que realmente dá uma chance justa ao servidor de threads escolhido. Pelo menos dessa forma, eu teria que parar de acreditar que as afirmações são essencialmente falsas;> ([editar] provavelmente é mais forte do que eu pretendia, mas eu sinto que as explicações dadas para os benefícios de desempenho são incompletos na melhor das hipóteses, e o benchmarks mostrados não são razoáveis).

Felicidades, Toby

28
Toby Eggitt

O que eu não entendo é o ponto que o Node.js ainda está usando threads.

Ryan usa threads para as partes que estão bloqueando (A maioria dos node.js usa IO não bloqueante) porque algumas partes são insanas e difíceis de serem escritas como não-bloqueantes. Mas acredito que o desejo de Ryan é ter tudo sem bloqueio. Em slide 63 (design interno) você vê Ryan usa libev (biblioteca que abstrai a notificação de evento assíncrona) para o não-bloqueio eventloop . Por causa do event-loop, o node.js precisa de threads menores, o que reduz a troca de contexto, o consumo de memória, etc.

14
Alfred

Threads são usados ​​apenas para lidar com funções que não possuem facilidade assíncrona, como stat().

A função stat() está sempre bloqueando, portanto node.js precisa usar um thread para executar a chamada real sem bloquear o thread principal (event loop). Potencialmente, nenhum thread do pool de threads será usado se você não precisar chamar esse tipo de função.

11
gawi

Não sei nada sobre o funcionamento interno do node.js, mas posso ver como o uso de um loop de eventos pode superar o processamento de E/S com thread. Imagine uma solicitação de disco, me dê staticFile.x, faça 100 solicitações para esse arquivo. Cada requisição normalmente pega um thread recuperando aquele arquivo, são 100 threads.

Agora imagine a primeira requisição criando um thread que se torna um objeto publisher, todas as 99 outras requisições primeiro olham se existe um objeto publicador para staticFile.x, se assim for, ouça enquanto está fazendo seu trabalho, caso contrário inicie um novo thread e assim um novo objeto de editor.

Depois que o thread único é concluído, ele passa staticFile.x para todos os 100 listeners e se destrói, portanto, a próxima solicitação cria um novo segmento e um objeto publicador.

Portanto, são 100 threads vs 1 thread no exemplo acima, mas também 1 lookup de disco em vez de 100 lookups de disco, o ganho pode ser bastante fenomenal. Ryan é um cara inteligente!

Outra maneira de olhar é um dos seus exemplos no início do filme. Ao invés de:

pseudo code:
result = query('select * from ...');

Mais uma vez, 100 consultas separadas a um banco de dados versus ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Se uma consulta já estava em andamento, outras consultas iguais simplesmente saltariam no movimento, para que você possa ter 100 consultas em uma única ida e volta do banco de dados.

7
BGerrissen