Node.js e solicitações intensivas de CPU

215

Comecei a mexer no servidor HTTP Node.js. e gosto muito de escrever Javascript no servidor, mas algo está me impedindo de começar a usar o Node.js. para meu aplicativo da web.

Entendo todo o conceito de E / S assíncrona, mas estou um pouco preocupado com os casos extremos em que o código processual consome muita CPU, como manipulação de imagem ou classificação de grandes conjuntos de dados.

Pelo que entendi, o servidor será muito rápido para solicitações simples de páginas da web, como exibir uma lista de usuários ou exibir uma postagem no blog. No entanto, se eu quiser escrever um código muito intensivo da CPU (no back-end do administrador, por exemplo) que gere gráficos ou redimensione milhares de imagens, a solicitação será muito lenta (alguns segundos). Como esse código não é assíncrono, todas as solicitações que chegam ao servidor durante esses poucos segundos serão bloqueadas até que minha solicitação lenta seja concluída.

Uma sugestão foi usar Web Workers para tarefas intensivas da CPU. No entanto, receio que os funcionários da Web tornem difícil escrever código limpo, pois ele funciona incluindo um arquivo JS separado. E se o código intensivo da CPU estiver localizado no método de um objeto? É meio ruim escrever um arquivo JS para cada método que consome muita CPU.

Outra sugestão foi gerar um processo filho, mas isso torna o código ainda menos sustentável.

Alguma sugestão para superar esse obstáculo (percebido)? Como você escreve um código orientado a objeto limpo com o Node.js enquanto garante que tarefas pesadas da CPU sejam executadas de forma assíncrona?

Olivier Lalonde
fonte
2
Olivier, você fez a mesma pergunta que eu tinha em mente (nova no nó) e especificamente em relação ao processamento de imagens. Em Java, posso usar um ExecutorService de encadeamento fixo e passar todos os trabalhos de redimensionamento e esperar que ele termine de toda a conexão. No nó, não descobri como organizar o trabalho para um módulo externo que limita (vamos digamos) o número máximo de operações simultâneas para 2 por vez. Você encontrou uma maneira elegante de fazer isso?
Riyad Kalla

Respostas:

55

O que você precisa é de uma fila de tarefas! Mover suas tarefas de longa execução para fora do servidor da web é uma coisa BOM. Manter cada tarefa em um arquivo js "separado" promove a modularidade e a reutilização de código. Obriga você a pensar em como estruturar seu programa de maneira a facilitar a depuração e a manutenção a longo prazo. Outro benefício de uma fila de tarefas é que os trabalhadores podem ser escritos em um idioma diferente. Basta executar uma tarefa, fazer o trabalho e escrever a resposta de volta.

algo assim https://github.com/resque/resque

Aqui está um artigo do github sobre por que eles o criaram http://github.com/blog/542-introducing-resque

Tim
fonte
35
Por que você está vinculando às bibliotecas Ruby em uma pergunta especificamente fundamentada no mundo dos nós?
Jonathan Dumaine
1
@ JonathanDumaine É uma boa implementação de uma fila de tarefas. Rad o código ruby ​​e reescreva-o em javascript. LUCRO!
Simon Stender Boisen
2
Sou um grande fã de artesãos para isso, os trabalhadores não consultam um servidor para novos trabalhos - novos trabalhos são instantaneamente enviados aos trabalhadores. Muito sensível
Casey Flynn
1
Na verdade, alguém tem portado para o mundo do nó: github.com/technoweenie/coffee-resque
FrontierPsycho
@ pacerier, por que você diz isso? O que você propõe?
Luis.espinal
289

Isso é um mal-entendido da definição de servidor da Web - ele deve ser usado apenas para "conversar" com os clientes. Tarefas de carga pesada devem ser delegadas a programas independentes (é claro que também podem ser escritos em JS).
Você provavelmente diria que está sujo, mas garanto que um processo de servidor da Web preso ao redimensionamento de imagens é apenas pior (até digamos o Apache, quando não bloqueia outras consultas). Ainda assim, você pode usar uma biblioteca comum para evitar redundância de código.

EDIT: Eu propus uma analogia; aplicação web deve ser como um restaurante. Você tem garçons (servidor web) e cozinheiros (trabalhadores). Os garçons estão em contato com os clientes e realizam tarefas simples, como fornecer menu ou explicar se algum prato é vegetariano. Por outro lado, eles delegam tarefas mais difíceis à cozinha. Como os garçons estão fazendo apenas coisas simples, eles respondem rapidamente e os cozinheiros podem se concentrar em seu trabalho.

O Node.js aqui seria um garçom único, mas muito talentoso, capaz de processar muitas solicitações de cada vez, e o Apache seria um bando de garçons burros que apenas processam uma solicitação cada. Se esse garçom do Node.js começasse a cozinhar, seria uma catástrofe imediata. Ainda assim, o cozimento também pode esgotar até uma grande quantidade de garçons Apache, sem mencionar o caos na cozinha e a diminuição progressiva da responsividade.

mbq
fonte
6
Bem, em um ambiente em que os servidores da Web são multiencadeados ou com vários processos e podem lidar com mais de uma solicitação simultânea, é muito comum gastar alguns segundos em uma única solicitação. As pessoas esperam isso. Eu diria que o mal-entendido é que o node.js é um servidor Web "regular". Usando o node.js, você precisa ajustar um pouco o seu modelo de programação, e isso inclui enviar trabalho de "longa execução" para algum trabalhador assíncrono.
Thilo
13
Não gere um processo filho para cada solicitação (que anula o objetivo do node.js). Crie trabalhadores apenas dentro de seus pedidos pesados. Ou direcione seu trabalho pesado em segundo plano para algo diferente de node.js.
Thilo
47
Boa analogia, mbq!
Lance Fisher
6
Ha, eu realmente gosto disso. "Node.js: fazendo com que más práticas funcionem mal"
ethan 27/09
7
@mbq Eu gosto da analogia, mas poderia usar algum trabalho. O modelo tradicional multithread seria uma pessoa que é garçom e cozinheira. Depois que o pedido é feito, essa pessoa precisa voltar e cozinhar a refeição antes de poder lidar com outro pedido. O modelo node.js possui os nós como garçons e os trabalhadores da web como cozinheiros. Os garçons tratam de buscar / resolver as solicitações enquanto os trabalhadores gerenciam as tarefas mais demoradas. Se você precisar aumentar a escala, apenas torne o servidor principal um cluster de nós e faça o proxy reverso das tarefas intensivas da CPU para outros servidores criados para o processamento milti-threaded.
Evan Plaice
16

Você não deseja que o código intensivo da CPU execute assíncrono, mas que ele seja executado em paralelo . Você precisa obter o trabalho de processamento do encadeamento que atende solicitações HTTP. É a única maneira de resolver esse problema. Com o NodeJS, a resposta é o módulo de cluster, para gerar processos filhos para fazer o trabalho pesado. (O Nó AFAIK não tem nenhum conceito de threads / memória compartilhada; é processos ou nada). Você tem duas opções para estruturar seu aplicativo. Você pode obter a solução 80/20 gerando 8 servidores HTTP e lidando com tarefas intensivas em computação de forma síncrona nos processos filhos. Fazer isso é bastante simples. Você pode levar uma hora para ler sobre isso nesse link. De fato, se você apenas copiar o código de exemplo na parte superior desse link, terá 95% do caminho até lá.

A outra maneira de estruturar isso é configurar uma fila de tarefas e enviar grandes tarefas de computação pela fila. Observe que há muita sobrecarga associada ao IPC para uma fila de trabalhos, portanto isso é útil apenas quando as tarefas são sensivelmente maiores que a sobrecarga.

Estou surpreso que nenhuma dessas outras respostas sequer mencione cluster.

Antecedentes: código assíncrono é um código que é suspenso até que algo aconteça em outro lugar ; nesse momento, o código é ativado e continua a execução. Um caso muito comum em que algo lento deve acontecer em outro lugar é a E / S.

O código assíncrono não é útil se o seu processador for responsável por fazer o trabalho. Esse é precisamente o caso das tarefas "intensivas em computação".

Agora, pode parecer que o código assíncrono é um nicho, mas na verdade é muito comum. Acontece que não é útil para tarefas intensivas de computação.

Esperar na E / S é um padrão que sempre acontece em servidores da web, por exemplo. Todo cliente que se conecta ao seu servidor recebe um soquete. Na maioria das vezes, os soquetes estão vazios. Você não deseja fazer nada até que um soquete receba alguns dados; nesse momento, você deseja manipular a solicitação. Um servidor HTTP como o Node está usando uma biblioteca de eventos (libev) para acompanhar os milhares de soquetes abertos. O sistema operacional notifica a libev e, em seguida, libev notifica o NodeJS quando um dos soquetes obtém dados e, em seguida, o NodeJS coloca um evento na fila de eventos, e seu código http entra em ação nesse momento e manipula os eventos um após o outro. Os eventos não são colocados na fila até que o soquete tenha alguns dados; portanto, os eventos nunca esperam pelos dados - eles já estão lá.

Servidores da Web baseados em eventos com thread único fazem sentido como um paradigma quando o gargalo aguarda várias conexões de soquete vazias e você não deseja um thread ou processo inteiro para cada conexão inativa e não deseja pesquisar seus 250k soquetes para encontrar o próximo que contém dados.

masonk
fonte
deve ser a resposta correta .... quanto à solução em que você gera 8 grupos, você precisaria de 8 núcleos, certo? Ou balanceador de carga com vários servidores.
Muhammad Umer
também o que é uma boa maneira de aprender sobre a segunda solução, configurando uma fila. O conceito de fila é bastante simples, mas faz parte da mensagem entre processos e fila que é externo.
Muhammad Umer
Está certo. Você precisa colocar o trabalho em outro núcleo, de alguma forma. Para isso, você precisa de outro núcleo.
masonk
Re: filas. A resposta prática é usar uma fila de trabalhos. Existem alguns disponíveis para o nó. Eu nunca usei nenhum deles, então não posso fazer uma recomendação. A resposta da curiosidade é que os processos de trabalho e os processos de fila finalmente se comunicam por soquetes.
masonk
7

Algumas abordagens que você pode usar.

Como o @Tim observa, você pode criar uma tarefa assíncrona que fica do lado de fora ou paralela à sua lógica de exibição principal. Depende de seus requisitos exatos, mas até o cron pode atuar como um mecanismo de enfileiramento.

Os WebWorkers podem trabalhar para seus processos assíncronos, mas atualmente eles não são suportados pelo node.js. Existem algumas extensões que fornecem suporte, por exemplo: http://github.com/cramforce/node-worker

Você ainda pode reutilizar módulos e códigos através do mecanismo padrão "requer". Você só precisa garantir que o despacho inicial para o trabalhador passe todas as informações necessárias para processar os resultados.

Toby Hede
fonte
0

O uso child_processé uma solução. Mas cada processo filho gerado pode consumir muita memória em comparação com o Gogoroutines

Você também pode usar soluções baseadas em fila, como kue

neo
fonte