Nó.js
Nó.js

Seguimento

24 de fevereiro de 2017 – 14 min. lido

Este artigo vem de Tomislav Capan, consultor técnico e entusiasta do Node.js. Tomislav publicou este artigo originalmente em agosto de 2013 no blog Toptal – você pode encontrar o artigo original aqui; o blog foi ligeiramente atualizado. O assunto a seguir é baseado na opinião e experiências deste autor.

JavaScript’s crescente popularidade trouxe com ele muitas mudanças, e a face do desenvolvimento web hoje é dramaticamente diferente. As coisas que podemos fazer na web hoje em dia com o JavaScript rodando no servidor, bem como no navegador, foram difíceis de imaginar há apenas alguns anos, ou foram encapsuladas dentro de ambientes sandbox como Flash ou Java Applets.

Antes de escavar no Node.js, você pode querer ler sobre os benefícios do uso do JavaScript através da pilha, que unifica a linguagem e o formato de dados (JSON), permitindo que você reutilize de forma ideal os recursos do desenvolvedor. Como este é mais um benefício do JavaScript do que o Node.js especificamente, nós não discutiremos muito sobre isso aqui. Mas é uma vantagem chave para incorporar o Node.js na sua pilha.

Node.js é um ambiente de execução JavaScript construído sobre o motor JavaScript V8 do Chrome. Vale a pena notar que Ryan Dahl, o criador do Node.js, pretendia criar sites em tempo real com capacidade de “push”, “inspirado em aplicações como o Gmail”. No Node.js, ele deu aos desenvolvedores uma ferramenta para trabalhar no paradigma de E/S não-bloqueio, orientado por eventos.

Em uma frase: Node.js brilha em aplicações web em tempo real, empregando tecnologia push sobre websockets. O que há de tão revolucionário nisso? Bem, depois de mais de 20 anos de web stateless baseada no paradigma de resposta sem estado, finalmente temos aplicações web com conexões bidirecionais em tempo real, onde tanto o cliente quanto o servidor podem iniciar a comunicação, permitindo que eles troquem dados livremente.

Isso está em contraste com o típico paradigma de resposta web, onde o cliente sempre inicia a comunicação. Além disso, tudo é baseado na pilha web aberta (HTML, CSS e JS) rodando sobre a porta padrão 80.

Pode-se argumentar que temos isso há anos na forma de Flash e Java Applets – mas na realidade, esses eram apenas ambientes sandboxed usando a web como um protocolo de transporte para ser entregue ao cliente. Além disso, eles eram executados isoladamente e frequentemente operados sobre portas não-padrão, que podem ter exigido permissões extras e tais.

Com todas as suas vantagens, Node.js agora desempenha um papel crítico na pilha de tecnologia de muitas empresas de alto perfil que dependem de seus benefícios únicos. A Fundação Node.js consolidou todas as melhores ideias sobre o porquê das empresas considerarem o Node.js numa breve apresentação que pode ser encontrada na página de Estudos de Caso da Fundação Node.js.

Neste post, vou discutir não só como essas vantagens são alcançadas, mas também porque você pode querer usar o Node.js – e porque não – usando alguns dos modelos clássicos de aplicações web como exemplos.

Como Funciona?

A idéia principal do Node.js: usar E/S não bloqueadoras e acionadas por eventos para permanecer leves e eficientes diante de aplicativos em tempo real que rodam em dispositivos distribuídos.

Isso é uma boca cheia.

O que realmente significa é que o Node.js não é uma nova plataforma prateada que irá dominar o mundo do desenvolvimento web. Ao invés disso, é uma plataforma que preenche uma necessidade particular. E a compreensão disto é absolutamente essencial. Você definitivamente não quer usar o Node.js para operações intensivas de CPU; de fato, usá-lo para computação pesada irá anular quase todas as suas vantagens. Onde o Node.js realmente brilha é na construção de aplicações de rede rápidas e escaláveis, pois ele é capaz de lidar com um grande número de conexões simultâneas com alto throughput, o que equivale a alta escalabilidade.

Como ele funciona sob o capô é bastante interessante. Em comparação com as técnicas tradicionais de serviço web, onde cada conexão (pedido) gera uma nova thread, ocupando a RAM do sistema e eventualmente maximizando a quantidade de RAM disponível, Node.js opera em uma única linha, usando chamadas I/O sem bloqueio, permitindo que ele suporte dezenas de milhares de conexões simultâneas (realizadas no loop do evento).

*Imagem tirada do post original do blog.

Um cálculo rápido: assumindo que cada thread tem potencialmente 2 MB de memória com ele, rodando em um sistema com 8 GB de RAM nos coloca em um máximo teórico de 4000 conexões simultâneas (cálculos tirados do artigo de Michael Abernethy “Just what is Node.js?”, publicado no IBM developerWorks em 2011; infelizmente, o artigo não está mais disponível), mais o custo de troca de contexto entre threads. Esse é o cenário com o qual você normalmente lida nas técnicas tradicionais de web-serving. Ao evitar tudo isso, o Node.js atinge níveis de escalabilidade de mais de 1M conexões simultâneas, e mais de 600k conexões simultâneas de websockets.

Há, é claro, a questão de compartilhar um único thread entre todos os pedidos de clientes, e é uma armadilha potencial de escrever aplicações Node.js. Em primeiro lugar, o cálculo pesado poderia sufocar o único thread do Node e causar problemas para todos os clientes (mais sobre isso mais tarde), pois as solicitações de entrada seriam bloqueadas até que o cálculo fosse concluído. Em segundo lugar, os desenvolvedores precisam ser realmente cuidadosos para não permitir que uma exceção borbulhe até o núcleo (topmost) do Node.js event loop, o que fará com que a instância Node.js termine (efetivamente travando o programa).

A técnica usada para evitar exceções borbulhando até a superfície é passar os erros de volta para o chamador como parâmetros de callback (ao invés de jogá-los, como em outros ambientes). Mesmo que algumas exceções sem soltar consigam borbulhar, ferramentas foram desenvolvidas para monitorar o processo Node.js e realizar a recuperação necessária de uma instância travada (embora você provavelmente não será capaz de recuperar o estado atual da sessão do usuário), a mais comum sendo o módulo Forever, ou usando uma abordagem diferente com ferramentas de sistema externas upstart e monit, ou até mesmo apenas upstart.

npm: O Node Package Manager

Quando se discute o Node.js, uma coisa que definitivamente não deve ser omitida é o suporte embutido para gerenciamento de pacotes usando a ferramenta npm que vem por padrão com cada instalação do Node.js. A idéia dos módulos npm é bastante similar à do Ruby Gems: um conjunto de componentes reutilizáveis disponíveis publicamente, disponíveis através de fácil instalação através de um repositório online, com gerenciamento de versão e dependência.

Uma lista completa de módulos empacotados pode ser encontrada no site npm, ou acessada usando a ferramenta npm CLI que se instala automaticamente com o Node.js. O ecossistema de módulos está aberto a todos, e qualquer um pode publicar seu próprio módulo que será listado no repositório npm. Uma breve introdução ao npm pode ser encontrada em um Guia para Iniciantes, e detalhes sobre a publicação de módulos no Tutorial de Publicação npm.

Alguns dos módulos npm mais úteis atualmente são:

  • express – Express.js, um framework de desenvolvimento web inspirado no Sinatra para o Node.js, e o padrão de fato para a maioria das aplicações Node.js de hoje.
  • hapi – um framework muito modular e simples de usar para construir aplicações web e serviços
  • connect – Connect é um framework de servidor HTTP extensível para Node.js, fornecendo uma coleção de “plugins” de alto desempenho conhecidos como middleware; serve como base para Express.
  • socket.io e sockjs – Componente do lado do servidor dos dois componentes de websockets mais comuns atualmente.
  • pug (anteriormente Jade) – Um dos populares motores de templates, inspirado no HAML, um padrão no Express.js.
  • mongodb e mongojs – MongoDB wrappers para fornecer a API para bancos de dados de objetos MongoDB no Node.js.
  • redis – Biblioteca cliente Redis.
  • lodash (underscore, lazy.js) – O cinto utilitário JavaScript. Underscore iniciou o jogo, mas foi derrubado por uma de suas duas contrapartidas, principalmente devido à melhor performance e implementação modular.
  • forever – Provavelmente o utilitário mais comum para garantir que um determinado script de nó seja executado continuamente. Mantém o seu processo Node.js em produção diante de qualquer falha inesperada.
  • bluebird – Uma implementação Promises/A+ completa com uma performance excepcionalmente boa
  • moment – Uma biblioteca de datas JavaScript leve para analisar, validar, manipular e formatar datas.

A lista continua. Há toneladas de pacotes realmente úteis por aí, disponíveis para todos (sem ofensa para aqueles que omiti aqui).

Where Node.js Should Be Used

Chat é a mais típica aplicação em tempo real, multi-usuário. Desde o IRC (antigamente), através de muitos protocolos proprietários e abertos rodando em portas não padrão, até a habilidade de implementar tudo hoje em dia no Node.js com websockets rodando sobre a porta padrão 80.

A aplicação de chat é realmente o exemplo sweet-spot para o Node.js: é uma aplicação leve, de alto tráfego, intensiva em dados (mas de baixo processamento/computação) que roda através de dispositivos distribuídos. Também é um excelente caso de uso para aprender, pois é simples, mas cobre a maioria dos paradigmas que você usará em uma típica aplicação Node.js.

Tentemos retratar como funciona.

No cenário mais simples, temos uma única sala de chat no nosso site onde as pessoas vêm e podem trocar mensagens de uma para muitas (na verdade, todas). Por exemplo, digamos que temos três pessoas no website todas ligadas ao nosso quadro de mensagens.

No lado do servidor, temos uma simples aplicação Express.js que implementa duas coisas: 1) um GET ‘/’ request handler que serve a página web contendo um quadro de mensagens e um botão ‘Send’ para inicializar a entrada de novas mensagens, e 2) um servidor websockets que escuta as novas mensagens emitidas pelos clientes websocket.

No lado do cliente, temos uma página HTML com um par de manipuladores configurados, um para o evento ‘Send’ button click, que pega a mensagem de entrada e a envia para baixo do websocket, e outro que escuta as novas mensagens recebidas no cliente websockets (ou seja, mensagens enviadas por outros usuários, que o servidor agora quer que o cliente exiba).

Quando um dos clientes envia uma mensagem, eis o que acontece:

  • Browser pega o botão ‘Enviar’, clica através de um manipulador JavaScript, pega o valor do campo de entrada (ou seja o texto da mensagem), e emite uma mensagem websocket usando o cliente websocket conectado ao nosso servidor (inicializado na inicialização da página web).
  • O componente do lado do servidor da conexão websocket recebe a mensagem e a encaminha para todos os outros clientes conectados usando o método de transmissão.
  • Todos os clientes recebem a nova mensagem como uma mensagem push através de um componente do lado do cliente websocket rodando dentro da página web. Eles então pegam o conteúdo da mensagem e atualizam a página web anexando a nova mensagem ao quadro.

Image retirado do blog original.

Este é o exemplo mais simples. Para uma solução mais robusta, você pode usar um cache simples baseado na loja Redis. Ou em uma solução ainda mais avançada, uma fila de mensagens para lidar com o roteamento de mensagens para clientes e um mecanismo de entrega mais robusto que pode cobrir perdas temporárias de conexão ou armazenamento de mensagens para clientes registrados enquanto eles estão offline. Mas independentemente das melhorias que você fizer, Node.js ainda estará operando sob os mesmos princípios básicos: reagir a eventos, lidar com muitas conexões simultâneas, e manter a fluidez na experiência do usuário.

API ON TOP OF AN OBJECT DB

Além de Node.js realmente brilhar com aplicações em tempo real, é um ajuste natural para expor os dados de objetos DBs (por exemplo, MongoDB). Os dados armazenados no JSON permitem que o Node.js funcione sem o descasamento de impedância e conversão de dados.

Por exemplo, se você estiver usando Rails, você converteria do JSON para modelos binários, então os exporia de volta como JSON sobre o HTTP quando os dados são consumidos pelo React.js, Angular.js, etc., ou até mesmo chamadas jQuery AJAX simples. Com Node.js, você pode simplesmente expor seus objetos JSON com uma API REST para que o cliente os consuma. Além disso, você não precisa se preocupar em converter entre JSON e qualquer outra coisa ao ler ou escrever a partir do seu banco de dados (se você estiver usando MongoDB). Em suma, você pode evitar a necessidade de múltiplas conversões usando um formato uniforme de serialização de dados através do cliente, servidor e banco de dados.

QUEUED INPUTS

Se você estiver recebendo uma grande quantidade de dados simultâneos, seu banco de dados pode se tornar um gargalo de estrangulamento. Como descrito acima, o Node.js pode facilmente lidar com as conexões simultâneas em si. Mas como o acesso à base de dados é uma operação de bloqueio (neste caso), nós temos problemas. A solução é reconhecer o comportamento do cliente antes que os dados sejam realmente escritos no banco de dados.

Com essa abordagem, o sistema mantém sua capacidade de resposta sob uma carga pesada, o que é particularmente útil quando o cliente não precisa da confirmação firme de uma escrita de dados bem sucedida. Exemplos típicos incluem: o registro ou escrita de dados de rastreamento de usuários, processados em lotes e não utilizados até um momento posterior; assim como operações que não precisam ser refletidas instantaneamente (como atualizar uma contagem de ‘Gostos’ no Facebook) onde a consistência eventual (tão freqüentemente utilizada no mundo NoSQL) é aceitável.

Os dados são enfileirados através de algum tipo de infraestrutura de cache ou de enfileiramento de mensagens (MQ) (por exemplo, RabbitMQ, ZeroMQ) e digerido por um processo de escrita em lote de banco de dados separado, ou serviços backend de processamento intensivo de computação, escrito em uma plataforma de melhor desempenho para tais tarefas. Comportamento similar pode ser implementado com outras linguagens/frameworks, mas não no mesmo hardware, com o mesmo alto, mantido através do throughput.

Image retirado do artigo original.

Em resumo: com Node, você pode empurrar o banco de dados para o lado e lidar com eles mais tarde, procedendo como se eles tivessem conseguido.

DATA STREAMING

Em plataformas web mais tradicionais, os pedidos e respostas HTTP são tratados como eventos isolados; na verdade, eles são, na verdade, fluxos. Esta observação pode ser utilizada em Nodo.js para construir alguns recursos legais. Por exemplo, é possível processar arquivos enquanto eles ainda estão sendo carregados, pois os dados chegam através de um fluxo e podemos processá-los de uma forma online. Isso pode ser feito para codificação de áudio ou vídeo em tempo real, e proxy entre diferentes fontes de dados (veja próxima seção).

PROXY

Node.js é facilmente empregado como um proxy do lado do servidor, onde ele pode lidar com uma grande quantidade de conexões simultâneas de uma forma não bloqueada. É especialmente útil para proxyar diferentes serviços com diferentes tempos de resposta, ou coletar dados de múltiplos pontos de origem.

Um exemplo: considere um aplicativo do lado do servidor comunicando-se com recursos de terceiros, puxando dados de diferentes fontes, ou armazenando ativos como imagens e vídeos para serviços em nuvem de terceiros.

Embora existam servidores proxy dedicados, o uso do Node pode ser útil se sua infraestrutura de proxy não existir ou se você precisar de uma solução para o desenvolvimento local. Com isso, quero dizer que você poderia construir uma aplicação do lado do cliente com um servidor de desenvolvimento Node.js para ativos e solicitações de API proxying/stubbing, enquanto na produção você lidaria com tais interações com um serviço de proxy dedicado (nginx, HAProxy, etc.).

BROKERAGE – STOCK TRADER’S DASHBOARD

Vamos voltar ao nível da aplicação. Outro exemplo onde o software desktop domina, mas poderia ser facilmente substituído por uma solução web em tempo real é o software de negociação dos corretores, usado para acompanhar os preços das ações, realizar cálculos/análises técnicas e criar gráficos/gráficos.

Comutar para uma solução web em tempo real permitiria aos corretores trocar facilmente de estação de trabalho ou local de trabalho. Em breve, poderemos começar a vê-los na praia da Florida… ou Ibiza… ou Bali.

APLICATION MONITORING DASHBOARD

Outro caso de uso comum em que Node-with-web-sockets se encaixa perfeitamente: rastrear os visitantes do site e visualizar suas interações em tempo real. Você poderia estar reunindo estatísticas em tempo real de seu usuário, ou mesmo movendo-o para o próximo nível, introduzindo interações direcionadas com seus visitantes, abrindo um canal de comunicação quando eles chegam a um ponto específico em seu funil – um exemplo disso pode ser encontrado com CANDDi.

Imagine como você poderia melhorar seu negócio se você soubesse o que seus visitantes estavam fazendo em tempo real – se você pudesse visualizar suas interações. Com as tomadas de duas vias em tempo real do Node.js, agora você pode.

SYSTEM MONITORING DASHBOARD

Agora, vamos visitar o lado da infra-estrutura das coisas. Imagine, por exemplo, um provedor SaaS que quer oferecer aos seus usuários uma página de monitoramento de serviços (por exemplo, a página de Status do GitHub). Com o loop de eventos do Node.js, podemos criar um poderoso painel baseado na web que verifica os status dos serviços de forma assíncrona e empurra os dados para clientes usando websockets.

Bambos os status internos (intra-empresa) e de serviços públicos podem ser reportados ao vivo e em tempo real usando esta tecnologia. Empurre essa idéia um pouco mais longe e tente imaginar um Centro de Operações de Rede (NOC) monitorando aplicações em um operador de telecomunicações, provedor de cloud/network/hosting, ou alguma instituição financeira, todos executados na pilha web aberta apoiada pelo Node.js e websockets ao invés de Java e/ou Java Applets.

Nota: Não tente construir sistemas duros em tempo real no Node.js (ou seja, sistemas que requerem tempos de resposta consistentes). Erlang é provavelmente uma escolha melhor para essa classe de aplicação.

SERVER-SIDE WEB APPLICATIONS

Node.js com Express.js também pode ser usado para criar aplicações web clássicas no lado do servidor. No entanto, embora possível, este paradigma de resposta a solicitações no qual o Node.js estaria carregando HTML renderizado não é o caso mais típico de uso. Há argumentos a favor e contra esta abordagem. Aqui estão alguns fatos a considerar:

Pros:

  • Se sua aplicação não tem nenhum cálculo intensivo de CPU, você pode compilá-lo em Javascript top-to-bottom, mesmo até o nível do banco de dados se você usar o JSON storage Object DB como o MongoDB. Isto facilita o desenvolvimento (incluindo a contratação) significativamente.
  • Rastejadores recebem uma resposta HTML totalmente renderizada, que é muito mais amigável ao SEO do que, digamos, uma Aplicação de Página Única ou uma aplicação websockets executada no topo do Node.js.

Cons:

  • Any CPU intensive computation irá bloquear a capacidade de resposta do Node.js, por isso uma plataforma com threads é uma abordagem melhor. Alternativamente, você poderia tentar escalar o cálculo(*).
  • Usar o Node.js com uma base de dados relacional ainda é uma grande dor (veja abaixo para mais detalhes). Faça um favor a si mesmo e pegue qualquer outro ambiente como Rails, Django, ou ASP.Net MVC se você estiver tentando realizar operações relacionais.

(*) Uma alternativa aos cálculos intensivos de CPU é criar um ambiente MQ altamente escalável com processamento back-end para manter o Node como um ‘balconista’ de frente para lidar com pedidos de clientes de forma assíncrona.

APLICAÇÃO WEB SERVER-SIDE COM UMA BASE DE DADOS RELACIONAL BEHIND

Comparando Nó.js com Express.js contra Ruby on Rails, por exemplo, há uma decisão limpa a favor deste último quando se trata de acesso a dados relacionais.

Ferramentas DB relacionais para Nó.js ainda estão bastante subdesenvolvidas, em comparação com a concorrência. Por outro lado, Rails fornece automaticamente a configuração de acesso aos dados logo de cara com as ferramentas de suporte a migrações de esquemas de DB e outras Gems (pun pretendido). Rails e seus frameworks têm implementações de Active Record ou Data Mapper, que você sentirá falta se tentar replicá-los em JavaScript puro.(*)

Still, se você estiver realmente inclinado a permanecer JS em todo o caminho, verifique Sequelize e Node ORM2.

(*) É possível e não raro usar Node.js somente como fachada voltada para o público, enquanto mantém o back-end do Rails e seu fácil acesso a um DB.

COMPUTAÇÃO/PROCESSAMENTO DO LADO CÉLIDO DO SERVIDOR

Quando se trata de computação pesada, Node.js não é a melhor plataforma ao redor. Não, você definitivamente não quer construir um servidor de computação Fibonacci no Node.js. Em geral, qualquer operação intensiva de CPU anula todos os benefícios de throughput que o Node.js oferece com seu modelo de I/O não bloqueado por eventos, porque qualquer pedido de entrada será bloqueado enquanto a thread estiver ocupada com o seu número-crunching.

Como dito anteriormente, o Node.js é de uma única thread e usa apenas um único núcleo de CPU. Quando se trata de adicionar concorrência em um servidor multi-core, há algum trabalho sendo feito pela equipe do núcleo do Node na forma de um módulo de cluster. Você também pode executar várias instâncias do servidor Node.js muito facilmente atrás de um proxy reverso via nginx.

Com o clustering, você ainda deve descarregar todos os processos pesados de computação em segundo plano escritos em um ambiente mais apropriado para isso, e tê-los comunicando através de um servidor de fila de mensagens como RabbitMQ.

Even, embora seu processamento em segundo plano possa ser executado no mesmo servidor inicialmente, tal abordagem tem o potencial para uma escalabilidade muito alta. Esses serviços de processamento em background poderiam ser facilmente distribuídos para servidores de trabalhadores separados sem a necessidade de configurar as cargas dos servidores web front-facing.

Obviamente, você usaria a mesma abordagem em outras plataformas também, mas com o Node.js você obtém aquele alto rendimento reqs/sec de que falamos, já que cada pedido é uma pequena tarefa tratada muito rápida e eficientemente.

Conclusão

Discutimos Node.js da teoria à prática, começando com seus objetivos e ambições, e terminando com seus pontos doces e armadilhas. Quando as pessoas se deparam com problemas com o Nodo, quase sempre se resume ao fato de que operações de bloqueio são a raiz de todo o mal – 99% dos abusos do Nodo vêm como conseqüência direta.

Remmbrar: O Node.js nunca foi criado para resolver o problema de escalonamento computacional. Ele foi criado para resolver o problema de escala de E/S, o que faz muito bem.