Desde meu relato sobre a RailsConf eu já havia dito que Josh Peek tinha um branch do Rails no Github, num trabalho para o Google Summer of Code, tendo Michael Koziarski como mentor.

Finalmente, David Hansson anunciou que Josh Peek passa a fazer parte do Core Team, e uma das coisas que isso implica é finalmente fazer o merge do branch de thread-safe do Josh para dentro do trunk oficial!

Agora, Charles Nutter – sempre à frente dessas polêmicas iniciais – resolveu escrever uma lista de perguntas e respostas (Q/A) sobre que efeitos thread-safety causa no Rails, e especialmente sobre o mundo JRuby. Naturalmente existem muitas opiniões do próprio Charles aqui, mas a maior parte deve ser factualmente correta.

Segue a tradução do artigo dele:

O que significa fazer Rails ser thread-safe?

Estou certo que Josh ou Michael Koziarski, seu mentor do GSoC, podem explicar em mais detalhes o trabalho envolvido, mas basicamente significa remover o lock gigante único sobre cada requisição que chega e substituí-lo por locks menores e mais granulados somente sobre os recursos que precisam ser compartilhados entre as threads. Então, por exemplo, estruturas de dados dentro do sub-sistema de logging ou foram modificadas para não serem compartilhadas entre threads, ou apropriadamente travadas para ter certeza que duas threads não interfiram uam com a outra ou torne as estruturas de dados inválidas ou corrompidas. Em vez de uma única conexão e banco de dados para cada instância de Rails, haverá um pool de conexões, permitindo N conexões de banco de dados para serem usadas por M requisições executando concorrentemente. Isso também significa permitir requisições de potencialmente executar sem consumir uma conexão, então o número de conexões vivas e ativas normalmente será menor do que o número de requisições que se pode lidar concorrentemente.

Por que isso é importante? Já não temos concorrência de verdade com a arquitetura Rails de ‘shared-nothing’ (nada compartihado) e múltiplos processos?

Sim, processos e shared-nothing nos dão concorrência completa, ao custo de ter múltiplos processos para gerenciar. Para muitas aplicações isso é concorrência “boa o suficiente”. Mas existe um problema em requerer tantos processos quanto requisições concorrentes: uso ineficiente de recursos compartilhados. Em uma típica configuração de Mongrel, lidar com 10 requisições concorrentes significa ter 10 cópias do Rails carregadas, 10 cópias da sua aplicação carregada, 10 caches de dados em memória, 10 conexões de banco de dados … tudo tem que ser escalado em escala de travamento para cada requisição adicional que quiser lidar concorrentemente. Multiplique as N cópias de tudo vezes M diferentes aplicações, e você estará comendo muitas, muitas vezes mais memória do que deveria.

Claro existem soluções parciais a isso que não requerem thread-safety. Já que muito do código carregado e parte dos dados pode ser o mesmo entre todas as instâncias, soluções de deployment como Passenger da Phusion conseguem usar forking e melhorias no modelo de memória no Ruby Enterprise Edition da Phusion para permitir todas as instâncias de compartilhar a mesma porção de memória que é igual. Então você reduz a carga de memória pelo tanto de código e dados que são os mesmos em cada instância, o que normalmente incluiria o próprio Rails, o código estático de sua aplicação, e até certo ponto outras bibliotecas carregadas pelo Rails e sua aplicação. Mas você ainda paga a duplicação de conexões de banco de dados, código da aplicação, e dados em memória carregados ou criados depois da inicialiação. E você ainda não terá concorrência melhor do que o locking gigante porque Ruby Enterprise Edition é tão green threaded quanto o Ruby normal.

Então para implementações green-threaded como Ruby, Ruby EE e Rubinius, threads nativas não oferecem benefício?

Isso não é bem verdade, Rails thread-safe significará que uma instância individual, mesmo com green threads, podem lidar com múltiplas requisições ao mesmo tempo. E por “ao mesmo tempo” não quero dizer concorrentemente .. green threads nunca permitirão duas requisições de realmente rodar concorrentemente ou utilizar múltiplos cores. O que quero dizer é que se dada requisição terminar bloqueando o I/O – o que acontece em quase todas as requisições (por causa de hits REST, hits de banco de dados, hits de filesystem e assim por diante), Ruby agora terá a opção de agendar outra requisição para executar. Colocado de outro jeito, remover o lock gigante irá pelo menos melhorar a concorrência até o “melhor” que as implementações green threaded podem ir, o que não é tão ruim.

A implicação prática disso é que em vez de ter que rodar uma instância de Rails para cada processo que quiser lidar ao mesmo tempo, você terá apenas que rodar um certo número constante de instâncias por core do seu sistema. Algumas pessoas usam N + 1 ou 2N como métricas para mapear (N) cores para o número de instâncias que precisará para efetivamente utilizar esses cores. E isso significa que você provavelmente nunca precisará de mais que algumas instâncias de Rails e um sistema com um único core. Claro você precisará testar isso você mesmo e ver qual métrica funciona melhor para sua aplicação, mas no final, mesmo em implementações green threaded você deverá ser capaz de reduzir o número de instâncias que precisa.

Ok, e quanto a implementações com threads nativas como JRuby?

No JRuby, a situação melhora muito mais do que em implementações green-threaded. Como JRuby implementa threads de Ruby como threads nativas de kernel, uma aplicação Rails precisará de apenas uma instância para lidar com todas as requisições concorrentes em todos os cores. E por uma instância, eu quero dizer “quase uma instância” já que podem existir casos específicos onde uma dada aplicação engargale em algum recurso compartilhado, e você poderá querer duas ou três para reduzir o gargalo. No geral, entretanto, eu espero que esses casos sejam extremamente raros, e a maioria seria bugs do JRuby ou Rails que precisamos corrigir.

Isso significa o que parece: deployments de Rails em JRuby usarão 1/N da quantidade de memória que usam agora, onde N é o número de instâncias Rails inseguras de threads atualmente requeridas para lidas com requisições concorrentes. Mesmo comparado com implementações green-threaded rodando Rails thread-safe, ele usará 1/M de memória onde M é o número de cores, já que ele pode facilmente paralelizar entre cores com somente “uma” instância.

Isso não é uma grande coisa?

Sim, isso é realmente grande. Sei que usuários existentes de JRuby on Rails ficarão absolutamente empolgados sobre isso. E com sorte mais pessoas considerarão usar JRuby on Rails em produção como resultado.

E isso não acaba em utilização de recursos no caso do JRuby. Com uma única instância de Rails, JRuby será capaz de “aquecer” muito mais rapidamente, já que código que compilamos e otimizamos em runtime será imediatamente aplicável a todas as requisições que chegarem. Os truque que precisamos fazer para algumas otimizações (para reduzir o consumo geral de memória) pode ser que nem sejam mais necessários. Suporte de poolings de conexão JDBC existentes serão mais confiáveis e mais eficientes, mesmo permitindo compartilhar conexões de aplicação a aplicação assim como entre instâncias. E isso colocará Rails no JRuby pau a pau com outros frameworks que sempre foram (provavelmente) thread-safe como Merb, Groovy on Grails e todos os frameworks Java.

Naturalmente, eu estou empolgado!

Nota do Akita: e para entender mais a discussão toda entre green-threads e native-threads, leia esta excelente matéria da InfoQ sobre Ruby 1.9 e o futuro de threads no Ruby.

E só para esclarecer, eu traduzi “coarse-grained locks” como “locks gigantes”. É meio complicado traduzir esses termos ;-)

comentários deste blog disponibilizados por Disqus