/ 22.Feb.2008 at 01:28pm
De vez em quando saem alguns comentários interessantes no Ruby on Rails: Core. Esta semana um cara chamado Stephen estava com uma dúvida: ele começou a fazer testes para entender isso.
Ele criou um script que mede o uso real de memória depois de cada request a uma aplicação Rails 2.0 simples de um único model. Ele faz 4 requests para a página default e depois mais 4 vezes acessando a listagem desse model e daí mede a memória.
Ele notou que a cada conjunto desse o processo do Rails consumia um pouco mais de memória (algo em torno de 8 a 20bytes por request). Então ele criou 100 cópia do model, cada um com cerca de 1kb de dados e duplicou os testes.
Daí ele repetiu para 1000 e depois 10.000 objetos do model. Entre 0 e 1.000 cópias do model, o tamanho do processo variou entre 55 e 70Mb. Mas quando ele foi para 10.000 objetos o processo passou a consumir 220Mb!
O banco de dados é SQLite com apenas 11Mb quando está com 10.000 linhas.
Agora vem a dúvida dele: “se o Ruby tem Garbage Collector, porque continua consumindo mais e mais memória?”
No post seguinte do thread, Piers matou a charada: de fato o Ruby tem um garbage collector conservador de mark and sweep que funciona bem. Porém, Ruby – assim como a maioria dos interpretadores modernos como perl e python – não devolvem memória de volta ao sistema. O motivo é simples: é caro alocar e desalocar memória do sistema o tempo todo.
Além disso, assume-se que se você chegou a usar esse tanto X de memória é provável que vá usar de novo. Como o Paul Dix explicou neste post todas as bibliotecas que normalmente subimos uma aplicação Rails típica vai utilizar pelo menos cerca de 30Mb de memória. Lorens Naude demonstrou neste post que certas bibliotecas são enormes. Imagine carregar um Globalize com 8 línguas, 1.400 traduções por linguagem, pré-carregadas. Fora outros grandes como ImageMagick, RMagick, PDF Writer, TZInfo (que também tem timezones de todos os países).
Quem é de Java vai se lembrar que a gente pré-configura a JVM para começar carregando pouca memória (-Xms) até um limite do heap (-Xmx). Normalmente se for um servidor interno, já deixamos -Xmx=1.5G (se não me engano não dava para pré-alocar mais do que isso em sistemas de 32-bits). E mesmo Java – com um dos melhores gerenciadores de memória em virtual machines – também tem memory leaks ocasionais. Quantos de nós já não vimos o erro OutOfMemory?
Às vezes nossos mongrels alcançam um pico temporário, mas poucos, e gostaríamos de não deixá-los rodando com tudo isso pré-alocado. Por isso temos serviços como Monit ou God.
Fora isso Ruby também sofre de memory leaks, principalmente por conta de extensions feitas em C. Alguns que já tiveram casos discutidos foram RMagick e Ferret. Scott Laird explica algumas maneiras de fazer o profile de um memory leak em Ruby, e esta thread na lista do Mongrel também dá algumas dicas.
Enfim, não existem soluções perfeitas, mas podemos chegar muito próximo de algo muito estável se soubermos com o que estamos lidando e colocando as devidas proteções no lugar. No fundo, um mongrel cluster monitorado por um monit ou god, com algum monitoramento periódico para ver se nada está se comportando mal, deve ser o básico.
5 Comments
Bem interessante.
O curioso é que muito “cabeça fechada” pega esse tipo de coisa e faz uma tempestade sem saber do que está falando. Ainda não achei plataforma perfeita, sem qualquer tipo de leaks.
Acredito que a idéia é conhecer a plataforma da sua aplicação e utilizar as ferramentas apropriadas para cada serviço.
lucas húngaro / 22.Feb.2008 at 03:21pm
Akita, não seria possível então, criar um script god que força o GC quando a memória ultrapassar tantos megas?
neves / 22.Feb.2008 at 04:11pm
Neves, pelo que entendo do GC do Ruby, ele pode até “matar” os objetos marcados, mas não devolve a memória ao sistema.
Ele “acredita” que, se você já bateu 100mb de memória em uso, vai precisar outra vez e, como alocar memória é caro, mantém essa memória alocada.
lucas húngaro / 22.Feb.2008 at 04:39pm
Foi o que eu disse :
) O GC funciona, o interpretador instanciou 40 Mb em objetos. Digamos que 30Mb o GC consegue limpar e agora vc tem só 10Mb, mas o sistema ainda vai enxergar o processo usando 40 Mb. Mas agora, se vc instanciar mais 10Mb, vai consumir um total de 20Mb mas eu processo continua tendo 40Mb. Se vc instanciar mais 30Mb, daí seu processo sobe pra 50Mb. -a grosso modo é isso.akitaonrails / 22.Feb.2008 at 07:27pm
Exato, Akita. Apenas repeti o que você escreveu pra enfatizar que não adianta forçar chamadas a GC, pois a memória continua alocada.
lucas húngaro / 23.Feb.2008 at 11:38am
Leave a Comment