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.

96a846bf1220d8e02ee5b5040e825bb5?s=128

Chris Kelly

July 23, 2012
Tweet

Transcript

  1. Intro to Rails #webperf

  2. None
  3. @amateurhuman

  4. 1.9.2 1.8.7 1.9.3 1.8.6 ruby -v

  5. MRI 1.9.3 MRI 1.8.7 require ‘benchmark’ RBX 1.2.4 JRuby 1.6.7

  6. Passengers, Unicorns, Pumas. Oh my!

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

    for multi-app environments. Great for low resource environments. Attached to Nginx/Apache HTTPD.
  8. 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.
  9. Puma Based on Mongrel. Designed for concurrency. Uses real threads.

  10. Anatomy of a Web Request

  11. Redirects Cache DNS TCP SSL Request App Response DOM Render

    Link Clicked First Byte DOM Loaded Load
  12. 80% in Front-End

  13. Inside the Web App

  14. Database Performance

  15. 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.
  16. 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 %>
  17. 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
  18. 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)
  19. Finding N+1

  20. 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.
  21. Missing Indexes Hurt

  22. 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
  23. Cache All The Things

  24. 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; }
  25. 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
  26. 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
  27. Expiring Caches is Hard

  28. 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
  29. Background Jobs

  30. Procrastinate Reporting. Sending email. Processing images. Call external services. Building

    & Expiring Caches.
  31. 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
  32. 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
  33. It’s Free in Rails 4 Establishing basic Queue API. Implement

    push and pop. Easily swap out for Resque, Sidekiq, Delayed job.
  34. What’s One More Second?

  35. 7% Fewer Conversions

  36. 11% Fewer Page Views

  37. Time is Money

  38. 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.
  39. 30-day free trial at newrelic.com/30 Q?