Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Intro to Rails Web Performance

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
Tweet

More Decks by Chris Kelly

Other Decks in Programming

Transcript

  1. Passenger Simple to operate. Simple configuration. Handles worker management. Great

    for multi-app environments. Great for low resource environments. Attached to Nginx/Apache HTTPD.
  2. 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.
  3. Redirects Cache DNS TCP SSL Request App Response DOM Render

    Link Clicked First Byte DOM Loaded Load
  4. 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.
  5. 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 %>
  6. 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
  7. 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)
  8. 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.
  9. 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; }
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. It’s Free in Rails 4 Establishing basic Queue API. Implement

    push and pop. Easily swap out for Resque, Sidekiq, Delayed job.
  16. 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.