qconsp 2015 - dicas de performance para aplicações web
TRANSCRIPT
DICAS DE PERFORMANCEWEB
Fabio Akita @AkitaOnRails
Back EndBrowser
Front EndRender
ExternalServices
9 DICAS
SLIDESHARE.NET/AKITAONRAILS
MONITORING
1.4 MILHOES DE VISITANTES UNICOS!!
Ruby 2.1
Ruby 2.2
CACHING
def sitemap sleep 3 @posts = Post.select([ :id, :slug, :updated_at, :published_at ]).published respond_to do |format| format.xml { render layout: false } end end
Processing by ArchivesController#sitemap as XML Rendered archives/sitemap.xml.builder (785.5ms) Completed 200 OK in 3879.4ms (Views: 770.4ms | ActiveRecord: 18.7ms | Solr: 0.0ms)
def sitemap @posts = cache('sitemap', :expires_in => 12.hours) { sleep 3 Post.select([ :id, :slug, :updated_at, :published_at ]).published } respond_to do |format| format.xml { render layout: false } end end
Processing by ArchivesController#sitemap as XML Rendered archives/sitemap.xml.builder (763.3ms) Completed 200 OK in 842.4ms (Views: 754.2ms | ActiveRecord: 13.3ms | Solr: 0.0ms)
def sitemap @posts = cache('sitemap', :expires_in => 12.hours) { Post.select([ :id, :slug, :updated_at, :published_at ]).published } if stale?(last_modified: @posts.first.updated_at.utc, etag: "posts/#{@posts.count}-‐#{@posts.first.updated_at.utc}") respond_to do |format| format.xml { render layout: false } end end end
Processing by ArchivesController#sitemap as XML Completed 304 Not Modified in 6.2ms (ActiveRecord: 1.7ms)
2s!
198ms!
ASSET PIPELINE
}}
UPLOADS
Browser
1s
BackendBrowser
1s
ImageMagickBackendBrowser
1s
S3ImageMagickBackendBrowser
1s
S3ImageMagickBackendBrowser
1s 2 segundos
Browser
1s
Browser
1s
S3
BackendBrowser
1s
S3
BackendBrowser
1s 200 ms
S3
BackendBrowser
1s 200 ms
S3
ImageMagick
700 ms
# Gemfile gem 'cloudinary' gem 'attachinary'
# Gemfile gem 'cloudinary' gem 'attachinary'
# config/cloudinary.yml production: cloud_name: "sample" api_key: "874837483274837" api_secret: "a676b67565c6767a6767d6767f676fe1"
# Gemfile gem 'cloudinary' gem 'attachinary'
# config/cloudinary.yml production: cloud_name: "sample" api_key: "874837483274837" api_secret: "a676b67565c6767a6767d6767f676fe1"# app/models/user.rb
class User < ActiveRecord::Base attr_accessible :name has_attachment :avatar has_attachments :photos, maximum: 3 end
# Gemfile gem 'cloudinary' gem 'attachinary'
# config/cloudinary.yml production: cloud_name: "sample" api_key: "874837483274837" api_secret: "a676b67565c6767a6767d6767f676fe1"# app/models/user.rb
class User < ActiveRecord::Base attr_accessible :name has_attachment :avatar has_attachments :photos, maximum: 3 end
# app/views/users/_form.html.slim = form_for(@user) do |user_form| = user_form.text_field(:name) = attachinary_file_field_tag ‘user[avatar]', @user, :avatar = attachinary_file_field_tag ‘user[photos]’, @user, :photos = user_form.submit("Save")
# Gemfile gem 'cloudinary' gem 'attachinary'
# config/cloudinary.yml production: cloud_name: "sample" api_key: "874837483274837" api_secret: "a676b67565c6767a6767d6767f676fe1"# app/models/user.rb
class User < ActiveRecord::Base attr_accessible :name has_attachment :avatar has_attachments :photos, maximum: 3 end
# app/views/users/_form.html.slim = form_for(@user) do |user_form| = user_form.text_field(:name) = attachinary_file_field_tag ‘user[avatar]', @user, :avatar = attachinary_file_field_tag ‘user[photos]’, @user, :photos = user_form.submit("Save")
# app/controllers/users_controller.rb ... def create @user = User.new(params[:user]) @user.save! end
# app/views/users/show.html.slim -‐ if user.avatar.present? = cl_image_tag(user.avatar.path, width: 80, height: 100, crop: :thumb, gravity: :face) -‐ user.photos.each do |photo| = cl_image_tag(photo.path, size: '70x50', crop: :fill, radius: 20)
# Gemfile gem 'cloudinary' gem 'attachinary'
# config/cloudinary.yml production: cloud_name: "sample" api_key: "874837483274837" api_secret: "a676b67565c6767a6767d6767f676fe1"# app/models/user.rb
class User < ActiveRecord::Base attr_accessible :name has_attachment :avatar has_attachments :photos, maximum: 3 end
# app/views/users/_form.html.slim = form_for(@user) do |user_form| = user_form.text_field(:name) = attachinary_file_field_tag ‘user[avatar]', @user, :avatar = attachinary_file_field_tag ‘user[photos]’, @user, :photos = user_form.submit("Save")
# app/controllers/users_controller.rb ... def create @user = User.new(params[:user]) @user.save! end
CDN
AssetsRailsBrowser
1s 5 segundos
Assets
RailsBrowser
1s 200 ms
4 segundos
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://www.mydomain.com/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://www.mydomain.com/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://www.mydomain.com/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://www.mydomain.com/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://d3vam04na8c92l.cloudfront.net/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
http://www.mydomain.com/stylesheets/application-6502e5a88f02b90aeb32c2dd21ea37ab.css
# config/environments/production.rb config.action_controller.asset_host = "d3vam04na8c92l.cloudfront.net"
<%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= image_tag("rails.png") %>
<link href="http://d3vam04na8c92l.cloudfront.net/stylesheets/application-‐6502e5a88f02b90aeb32c2dd21ea37ab.css" rel="stylesheet" />
DATABASE
# Gemfile gem "lol_dba", group: :development
# no Terminal bundle install bundle exec rake db:find_indexes
# app/controllers/posts_controller.rb def index @posts = Post.all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
# app/controllers/posts_controller.rb def index @posts = Post.all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
<% @posts.each do |post| %> <tr> <td><%= post.name %></td> <td><%= post.comments.size %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %>
# app/controllers/posts_controller.rb def index @posts = Post.all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
<% @posts.each do |post| %> <tr> <td><%= post.name %></td> <td><%= post.comments.size %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %>
Started GET "/posts/" for 192.168.47.2 at 2015-‐03-‐23 18:31:11 +0000 Cannot render console from 192.168.47.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 ActiveRecord::SchemaMigration Load (0.1ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by PostsController#index as HTML Post Load (0.2ms) SELECT "posts".* FROM "posts" Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Rendered posts/index.html.erb within layouts/application (25.3ms) Completed 200 OK in 679ms (Views: 666.4ms | ActiveRecord: 1.0ms)
# app/controllers/posts_controller.rb def index @posts = Post.all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
<% @posts.each do |post| %> <tr> <td><%= post.name %></td> <td><%= post.comments.size %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %>
Started GET "/posts/" for 192.168.47.2 at 2015-‐03-‐23 18:31:11 +0000 Cannot render console from 192.168.47.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 ActiveRecord::SchemaMigration Load (0.1ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by PostsController#index as HTML Post Load (0.2ms) SELECT "posts".* FROM "posts" Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Rendered posts/index.html.erb within layouts/application (25.3ms) Completed 200 OK in 679ms (Views: 666.4ms | ActiveRecord: 1.0ms)
Started GET "/posts/" for 192.168.47.2 at 2015-‐03-‐23 18:37:31 +0000 Cannot render console from 192.168.47.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 ActiveRecord::SchemaMigration Load (0.1ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by PostsController#index as HTML Post Load (0.2ms) SELECT "posts".* FROM "posts" Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2) Rendered posts/index.html.erb within layouts/application (25.8ms) Completed 200 OK in 109ms (Views: 96.4ms | ActiveRecord: 0.9ms)
# app/controllers/posts_controller.rb def index @posts = Post.includes(:comments).all
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end
<% @posts.each do |post| %> <tr> <td><%= post.name %></td> <td><%= post.comments.size %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %>
SEARCH
ASYNC JOBS
Browser
1s
BackendBrowser
1s
Tarefa DemoradaBackendBrowser
1s
Tarefa DemoradaBackendBrowser
1s 4 segundos
Browser
1s
BackendBrowser
1s
BackendBrowser
1s
Redis
BackendBrowser
1s 200 ms
Redis
BackendBrowser
1s 200 ms
Redis
Tarefa Demorada(Worker)
3.8 segundos
class VerySlowJob < ActiveJob::Base queue_as :default def perform(*args) # Tarefa Demorada end end
class VerySlowJob < ActiveJob::Base queue_as :default def perform(*args) # Tarefa Demorada end end
VerySlowJob.perform_later record
class VerySlowJob < ActiveJob::Base queue_as :default def perform(*args) # Tarefa Demorada end end
VerySlowJob.perform_later record
VerySlowJob.set(wait_until: Date.tomorrow.noon).perform_later(record)
class VerySlowJob < ActiveJob::Base queue_as :default def perform(*args) # Tarefa Demorada end end
VerySlowJob.set(wait: 1.week).perform_later(record)
VerySlowJob.perform_later record
VerySlowJob.set(wait_until: Date.tomorrow.noon).perform_later(record)
# app/workers/report_job.rb class ReportJob < ActiveJob::Base queue_as :default def perform(email, from_date = nil, to_date = nil) now = Time.zone.now from_date ||= now -‐ 1.year to_date ||= now file_path = ModelGigante.generate_excel(from_date, to_date) NotifyReportMailer.send(email, file_path) end end
# app/controllers/reports_controller.rb # ... def create ReportJob.perform_later(params[:email], params[:from_date], params[:to_date]) flash[:notice] = "Report is being generated. It will be sent to your email." redirect_to reports_path end # ...
# app/workers/report_job.rb class ReportJob < ActiveJob::Base queue_as :default def perform(email, from_date = nil, to_date = nil) now = Time.zone.now from_date ||= now -‐ 1.year to_date ||= now file_path = ModelGigante.generate_excel(from_date, to_date) NotifyReportMailer.send(email, file_path) end end
ASYNC MESSAGES
http://blog.tryneighborly.com/amazon-sns-for-apns-on-rails/
<!-‐-‐ chat.html -‐-‐> <script src="//js.pusher.com/2.2/pusher.min.js"></script> <script> $(function() { var pusher = new Pusher('YOUR_APP_KEY'); var channel = pusher.subscribe('my-‐app-‐chat'); channel.bind('new-‐message', function(data) { $('#chat-‐box').append(data.message); }); }) </script>
<!-‐-‐ chat.html -‐-‐> <script src="//js.pusher.com/2.2/pusher.min.js"></script> <script> $(function() { var pusher = new Pusher('YOUR_APP_KEY'); var channel = pusher.subscribe('my-‐app-‐chat'); channel.bind('new-‐message', function(data) { $('#chat-‐box').append(data.message); }); }) </script>
# config/initializer/pusher.rb Pusher.app_id = ENV['PUSHER_APP_ID'] Pusher.key = ENV['PUSHER_APP_KEY'] Pusher.secret = ENV['PUSHER_APP_SECRET']
<!-‐-‐ chat.html -‐-‐> <script src="//js.pusher.com/2.2/pusher.min.js"></script> <script> $(function() { var pusher = new Pusher('YOUR_APP_KEY'); var channel = pusher.subscribe('my-‐app-‐chat'); channel.bind('new-‐message', function(data) { $('#chat-‐box').append(data.message); }); }) </script>
# config/initializer/pusher.rb Pusher.app_id = ENV['PUSHER_APP_ID'] Pusher.key = ENV['PUSHER_APP_KEY'] Pusher.secret = ENV['PUSHER_APP_SECRET']
# app/workers/new_message_worker.rb require 'pusher' class NewMessageWorker include Sidekiq::Worker def perform(message_id) @message = Message.find(message_id) Pusher.trigger('my-‐app-‐chat', 'new-‐message', {:message => @message}) end end
<!-‐-‐ chat.html -‐-‐> <script src="//js.pusher.com/2.2/pusher.min.js"></script> <script> $(function() { var pusher = new Pusher('YOUR_APP_KEY'); var channel = pusher.subscribe('my-‐app-‐chat'); channel.bind('new-‐message', function(data) { $('#chat-‐box').append(data.message); }); }) </script>
# app/controllers/messages_controller.rb # ... def create @message = Message.new(params[:message]) if @messsage.save NewMessageWorker.perform_async(@message.id) # ... end # ...
# config/initializer/pusher.rb Pusher.app_id = ENV['PUSHER_APP_ID'] Pusher.key = ENV['PUSHER_APP_KEY'] Pusher.secret = ENV['PUSHER_APP_SECRET']
# app/workers/new_message_worker.rb require 'pusher' class NewMessageWorker include Sidekiq::Worker def perform(message_id) @message = Message.find(message_id) Pusher.trigger('my-‐app-‐chat', 'new-‐message', {:message => @message}) end end
TUNING RUBY
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
• Mark and Sweep GC (1.8+)
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
• Mark and Sweep GC (1.8+)
• Bitmap Marking GC (1.9.3+)
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
• Mark and Sweep GC (1.8+)
• Bitmap Marking GC (1.9.3+)
• Lazy Sweep GC (2.0.0+)
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
• Mark and Sweep GC (1.8+)
• Bitmap Marking GC (1.9.3+)
• Lazy Sweep GC (2.0.0+)
• Restricted Generational GC (RGenGC 2.1.0+)
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
• Mark and Sweep GC (1.8+)
• Bitmap Marking GC (1.9.3+)
• Lazy Sweep GC (2.0.0+)
• Restricted Generational GC (RGenGC 2.1.0+)
• Restricted Incremental GC (RIncGC 2.2.0+)
http://www.infoq.com/news/2014/12/ruby-2.2.0-released
TRADE-OFF:+ GC OU + RAM
# Gemfile source 'https://rubygems.org'
ruby "2.2.1"
# performance tuning gem 'escape_utils' gem 'fast_blank' gem 'oj' gem 'oj_mimic_json'
http://marianposaceanu.com/articles/improve-rails-performance-by-adding-a-few-gems
# Gemfile source 'https://rubygems.org'
if ENV['RAILS_ENV'] == 'production' ruby '1.9.3', :engine => 'jruby', :engine_version => '1.7.18' else ruby '2.2.1' end
gem 'rails', '4.2.0' gem 'rails-‐api' gem 'srt' gem 'pg', platforms: 'mri' gem 'activerecord-‐jdbcpostgresql-‐adapter', platforms: 'jruby'
gem 'sidekiq' gem 'dalli' gem 'puma' gem 'rack-‐cache' gem 'rack-‐cors', require: 'rack/cors' gem 'rails_12factor' gem 'newrelic_rpm'
# Gemfile source 'https://rubygems.org'
if ENV['RAILS_ENV'] == 'production' ruby '1.9.3', :engine => 'jruby', :engine_version => '1.7.18' else ruby '2.2.1' end
gem 'rails', '4.2.0' gem 'rails-‐api' gem 'srt' gem 'pg', platforms: 'mri' gem 'activerecord-‐jdbcpostgresql-‐adapter', platforms: 'jruby'
gem 'sidekiq' gem 'dalli' gem 'puma' gem 'rack-‐cache' gem 'rack-‐cors', require: 'rack/cors' gem 'rails_12factor' gem 'newrelic_rpm'
# Gemfile source 'https://rubygems.org'
if ENV['RAILS_ENV'] == 'production' ruby '1.9.3', :engine => 'jruby', :engine_version => '1.7.18' else ruby '2.2.1' end
gem 'rails', '4.2.0' gem 'rails-‐api' gem 'srt' gem 'pg', platforms: 'mri' gem 'activerecord-‐jdbcpostgresql-‐adapter', platforms: 'jruby'
gem 'sidekiq' gem 'dalli' gem 'puma' gem 'rack-‐cache' gem 'rack-‐cors', require: 'rack/cors' gem 'rails_12factor' gem 'newrelic_rpm'
# Gemfile source 'https://rubygems.org'
if ENV['RAILS_ENV'] == 'production' ruby '1.9.3', :engine => 'jruby', :engine_version => '1.7.18' else ruby '2.2.1' end
gem 'rails', '4.2.0' gem 'rails-‐api' gem 'srt' gem 'pg', platforms: 'mri' gem 'activerecord-‐jdbcpostgresql-‐adapter', platforms: 'jruby'
gem 'sidekiq' gem 'dalli' gem 'puma' gem 'rack-‐cache' gem 'rack-‐cors', require: 'rack/cors' gem 'rails_12factor' gem 'newrelic_rpm'
9 DICAS
Monitoring New Relic
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Async Jobs Sidekiq, Redis
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Async Jobs Sidekiq, Redis
Async Messages pusher.com
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Async Jobs Sidekiq, Redis
Async Messages pusher.com
Auto scale HireFire, AdeptScale
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Async Jobs Sidekiq, Redis
Async Messages pusher.com
Auto scale HireFire, AdeptScale
Rubies Ruby 2.2.1, JRuby
Monitoring New Relic
Caching Memcache, ETAG
Assets CloudFlare, Cloudinary
Database PostgreSQL
Search Elastic
Async Jobs Sidekiq, Redis
Async Messages pusher.com
Auto scale HireFire, AdeptScale
Rubies Ruby 2.2.1, JRuby
Deployment Heroku
OBRIGADOslideshare.net/akitaonrails