Share
Embed
Intro to Rails Web Performance
An introduction to Rails web performance. Explores improvements that can be found in database, caching and background jobs.
Chris Kelly
July 23, 2012
Transcript
39 slides
-
Passenger Simple to operate. Simple configuration. Handles worker management. Great
for multi-app environments. Great for low resource environments. Attached to Nginx/Apache HTTPD. -
Unicorn Highly configurable. Independent of front-end web server. Master will
reap children on timeout. Great for single app environments. Allows for zero downtime deploys. -
Redirects Cache DNS TCP SSL Request App Response DOM Render
Link Clicked First Byte DOM Loaded Load -
Lazy Loading ๏ ORMs make it easy to access data.
๏ Easy access to data can create issues. ๏ Performance issues are hard to see in development mode. ๏ Look to production metrics to optimize and refactor. -
N+1 Query Creep # app/models/customer.rb class Customer < ActiveRecord::Base has_many
:addresses end # app/models/address.rb class Address < ActiveRecord::Base belongs_to :customer end # app/controllers/customers_controller.rb class CustomersController < ApplicationController def index @customers = Customer.all end end # app/views/customers/index.html.erb <% @customers.each do |customer| %> <%= content_tag :h1, customer.name %> <% end %> -
N+1 Query Creep # app/views/customers/index.html.erb <% @customers.each do |customer| %>
<%= content_tag :h1, customer.name %> <%= content_tag :h2, customer.addresses.first.city %> <% end %> If @customers has 100 records, you'll have 101 queries: SELECT "customers".* FROM "customers" SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 1 AND "addresses"."primary" = 't' LIMIT 1 SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 2 AND "addresses"."primary" = 't' LIMIT 1 SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 3 AND "addresses"."primary" = 't' LIMIT 1 ... ... SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 100 AND "addresses"."primary" = 't' LIMIT 1 -
Eager Load Instead # app/controllers/customers_controller.rb class CustomersController < ApplicationController def
index @customers = Customer.includes(:addresses).all end end If @customers has 100 records, now we only have 2 queries: SELECT "customers".* FROM "customers" SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100) -
Missing Indexes ๏ Searching a 1,000 row table with an
index is 100x faster than searching without. ๏ Put indexes anywhere you might need to query; less is not more with indexes. ๏ Writing an index will lock your tables. -
Indexes are Easy # db/migrate/20120201040247_add_index_for_shop_id_on_orders.rb class AddIndexForShopIdOnOrders < ActiveRecord::Migration def
change add_index :orders, :shop_id end end -
Page Caching # app/controllers/products_controller.rb class ProductsController < ActionController caches_page :index
def index @products = Products.all end def create expire_page :action => :index end end # /opt/nginx/conf/nginx.conf location / { gzip_static on; } -
Action Caching # app/controllers/products_controller.rb class ProductsController < ActionController before_filter :authenticate
caches_action :index def index @products = Product.all end def create expire_action :action => :index end end -
Fragment Caching # app/views/products/index.html.erb <% Order.find_recent.each do |o| %> <%=
o.buyer.name %> bought <%= o.product.name %> <% end %> <% cache(‘all_products’) do %> All available products: <% Product.all.each do |p| %> <%= link_to p.name, product_url(p) %> <% end %> <% end %> # app/controllers/products_controller.rb class ProductsController < ActionController def update expire_fragment(‘all_products’) end end -
Russian Doll Caching # app/views/products/show.html.erb <% cache product do %>
Product options: <%= render product.options %> <% end %> # app/views/options/_option.html.erb <% cache option do %> <%= option.name %> <%= option.description %> <% end %> # app/models/product.rb class Product < ActiveRecord::Base has_many :options end # app/models/option.rb class Option < ActiveRecord::Base belongs_to :product, touch: true end -
Rescued by Resque class ReferralProcessor @queue = :referrals_queue def self.perform(schema_name,
order_item_id) order_item = OrderItem.find(order_item_id) order = order_item.order user = order.user credit = AccountCredit.credit(order_item.unit_price, user, 'referral') credit.message = I18n.t('account_credits.predefined_messages.referral', :description => order_item.description) credit.save! debit = Transaction.account_debit(credit.amount, user) debit.order = order debit.save! order.issue_refund(return_to_inventory: false, gateway_first: true, cancel_items: false, cancel_certificates: false, amount: credit.amount, as: 'original', notify_user: false) if user.receives_mail_for?(:referral_purchase) SystemMailer.referral_refund(order_item, credit).deliver end end end -
Get in Line class ReferralObserver < ActiveRecord::Observer def after_create(referral) Resque.enqueue_in(1.day,
ReferralProcessor, referral.item.id) end end # Get it started $ PIDFILE=./resque.pid \ BACKGROUND=yes \ QUEUE=referrals_queue \ rake environment resque:work -
It’s Free in Rails 4 Establishing basic Queue API. Implement
push and pop. Easily swap out for Resque, Sidekiq, Delayed job. -
Monitor your applications. Performance is not set it and forget
it. Database indexes are cheap, make more. Cache something, somewhere. Push work off to the background. Don’t neglect front-end performance.