PHP vs Node.js: Entendendo o modelo de execução

Examinamos como cada ambiente executa nosso código para entender porque o Node.js é “rápido, escalável e econômico”

Desde 2009, o Node.js vem ganhando fãs e ampla adoção na indústria por ter colocado a linguagem mais popular da Web no lado do servidor, e também devido a seu modelo assíncrono de entrada/saída que garante alta performance e escalabilidade para aplicações de rede e tempo real. Mas o que faz a “mágica” do Node.js acontecer? É preciso entender os mecanismos de sistema operacional utilizados e como cada plataforma executa o código do usuário. Para isso escolhi fazer uma comparação com o PHP, que executa basicamente da mesma forma desde sua primeira versão, claro, com alterações significativas desde lá, como FastCGI e cache do op-code.

PHP e o servidor Web

O PHP é executado basicamente de duas formas: via módulo do apache (fork) ou via FastCGI. Quando executado como módulo do apache, o PHP é carregado na memória do processo do Apache, e a cada requisição, o Apache faz fork() de seu processo, ou seja, cria um novo processo que é clone do primeiro, consumindo tanta memória quanto o processo pai. Com o novo processo, o Apache agora irá executar o arquivo PHP requisitado. Se pegarmos por exemplo um framework que usa o padrão Front Controller, será executado sempre o index.php, que vai incluir, instanciar e rodar todo resto da aplicação.

Em seguida, o PHP vai executar sequencialmente: incluir, instanciar e rodar o Bootstrap da aplicação -> carregar o arquivo de configuração (se houver) -> incluir, instanciar e rodar o FrontController -> incluir, instanciar e rodar o roteamento de URL -> incluir, instanciar e rodar os métodos do Controller apropriado -> incluir, instanciar e rodar a camada de acesso aos dados -> incluir, instanciar e rodar a view apropriada. Vemos que o padrão incluir, instanciar e rodar se repete para cada classe utilizada, resultando em repetidas tarefas e consumindo ciclos de processamento. Sem falar que o código PHP é primeiro compilado para um código intermediário que só então é interpretado. Esse padrão melhora quando usamos um cache de opcode, como o que já vem por padrão no PHP 5.5 ou o APC.

Usando Fast-CGI com Apache, temos um processo do Apache (worker) que usará threads e que se comunicará com um pool de processos PHP usando o protocolo Fast-CGI. Este método é mais eficiente, já que podemos limitar a quantidade de processos PHP executando e remove o processo PHP do Apache, além do uso de threads, que consome menos memória processamento. Esse método foi melhorado com o advento de servidores Web capazes de lidar com o problema C10K (10 mil conexões concorrente) como o Lighttpd e o Nginx, que usam uma arquitetura orientada a eventos assíncronos para lidar com as requisições. A arquitetura de eventos assíncronos permite lidar com problem C10K com uma única thread e menos consumo de memória enquanto que o Apache tem dificuldade com várias threads.

Node.js: Uma thread, muitos eventos

O Node.js nasceu da necessidade de criar sites com mensagens push (o servidor notifica o cliente) de forma eficiente. Javascript foi a linguagem escolhida por Ryan Dahl por esta não possuir uma API de entrada/saída estabelecida, o que lhe permitiu definir convenções para entrada/saída assíncrona não bloqueante. O uso de closures e callbacks em Javascript encaixou-se naturalmente com o modelo assíncrono orientado a eventos.

Um dos pontos mais importantes para o sucesso do Node.js foi o uso do engine Javascript V8, usado pelo Google no Google Chrome. O V8 é um engine Javascript que compila para código de máquina e executa em memória (não escreve o código objeto como programa no disco). Junto com a API libuv para entrada/saída não bloqueante, o V8 forma a base tecnológica do Node.js.

Assim como o Apache utiliza fork() ou pthread_create() para concorrência, o Node.js (a libuv especificamente) usa epoll(). epoll() é um mecanismo de entrada/saída escalável do kernel Linux, que permite esperar por eventos de entrada/saída e ser notificado sobre estes eventos. Isso permite que o Node.js associe callbacks a estes eventos, que serão executados após a notificação.

Examinando como o framework Web Express executa geralmente é seguida a sequência: importação dos módulos e bibliotecas que serão utilizados -> instanciação ou execução de funções de configuração etc -> instanciação da aplicação Express -> registro dos callbacks de middleware que processarão as requisições -> registro das rotas de URL e callbacks que processarão as rotas e responderão a requisição. Só depois é que o script chama a função http.createServer(callback).listen(port) para começar a escutar por requisições. Quando a requisição chega, são executados os callbacks de middleware e finalmente os callbacks que responderão a requisição. Veja que não há chamadas para importação de módulos no momento da requisição, não há acesso de entrada/saída e ciclos de processamento desnecessários.

Quando devo usar Node.js?

Se você tem uma aplicação pesada em entrada/saída, o Node.js vai cair como uma luva. Trabalhar com arquivos, TCP, UDP, HTTP, Websockets é fácil com Node.js. Aplicações Web para desktop e dispositivos móveis, Notificações Push, sistemas em tempo real leves são aplicações frequentes. Proxy e load balancers para a nuvem são também aplicações bastante usadas. Não use para sistemas de tempo real críticos, onde o tempo de resposta tem que ser previsível, o Node.js não foi feito para isso. Bancos de dados relacionais já são bem suportados e bibliotecas como a Sequelize.js já te dão algo do nível do ORM Doctrine. Mas em termos de bibliotecas e APIs, as coisas ainda estão amadurencendo na comunidade Node.js. Não espere encontrar um Zend Framework ou um Symphony. Já as bibliotecas de bancos de dados NoSQL são muitas e maduras, considere um banco de dados como o Apache Cassandra para a sua próxima aplicação de um milhão de usuários. Aplicações de uso intenso de CPU também não são favoráveis, o uso de uma única thread irá bloquear o seu processo. Algoritmos complexos podem rodar mais rápido em Java do que no V8, é possível, mas pode-se chamar outros processos através do Node.js e usar a saída desse processo, ou então, pode-se escrever um módulo para o Node.js em C, C++, e chamar funções desse módulo via Javascript.

O Node.js tem suas vantagens e desvantagens, e cabe estudar cada caso e enteder como ele funciona antes optar por seu uso. Verifique se existem bibliotecas que suprem suas necessidades. Pesquise e desenhe a arquitetura da sua aplicação antes de iniciar seu desenvolvimento, isso vale para qualquer projeto. Espero ter ajudado um pouco mais para a compreensão da plataforma Node.js.

Referências:

http://en.wikipedia.org/wiki/Nodejs
http://www.toptal.com/nodejs/why-the-hell-would-i-use-node-js
http://en.wikipedia.org/wiki/Epoll
http://nodejs.org/about/