18
Minha expedição ao mundo do Node.js
Este final de semana resolvi mergulhar no Node.js: apesar de ser uma plataforma que acompanho desde o lançamento nunca lhe dei a devida atenção que esta merece, então resolvi corrigir esta desfeita.
Este mergulho foi uma experiência incrível, intensa e que me fez refletir sobre diversos assuntos. Aprendi horrores e então pra finalizar (dar o primeiro grande passo), nada melhor que compartilhar com vocês minhas conclusões iniciais.
Eu e o Node.js
Talvez a melhor expressão para descrever meu relacionamento com Node.js até agora seja "curiosidade distante". Meu primeiro contato com a plataforma foi quando surgiu: escrevi alguns mocks de webservices e APIs REST lá pelos idos de 2009/2010.
Se não me falha a memória estes mocks foram escritos usando o módulo HTTP mesmo. Na época fiquei muito impressionado pois com pouquíssimo código eu conseguia implementar aqueles servidores. Parecia fantástico (e era).
Mas na época (e ainda hoje) a JVM dominava minha vida (estou nela desde o Java 1.1 1996/97). Pra piorar, desde então vi muitas histórias de terror envolvendo o mau uso: essencialmente a má compreensão do modelo assíncrono, apresentações muito ruins sobre o assunto (muito ruins mesmo), hype excessivo, fanboys... Tudo isto gerou uma péssima impressão em mim, o que acabou me distanciando da plataforma.
(eu sei que devia focar minhas impressões em aspectos objetivos, mas é inegável (e perigoso) o poder do subjetivo)
De lá pra cá meu uso do Node.js sempre foi indireto: ou tendo como base ferramentas como o Apache Cordova, Vue.js, até mesmo a escrita de pequenos scripts internos para resolver coisas pequenas do meu dia a dia (usava o comando Node como calculadora). Nada de avançado. Nunca fiz uma aplicação web real.
Além disto sou da geração de programadores que não via o JavaScript com bons olhos. Me surpreende a popularidade da linguagem que, todos sabemos, não foi construída sobre a melhor das bases. E esta primeira impressão ainda exerce influencia sobre mim. Curiosamente mesmo assim JavaScript sempre foi uma das linguagens que mais dominei. Um sentimento do tipo: "sei que você tem muitos problemas, mas os ignoro e gosto de vocẽ".
Começa a expedição ao redor do meu quarto
Navegando pelo Udemy topei com uma promoção envolvendo este curso: The Complete Node.js Developer Course (2nd Edition). Custava R$ 20,00, os livros em português que havia lido a respeito detestei (idem os cursos), vi a ementa, li muitos reviews positivos, tinha um fim de semana livre, o negócio era barato e me bateu aquela vontade de aprender Node.js. Comprei. (a propósito, o curso é maravilhoso, recomendo)
De onde bateu esta vontade de aprender Node.js? Honesta e pura curiosidade e vontade de tirar a má impressão que tinha da coisa. E dado que já estou há mais da metade da minha vida na JVM, que se tornou uma espécie de "ilha de Lost" pra mim, por que não tentar sair um pouquinho deste mundo e minimizar meu determinismo linguístico? De quebra eu ainda aprenderia um pouco mais sobre o ES6 e algumas ferramentas novas. Começava a expedição.
(spoiler: ainda considero a JVM como a melhor plataforma de todos os tempos)
Mas este meu mergulho deveria ter um objetivo final: eu saberia se Node.js valeria à pena se construísse uma prova de conceito que o validasse enquanto tecnologia. Qual prova de conceito? Simples: reescrever parcialmente o /dev/All em Node.js e descobrir se o negócio escala mesmo, assim como se o ferramental me fornece produtividade similar à que tenho com Grails.
(o resultado foi muito interessante, aguarde e verá)
/dev/All - Node.js ou Grails?
O /dev/All tem dois componentes: o "Feed Hunter", que é o responsável por obter os links que aparecem no site (escrito em Java usando Spring, Apache Camel e outras cositas sobre as quais escreverei em breve por que vêm surpresa aí) e o "Front-end", feito inteiramente em Grails (3.1.9) e Vue.js.
Um dos nossos objetivos na evolução do Front-end é transformá-lo em um SPA, desacoplando-o completamente do código Grails (sim, eventualmente haverá um app, no qual já estou trabalhando). Já demos alguns passos nesta separação, o que se manifesta na adoção do Vue.js: o código Grails seria então apenas uma API REST dali pra frente.
Apesar de todos os nossos esforços, ainda acho este componente pesado: ocupa no mínimo algo em torno de 300 a 400 Mb de RAM no servidor. A vida inteira escuto que Java devora memória. Sendo assim decidi que minha prova de conceito seria a implementação de uma API que já existe no /dev/All: aquela responsável por obter os posts apresentados na página inicial. Este endpoint aqui.
Um pouco mais sobre o modelo de desenvolvimento atual e o da prova de conceito
O modelo de desenvolvimento atual usa como base aquilo que o Grails nos provê por padrão: usamos o GORM como ORM e a própria estrutura de controladores do framework para implementar estas APIs. É notório portanto que há aqui um custo adicional de memória/desempenho relativo ao ORM, entretanto no que diz respeito à produtividade, comparando o custo do desenvolvedor e do servidor, produtividade ganha e portanto o ORM fica.
O SGBD adotado é o MySQL: e aí entra a primeira dificuldade em relação ao material existente sobre o Node.js: 99% do que existe hoje usa o MongoDB como base de dados. Eu teria de aprender portanto como usar o MySQL com Node.js. Usei o módulo mysql na versão 2.5.4 (e o aprendizado foi super rápido).
No caso do Node.js não encontrei um módulo de ORM com bases relacionais e, sinceramente, desta vez quis evitar. Um dos meus objetivos foi também fugir um pouco do desenvolvimento estritamente orientado a objetos e partir para uma abordagem mais funcional (quem acompanha este blog sabe que tenho lá meus problemas com OO).
As impressões
JavaScript - ES6
Foi uma excelente oportunidade para aprender de vez o ES6 e aqui aquela minha antiga impressão a respeito da linguagem foi embora. Querendo ou não eu acabava escrevendo código JavaScript tal como havia o conhecido lá no início da minha carreira no final dos anos 90: de repente veio um upgrade gigantesco e muitas das coisas que não conseguia entender se tornaram claras.
Já faz algum tempo que estava me dedicando ao estudo da linguagem, mas ainda não havia me debruçado sobre o ES6. Foi sem sombra de dúvidas uma verdadeira reciclagem neste aspecto. Muitas coisas que não entendia agora fazem sentido:funções arrow, a modularidade, e muitos aspectos envolvendo melhorias da própria sintaxe.
Ferramental do Node.js e tempo de execução
Uma surpresa maravilhosa: tal como no Grails, tudo o que preciso para trabalhar é da interface de linha de comando e um editor de textos. Mas mais do que isto, as ferramentas em si são bastante produtivas: nodemon para carregamento automático das mudanças que realizo no código fonte, as ferramentas de depuração nativas do Node, o próprio npm (que já conhecia e inclusive devemos lançar um guia esta semana)... Fantástico pra dizer o mínimo.
O carregamento do código e o tempo de execução foram surpreendentes: muito mais rápido que o que eu esperava. Sobre isto vou inclusive falar mais à frente.
Escrita de testes com Mocha, Expect e Supertest
Sempre que vou aprender algo novo os testes viram meu laboratório. Até então escrevia meus testes no navegador usando o Jasmine. É uma solução legal, mas nesta expedição acabei conhecendo o Mocha, que é inclusive muito parecido. Não houve grandes mudanças para mim neste ponto portanto.
O interessante veio com o Expect: ele tem uma funcionalidade muito interessante chamada "spy". Essencialmente é um "AOP para testes", que te permite verificar se um método foi ou não chamado.
O Supertest também achei muito bacana: é usado para escrever testes em cima de requisições HTTP geradas pelo Express, ou seja, me permite escrever os testes funcionais de uma forma bastante simples.
O bacana é que com o nodemon podemos ter os testes executando a cada alteração que fazemos no código, isto se mostrou uma mão na roda no meu laboratório interno.
Do lado JVM já temos alternativas que seguem a direção do ExpressJS, como o Ratpack (não conhece? devia!) e o Vert.x (literalmente o Node na JVM). É o modelo de desenvolvimento que considero ideal quando estamos escrevendo APIs: fácil, direto, focado no que vamos fazer (a implementação dos endpoints).
Confesso que apenas amei o ExpressJS. A documentação não é tão boa quanto a do Grails, mas te fornece o essencial para que você possa fazer praticamente tudo com ele, e de uma forma muito simples.
Nem tudo é claro: o uso de sessões, por exemplo, não é tão óbvio (mais a frente conto o por quê das sessões), idem no que diz respeito à implementação de coisas como o CORS. Entretanto, quando você conhece o conceito de middlewares (o equivalente aos filtros da API Servlet) a coisa deslancha.
Sobre os templates, sim: há a renderização de páginas tal como o JSP do Java EE ou o GSP do Grails. Para tal experimentei o Mustache, Pug e EJS. Comparado ao que temos do lado Java são soluções muito primitivas: o GSP sem sombra de dúvidas está anos luz na frente. Mas é natural isto: a pegada do Node.js sempre foi muito mais no desenvolvimento de aplicações que seguem o padrão SPA, o que joga este tipo de solução para o segundo plano. Acabei optando pelo hbs (Handlebars) na minha prova de conceito.
No frigir dos ovos é um framework extremamente produtivo. No meu caso, que só conhecia (e muito pouco) o módulo http, foi uma bela surpresa.
O poder e a ilusão de poder
Quase tudo que vi no Node achei muito produtivo: e é mesmo, mas apenas se você sabe o que está fazendo. Parece óbvio, né? Mas não é: JavaScript ainda é aquela linguagem que a maior parte das pessoas diz conhecer mas nunca estudou a respeito.
Lembra as histórias de terror que mencionei no início deste post? Pelo que pude ver sempre surgiram das mesmas causas:
- Falta de conhecimento acerca do modelo de desenvolvimento assíncrono que o Node adota (o não conhecimento do loop de eventos é fatal).
- Tem de conhecer o paradigma funcional.
- A falta de conhecimento sobre desenvolvimento backend - Node foi feito para ser executado no servidor. Vi muita gente sem conhecimento algum desta área, mas muito de JavaScript cometendo erros absurdos aqui.
- Desconhecimento das nuances do JavaScript (coisas como == e ===, por exemplo, o próprio escopo de variáveis, etc)
A ferramenta é realmente muito poderosa, é muito fácil de usar e você de fato tem as coisas rodando num tempo muito menor. Mas quando ignora estes pontos acima a coisa fica feia, muito feia.
A armadilha surge no fato do Node ter uma única thread (é possível ter um servidor com mais de um processo, basta usar o módulo cluster, mas não é o padrão). Qualquer operação de I/O que agarre, prende todas as requisições que chegam no seu servidor: sendo assim você não "tem de pensar assincronamente", você é obrigado.
Mais do que pensar assincronamente, você precisa pensar funcionalmente. Se o desenvolvedor não tiver bem fixados os conceitos do paradigma funcional é quase certo que vai dar merda. E quer saber de uma coisa? Acho isto fantástico, por que sair um pouco do OO foi uma experiência quase terapêutica para mim (já mencionei que não curto tanto OO?).
Se não souber estas coisas, você não tem poder: tem a ilusão de poder e a garantia de estar criando mais histórias tristes que se propagarão por aí. Rapadura é doce, mas não é mole não.
E a sua prova de conceito, como ficou?
Bom, vamos aos resultados então. Comecei pela implementação de um único endpoint: o responsável por realizar a busca por posts no /dev/All (este aqui). Foi algo fácil de fazer: este endpoint me retorna a lista de posts, e cada elemento no post tem uma estrutura similar à seguinte:
{
id:"id do post",
titulo:"titulo do post",
resumo:"o resumo do post",
dataPublicacao:"a data em que o post foi publicado no post",
dataInclusao:"a data em que o /dev/All encontrou o post e o incluiu no banco de dados",
cliques:"quantos cliques recebeu o post",
site:{
id:"identificador do blog que contém o post",
nome:"o nome do blog",
url:"a URL do blog",
autor:{
id:"o identificador do autor do blog",
nome:"O nome do autor do blog"
}
}
}
Há portanto três tabelas no banco de dados unidas por join: post, site e autor. Lembre: não estou usando MongoDB aqui, mas sim o MySQL. Será que a coisa escala? Comecei a realizar então alguns testes de desempenho e o resultado foi "apenas" assustador como mostrarei na sequência.
A chamada padrão a este endpoint retorna os últimos 20 posts cadastrados no /dev/All. Em média o tamanho da resposta é 20kb.
Teste de desempenho e escalabilidade
Para realizar o teste usei uma ferramenta chamada "siege", que me permite realizar testes de carga usando o protocolo HTTP. Caso esteja usando Linux, você pode instalá-la usando o comando apt-get install siege
.
Inicialmente peguei a mesma implementação feita em Grails e a instalei em um servidor Tomcat local (exatamente como se encontra em produção). Na sequência, executei os testes usando o Siege levando em consideração o tempo de um minuto e 255 usuários simultâneos. Vamos aos valores aproximados para a versão escrita em Grails:
Transactions: 21046 hits
Availability: 100.00 %
Elapsed time: 59.95 secs
Data transferred: 245.47 MB
Response time: 0.47 secs
Transaction rate: 351.06 trans/sec
Agora, vamos aos resultados na mesma API, implementada em Node.js usando as mesmas configurações:
Transactions: 32596 hits
Availability: 100.00 %
Elapsed time: 59.06 secs
Data transferred: 131.28 MB
Response time: 0.21 secs
Transaction rate: 551.91 trans/sec
Na média a mesma API escrita em Node.js consegue um throughput maior: 40 a 50% a mais de transações por segundo.
E sobre o consumo de memória? Na média enquanto o Tomcat consome 1 Gb durante o teste, a instância do Node consome 170Mb. 80% a menos.
Mas este benchmark não é preciso, então não comemore ainda
Não comemore ainda: pra começar este é um benchmark muito vagabundo. Você deve levar em consideração os seguintes pontos:
- O código escrito em Grails usa o GORM e contém uma série de funcionalidades carregadas junto com a aplicação que não existem ainda no código escrito em Node.js.
- O código escrito em Node.js usa apenas SQL nativo para obter os dados, o que dá um ganho de desempenho em relação à adoção de qualquer ORM.
Não cheguei a implementar uma versão usando apenas SQL do mesmo endpoint na aplicação, entretanto creio que o resultado seria muito parecido mesmo assim, pois ainda há uma enorme pilha por trás. Além disto, é fato conhecido que sim, Java sempre consome uma quantidade significativamente maior de memória.
Os testes foram além: depois executei verificações com 500, 1000 usuários simultâneos. A disponibilidade e escalabilidade do Node.js ganhou nestes casos (note: estou testando apenas um endpoint).
Resumindo: obtive resultados melhores do ponto de vista empírico com Node: mas minha metodologia de teste é muito furada e não deve ser levada como palavra final.
E depois de ter implementado a API?
Bom: aí eu empolguei e implementei quase todo o comonente Frontend do /dev/All em Node.js. Há uma versão muito tosca online caso queira conferir: ela tem apenas a página inicial, mas dá pra pelo menos você experimentar. Basta acessar http://devall.com.br:3000 (não sei até quando este link estará disponível, pois é apenas para testes).
Escrevi a página inicial usando o hbs, ou seja, não é uma aplicação SPA: meu objetivo era apenas aprender e testar a tecnologia, sendo assim leve isto em consideração quando a estiver accessando, ok?
Minhas considerações finais
Node.js com certeza faz parte agora do meu cinto de utilidades, e saibam que em pouquíssimo tempo teremos um novo front-end do /dev/All 100% implementado nesta tecnologia pelas seguintes razões:
- Nosso front-end é muito pequeno, então é viável de ser reescrito (todo o trabalho pesado é feito pelo Feed Hunter).
- O consumo de memória é realmente muito menor, o que nos permite aproveitar melhor nossos servidores e reduzir o custo de operação.
- A escalabilidade se mostrou bastante superior.
- E nesta minha empolgação já estou com 80% disto implementado e, no processo, sem os vícios das versões anteriores do código fonte. :)
É vital no entanto lembrar do que escrevi acima a respeito da ilusão de poder. É assustadoramente fácil escrever código lento e que não escalará em Node.js. Ficou óbvio pra mim a origem das histórias tristes que havia mencionado antes. Se você não souber programação funcional, entender o modelo assíncrono e de eventos do Node.js, é quase certo que vai dar errado.
(é importante lembrar que JavaScript ainda é aquela linguagem que a maioria julga saber mas nunca estudou de verdade, e isto é a origem de inúmeros problemas)
Sobre a produtividade em relação ao Grails
Do ponto de vista de produtividade, comparado ao Grails, sinceramente não posso dizer que seja mais produtivo que este. Os plug-ins do Grails, além do próprio GSP o tornam matador quando há renderização do lado servidor. Isto sem mencionar que a linguagem Groovy também é melhor que JavaScript (ao menos é construída sobre bases bem mais sólidas).
Ainda sobre produtividade comparado ao Grails: aqui nós pensamos de forma síncrona, que é muito mais natural para a esmagadora maioria das pessoas. No Node.js pensamos essencialmente em callbacks e promises e código que escrevemos para ser executado no futuro, e não no agora. E sim: se adequar a esta outra realidade leva tempo e, portanto, boa parte da sua produtividade também.
Entretanto no que diz respeito ao carregamento do código fonte e sua modificação durante a execução, Node.js chuta a bunda do Grails diversas vezes. É muito mais rápido, e isto é fundamental quando vamos executar uma grande bateria de testes.
Sobre modularidade e grandes bases de código
A questão da modularidade também é importante mencionar: já trabalhei em projetos gigantescos com Grails (e Java em geral). No caso do Node.js, ainda não peguei um projeto com uma grande base de código. Entretanto, para escrever micro-serviços, Node.js se mostrou uma ferramenta extremamente interessante e com certeza está no centro do meu radar para estes casos.
Sobre o consumo de recursos e escalabilidade
Não há muito o que dizer: consome uma quantidade muito menor de memória e quando bem projetado escala maravilhosamente bem. É portanto um forte candidato em situações nas quais temos servidores limitados (que é justamente uma área na qual venho pesquisando bastante nos últimos anos).
O consumo da CPU também se mostrou muito inferior. No caso dos testes que realizei, código na JVM chegava a consumir 350% da CPU fácil, enquanto o Node ficava na faixa dos 120%.
Resumindo: se você souber o que está fazendo o resultado é lindo.
Renderização do lado servidor
Não é algo lindo: tal como mencionei as opções que encontrei ainda são muito primitivas quando comparadas ao que temos em Java, Groovy ou PHP. Natural, não é o foco deste público. Para aplicações que requeiram a criação de vários CRUDs algo como Grails ainda é uma solução bem mais interessante.
Mas aqui leve em consideração minha pouca experiência no assunto. Pode ser que existam soluções que eu ainda não conheça.
E finalmente
Este foi um final de semana maravilhoso e estas foram as minhas conclusões iniciais sobre aquilo que estudei (por isto o post longo).
Neste primeiro momento recomendo o Node para pequenos projetos, especialmente se for projetos nos quais você implementará apenas uma API. É uma tecnologia muito bacana e que vale à pena estudar.
Conforme progrido no estudo conto mais pra vocês aqui.
18