19 de jul. de 2011

C10K Problem. O quê Deft, Node.js, Netty e outros estão solucionando

Dae gurizada,

A idéia de olhar para esse problema surgiu depois que o Diego Pacheco me deu a missão de falar sobre Deft e Loft em um lighting talk que fizemos há algumas semanas atrás.

O grande impecílio que tive para entender o conceito dos servidores Deft e Loft foi de entender o problema. Eis a motivação do post.

O que vou tentar explicar são as limitações existetes que fazem com alguns servidores não consigam processar mais que 10mil requisições simultâneas. E (se tudo der certo) você vai entender porque servidors como Deft, Loft, Tornado, Netty, Node.js e outros tem um desempenho diferente que os sservidores normais. 

C10k Problem

C10k significa 10.000 (dez mil) conexões simultâneas. Este é a limitação de conexões silmutaneas da maioria dos servidores de aplicação.

Isso acontece devido a forma que os servidores trabalham com cada conexão recebida.

Muitas destes problemas são ocasionaos pela forma que o servidor foi impelmentado e de como o Sistema Operaciona (OS) trabalha com sockets.

Eu vou descrever algumas destas limitações abaixo.


Análise de um servidor
Quem já trabalhou com Sockets em Java, já devem ter visto esse código:

...

ServerSocket welcomeSocket = new ServerSocket(8080);
while(true){
          Socket connectionSocket = welcomeSocket.accept();
          ...
}
...


Código simples, abre um socket na porta 8080 e fica esperando conexões.

Basicamente isso é que os servidores web fazem. É óbvio que eles trabalham de uma forma mais complexa envolvendo threads, alocações de recursos, etc.

Seguindo no nosso exemplo simples, poderíamos criar uma thread para processar cada requsição. Fazendo assim que o servidor estejá disponível para aceitar (accept()) uma nova conexão.

...
ServerSocket welcomeSocket = new ServerSocket(6789);
while(true){

     Socket connectionSocket = welcomeSocket.accept();
     final new Thread(new Runnable() {
          @Override public void run() {
                    //do anything
          }
       
     }).start();
...




Nesa forma por exemplo, vamos criar diversar thread para atender as requisições dos clientes. Na perspetiva do SO, temos uma Thread em nível de SO e várias threads na JVM.

Além dessas threads que estamos criando, o sitema operacional também exerce um tipo de controle sobre as conexões abertas.  Cada cliente é um "flie descriptor" aberto no SO.

O importante aqui é entender que cada accept() vai gerar um file descriptor novo.


File Descriptor (FD)?

O SO tem o chamado "file descriptor" que basicamente é cada processo (socket, arquivo, fila, etc) existente no SO. Isso incluí processos ativos e "em espera" (waiting).

Existe um limite de open files que o sistema operacional consegue gerenciar. Por esse motivo utilizar uma thread para cada cliente pode ser tornar um problema, pois irá consumir uma quantidade considerável de recusros.

OBS: Calma! Não é qualquer sistema que precisa se preocupar com isso. Tudo depende da "Frequência" de acessos e do "Volume" de dados ... ;)

O Peso de ter muitos processos


Não vou entrar muito afundo nesse ponto, mas a explicação simples é: ter muitos processos abertos CUSTA CARO para o SO.

Óbvio, quanto mais processos mais difícil vai ser o trabalho do escalonador do OS.

Todos esperando sua hora de entrar...
 Além disso, temos o consumo de memória. Quando mais processos mais memória e... você sabe


Out of memory... 
Essa apresentação fala muito bem sobre isso: http://bulk.fefe.de/scalable-networking.pdf


Blocking e Non-blocking I/O


Seguindo os conceitos importantes de entender é o Blocking e Non-blocking I/O.

Imagine uma conexão com um servidor, onde é feito uma operação de leitura. Neste momento NÃO há nada para ser lido. Nesse caso, podem acontecer duas coisas:

1) Esperar até que uma resposta seja dada pelo servidor (blocking)
2) Retornar imediatamente um erro dizendo que não há informações para serem lidas (non-blocking) 

OBS: quando falo em resposta, não me refiro repsota para o usuário final, mas sim em baixo nível mesmo.

Blocking
Comportamento comum dos servidores. Você chama uma operação "read()" e o programa fica esperando até que uma resposta seja dada.

Non-Blocking
Existem duas formas de trabalhar com Non-Blocking I/O.

1) Fazendo polling, que é ficar tentando ler alguma coisa a cada X tempo.
2) Utlizar notificação assíncrona. Isso significa que o servidor irá gerar um evento infornado que existe coisa para ser lida.

O SO gera um evento informando que o socket/FD está pronto para ser lido?? Como???


Como são feitos Non-blockings I/O

Vou explicar alguns recursos do SO que "produzem" os non-blockings I/O. Esses são os comandos que o SO provê para você saber quando tem algo para ser lido em um socket/FD.

Ao longo do tempo alguns comandos surgiram e forão substituídos: "Select", "Poll", "Dev/Poll", "epoll" e "kqueue". Cada sistema operacional tem seus próprios comandos.

poll0
O comando poll() avisa quando um conjunto de FDs está pronto para ser lido. Esse comando fica lento quando se tem milhares de FDs, pois muitos dos FDs podem estar idle (ocioso) e varrer todos leva muito tempo.

Se nada for lido nesse File Descriptor, o próximo comando Poll vai dizer que ainda tem coisa para ser  lida.

dev/poll
Com este comando você avisa o SO apenas uma fez quais os FD vocês está interessado. Você faz isso com um "handler". A partir daí você lê um conjunto de FDs prontos para serem lidos, apartir desse "handler".


Esse comando foi cirado para subistituir o poll() no Solaris. Não é recomendada sua utlização no Linux. 


epoll()
Esse é o comando que subistituí o poll() na versão 2.6 do Kernel do Linux. Ele agrupa eventos redundantes e tem trabalha the forma eficiente com eventos em massa, fazendo que tenha uma ótima escalabilidade quando utilizado com grande número de FDs.

Ele pode ser utilizado com edge-triggered e level-triggered, que são formas de interrupções utilizadas para otimizar a utlização de CPU.

kqueue()/kevent()
Esse comando é similiar com o epoll(). Na verdade foi criado antes dele, mas para o FreeBSD. O pessoal do Linux criou um novo comando ao invés de uitlizar este.




Estratégias de I/O

Um pouco mais embasado, podemos olhar algumas estratégias de implementar um servidor. Usando non-blocking, usando threads, etc.

Isso é uma cópia direta desse link.
  • Como utitilizar  deversas chamadas de I/O para uma simples thread.
    • Não usar blocking I/O e se possível usar multi-threading e processos para alcançar concorrência. 
    • Usar chamadas non-blocking (ex: write() em um socket setado O_NONBLOCK) para iniciar o I/O, e as notificações de leitura de eventos (e.g. poll() ou /dev/poll) para saber quando o canal está pronto para leitura. 
    • Usar chamadas assíncronas para começar o I/O e notificações de de encerramento do I/O.
  • Como controlar o código de cada cliente
    • Um processo para cada cliente (abordagem clássica do Unix)
    • Uma thread no SO lida com vários clientes. Cada cliente pode ser controlado por: 
      • user-level thread 
      • state machine
      • continuation
    • Uma OS-level thread para cada cliente (e.g. classic Java with native threads)
    • Uma OS-level thread para cada cliente ativo (e.g. Tomcat com Apache na frente; NT completion ports; thread pools)
Conclusão

Utilizar non-blocking I/O e evitar grande número de threads é a basde da nova geração de servidores. Com isso é possível atender um númeor muito maior de requisições por máquina.
"Processos são necessários para escalar computadores muilt-core, não threads compartilhadoras de memória. Os fundamentos dos systemas escaláveis são rápido networking e non-blocking design. O resto é mensageria"
Node.js website
Referências

http://www.kegel.com/c10k.html
http://nodejs.org/#about
http://bulk.fefe.de/scalable-networking.pdf
http://www.citi.umich.edu/projects/linux-scalability/reports/accept.html
http://urbanairship.com/blog/2010/08/24/c500k-in-action-at-urban-airship/
http://www.techrepublic.com/article/using-the-select-and-poll-methods/1044098
http://www.developerfusion.com/article/28/introduction-to-tcpip/8/
http://www.kegel.com/dkftpbench/nonblocking.html

Um comentário:

Caio disse...

Cara muito boa sua explicação!