JavaEE 6: contexto assíncrono das Servlets para o Ajax push
Postado em 27. set, 2010 por Paulo Silveira em Java
Desenvolvedores de backend que precisam de muita escalabilidade já conhecem os truques de IO não-blocante há muito tempo: selector, poll e epoll no Linux, kqueue no MacOS e BSD e I/O Completion Ports no Windows. A palestra do Renato Lucindo no QConSP abordava questões e soluções que podiam ser implementadas com essas abordagens. Curiosamente são padrões e implementações já bastante antigos, e agora surgem novamente com força. Mas onde o IO não-blocante e estratégias de conexão podem nos ajudar numa aplicação web do dia a dia? No Ajax reverso.
Considerando o exemplo trivial de ter de atualizar o valor de um produto em leilão a cada alteração de preço, podemos fazer com que o browser dispare uma requisição Ajax a cada 5 segundos, e, caso haja alguma modificação no preço, o servidor vai responder o valor novo e via javascript atualizamos esse valor. Essa é a implementação mais básica de ajax reverso, o polling, dando a impressão para o usuário de que a aplicação está atualizada com o servidor. A desvantagem aqui é que num intervalo de cerca de 5 segundos, você pode estar vendo dados stale, o que é aceitável para grande maioria das aplicações.
Para algo mais sofisticado, precisamos realmente fazer um push do servidor: quando houver dados, mandamos para o cliente. Muitos se referem as técnicas de Ajax push como comet, e são duas as mais usadas: fazer um long polling, e o servidor segura a requisição aberta por alguns segundos ou até ter algum dado para enviar; ou fazer um verdade streaming, mantendo a conexão aberta por tempo indeterminado, e ir enviando os dados novos para o cliente na medida que houver. Em ambos os casos podemos facilmente implementar o cliente, mas como fazer no servidor?
No servidor, precisamos manter a socket aberta com o cliente. Em uma implementação ingênua, teremos, para cada HttpServletResponse, uma thread em waiting. Quando o evento que estamos esperando for disparado, fazemos o notify/notifyAll necessário (ou ainda usar BlockingQueues). Essa implementação de thread-per-request é bastante cara no Java, tanto pelo footprint de memória quanto por criar native threads. Quando o número de clientes aumentar, a grande quantidade de threads vai atrapalhar bastante o desempenho do sistema, mesmo se praticamente todas as conexões estiverem ociosas, que é um caso bastante frequente para o nosso cenário.
Em muitos casos o melhor será usar algumas poucas (muitas vezes apenas uma) threads que façam o event loop e verifiquem se algum SocketChannel daquele Selector está pronto para leitura/escrita, e realize a operação correspondente caso necessário. Implementar isso com java.nio não é difícil, e mais ainda, os servlet containers começaram a implementar essa estratégia e fornecer abstrações bem mais simples para não ter de trabalhar com event loops nem máquinas de estado. O Tomcat 6 já trazia uma abstração para Comet com diversos callbacks para o estado de cada socket, enquanto o Jetty 6 já fazia o mesmo através de uma implementação limitada de continuations, porém bastante simples. Em ambos a ideia era fornecer uma maneira de suspender e continuar o processamento daquela requisição, e que, enquanto o processamento estivesse suspenso, aquela thread não entrasse em estado blocked, podendo voltar a processar outras requisições que possuíssem sockets prontas para ler/escrever sem contenção.
Desde novembro de 2009 não precisamos mais adotar uma solução específica de servlet container: na Servlet 3 temos suporte ao que foi batizado de contexto assíncrono. Se precisamos fazer um push para todos os clientes conectados, basta avisarmos ao container que não queremos que a requisição termine ao fim do método service (e, portanto, do doGet, doPost, etc). Fazemos isso com um simples req.startAsync(). Depois adicionamos o contexto devolvido em uma coleção, para poder fazer o broadcast do evento que os clientes estão todos aguardando.
private Queue<AsyncContext> clients = new ConcurrentLinkedQueue<AsyncContext>();
protected void doGet(HttpServletRequest req, HttpServletResponse arg1)
throws ServletException, IOException {
AsyncContext ctx = req.startAsync();
ctx.setTimeout(3000000);
clients.add(ctx);
System.out.println("novo cliente conectou." );
}
Você já pode rodar esse teste com seu servlet container (Tomcat 7 e Jetty 8 possuem suporte). Para isso falta configurar a Servlet através de anotações para que ela tenha suporte a chamada assíncrona:
@WebServlet(urlPatterns = { "/subscribe" }, asyncSupported = true)
public class ChatServlet extends HttpServlet {
Se você fizer uma requisição para /subscribe do seu servidor, perceberá que o browser ficará em aguardo de uma resposta, já que o servidor não encerrou a requisição. Queremos disparar uma mensagem para todos os clientes que estiverem esperando nosso push (que tiver feito o “subscribe“). Para isso, vamos fazer com que o post em /subscribe envie uma mensagem. Por enquanto vamos adicionar essa mensagem no fim de nossa fila, com um id próprio:
private BlockingQueue<String> messages = new LinkedBlockingQueue<String>();
private AtomicInteger contador = new AtomicInteger();
protected void doPost(HttpServletRequest req, HttpServletResponse arg1)
throws ServletException, IOException {
System.out.println("enviando mensagem para todos cliente");
messages.add(String.format("mensagem número %d %n", contador.incrementAndGet()));
}
Precisamos que alguém seja responsável por verificar se há mensagens novas na fila e, caso positivo, enviar essa mensagem a todos os clientes da nossa lista de AsyncContext. Faremos isso numa thread que será iniciada durante a criação de nossa servlet, e ficará esperando por mensagens novas no método blocante BlockingQueue.take:
public void run() {
while (true) {
final String message = messages.take();
for (final AsyncContext ctx : clients) {
public void run() {
PrintWriter writer = ctx.getResponse().getWriter();
writer.println(message);
writer.flush();
}
}
}
}
Removi o tratamento das checked exceptions por uma questão de legibilidade, e o código completo desta ChatServlet pode ser encontrada aqui.
E como realizar um teste com milhares de clientes? Isso pode ser um problema: caso você utilize alguma biblioteca que faça thread-per-request, não vai conseguir abrir muitos clientes simultaneamente, dado o número excessivo de threads criadas. Você vai precisar fazer um cliente usando java.nio puro, ou ainda em conjunto com alguma biblioteca http que utilize a API não-blocante. Esse é o caso das novas versões do Apache Http Core, e aqui há um pequeno teste de exemplo.
Você ficará bastante impressionado ao ver que uma máquina caseira pode suportar mais de 5 mil clientes simultâneos com o Jetty 8, e que só vai parar nesse número provavelmente pelo limite padrão de arquivos abertos no seu sistema operacional.
Economizar recursos caros, como threads, conexões e descritores de arquivos, é sempre uma preocupação. Na Servlet 3 conseguimos balancear esses recursos, minimizando o uso de memória e processamento, porém pagando um pouco mais alto no uso de descritores abertos, mas que ajudam muito nesse cenário de ajax reverso com push.
Expressivos agradecimentos ao Renato Lucindo e Gleicon Moraes pelo aprendizado, diversas explicações, conversas e correções.
Paulo Silveira (Google+)
Mais sobre o autor
14 Respostas para “JavaEE 6: contexto assíncrono das Servlets para o Ajax push”
Trackbacks/Pingbacks
-
-
fevereiro 9, 2011
[...] Quando pensamos na arquitetura de sistemas com grande volume de dados a primeira palavra que vem a mente é escalar. Além de desejar que cada uma das pesquisas em nosso sistema execute o mais rápido possível, precisamos criar meios para que, quando necessário, seja fácil adicionar mais recursos (como memória ou novos servidores) e o sistema consiga tirar proveito deles. Para isso muitas vezes precisamos ir além das diversas otimizações de performance e escalabilidade, como por exemplo a criação de um índice para buscas, o uso de caches e de chamadas assíncronas. [...]
ASSINE NOSSO RSS
bruno taboada
27. set, 2010
Primeiramente, o post está bom, gostei a imagem do sabão em pó HAHA!
Agora somente uma coisinha que achei estranho, “agora surgem novamente com força”.
Isso não é verdade, talvez seja para programadores em java, ruby e outras tecnologias.
Essas técnicas existem a muito tempo no mundo *nix, como você disse, e sempre tiveram força e com certeza são técnicas utilizadas em muitos softwares profissionais, servidores de jogos e etc.
Mas o restante do post esta muito bom.
Paulo Silveira
27. set, 2010
Oi Bruno. Tem toda razão, era esse o sentido mesmo: esta surgindo com força na comunidade de desevolvimento de aplicações web, mesmo sendo antiga conhecida do pessoal *nix, c/c++, e python inclusive.
Arthur
27. set, 2010
Estava tentando seguir passo à passo.
Travei na implementação da Thread com a váriavel executors. De onde vem ela?
Valeu.
Paulo Silveira
27. set, 2010
Ola Arthur. O executors que eu estava usando era pra nao fazer todo o push numa única thread, mas o pessoal recomenda fazer sim numa só desde que seja um laço sem contenção. Aqui tem o código mais simplificado sem o pool de threads como executors:
http://github.com/peas/asyncservlets-test/blob/master/src/main/java/br/com/caelum/chat/ChatServlet.java
e aqui tem a versão com executor:
http://github.com/peas/asyncservlets-test/blob/b6f5376af8ee77c772fcd90ff04f0fc335a54403/src/main/java/br/com/caelum/chat/ChatServlet.java
Atualizei o post para deixar o código da versão mais simples
Francis
28. set, 2010
Parabéns Paulo, muito legal o seu post! Em um projeto recente na empresa que trabalho precisamos utilizar a técnica de Comet/Reverse Ajax, para atualizar as informações no lado do cliente e após algum tempo de pesquisa, optamos pelo framework DWR que inclusive vinha junto com o Tomcat 5 ou 6 se não me engano. A implementação do framework é simples mas é sempre bom saber como as coisas funcionam por debaixo dos panos.
Abraços!
Fernando H. Gomes
29. set, 2010
Olá, muito bom o post. Gostaria até de agradecer o mesmo
Estou concluindo a faculdade e o tema do meu TCC é justamente esse, Ajax Reverso.
A poucas semanas entreguei o meu pré-projeto onde dei uma pincelada no tema.
Não sei se eu não estava procurando direito, mas não encontrei muita coisa (bons conteúdos).
Gostaria de saber se você tem algumas indicações, principalmente algum livro que possa ajudar. (tcc = referencias).
Qualquer coisa manda no meu email
John
29. set, 2010
Em algum momento os clientes são removidos da fila?
Paulo Silveira
29. set, 2010
Oi John. Sem dúvida você precisa ter essa preocupação. O ideal é você colocar um listener para descobrir se o timeout estourou, e fazer um tratamento adequado das exceptions, para remover o AsyncContext da Queue nesse caso (pode remover pelo iterator).
Nesse simples código de exemplo, se um cliente sair, o AsyncContext.getResponse().getWriter() vai lançar exception quando você tentar escrever.
Ricardo Lecheta
03. out, 2010
Artigo bem legal, não conhecia este recurso dos Servlets…Eu usei o IO nao bloqueante uma vez.. mas era para um chat via celulares.. onde todos mantiam a referência no servidor, e o servidor enviava o broadcast pra todos conectados..
Thiago
29. out, 2012
Paulo,
Deve-se chamar o método complete do AyncContext para que a String seja enviada ao cliente ? Isso vai descenectar o Cliente ?
Eu precisava ficar enviando uma mensagem ao cliente de hora em hora sem precisar “registrar” o cliente novamente no Servlet, isso é possível ?
Obrigado
Paulo Silveira
29. out, 2012
nao vai desconectar o cliente nao. so quando terminar o contextoa ssincrono e o doService da servlet terminar.
Thiago
10. nov, 2012
Olá Paulo,
Nessa parte do código:
try {
PrintWriter writer = ctx.getResponse().getWriter();
writer.println(json);
writer.flush();
} catch (IOException e) {
clients.remove(ctx);
}
Se o cliente desconectou ele irá cari na exceção, certo?
O que posso estar fazendo de errado no cliente para ele não disparar a exceção ? E escrever no cluxo de um cliente desconectado ?
Obrigado
Paulo Silveira
12. nov, 2012
oi Thiago
Não há exceptions nesse trecho, ja que o PrintWriter engole as exceptions de IO (de uma olhada no javadoc). Voce precisaria usar o checkErrors para verificar isso.
Ou entao usar o outputStream pra isso, que ai a exception nao é engolida.