Rodrigo Kumpera Weblog

Meus achados sobre tecnologia

Java continuations e NIO, o casamento perfeito

October 12th, 2006 · 7 Comments

Eu estava quase perdendo as esperanças de conseguir utilizar NIO para um servidor. Sempre achei muito dificil programar utilizando um socket não-bloqueante, o parsing de qualquer protocolo um mais dificil é um inferno. Eu resolvi então experimentar escrever um servidor usando continuation-passing-style e o resultado foi supreendemente facil, intuitivo e escalavel.

Minha vontade de usar CPS veio das minhas brincadeiras com Erlang, processos são tão leves nessa linguagem que me leva a acreditar que threads em user-land com scheduling (pseudo)cooperativo são a melhor receita para escalabilidade e facilidade de programação. Quem lembra do windows 3.1, vai me afirmar que cooperative threading é uma enorme roubada e eu concordo, que para aplicações desktop, não funciona bem. Agora pensando em web-application, que basicamente se resume a fazer pequenas tarefas e esperar um tempão pelas operações de I/O completarem, cada thread decidir quando pausar não soa ruim.

Bom, vou descrever um pouco a solução que eu adotei. Utilizei o commons-javaflow para suportar continuations em Java. A biblioteca ainda é experimental, mas já estavel e usavel o suficiente para brincar. O ruim é que você precisa brincar com classloading ou pos-processamento dos arquivos .class. Fora isso uso o feijão com arroz de io multiplexado. Que é manter um Selector para verificar a disponibilidade de todos os sockets e I/O não bloqueante para transmissão de dados.

A grande sacada de usar i/o não bloqueante e continuations é até que simples, sempre que uma operação falhar por não ter dados disponiveis (leitura), ou falta de buffer (escrita), você registra o socket junto ao selector e da yield na continuation (suspende a execução dela). Depois disso, quando a operação ficar disponível, você simplesmente retoma a continuation. Eu implementei um servidor de echo, o código que implementa isso no servidor se resume a:


public void run() {
try {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.write("\n");
writer.flush();
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}

O código parece o mesmo que se usarmos um socket normal e i/o bloqueante, de fato é, com a diferença que as classe de streams são customizadas. O código que se utiliza de continuations não é muito complicado, bem diferente de vários frameworks como mina ou seda, como mostro a seguir o método que faz a escrita usando NIO. O método current() retorna uma referencia a uma classe que controla a interface com o selector e o escalonador.


public void write(byte[] b, int off, int len) throws IOException {
ByteBuffer buff = ByteBuffer.wrap(b, off, len);
while (buff.remaining() > 0) {
channel.write(buff);
if (buff.remaining() == 0)
break;
current().register(channel, SelectionKey.OP_WRITE);
current().ioSuspend();
}
}

Eu ainda estou começando a bricandeira, ainda pretendo colocar um bom parser de HTTP para funcionar nesse esquema, algum driver opensource para banco de dados e, finalmente, file I/O assíncrono. A partir deste ponto já fica possivel realmente contribuir um sistema com essa tecnologia. Para quem ficou curioso, os links são: commons-javaflow e meu exemplo. Para quem gostou, pode ter certeza que vem muito mais por ai.

Tags: Programming · Scalability · concurrency

7 responses so far ↓

  • 1 Rodrigo Urubatan // Oct 12, 2006 at 1:48 pm

    ótima a solução, mas tu tem problemas :D
    não era mais fácil usar o jetty em vez de implementar isto novamente?

  • 2 kumpera // Oct 12, 2006 at 2:19 pm

    Jetty não resolve meu problema. Beeem longe disso.
    Jetty tem um treco que alega “suportar continuations”, mas na verdade é um mecanismo pelo qual você tem que implementar o suspend/restore manualmente, ou seja, tem uso super-super específico: comet.

    Além disso, o jetty funciona usando threading e I/O bloqueante para todo o resto, o que não resolve meus problemas de escalabilidade. Para terminar, não adianta eu ter um servidor escalavel se eu vou continuar esbarrando no driver JDBC.

  • 3 Rodrigo Urubatan // Oct 12, 2006 at 8:18 pm

    isto é verdade :D
    e parece que estou mal informado :D
    acreditei na propaganda do jetty, e achava que ele estava usando NIO para tudo :(

  • 4 Luca Bastos // Oct 17, 2006 at 10:54 am

    Agora que o Gilad Bracha saiu da Sun, fui ler o antigo blog dele na Sun e o encontrei falando de continuantions. Lembrei logo de ti.

    Não ainda não leu, dê uma lida em:
    http://blogs.sun.com/gbracha/entry/will_continuations_continue
    http://blogs.sun.com/gbracha/entry/continuations_continued

  • 5 Willian Mitsuda // Oct 18, 2006 at 12:15 pm

    Rodrigo, vc chegou a estudar a possibilidade de usar o Apache MINA?

    Se entendi bem o seu problema, acho que ele já implementa toda a infra que vc precisa, só precisando implementar um codec p/ o seu protocolo.

    http://directory.apache.org/subprojects/mina/index.html

  • 6 kumpera // Oct 18, 2006 at 12:31 pm

    Willian, eu já utilizei antes o Apache MINA. A arquitetura é boa e o software é de estavel. O grande problema é o inferno que é programar codecs e afins.

    Com MINA você escreve basicamente 2 componentes, o (de)codicador de mensagens e o tratador de mensagens. O primeiro é responsavel por traduzir de/para o formato de rede (de stream para ServletRequest, por exemplo); o segundo lida apenas com objetos da aplicação e possui as regras de negocios, por assim dizer.

    Implementar um codec é exponencialmente dificil em relação a complexidade do protocolo, e de quao eficiente você quer que ele seja. É basicamente implementar uma máquina de estados enorme. Já fiz para HTTP 1.1 parcialmente e conheci o inferno.

    Depois vem o protocol handler, ele tem que se preocupar com coisas como flow control, para não mandar dados rápido demais e deixar um toneladas de buffers na mão do framework. Fora que você tem que programar utilizando um modelo orientado a eventos. Uma delicia. Willian, vou mostrar num artigo futuro então a diferença de dificuldade com um protocolo não trivial.

  • 7 felipe // Nov 7, 2006 at 4:33 pm

    Rodrigo,

    gostaria de entender melhor o objetivo de usar i/o não bloqueante com continuations na leitura e escrita dos channels, já que o i/o não bloqueante já meio que funciona como se tivesse continuation (é lido o que está disponivel, mesmo que essa leitura precise continuar depois e o mesmo vale para escrita).

    Se não existem dados para ler, qual o sentido de eu suspender o processamento?

    Qual o ganho real dessa suspensao(usando continuations) se o i/o não é bloqueante? (ou seja, o thread continua executando outras coisas ao invés de ficar parado)

Leave a Comment