Criando um Chat com Reactor e WebSockets

2010 January 12, 21:04 h - tags: rails obsolete

Obs: Este artigo tem a ver com a prova-de-conceito Cramp Chat Demo disponível no Github.

Existe um caso de uso de aplicações web que é o envio frequente de conteúdo adicional. Por exemplo uma conversa de chat, ou um livestream de feeds, ou até mesmo APIs públicas muito acessadas. O padrão de solução costuma ser um javascript que, em intervalos regulares, faz requisições Ajax a um servidor que retorna o conteúdo adicional a ser acrescentado na página. Isso é um polling.

Outro padrão é manter uma conexão HTTP aberta, recebendo conteúdo continuamente num stream. Para isso temos termos/técnicas guarda-chuva como Comet, Ajax Push, Reverse Ajax, HTTP server push, etc.

As técnicas no cliente variam bastante. Mas os backends não variam muito. Por exemplo, uma aplicação Rails é limitada ao número de conexões simultâneas (ativas exatamente ao mesmo tempo) pelo número de processos carregados. Se tivermos 10 processos Rails de pé, só podemos ter 10 conexões simultâneas. Essa característica é mais importante se o tempo de resposta por conexão for muito alto (e é por isso que sempre falamos em fazer o mínimo possível numa requisição e deferir processamentos mais pesados para tarefas em background, usando tecnologias simples como Delayed Job, Resque, ou se a coisa for mais complexa, servidores de fila e mensagens como RabbitMQ).

Se possível, alguns trechos da aplicação que são carregados o tempo todo, por exemplo, por causa do efeito de polling de clientes fazendo requisições muito frequentes, é importante que seja possível maximizar o uso de um processo diminuindo o tempo de resposta. Para isso existem soluções como Rails Metal, Sinatra ou algo ainda mais leve usando Rack puro. A aplicação Campfire da 37signals, por exemplo, tinha um componente feito em C++ que depois foi migrado para outro que eles escreveram em Erlang, para possibilitar alta concorrência junto com alta performance.

O problema a que nos referimos é o que, há 10 anos, chamamos de Problema C10K ou, literalmente, como suportar 10 mil conexões simultâneas ou mais? Assim como Rails, PHP, ASP, Perl tem o mesmo problema: cada conexão é um processo. Começa a ficar bastante difícil administrar uma quantidade tão massiva de processos abertos simultaneamente.

Alguns podem imaginar que o problema é a simples falta de multi-thread nativo (Ruby, Python, tem green-threads, ou lightweight threads). Portanto servidores Java (Tomcat, JBoss, Glassfish) ou .NET (Application Pool de IIS) seriam melhores. De fato, são melhores, mas não são “a” solução para o problema C10K.

O real problema é I/O bloqueante. Independente se é um processo ou um thread, se ele precisar gravar um arquivo, fazer uma query num banco de dados, esse processo precisa esperar o processamento de I/O terminar para prosseguir. Isso “pendura” esse processo ou thread, desde o cliente até a base de dados, por exemplo. A única diferença é que um servidor multi-thread vai aguentar mais tempo, mas o problema é o mesmo.

Portanto, uma solução é unir o modelo multi-processo/multi-thread com I/O assíncrono ou não-bloqueante, mais do que isso, mudar o paradigma procedural por um que é baseado em eventos. Existe um pattern para isso, chamado Reactor.

O mundo Java começou isso antes, com a implementação de NIO a partir do JDK 1.4.2. Sobre ela surgiram servidores de aplicação/frameworks como Grizzly, Apache Mina ou JBoss Netty. No mundo Python existe o framework Twisted e, felizmente para nós rubistas, temos o EventMachine.

Esse tipo de tecnologia, aliado a coisas como o driver assíncrono MySQLPlus para MySQL, permite uma quantidade absurda de conexões simultâneas não-bloqueantes por processo Ruby. Um processo de EventMachine poderia lidar com milhares ao mesmo tempo.

Daí vem o novo framework web Cramp, desenvolvido pelo Pratik Naik, do Rails Core Team, como um mini-framework de aplicações web assíncronas. Minha prova de conceito é justamente um chat – que demandaria muitas conexões simultâneas – e poderia facilmente ser transformado em livestream (que não deixa de ser um chat read-only, a grosso modo).

Eu implementei duas versões: a primeira é o jeito clássico onde o browser faz conexões Ajax em um intervalo regular. Aliado ao Cramp/EventMachine isso já ajuda porque ele suportaria várias conexões simultâneas.

Mas a segunda versão é mais interessante, porque usa uma tecnologia que está disponível no HTML 5, chamado WebSocket. Assim como o XmlHttpRequest, será um componente acessível por Javascript disponível em todo browser que implementar HTML 5. Já existe no Google Chrome 4.x, no Safari/WebKit e no Firefox 3.7.

Com o WebSocket é possível abrir uma conexão HTTP, mantê-la aberta, “escutar” seu stream por novas mensagens e reagir a elas via javascript e inclusive enviar novos dados (como uma nova mensagem), tudo na mesma conexão, sem precisar passar pelo peso da rotina de abrir conexão, enviar/receber dados, fechar conexão e repetir daqui a alguns segundos. Ou seja, a vantagem é diminuir a quantidade enorme de conexões batendo no servidor para uma ordem de grandeza menos.

Obviamente vai demorar um bom tempo para aparecer no IE, mas não temam, existe uma alternativa muito interessante chamada Web-Socket.js e que eu implemento no meu demo. Ela simula a mesma API do WebSocket padrão (e no Chrome ela nem se ativa), mas usa um pequeno Flash como ponte para criar a conexão HTTP permanente, com os mesmos eventos.

A imagem que você vê acima é justamente de Firefox 3.5, Safari 4 e Chrome 4 conversando simultaneamente com 3 conexões permanentes ao servidor baseado em Cramp. O WebSocket tem um handshake próprio para iniciar uma conversa, que precisa ser implementado do lado do servidor, depois disso é HTTP normal. No meu caso, eu trafego dados em formato JSON.

Como era uma prova de conceito, eu armazeno as mensagens num banco de dados MySQL, mas possivelmente eu teria implementado uma fila de mensagens usando um RabbitMQ ou coisa parecida. Outra coisa, obviamente ele não tem diversas funcionalidades básicas como controle de sessões e outras perfumarias.

Somando as tecnologias de WebSocket, servidor web baseado em Reactor, drivers de banco assíncronos e I/O assíncrono em geral, estamos caminhando para um paradigma levemente diferente do atual para escrever aplicações Web de alta demanda. Vale a pena entender essas tecnologias, especialmente para os casos que citei no começo: chat, livestream, apis e tudo mais que demanda alto nível de concorrência.

A prova de conceito do chat está disponível no Github e imagino que o README que deixei seja suficiente para vocês conseguirem duplicar o mesmo ambiente para rodar a aplicação. E, como sempre, pull requests são bem vindos :-)

Comments

comentários deste blog disponibilizados por Disqus