Acabei de assistir o seguinte vídeo: Introdução ao Node.js e ainda não entendo como você obtém os benefícios de velocidade.
Principalmente, a certa altura, Ryan Dahl (criador do Node.js.) diz que o Node.js é baseado em loop de eventos, em vez de em threads. Os threads são caros e devem ser deixados apenas para os especialistas em programação simultânea a serem utilizados.
Posteriormente, ele mostra a pilha de arquitetura do Node.js que possui uma implementação C subjacente e que possui seu próprio pool de threads internamente. Então, obviamente, os desenvolvedores do Node.js. nunca lançariam seus próprios threads ou usariam o pool de threads diretamente ... eles usam retornos de chamada assíncronos. Isso eu entendo.
O que eu não entendo é o ponto em que o Node.js ainda está usando threads ... está apenas ocultando a implementação. Por que isso é mais rápido se 50 pessoas solicitam 50 arquivos (atualmente não estão na memória) e não são necessários 50 threads? ?
A única diferença é que, como é gerenciado internamente, o desenvolvedor do Node.js. não precisa codificar os detalhes do encadeamento, mas por baixo ainda está usando os encadeamentos para processar as solicitações de arquivo IO (bloqueio).
Então você não está apenas pegando um problema (encadeamento) e ocultando-o enquanto esse problema ainda existe: principalmente vários threads, alternância de contexto, travas ... etc?
Deve haver alguns detalhes que ainda não entendo aqui.
fonte
select()
é mais rápida que as trocas de contexto de thread.Respostas:
Na verdade, existem algumas coisas diferentes sendo confundidas aqui. Mas começa com o meme de que os tópicos são realmente difíceis. Portanto, se eles são difíceis, é mais provável que, ao usar threads para 1) quebrar devido a bugs e 2) não os use da maneira mais eficiente possível. (2) é sobre o que você está perguntando.
Pense em um dos exemplos que ele fornece, em que uma solicitação é recebida e você executa alguma consulta e, em seguida, faz algo com os resultados dela. Se você o escrever de maneira processual padrão, o código poderá ser assim:
Se a solicitação recebida fez com que você criasse um novo encadeamento que executasse o código acima, você terá um encadeamento ali, sem fazer nada enquanto
query()
estiver em execução. (O Apache, de acordo com Ryan, está usando um único encadeamento para satisfazer a solicitação original, enquanto o nginx está superando-o nos casos de que ele está falando porque não está.)Agora, se você fosse realmente inteligente, expressaria o código acima de uma maneira em que o ambiente pudesse disparar e fazer outra coisa enquanto você estiver executando a consulta:
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 os fechamentos - seu código de forma que o ambiente possa ser inteligente sobre o que é executado e quando. Dessa forma, o node.js não é novo no sentido de que inventou E / S assíncrona (não que alguém tenha reivindicado algo assim), mas é novo no modo como é expresso é 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 usado para iniciar algumas E / S agora pode ser usado para lidar com outra solicitação ou algum cálculo que possa ser feito em paralelo ou inicie outra E / S paralela. (Não sei se o nó é sofisticado o suficiente para iniciar mais trabalho para a mesma solicitação, mas você entendeu.)
fonte
Nota! Esta é uma resposta antiga. Embora ainda seja verdade no esboço, alguns detalhes podem ter sido alterados devido ao rápido desenvolvimento do Node nos últimos anos.
Está usando threads porque:
Para falsas E / S sem bloqueio, os threads são necessários: faça o IO em um thread separado. É uma solução feia e causa muita sobrecarga.
É ainda pior no nível do hardware:
Isso é simplesmente estúpido e ineficiente. Mas funciona pelo menos! Podemos aproveitar o Node.js porque oculta os detalhes feios e pesados por trás de uma arquitetura assíncrona orientada a eventos.
Talvez alguém implemente O_NONBLOCK para arquivos no futuro? ...
Edit: Eu discuti isso com um amigo e ele me disse que uma alternativa para threads está pesquisando com select : especifique um tempo limite de 0 e faça IO nos descritores de arquivo retornados (agora que eles garantem que não serão bloqueados).
fonte
Receio estar "fazendo a coisa errada" aqui, se assim for, me exclua e peço desculpas. Em particular, não vejo como crio as pequenas anotações que algumas pessoas criaram. No entanto, tenho muitas preocupações / observações a fazer sobre este tópico.
1) O elemento comentado no pseudo-código em uma das respostas populares
é essencialmente falso. Se o encadeamento estiver computando, então não está girando o polegar, está fazendo o trabalho necessário. Se, por outro lado, é simplesmente aguardando a conclusão de IO, então é não usar tempo de CPU, toda a questão da infra-estrutura de controle fio no kernel é que a CPU vai encontrar algo útil para fazer. A única maneira de "mexer os polegares", como sugerido aqui, seria criar um loop de pesquisa, e ninguém que codificou um servidor da Web real é inepto o suficiente para fazer isso.
2) "Threads are hard", só faz sentido no contexto do compartilhamento de dados. Se você tiver threads essencialmente independentes, como é o caso ao lidar com solicitações independentes da Web, o encadeamento é trivialmente simples, basta codificar o fluxo linear de como lidar com um trabalho e ficar tranquilo sabendo que ele tratará vários pedidos e cada um será efetivamente independente. Pessoalmente, eu arriscaria que, para a maioria dos programadores, aprender o mecanismo de fechamento / retorno de chamada é mais complexo do que simplesmente codificar a versão do thread de cima para baixo. (Mas sim, se você precisar se comunicar entre os threads, a vida fica muito difícil muito rapidamente, mas não estou convencido de que o mecanismo de fechamento / retorno de chamada realmente mude isso, apenas restringe suas opções, porque essa abordagem ainda é possível com threads Enfim, isso '
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 incorporados, nada tão sofisticado quanto um sistema operacional "real") sugere que esse não seria o caso.
4) Todas as ilustrações que vi até agora que pretendem mostrar o quão mais rápido o Node é do que outros servidores da Web são terrivelmente falhas, no entanto, são falhas de uma maneira que ilustra indiretamente uma vantagem que eu definitivamente aceitaria para o Node (e não é de forma alguma insignificante). O nó não parece precisar (ou até mesmo permitir) de ajuste. Se você tiver um modelo encadeado, precisará criar encadeamentos suficientes para lidar com a carga esperada. Faça isso mal e você terá um desempenho ruim. Se houver muito poucos encadeamentos, a CPU estará ociosa, mas incapaz de aceitar mais solicitações, criar muitos encadeamentos e você desperdiçará memória do kernel e, no caso de um ambiente Java, também estará desperdiçando a memória heap principal . Agora, para Java, desperdiçar heap é a primeira e melhor maneira de estragar o desempenho do sistema, porque a coleta eficiente de lixo (atualmente, isso pode mudar com o G1, mas parece que o júri ainda está nesse ponto desde o início de 2013, pelo menos) depende de ter muita pilha de reposição. Portanto, existe o problema: ajuste-o com muito poucos threads, você terá CPUs ociosas e taxa de transferência ruim, ajuste-o com muitos threads e atolará de outras maneiras.
5) Existe outra maneira pela qual 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 encadeamento usa um modelo de comutação de contexto com fatias de tempo, em camadas sobre o modelo preemptivo mais apropriado (alerta de julgamento do valor :) e mais eficiente (não um julgamento do valor). Isso acontece por duas razões: primeiro, a maioria dos programadores parece não entender a preempção de prioridade; e, segundo, se você aprender a segmentação em um ambiente Windows, o intervalo de tempo existe, quer você goste ou não (é claro, isso reforça o primeiro ponto) ; notavelmente, as primeiras versões do Java usavam preempção de prioridade nas implementações do Solaris e tempo no Windows. Porque a maioria dos programadores não entendeu e reclamou que "o encadeamento não funciona no Solaris" eles mudaram o modelo para timeslice em todos os lugares). De qualquer forma, a linha inferior é que o timelicing cria opções de contexto adicionais (e potencialmente desnecessárias). Cada troca de contexto leva 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 mudança de contexto por causa da divisão do tempo não deve ser superior a uma porcentagem muito pequena do tempo total, a menos que algo bastante estranho esteja acontecendo, e não há motivo para esperar que esse seja o caso em um servidor web simples). Portanto, sim, as excessivas alternâncias de contexto envolvidas na divisão do tempo são ineficientes (e isso não ocorre em 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 mudança de contexto por causa da divisão do tempo não deve ser superior a uma porcentagem muito pequena do tempo total, a menos que algo bastante estranho esteja acontecendo, e não há motivo para esperar que esse seja o caso em um servidor web simples). Portanto, sim, as excessivas alternâncias de contexto envolvidas na divisão do tempo são ineficientes (e isso não ocorre em 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 mudança de contexto por causa da divisão do tempo não deve ser superior a uma porcentagem muito pequena do tempo total, a menos que algo bastante estranho esteja acontecendo, e não há motivo para esperar que esse seja o caso em um servidor web simples). Portanto, sim, as excessivas alternâncias de contexto envolvidas na divisão do tempo são ineficientes (e isso não ocorre emcomo regra, os threads do kernel ), mas a diferença será de alguns por cento da taxa de transferência, e não do tipo de fatores de número inteiro que estão implícitos nas declarações de desempenho que geralmente estão implícitas no Node.
De qualquer forma, peço desculpas por tudo isso ser longo e desmedido, mas eu realmente sinto que até agora, a discussão não provou nada, e eu ficaria feliz em ouvir alguém de uma dessas situações:
a) uma explicação real de por que o Node deve ser melhor (além dos dois cenários que descrevi acima, o primeiro dos quais (mau ajuste) acredito ser a explicação real de todos os testes que vi até agora. ], na verdade, quanto mais eu penso sobre isso, mais me pergunto se a memória usada por um grande número de pilhas pode ser significativa aqui. Os tamanhos de pilha padrão para threads modernos tendem a ser bastante grandes, mas a memória alocada por um sistema de eventos baseado em fechamento seria apenas o necessário)
b) uma referência real que realmente oferece uma boa chance para o servidor de escolha. Pelo menos dessa maneira, eu teria que parar de acreditar que as afirmações são essencialmente falsas; referências mostradas não são razoáveis).
Saúde, Toby
fonte
open()
não possa ser bloqueado?). Dessa forma, amortiza qualquer ocorrência de desempenho em que o modelo tradicionalfork()
/pthread_create()
sob solicitação tenha que criar e destruir threads. E, como mencionado no postscript a), isso também amortiza a questão do espaço de pilha. Provavelmente, você pode atender a milhares de solicitações com, digamos, 16 threads de IO.Ryan usa threads para as partes que estão bloqueando (a maioria do node.js usa E / S não bloqueadora) porque algumas partes são insanas e difíceis de gravar sem bloqueio. Mas acredito que Ryan deseja ter tudo sem bloqueio. No slide 63 (design interno), você vê Ryan usando a libev (biblioteca que abstrai a notificação de evento assíncrona) para o loop de eventos sem bloqueio . Por causa do loop de eventos node.js precisa de threads menores, o que reduz a alternância de contexto, o consumo de memória etc.
fonte
Threads são usados apenas para lidar com funções que não possuem facilidade assíncrona, como
stat()
.A
stat()
função está sempre bloqueando, portanto, o node.js precisa usar um encadeamento para executar a chamada real sem bloquear o encadeamento principal (loop de eventos). Potencialmente, nenhum encadeamento do conjunto de encadeamentos será usado se você não precisar chamar esse tipo de função.fonte
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 tratamento de E / S encadeado. Imagine uma solicitação de disco, me dê staticFile.x, faça 100 solicitações para esse arquivo. Cada solicitação normalmente ocupa um thread que recupera esse arquivo, ou seja, 100 threads.
Agora imagine a primeira solicitação criando um encadeamento que se torna um objeto publicador; todas as outras 99 solicitações primeiro examinam se existe um objeto publicador para staticFile.x; se houver, escute-o enquanto ele está fazendo seu trabalho; caso contrário, inicie um novo encadeamento e, portanto, um novo objeto de editor.
Depois que o encadeamento único é concluído, ele passa staticFile.x para todos os 100 ouvintes e se destrói, portanto, a próxima solicitação cria um novo encadeamento e objeto publicador.
Portanto, são 100 threads versus 1 thread no exemplo acima, mas também 1 pesquisa de disco em vez de 100 pesquisas de disco, o ganho pode ser bastante fenomenal. Ryan é um cara esperto!
Outra maneira de ver é um de seus exemplos no início do filme. Ao invés de:
Novamente, 100 consultas separadas em um banco de dados versus ...:
Se uma consulta já estivesse em andamento, outras consultas iguais simplesmente iriam para o movimento, para que você possa ter 100 consultas em uma única ida e volta ao banco de dados.
fonte