A linguagem Ruby é frequentemente citada por sua flexibilidade. É possível, como disse Dick Sites, "escrever programas para escrever programas". O Ruby on Rails estende a linguagem principal Ruby, mas o Ruby em si torna aquela extensibilidade possível. O Ruby on Rails usa a flexibilidade da linguagem para facilitar escrever programas altamente estruturados sem muito código ou padronização extra: Você obtém uma grande quantidade de comportamento padrão sem trabalho extra. Apesar de este comportamento gratuito nem sempre ser perfeito, obtém-se muita arquitetura boa com seu aplicativo sem muito trabalho.
Por exemplo, o Ruby on Rails é baseado em um padrão Model-View-Controller (MVC), o que significa que a maioria dos aplicativos Rails é dividida de forma limpa em três partes. O modelo contém o comportamento necessário para gerenciar os dados de um aplicativo. Tipicamente, em um aplicativo Ruby on Rails, há um relacionamento 1:1 entre os modelos e as tabelas de banco de dados; ActiveRecord, o object-relation mapping (ORM) que o Ruby on Rails usa por padrão, gerencia a interação do modelo com o banco de dados, o que significa que o programa Ruby on Rails médio tem pouca, se tiver, codificação SQL. A segunda parte, a visualização, consiste no código que cria a saída enviada ao usuário; tipicamente consiste de HTML, JavaScript etc. A parte final, o controlador, transforma a entrada do usuário em chamadas para os modelos corretos e, a seguir, renderiza a resposta usando as visualizações apropriadas.
Proponentes do Rails frequentemente citam este paradigma MVC — juntamente com outros benefícios do Ruby e do Rails — como aumentando sua facilidade de uso, alegando que menos programadores podem produzir mais funcionalidade em menos tempo. Isto, é claro, significa mais valor de negócio para cada dólar de desenvolvimento de software, portanto o desenvolvimento em Ruby on Rails tornou-se significativamente mais popular.
No entanto, o custo inicial de desenvolvimento não é a figura completa. Existem outros custos contínuos, como custos de manutenção e custos de hardware para executar o aplicativo. Desenvolvedores do Ruby on Rails frequentemente usam testes e outras técnicas de desenvolvimento ágil para manter os custos de desenvolvimento baixos, mas pode ser fácil dar muito menos atenção à execução eficiente de seu aplicativo Rails com grandes quantidades de dados. Apesar de o Rails tornar fácil acessar seu banco de dados, nem sempre ele o faz de forma tão eficiente.
Por que os aplicativos Rails executam lentamente?
Aplicativos Rails podem executar lentamente por alguns motivos fundamentais. O primeiro é simples: O Rails faz pressuposições para acelerar o desenvolvimento. Normalmente, essas pressuposições são corretas e úteis. Mas elas não são sempre benéficas para o desempenho e podem resultar em um uso ineficiente de recursos — particularmente recursos de banco de dados.
Por exemplo, ActiveRecord seleciona todos os campos em uma consulta por padrão, usando uma instrução SQL equivalente a SELECT *. Em situações com um grande número de colunas — particularmente se algumas forem campos grandes VARCHAR ou BLOB — este comportamento poderá ser um problema significativo em termos de uso de memória e desempenho.
Outro desafio significativo é o problema N+1, que este artigo examina em detalhes. Essencialmente, isto resulta em várias consultas pequenas sendo realizadas, em vez de uma consulta grande. ActiveRecord não tem como saber, por exemplo, que um registro filho está sendo solicitado para cada conjunto de registros pai, portanto ele produzirá uma consulta de registro filho para cada registro pai. Por causa do custo adicional por consulta, este comportamento pode causar problemas significativos de desempenho.
Outros desafios estão relacionados mais intimamente a hábitos e atitudes de desenvolvimento de desenvolvedores de Ruby on Rails. Como o ActiveRecord facilita tantas tarefas, os desenvolvedores Rails podem muitas vezes criar um pensamento de que "SQL é ruim", desprezando o SQL mesmo quando seu uso faz mais sentido. Criar e manipular grandes quantidades de objetos ActiveRecord pode ser lento, portanto, em alguns casos, poderá ser muito mais rápido escrever uma consulta SQL diretamente que não instancie nenhum objeto.
Como o Ruby on Rails é frequentemente usado para reduzir o tamanho de equipes de desenvolvimento, e como os desenvolvedores de Ruby on Rails frequentemente realizam algumas das tarefas de administração de sistemas necessárias para implementar e manter seus aplicativos em produção, o conhecimento limitado sobre seu ambiente poderá causar problemas. Configurações de sistema operacional e banco de dados poderão não ser definidos corretamente. Apesar de não serem ideais, as configurações MySQL my.cnf são frequentemente deixadas como padrão em implementações do Ruby on Rails, por exemplo. Adicionalmente, poderá não haver ferramentas suficientes de monitoração e avaliação de desempenho para desenvolver um panorama de desempenho em longo prazo. Esta não é uma crítica aos desenvolvedores Ruby on Rails, é claro; é simplesmente uma consequência na falta de especialização; em alguns casos, desenvolvedores do Rails podem ser especialistas em ambas as áreas.
Uma questão final é que o Ruby on Rails encoraja programadores a desenvolver em um ambiente local. Fazer isto tem uma série de benefícios — como menor latência de desenvolvimento e maior distribuição — mas isto significa que é possível trabalhar com um conjunto de dados limitado por causa do tamanho menor das estações de trabalho. A diferença entre como eles desenvolvem e onde o código será implementado pode ser um grande problema. Pode ocorrer de trabalhar por muito tempo com um tamanho de dados pequeno em um servidor local sem carga com bom desempenho para, no final, descobrir que o aplicativo tem problemas significativos de desempenho com um tamanho maior de dados em um servidor congestionado.
É claro, existem muitos outros motivos possível para explicar porque um aplicativo Rails tem problemas de desempenho. A melhor forma de descobrir que problemas potenciais de desempenho seu aplicativo Rails tem é verificando as ferramentas de diagnóstico que podem oferecer medições precisas e repetíveis.
Detectando problemas de desempenho
Uma das melhores ferramentas é o registro de desenvolvimento do Rails, que reside em cada máquina de desenvolvimento no arquivo de log development/log. Ela tem várias métricas brutas disponíveis: tempo total gasto para responder à solicitação, porcentagem de tempo gasto no banco de dados, porcentagem do tempo gasto gerando a visualização etc. Estão disponíveis ferramentas para analisar o arquivo de log, como o development-log-analyzer.
Durante a produção, é possível encontrar informações valiosas examinando o mysql_slow_log. Os detalhes completos estão fora do escopo desta discussão, mas é possível saber mais na seção Recursos.
Uma das ferramentas mais poderosas e úteis é o plug-in query_reviewer (consulte Recursos). Este plug-in mostra quantas consultas estão sendo executadas na página e quanto tempo a página leva para ser gerada. E analisa automaticamente código SQL que o ActiveRecord gera para problemas potenciais. Por exemplo, ele encontra consultas que não usam um índice MySQL, portanto, se você tiver esquecido de indexar uma coluna importante e isto estiver causando problemas de desempenho, é possível encontrá-lo facilmente (consulte Recursos para obter mais informações sobre índices MySQL). O plug-in exibe todas essas informações em um pop-up <div>, que é visível somente durante o modo de desenvolvimento.
Finalmente, não se esqueça de usar ferramentas como Firebug, yslow, Ping e tracert para detectar se seus problemas de desempenho vêm de problemas de carga de ativos ou de rede.
A seguir, lidaremos com alguns problemas específicos do Rails e suas soluções.
O problema da consulta N+1 é um dos maiores problemas com aplicativos Rails. Por exemplo, quantas consultas o código na Listagem 1 produz? Este código é um ciclo simples através de todos os tópicos em uma tabela hipotética de tópicos, exibindo a categoria e o corpo do tópico.
Listagem 1. Código Post.all não otimizado
<%@posts = Post.all(@posts).each do |p|%> <h1><%=p.category.name%></h1> <p><%=p.body%></p> <%end%> |
Resposta: O código gera uma consulta mais uma consulta por linha em @posts. Por causa do custo adicional por consulta, este pode ser um desafio significativo. A culpada é a chamada para p.category.name. Esta chamada se aplica somente àquele objeto particular do tópico, não à array @posts inteira. Felizmente, é possível corrigir isto usando carga antecipada.
Carga antecipada significa que o Rails realizará automaticamente as consultas necessárias para carregar o objeto com quaisquer objetos filhos especificados. O Rails usará uma instrução SQL JOIN ou uma estratégia em que várias consultas são realizadas. No entanto, presumindo que todos os filhos que forem usados sejam especificados, isto nunca resultará em uma situação N+1, onde cada iteração em um ciclo produz uma consulta adicional. A Listagem 2 é uma versão do código da Listagem 1 que usa carga antecipada para evitar o problema N+1.
Listagem 2. Código Post.all otimizado com carga antecipada
<%@posts = Post.find(:all, :include=>[:category] @posts.each do |p|%> <h1><%=p.category.name%></h1> <p><%=p.body%></p> <%end%> |
Aquele código gera no máximo duas consultas, não importa quantas colunas existam na tabela tópicos.
É claro, nem todos os casos são tão simples. É mais trabalhoso lidar com situações de consulta N+1 mais complicadas. Vale a pena o esforço? Vamos fazer um rápido teste.
Usando o script na Listagem 3, é possível descobrir o quão lentas — ou rápidas — as consultas podem ser. A Listagem 3 demonstra como usar o ActiveRecord em um script independente para estabelecer uma conexão com o banco de dados, definir suas tabelas e carregar dados. A seguir, a biblioteca integrada de avaliação de desempenho do Ruby é usada para ver qual abordagem é mais rápida e o quanto mais rápida ela é.
Listagem 3. Script de referência de carga antecipada
require 'rubygems'
require 'faker'
require 'active_record'
require 'benchmark'
# This call creates a connection to our database.
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "127.0.0.1",
:username => "root", # Note that while this is the default setting for MySQL,
:password => "", # a properly secured system will have a different MySQL
# username and password, and if so, you'll need to
# change these settings.
:database => "test")
# First, set up our database...
class Category < ActiveRecord::Base
end
unless Category.table_exists?
ActiveRecord::Schema.define do
create_table :categories do |t|
t.column :name, :string
end
end
end
Category.create(:name=>'Sara Campbell\'s Stuff')
Category.create(:name=>'Jake Moran\'s Possessions')
Category.create(:name=>'Josh\'s Items')
number_of_categories = Category.count
class Item < ActiveRecord::Base
belongs_to :category
end
# If the table doesn't exist, we'll create it.
unless Item.table_exists?
ActiveRecord::Schema.define do
create_table :items do |t|
t.column :name, :string
t.column :category_id, :integer
end
end
end
puts "Loading data..."
item_count = Item.count
item_table_size = 10000
if item_count < item_table_size
(item_table_size - item_count).times do
Item.create!(:name=>Faker.name,
:category_id=>(1+rand(number_of_categories.to_i)))
end
end
puts "Running tests..."
Benchmark.bm do |x|
[100,1000,10000].each do |size|
x.report "size:#{size}, with n+1 problem" do
@items=Item.find(:all, :limit=>size)
@items.each do |i|
i.category
end
end
x.report "size:#{size}, with :include" do
@items=Item.find(:all, :include=>:category, :limit=>size)
@items.each do |i|
i.category
end
end
end
end
|
Este script esta a velocidade de realização de ciclos em 100, 1.000 e 10.000 objetos com e sem carga antecipada usando a cláusula :include. Para executar este script, poderá ser preciso substituir os parâmetros apropriados de conexão com o banco de dados, próximos à parte superior do script, por parâmetros adequados a seu ambiente local. Também será preciso criar um banco de dados MySQL chamado test. Finalmente, será preciso os gems ActiveRecord e faker, que podem ser obtidos executando gem install activerecord faker.
Executar o script em minha máquina produziu os resultados mostrados na Listagem 4.
Listagem 4. Saída do script de referência de carga antecipada
-- create_table(:categories) -> 0.1327s -- create_table(:items) -> 0.1215s Loading data... Running tests... user system total real size:100, with n+1 problem 0.030000 0.000000 0.030000 ( 0.045996) size:100, with :include 0.010000 0.000000 0.010000 ( 0.009164) size:1000, with n+1 problem 0.260000 0.040000 0.300000 ( 0.346721) size:1000, with :include 0.060000 0.010000 0.070000 ( 0.076739) size:10000, with n+1 problem 3.110000 0.380000 3.490000 ( 3.935518) size:10000, with :include 0.470000 0.080000 0.550000 ( 0.573861) |
Em todos os casos, o teste usando :include foi mais rápido — especificamente, 5,02, 4,52 e 6,86 vezes mais rápido, respectivamente. É claro, o resultado exato depende de sua situação particular, mas a carga antecipada pode claramente levar a ganhos de desempenho significativos.
E se você quiser referenciar uma relação aninhada — uma relação de uma relação? A Listagem 5 demonstra uma situação comum onde tal coisa poderá acontecer: o ciclo por todos os tópicos com exibição da imagem do autor, onde o Author tem um relacionamento belongs_to com Image.
Listagem 5. Caso de uso de carga antecipada aninhada
@posts = Post.all @posts.each do |p| <h1><%=p.category.name%></h1> <%=image_tag p.author.image.public_filename %> <p><%=p.body%> <%end%> |
Este código sofre do mesmo problema N+1 que antes, mas a sintaxe para correção não é imediatamente aparente, pois estão sendo usados relacionamentos de relacionamentos. Como, então, são feitas cargas antecipadas de relacionamentos aninhados?
A resposta correta é usar uma sintaxe de hash para a cláusula :include. A Listagem 6 fornece um exemplo de tal carga antecipada aninhada usando hashes.
Listagem 6. Solução de carga antecipada aninhada
@posts = Post.find(:all, :include=>{ :category=>[],
:author=>{ :image=>[]}} )
@posts.each do |p|
<h1><%=p.category.name%></h1>
<%=image_tag p.author.image.public_filename %>
<p><%=p.body%>
<%end%>
|
Como pode-se ver, é possível aninhar literais de hash e array. Note que a única diferença entre um hash e uma array, neste caso, é que o hash pode ter subitens aninhados, e a matriz não. Com exceção disso, eles são equivalentes.
Nem todas as instâncias do problema N+1 são tão facilmente percebidas. Por exemplo, quantas consultas a Listagem 7 produz?
Listagem 7. Caso de uso de carga antecipada indireta
<%@user = User.find(5)
@user.posts.each do |p|%>
<%=render :partial=>'posts/summary', :locals=>:post=>p
%> <%end%>
|
É claro, determinar o número de consultas requer conhecimento do posts/summary parcial. É possível ver uma parcial na Listagem 8.
Listagem 8. Carga antecipada indireta parcial: posts/_summary.html.erb
<h1><%=post.user.name%></h1> |
Infelizmente, a resposta é que a Listagem 7 e a Listagem 8 geram uma consulta extra por linha em post, buscando o nome do usuário — apesar de o objeto post ter sido gerado automaticamente por ActiveRecord a partir de um objeto User já em memória. Em resumo, o Rails ainda não associa registros filhos a seus pais.
A correção é usar carga antecipada autorreferencial. Essencialmente, como o Rails recarrega registros filhos gerados por registros pais, é preciso carregar antecipadamente os registros pais como se fossem um relacionamento totalmente separado. Ele se parece com o código na Listagem 9.
Listagem 9. Solução de carga antecipada indireta
<%@user = User.find(5, :include=>{:posts=>[:user]})
...snip...
|
Apesar de não ser intuitiva, esta técnica funciona de forma muito parecida com as técnicas acima. Infelizmente, é fácil aninhar de forma excessiva usando esta técnica, particularmente se for uma hierarquia complicada. Casos de uso simples são fáceis, como o mostrado na Listagem 9, mas aninhamentos pesados poderão causar problemas. Em alguns casos, a carga excessiva de objetos Ruby poderá, de fato, ser mais lenta do que lidar com o problema N+1 — particularmente se algum dos objetos não tiver toda a árvore percorrida. Naquele caso, outras soluções para o problema N+1 poderão ser mais adequadas.
Uma forma de fazer isto é usando técnicas de armazenamento em cache. O Rails V2.1 tem acesso a cache simples integrado. Usando o Rails.cache.read, Rails.cache.write e métodos relacionados, é possível criar seu próprio mecanismo de armazenamento em cache facilmente, e o backend poderá ser um backend simples de memória, um backend baseado em cache ou um servidor com cache de memória. Saiba mais sobre o suporte a armazenamento de cache integrado do Rails na seção Recursos. No entanto, não é preciso criar sua própria solução de armazenamento em cache; é possível usar um plug-in pré-construído do Rails, como o plug-in cache money de Nick Kallen. Este plug-in fornece armazenamento de cache com gravação e é baseado no código em uso pelo Twitter. Consulte Recursos para mais informações.
É claro, nem todos os problemas do Rails são relacionados ao número de consultas.
Cálculos agregados e agrupamento no Rails
Um problema que poderá ser encontrado envolve trabalho no Ruby que deveria ser feito pelo banco de dados. Esta é uma prova do quão poderoso o Ruby é. É difícil imaginar pessoal reimplementando voluntariamente partes de seus códigos de banco de dados em C sem um incentivo significativo, mas é fácil fazer cálculos similares em grupos de objetos ActiveRecord no Rails. Infelizmente, o Ruby é invariavelmente mais lento que o código de seu banco de dados. Não realize cálculos usando uma abordagem de Ruby puro, como mostrado na Listagem 10.
Listagem 10. Forma incorreta de realizar cálculos de agrupamento
all_ages = Person.find(:all).group_by(&:age).keys.uniq oldest_age = Person.find(:all).max |
Em vez disso, o Rails fornece uma série de funções agregadas e de agrupamento. Use-as como mostrado na Listagem 11.
Listagem 11. Forma correta de realizar cálculos de agrupamento
all_ages = Person.find(:all, :group=>[:age]) oldest_age = Person.calcuate(:max, :age) |
Há várias opções para ActiveRecord::Base#find que podem ser usadas para imitar o SQL. Saiba mais na documentação do Rails. Note que o método calculate funciona com qualquer função agregada válida que seu banco de dados suporta, como :min, :sum e :avg. Adicionalmente, calculate pode usar uma série de argumentos, como :conditions. Consulte a documentação do Rails para obter detalhes.
Mas nem tudo o que é possível fazer no SQL pode ser feito no Rails. Se os elementos integrados não forem suficientes, use SQL personalizado.
Suponha que haja uma tabela com uma lista de pessoas, suas profissões, idades e o número de acidentes nos quais estiveram envolvidas no último ano. É possível usar uma instrução SQL personalizada para recuperar as informações, como mostrado na Listagem 12.
Listagem 12. SQL personalizado com
ActiveRecord exemplo
sql = "SELECT profession,
AVG(age) as average_age,
AVG(accident_count)
FROM persons
GROUP
BY profession"
Person.find_by_sql(sql).each do |row|
puts "#{row.profession}, " <<
"avg. age: #{row.average_age}, " <<
"avg. accidents: #{row.average_accident_count}"
end
|
Este script produziria resultados como os da Listagem 13.
Listagem 13. SQL personalizado com
ActiveRecord saídaProgrammer, avg. age: 18.010, avg. accidents: 9 System Administrator, avg. age: 22.720, avg. accidents: 8 |
É claro, este é um caso simples. Pode-se imaginar, no entanto, como é possível estender este exemplo para instruções SQL de qualquer complexidade. É possível executar também outros tipos de instruções SQL, como instruções ALTER TABLE, usando o método ActiveRecord::Base.connection.execute, como mostra a Listagem 14.
Listagem 14. SQL personalizado não buscador com ActiveRecord
ActiveRecord::Base.connection.execute "ALTER TABLE some_table CHANGE COLUMN..." |
A maioria das manipulações de esquema, como adicionar e remover colunas, pode ser feita usando os métodos integrados do Rails. Mas a habilidade de executar código SQL arbitrário está disponível, se necessário.
Como todas as estruturas de trabalho, o Ruby on Rails pode sofrer com problemas de desempenho sem o cuidado e a atenção adequados. Felizmente, as técnicas corretas para monitorar e corrigir estes desafios são relativamente simples e fáceis de aprender, e mesmo problemas complexos podem ser solucionados com alguma paciência e o conhecimento da origem dos problemas de desempenho.
Aprender
-
Saiba mais sobre o Ruby on Rails em RubyonRails.org.
-
Confira a seção do manual do MySQL no registro de consulta lenta para ver detalhes do registro de consulta lenta do MySQL, que rastreia consultas que excedem um limite de tempo de execução.
-
Consulte a seção do manual do MySQL sobre índices para obter detalhes sobre índices do MySQL.
-
Este tópico do blog thewebfellas chamado "Rails 2.1-integrated caching API" mostra como usar a API de armazenamento integrado do Rails V2.1.
-
Para ouvir entrevistas e discussões interessantes para desenvolvedores de software, consulte os podcasts do developerWorks.
-
Mantenha-se atualizado com os eventos e webcasts técnicos do developerWorks.
-
Siga o developerWorks no Twitter.
-
Consulte as próximas conferências, feiras, webcasts e outros Eventos em todo o mundo que sejam de interesse dos desenvolvedores IBM de software livre.
-
Visite a Zona de software livre do developerWorks para obter informações abrangentes sobre procedimentos, ferramentas e atualizações de projetos que simplificam o desenvolvimento de tecnologias de software livre e a utilização destas com produtos IBM; e não deixe de passar pelos nossos artigos e tutoriais mais populares.
-
A comunidade My developerWorks é um exemplo de comunidade geral de sucesso que abrange uma ampla gama de assuntos.
-
Assista e aprenda sobre a IBM e as tecnologias e funções de produtos de software livre com as demos on demand do developerWorks grátis.
Obter produtos e tecnologias
-
Confira o analisador Rails Development Log para obter ajuda sobre a extração de informações a partir de registros de desenvolvimento.
-
Obtenha o plug-in query_reviewer para ajudar a detectar problemas de N+1.
-
E assegure-se de obter o plug-in cache-money, que fornece uma excelente forma para que aplicativos Rails realizem armazenamento em cache.
-
Inove em seu próximo projeto de desenvolvimento de software livre com o software de avaliação da IBM, disponível para download ou em DVD.
- Faça download das versões de avaliação de produtos IBM ou explore as avaliações on-line no IBM SOA Sandbox e utilize as ferramentas de desenvolvimento de aplicativos e produtos de middleware do DB2®, Lotus®, Rational®, Tivoli® e WebSphere®.
Discutir
-
Participe dos blogs developerWorks e participe da comunidade do developerWorks.

David Berube é um consultor, palestrante e o autor de Practical Rails Plugins, Practical Reporting with Ruby and Rails e Practical Ruby Gems. É possível entrar em contato com ele em info@berubeconsulting.com.