otimizando seu projeto rails
DESCRIPTION
Slides da palestra que apresentei no Rock and Rails de Vila Velha em outubro de 2012TRANSCRIPT
Otimizando seu projeto Rails
@timotta
Otimização prematura
Premature optimization is the root of all evil
-- DonaldKnuth
Ruby é...
Lenta
Rails é...
Lento
O argumento...
Porém...
EnfileiramentoDe requisições
Apache Benchmark
ab -n 1 -c 1 http://localhost:3000/politicos/piores
Resultado de 1 requisição
ab -n 1 -c 1 http://localhost:3000/politicos/piores
Time taken for tests: 1.239 secondsRequests per second: 0.81 [#/sec]Time per request: 1239.403 [ms]
Resultado de 400 por 20
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 595.433 secondsRequests per second: 0.67 [#/sec]Time per request: 1488.583 [ms]
Sempre a primeira supeita
Queries
Verificando log
> rm log/production.log> GET http://localhost:3000/politicos/piores> grep -c select log/production.log216
N+1 problem
Usando include:
Politico.piores.include(:cargo,:partido)
Queries
SELECT `cargos`.* FROM `cargos` WHERE `cargos`.`id` IN (4, 5, 1, 2, 8, 6, 3, 9)
SELECT `partidos`.* FROM `partidos` WHERE `partidos`.`id` IN (3, 9, 12, 11, 6, 4, 1)
> rm log/production.log> GET http://localhost:3000/politicos/piores> grep -c Load log/production.log191
N+1 problem
Agrupando no controller
@query = Avaliacao. where(politico_id: @politicos, eleitor_id: @eleitor)
@avaliacoes = @query.reduce({}) do |grupo, avaliacao| grupo[avaliacao.politico_id] = avaliacao grupoend
Usando na view
Antes:
@politico.avaliacoes.do_eleitor(@eleitor)
Agora:
@avaliacoes[@politico.id]
Queries
SELECT `avaliacoes`.* FROM `avaliacoes` WHERE `avaliacoes`.`politico_id` IN (640, 620, 639, 683, ...) AND `avaliacoes`.`eleitor_id` = 6275
> rm log/production.log> GET http://localhost:3000/politicos/piores> grep -c select log/production.log7
Apache Benchmark
ab -n 1 -c 1 http://localhost:3000/politicos/piores
Time taken for tests: 0.658 secondsRequests per second: 1.52 [#/sec]Time per request: 658.123 [ms]
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 298.956 secondsRequests per second: 1.34 [#/sec]Time per request: 747.390 [ms]
Evolução
Vamos analisar mais...
Cache dos filtros
def cargos_for_select Rails.cache.fetch('cargos_for_select') do Cargo.all.collect do |cargo| [ cargo.no_plural, cargo.slug ] end endend
<%= options_for_select(cargos_for_select) %>
Memcached
Gemfile:gem 'memcache-client'
environments:config.cache_store = :mem_cache_store,
['localhost:11211'], namespace: Rails.enva
Resultado
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 300.668 secondsRequests per second: 1.33 [#/sec]Time per request: 751.671 [ms]
Evolução
() sobre memcached
Rails 1
Rails 2
Rails 3
Memcache 1
Memcache 2
Memcache 3
Key ”ABC”
Key ”XYZ”
Key ”123”
Vamos aproveitar os CORES
Mas e o GIL?http://goo.gl/CdsyZ
Algumas opções
● Puma● Thin● Passenger● Unicorn
Algumas opções
● Puma● Thin● Passenger● Unicorn
config.threadsafe!
Unicorn
worker_processes 4preload_app truetimeout 30Listen 3000
after_fork do |server, worker| ActiveRecord::Base.establish_connectionend
Resultado
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 109.909 secondsRequests per second: 3.64 [#/sec]Time per request: 274.773 [ms]
Evolução
Indo mais fundo
Ferramentas paraencontrar gargalos
newrelic
gem 'newrelic_rpm'
ruby-prof
gem 'ruby-prof'
> rails generate performance_test politicos> rake test:profile
def test_piores ENV['RAILS_ENV'] = 'production' get '/politicos/piores' ENV['RAILS_ENV'] = 'test'end
ruby-prof
Ruby 1.9.3 e ruby-prof
http://goo.gl/u0hMd
Patching Garbage Collector
https://github.com/skaes/rvm-patchsets
> rvm install 1.9.3-p125 --patch railsexpress --name railsexpress
> rvm use 1.9.3-p125-railsexpress@webdemocracia
Patched Garbage Collector
GC.enable_statsENV['RAILS_ENV'] = 'production'4.times { get '/politicos/piores' }ENV['RAILS_ENV'] = 'test'
puts "allocated: #{GC.allocated_size/1024}K total” + "in #{GC.num_allocations} allocations, "puts "GC calls: #{GC.collections}, "puts "GC time: #{GC.time / 1000} msec"
4 acessos geram...
allocated: 46409K total in 65930 allocations, GC calls: 6, GC time: 262 msec
Gargalo nos ERBs
Transformando ERBs em Str
def botoes_para_avaliar(politico, avaliacao) html = <<-RETORNO <div class="avaliar"> #{texto_de_avaliacao avaliacao}:<br/> #{html_de_avaliacao_negativa politico, avaliacao} #{html_de_avaliacao_positiva politico, avaliacao} </div> RETORNO html.html_safeend
O que o GC nos diz
Antes:allocated: 46409K total in 65930 allocations, GC calls: 6, GC time: 262 msec
Depois:allocated: 45466K total in 63078 allocations, GC calls: 6, GC time: 261 msec
2852 a menos
E o Apache Benchmark
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 82.318 secondsRequests per second: 4.86 [#/sec]Time per request: 205.796 [ms]
Evolução
Ministério da segurança adverte
Trocar ERB por String podetornar seu código vulnerável
html e script injection
Trocando bibliotecas
gem 'memcache-client'gem 'dalli'
Benchmark
Benchmark.measure do 1000.times do |i| Rails.cache.write("a#{i}",i) Rails.cache.read("a#{i}") endend
Memcache-client: => 0.860000 0.020000 0.880000 ( 0.880819)
Dalli: => 0.270000 0.020000 0.290000 ( 0.302308)
Resultado
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 80.309 secondsRequests per second: 4.98 [#/sec]Time per request: 200.773 [ms]
Evolução
O que o ruby-prof nos diz...
41% do seu processamento
antes
Rack Middlewares
> RAILS_ENV=production rake middlewareuse Rack::Cacheuse #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x0000000293e930>use Rack::Runtimeuse Rack::MethodOverrideuse ActionDispatch::RequestIduse Rails::Rack::Loggeruse ActionDispatch::ShowExceptionsuse ActionDispatch::DebugExceptionsuse ActionDispatch::RemoteIpuse Rack::Sendfileuse ActionDispatch::Callbacksuse ActiveRecord::ConnectionAdapters::ConnectionManagementuse ActiveRecord::QueryCacheuse ActionDispatch::Cookiesuse ActionDispatch::Session::CookieStoreuse ActionDispatch::Flashuse ActionDispatch::ParamsParseruse ActionDispatch::Headuse Rack::ConditionalGetuse Rack::ETaguse ActionDispatch::BestStandardsSupportuse ExceptionNotifieruse OmniAuth::Builderrun Webcracia::Application.routes
Removendo alguns...
config.middleware.delete(ActionDispatch::RemoteIp) config.middleware.delete(Rack::Sendfile) config.middleware.delete(ActionDispatch::RequestId) config.middleware.delete(Rack::Cache) config.middleware.delete(ActionDispatch::Callbacks) config.middleware.delete(Rack::ConditionalGet) config.middleware.delete(Rack::ETag) config.middleware.delete(ActionDispatch::BestStandardsSupport) config.middleware.delete(ActiveSupport::Cache::Strategy::LocalCache) config.middleware.delete(ActionDispatch::DebugExceptions) config.middleware.delete(ActionDispatch::ShowExceptions) config.middleware.delete(ActionDispatch::Head)
Efeitos colaterais...
404 antes 404 depois
O que o GC nos diz...
Antes:allocated: 45466K total in 63078 allocations, GC calls: 6, GC time: 261 msec
Antes:allocated: 45329K total in 61967 allocations, GC calls: 6, GC time: 250 msec
1111 a menos
E o Apache Benchmark
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 75.588 secondsRequests per second: 5.29 [#/sec]Time per request: 188.970 [ms]
Evolução
Mais ruby-prof
15% paraGerar os filtros
Da lateral
Mais cache
def html_de_filtros Rails.cache.fetch('filtros') do html = <<-RETORNO <div class=\"filtros\">Filtrar por:<br/> #{select_de_cargos} #{select_de_partidos} #{select_de_estados} </div>" RETORNO html.html_safe endend
Resultado
ab -n 400 -c 20 http://localhost:3000/politicos/piores
Time taken for tests: 73.719 secondsRequests per second: 5.43 [#/sec]Time per request: 184.296 [ms]
Evolução
Requests por segundo
Ferramentas
● Apache Benchmark● Newrelic● Ruby-prof● Benckmark.mesure● Patched GC
Problemas/soluções comuns
● N+1 problem● Cache● Paralelização● ERB● Middlewares
Outras técnicas
● Separar site cacheávelhttp://goo.gl/SyTnB
● Utilizando o Nginx para escalarhttp://goo.gl/k9H7D