Post on 19-Jan-2015
description
transcript
Intro to Rails #webperf
@amateurhuman
1.9.2
1.8.7
1.9.3
1.8.6
ruby -v
MRI 1.9.3
MRI 1.8.7
require ‘benchmark’
RBX 1.2.4
JRuby 1.6.7
Passengers,Unicorns,Pumas.Oh my!
PassengerSimple 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.
Puma
Based on Mongrel.Designed for concurrency.Uses real threads.
Anatomy of a Web Request
Redirects Cache DNS TCP SSL Request App Response DOM Render
Link Clicked
First Byte
DOM Loaded
Load
80% in Front-End
Inside the Web App
Database Performance
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.rbclass Customer < ActiveRecord::Base has_many :addressesend
# app/models/address.rbclass Address < ActiveRecord::Base belongs_to :customerend
# app/controllers/customers_controller.rbclass CustomersController < ApplicationController def index @customers = Customer.all endend
# 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 1SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 2 AND
"addresses"."primary" = 't' LIMIT 1SELECT "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.rbclass CustomersController < ApplicationController def index @customers = Customer.includes(:addresses).all endend
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)
Finding N+1
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.
Missing Indexes Hurt
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 endend
Cache All The Things
Page Caching# app/controllers/products_controller.rbclass ProductsController < ActionController caches_page :index def index @products = Products.all end def create expire_page :action => :index end end
# /opt/nginx/conf/nginx.conflocation / { gzip_static on;}
Action Caching# app/controllers/products_controller.rbclass 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.rbclass ProductsController < ActionController def update expire_fragment(‘all_products’) end
end
Expiring Cachesis Hard
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.rbclass Product < ActiveRecord::Base has_many :optionsend
# app/models/option.rbclass Option < ActiveRecord::Base belongs_to :product, touch: trueend
Background Jobs
ProcrastinateReporting.Sending email.Processing images.Call external services.Building & Expiring Caches.
Rescued by Resqueclass 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 endend
Get in Lineclass 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.
What’s One More Second?
7%Fewer Conversions
11%Fewer Page Views
Time isMoney
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.
30-day free trial atnewrelic.com/30
Q?