Qual é a resposta do Haskell ao Node.js?

217

Acredito que a comunidade Erlang não tenha inveja do Node.js, pois faz E / S não-bloqueadora nativamente e tem maneiras de escalar implantações facilmente para mais de um processador (algo que nem mesmo está embutido no Node.js.). Mais detalhes em http://journal.dedasys.com/2010/04/29/erlang-vs-node-js e Node.js ou Erlang

E o Haskell? O Haskell pode fornecer alguns dos benefícios do Node.js., ou seja, uma solução limpa para evitar o bloqueio de E / S sem recorrer à programação multithread?


Há muitas coisas atraentes no Node.js

  1. Eventos: Sem manipulação de encadeamento, o programador fornece apenas retornos de chamada (como na estrutura Snap)
  2. É garantido que os retornos de chamada sejam executados em um único encadeamento: nenhuma condição de corrida é possível.
  3. API agradável e simples, compatível com UNIX. Bônus: Excelente suporte HTTP. DNS também disponível.
  4. Cada E / S é por padrão assíncrona. Isso facilita evitar bloqueios. No entanto, muito processamento da CPU em um retorno de chamada afetará outras conexões (nesse caso, a tarefa deve ser dividida em subtarefas menores e agendada novamente).
  5. Mesmo idioma para o lado do cliente e do servidor. (No entanto, não vejo muito valor neste. JQuery e Node.js compartilham o modelo de programação de eventos, mas o resto é muito diferente. Não vejo como o compartilhamento de código entre o lado do servidor e o lado do cliente poderia ser útil na prática.)
  6. Tudo isso embalado em um único produto.
gawi
fonte
17
Eu acho que você deveria fazer esta pergunta aos programadores .
Jonas
47
Não incluir um pedaço de código não o torna uma questão subjetiva.
Gawi
20
Não sei muito sobre o node.js, mas uma coisa me impressionou na sua pergunta: por que você acha a perspectiva de threads tão desagradável? Os encadeamentos devem ser exatamente a solução certa para a multiplexação de E / S. Eu uso o termo threads amplamente aqui, incluindo os processos de Erlang. Talvez você esteja preocupado com bloqueios e estado mutável? Você não precisa fazer as coisas dessa maneira - use mensagens ou transações, se isso fizer mais sentido para o seu aplicativo.
Simon Marlow
9
@ gawi Eu não acho que isso seja muito fácil de programar - sem preempção, você precisa lidar com a possibilidade de inanição e longas latências. Basicamente, os encadeamentos são a abstração correta para um servidor Web - não há necessidade de lidar com E / S assíncrona e todas as dificuldades que o acompanham, basta fazê-lo em um encadeamento. Aliás, escrevi um artigo sobre servidores Web em Haskell, que você pode achar interessante: haskell.org/~simonmar/papers/web-server-jfp.pdf
Simon Marlow
3
"É garantido que os retornos de chamada sejam executados em um único segmento: nenhuma condição de corrida é possível." Errado. Você pode facilmente ter condições de corrida no Node.js; apenas assuma que uma ação de E / S será concluída antes de outra e BOOM. O que é realmente impossível é um tipo específico de condições de corrida, ou seja, acesso simultâneo não sincronizado ao mesmo byte na memória.
rightfold

Respostas:

219

Ok, então, tendo assistido um pouco da apresentação do node.js. que @gawi me indicou, posso dizer um pouco mais sobre como Haskell se compara ao node.js. Na apresentação, Ryan descreve alguns dos benefícios do Green Threads, mas continua dizendo que não considera a falta de uma abstração de thread uma desvantagem. Eu discordo da posição dele, particularmente no contexto de Haskell: acho que as abstrações que os threads fornecem são essenciais para tornar o código do servidor mais fácil de acertar e mais robusto. Em particular:

  • o uso de um encadeamento por conexão permite escrever um código que expresse a comunicação com um único cliente, em vez de escrever um código que lide com todos os clientes ao mesmo tempo. Pense assim: um servidor que lida com vários clientes com threads é quase o mesmo que um que lida com um único cliente; a principal diferença é que há um forklugar no primeiro. Se o protocolo que você está implementando é complexo, gerenciar a máquina de estado para vários clientes simultaneamente fica bastante complicado, enquanto os encadeamentos permitem apenas script da comunicação com um único cliente. O código é mais fácil de acertar e mais fácil de entender e manter.

  • retornos de chamada em um único encadeamento do SO são multitarefa cooperativa, em oposição à multitarefa preventiva, que é o que você obtém com os encadeamentos. A principal desvantagem da multitarefa cooperativa é que o programador é responsável por garantir que não haja fome. Perde modularidade: cometa um erro em um só lugar e pode estragar todo o sistema. Isso é realmente algo com o qual você não quer se preocupar, e a preempção é a solução simples. Além disso, a comunicação entre retornos de chamada não é possível (seria um impasse).

  • a concorrência não é difícil no Haskell, porque a maioria dos códigos é pura e, portanto, é segura para threads por construção. Existem simples primitivas de comunicação. É muito mais difícil dar um tiro no pé com simultaneidade em Haskell do que em um idioma com efeitos colaterais irrestritos.

Simon Marlow
fonte
42
Ok, entendo que o node.js é a solução para 2 problemas: 1 - a concorrência é difícil na maioria dos idiomas; 2 - o uso de threads do sistema operacional é amplo. A solução Node.js é usar a simultaneidade baseada em eventos (w / libev) para evitar a comunicação entre os threads e para evitar problemas de escalabilidade dos threads do SO. Haskell não tem o problema nº 1 por causa da pureza. No segundo, Haskell possui threads leves + gerenciador de eventos que foram otimizados recentemente no GHC para contextos de grande escala. Além disso, o uso de Javascript não pode ser percebido como uma vantagem para qualquer desenvolvedor Haskell. Para algumas pessoas que usam o Snap Framework, o Node.js é "apenas ruim".
Gawi
4
O processamento de solicitações é na maioria das vezes uma sequência de operações interdependentes. Costumo concordar que o uso de retornos de chamada para todas as operações de bloqueio pode ser complicado. Threads são mais adequados que retorno de chamada para isso.
Gawi
10
Sim! E a nova multiplexação de E / S no GHC 7 torna os servidores de gravação em Haskell ainda melhores.
andreypopp
3
Seu primeiro ponto não faz muito sentido para mim (como alguém de fora) ... Ao processar uma solicitação no node.js, seu retorno de chamada lida com um único cliente. O gerenciamento de estado só se torna algo com que se preocupar ao escalonar para vários processos, e mesmo assim é muito fácil usar as bibliotecas disponíveis.
Ricardo Tomasi
12
Não é uma questão separada. Se essa pergunta for uma busca genuína pelas melhores ferramentas para o trabalho em Haskell, ou uma verificação da existência de excelentes ferramentas para o trabalho em Haskell, é necessário desafiar a suposição implícita de que a programação multithread seria inadequada, porque Haskell não tópicos de maneira bastante diferente, como Don Stewart aponta. As respostas que explicam por que a comunidade Haskell também não tem inveja do Node.js estão muito relacionadas a essa pergunta. a resposta de gawi sugere que foi uma resposta apropriada para sua pergunta.
AndrewC
154

O Haskell pode fornecer alguns dos benefícios do Node.js., ou seja, uma solução limpa para evitar o bloqueio de E / S sem recorrer à programação multithread?

Sim, de fato, eventos e threads são unificados no Haskell.

  • Você pode programar em threads leves explícitos (por exemplo, milhões de threads em um único laptop).
  • Ou; você pode programar em um estilo orientado a eventos assíncronos, com base em notificações de eventos escaláveis.

Na verdade , os encadeamentos são implementados em termos de eventos e são executados em vários núcleos, com migração contínua de encadeamentos, desempenho e aplicativos documentados.

Por exemplo, para

Coleções simultâneas nbody em 32 núcleos

texto alternativo

Em Haskell, você tem eventos e threads, e como todos os eventos estão ocultos.

Leia o documento que descreve a implementação.

Don Stewart
fonte
2
Obrigado. Eu preciso digerir tudo isso ... Parece ser específico do GHC. Eu acho que está tudo bem. A linguagem Haskell é, em algum momento, qualquer coisa que o GHC possa compilar. De maneira semelhante, a "plataforma" Haskell é mais ou menos o tempo de execução do GHC.
Gawi #
1
@ gawi: Esse e todos os outros pacotes que são agrupados diretamente para que sejam úteis imediatamente. E esta é a mesma imagem que eu vi no meu curso de CS; e a melhor parte é que não é difícil no Haskell alcançar resultados impressionantes semelhantes em seus próprios programas.
Robert Massaioli
1
Olá Don, você acha que poderia vincular ao servidor da Web haskell com o melhor desempenho (Warp) ao responder a perguntas como estas? Aqui está o benchmark bastante relevante contra o Node.js: yesodweb.com/blog/2011/03/…
Greg Weber
4
Apenas em teoria. Os "threads leves" Haskell não são tão leves quanto você pensa. É muito, muito, muito mais barato registrar um retorno de chamada em uma interface epoll do que agendar um chamado thread verde, eles são obviamente mais baratos que os threads do sistema operacional, mas não são gratuitos. Criar 100.000 deles usa aprox. 350 MB de memória e levam algum tempo. Tente 100.000 conexões com o node.js. Sem problema algum. Seria mágico se não fosse mais rápido, já que o ghc usa epoll sob o capô para que eles não possam ser mais rápidos do que usar epoll diretamente. A programação com interface de threads é bastante agradável, no entanto.
Kr0e
3
Além disso: O novo gerenciador de E / S (ghc) usa um algoritmo de agendamento que possui (m log n) complexidade (em que m é o número de threads executáveis ​​eno número total de threads). Epoll tem complexidade k (k é o número de fd legíveis / graváveis ​​=. Portanto, o ghc tem O (k * m log n) sobre toda a complexidade, o que não é muito bom se você enfrentar conexões de alto tráfego. O Node.js tem apenas a complexidade linear causada . por epoll E deixe-nos não falar sobre o desempenho do Windows ... Node.js é muito mais rápido porque usa IOCP.
Kr0e
20

Primeiro, não entendo que o node.js está fazendo a coisa certa, expondo todos esses retornos de chamada. Você acaba escrevendo seu programa no CPS (estilo de passagem de continuação) e acho que deve ser o trabalho do compilador fazer essa transformação.

Eventos: Sem manipulação de encadeamento, o programador fornece apenas retornos de chamada (como na estrutura Snap)

Portanto, com isso em mente, você pode escrever usando um estilo assíncrono, se desejar, mas, ao fazer isso, perderá a escrita em um estilo síncrono eficiente, com um encadeamento por solicitação. Haskell é ridiculamente eficiente em código síncrono, especialmente quando comparado a outros idiomas. Está tudo embaixo.

É garantido que os retornos de chamada sejam executados em um único encadeamento: nenhuma condição de corrida é possível.

Você ainda pode ter uma condição de corrida no node.js, mas é mais difícil.

Cada solicitação está em seu próprio encadeamento. Quando você escreve um código que precisa se comunicar com outros threads, é muito simples torná-lo seguro, graças às primitivas de simultaneidade do haskell.

API agradável e simples, compatível com UNIX. Bônus: Excelente suporte HTTP. DNS também disponível.

Dê uma olhada no hackage e veja por si mesmo.

Cada E / S é por padrão assíncrona (isso pode ser irritante às vezes). Isso facilita evitar bloqueios. No entanto, muito processamento da CPU em um retorno de chamada afetará outras conexões (nesse caso, a tarefa deve ser dividida em subtarefas menores e agendada novamente).

Você não tem esses problemas, o ghc distribuirá seu trabalho entre os threads reais do SO.

Mesmo idioma para o lado do cliente e do servidor. (Porém, não vejo muito valor neste. O JQuery e o Node.js compartilham o modelo de programação de eventos, mas o resto é muito diferente. Só não consigo ver como o compartilhamento de código entre o lado do servidor e o lado cliente poderia ser útil na prática.)

Haskell não pode ganhar aqui ... certo? Pense novamente, http://www.haskell.org/haskellwiki/Haskell_in_web_browser .

Tudo isso embalado em um único produto.

Baixe ghc, acenda a cabala. Há um pacote para todas as necessidades.

dan_waterworth
fonte
Eu estava apenas brincando de advogado do diabo. Então, sim, eu concordo com seus pontos. Exceto a unificação de idiomas do lado do cliente e do servidor. Embora eu ache que é tecnicamente viável, não acho que possa eventualmente substituir todo o ecossistema Javascript existente hoje (JQuery e amigos). Embora seja um argumento apresentado pelos apoiadores do Node.js., não acho que seja muito importante. Você realmente precisa compartilhar esse código entre a camada de apresentação e o back-end? Realmente pretendemos que programadores conheçam apenas um idioma?
Gawi
O verdadeiro ganho é que você pode renderizar páginas no servidor e no cliente, facilitando a criação de páginas em tempo real.
dan_waterworth
@dan_waterworth exatamente, veja meteor ou derby.js
MB21
1
@gawi Temos serviços de produção em que 85% do código é compartilhado entre o cliente e o servidor. Isso é conhecido como JavaScript universal na comunidade. Estamos usando o React para renderizar conteúdo dinamicamente no servidor para diminuir o tempo da primeira renderização útil no cliente. Embora esteja ciente de que você pode executar o Haskell no navegador, não conheço nenhum conjunto de práticas recomendadas "universais do Haskell" que permitam a renderização no servidor e no cliente usando a mesma base de código.
Eric Elliott
8

Pessoalmente, vejo o Node.js e a programação com retornos de chamada como algo desnecessariamente de baixo nível e um pouco artificial. Por que programar com retornos de chamada quando um bom tempo de execução, como o encontrado no GHC, pode lidar com retornos de chamada para você e com eficiência?

Enquanto isso, o tempo de execução do GHC melhorou bastante: agora apresenta um "novo novo gerente de IO" chamado MIO, onde "M" significa multicore. Ele é baseado no gerente de E / S existente e seu principal objetivo é superar a causa da degradação do desempenho de mais de 4 núcleos. Os números de desempenho fornecidos neste documento são bastante impressionantes. Veja você mesmo:

Com o Mio, os servidores HTTP realistas em Haskell escalam para 20 núcleos de CPU, atingindo desempenho máximo de até 6,5x em comparação com os mesmos servidores que usam versões anteriores do GHC. A latência dos servidores Haskell também foi aprimorada: [...] sob carga moderada, reduz o tempo de resposta esperado em 5,7x quando comparado com as versões anteriores do GHC

E:

Também mostramos que, com o Mio, o McNettle (um controlador SDN escrito em Haskell) pode ser escalado efetivamente para mais de 40 núcleos, atingir uma taxa de processamento superior a 20 milhões de novas solicitações por segundo em uma única máquina e, portanto, se tornar o mais rápido de todos os controladores SDN existentes .

Mio chegou à versão 7.8.1 do GHC. Pessoalmente, vejo isso como um grande passo à frente no desempenho de Haskell. Seria muito interessante comparar o desempenho de aplicativos Web existentes compilado pela versão anterior do GHC e 7.8.1.

vlprans
fonte
6

Eventos IMHO são bons, mas a programação por meio de retornos de chamada não é.

A maioria dos problemas que torna especial a codificação e a depuração de aplicativos da Web vem do que os torna escaláveis ​​e flexíveis. O mais importante, a natureza sem estado do HTTP. Isso aprimora a navegabilidade, mas impõe uma inversão de controle em que o elemento IO (o servidor da Web neste caso) chama diferentes manipuladores no código do aplicativo. Esse modelo de evento - ou modelo de retorno de chamada, mais precisamente dito - é um pesadelo, pois os retornos de chamada não compartilham escopos variáveis ​​e uma visão intuitiva da navegação é perdida. É muito difícil evitar todas as possíveis alterações de estado quando o usuário navega para frente e para trás, entre outros problemas.

Pode-se dizer que os problemas são semelhantes à programação da GUI, onde o modelo de evento funciona bem, mas as GUIs não têm navegação nem botão de retorno. Isso multiplica as transições de estado possíveis em aplicativos da web. O resultado da tentativa de resolver esse problema são estruturas pesadas com configurações complicadas, muitos identificadores mágicos difundidos sem questionar a raiz do problema: o modelo de retorno de chamada e sua falta inerente de compartilhamento de escopos variáveis ​​e sem sequenciamento, portanto a sequência deve ser ser construído através da ligação de identificadores.

Existem estruturas sequenciais, como o ocsigen (ocaml) à beira-mar (smalltalk) WASH (descontinuado, Haskell) e o mflow (Haskell) que resolvem o problema do gerenciamento de estado, mantendo a navegabilidade e a plenitude do REST. dentro dessas estruturas, o programador pode expressar a navegação como uma sequência imperativa em que o programa envia páginas e aguarda respostas em um único encadeamento, as variáveis ​​estão no escopo e o botão voltar funciona automaticamente. Isso produz inerentemente um código mais curto, mais seguro e mais legível, onde a navegação é claramente visível para o programador. (aviso justo: sou o desenvolvedor do mflow)

agocorona
fonte
No node.js, os retornos de chamada são usados ​​para manipular E / S assíncrona, por exemplo, para bancos de dados. Você está falando de algo diferente que, apesar de interessante, não responde à pergunta.
Robin Green
Você está certo. Levou três anos para ter uma resposta que, espero, atender às suas objeções: github.com/transient-haskell
agocorona
O nó agora suporta funções assíncronas, o que significa que você pode escrever um código imperativo que é realmente assíncrono. Ele usa promessas sob o capô.
Eric Elliott
5

A questão é bastante ridícula porque 1) Haskell já resolveu esse problema de uma maneira muito melhor e 2) aproximadamente da mesma maneira que Erlang. Aqui está a referência em relação ao nó: http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks

Dê ao Haskell 4 núcleos e ele pode fazer 100k (simples) solicitações por segundo em um único aplicativo. O nó não pode fazer o mesmo número e não pode escalar um único aplicativo entre núcleos. E você não precisa fazer nada para colher isso, porque o tempo de execução Haskell é non-blocking. A única outra linguagem (relativamente comum) que possui E / S não bloqueadora incorporada no tempo de execução é o Erlang.

Greg Weber
fonte
14
Ridículo? A questão não é "Haskell tem uma resposta", mas "qual é a resposta de Haskell". No momento em que a pergunta foi feita, o GHC 7 nem foi lançado, então Haskell ainda não estava "no jogo" (exceto talvez para estruturas usando libev como Snap). Fora isso, eu concordo.
Gawi
1
Não sei se isso foi verdade quando você postou esta resposta, mas agora existem, de fato, módulos de nó que permitem que os aplicativos de nó sejam escalonados facilmente entre os núcleos. Além disso, esse link está comparando o node.js em execução em um único núcleo com o haskell em quatro núcleos. Gostaria de vê-lo rodando novamente em uma configuração mais justa, mas, infelizmente, o repositório do github se foi.
Tim Gautier
2
Haskell usando mais de 4 núcleos prejudica o desempenho do aplicativo. Havia um artigo sobre esse assunto, ele foi trabalhado ativamente, mas ainda é um problema. Portanto, a execução de 16 instâncias do Node.js no servidor núcleo 16 provavelmente será muito melhor do que um único aplicativo ghc usando + RTS -N16, que na verdade será mais lento que + RTS -N1 por causa desse erro de tempo de execução. É porque eles usam apenas um IOManager que fica mais lento quando usado com muitos threads do SO. Espero que eles vão corrigir esse bug, mas ele existe desde sempre, então eu teria não muita esperança ...
Kr0e
Qualquer pessoa que observe esta resposta deve estar ciente de que o Node pode processar facilmente 100 mil solicitações simples em um único núcleo e é trivialmente fácil dimensionar um aplicativo Node sem estado em muitos núcleos. pm2 -i max path/to/app.jsserá automaticamente dimensionado para o número ideal de instâncias com base nos núcleos disponíveis. Além disso, o Node também não é bloqueado por padrão.
Eric Elliott
1

Assim como o nodejs eliminou a libev, o Snap Haskell Web Framework também eliminou a libev .

Chawathe Vipul S
fonte
1
Como isso responde à pergunta?
Dfeuer
1
@dfeuer O link deve ter a seguinte redação: Snap Haskell Web Framework caiu libev, não sei por que a formatação está falhando. O tempo de execução do servidor do nó era sobre o Linux libev quando começou, assim como o Snap Web FrameWork. O Haskell com Snap é como o ECMAscript com nodejs, portanto, como o Snap evolui junto com o nodejs é mais relevante que o Haskell, que pode ser comparado com o ECMAscript mais corretamente nesse contexto.
Chawathe Vipul S