39
In Today’s Session You Will Learn how to: Gain visibility on site performance Improve scalability and uptime Find and fix key bottlenecks

6 tips for improving ruby performance

Embed Size (px)

Citation preview

Page 1: 6 tips for improving ruby performance

In Today’s Session You Will Learn how to: •  Gain visibility on site performance •  Improve scalability and uptime •  Find and fix key bottlenecks

Page 2: 6 tips for improving ruby performance

•  Database •  Web Servers •  Caching •  Background Processing

New Relic + Engine Yard

Page 3: 6 tips for improving ruby performance

Web Request Overview

Page 4: 6 tips for improving ruby performance

Web Application Overview

Page 5: 6 tips for improving ruby performance

DATA BASE

Page 6: 6 tips for improving ruby performance

Lazy loading associated data can quickly lead to an N+1 query problem.

ORMs (ActiveRecord, DataMapper, etc.) make it easy to get our data but also make it easy to forget to optimize and refactor.

N+1 problems are hard to spot in development since you are working with limited data sets.

Database Performance

Page 7: 6 tips for improving ruby performance

# 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

Page 8: 6 tips for improving ruby performance

# 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

N+1 Query Creep

Page 9: 6 tips for improving ruby performance

# 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)

Eager Loading with .includes

Page 10: 6 tips for improving ruby performance

New Relic > App Server > Web Transactions > Performance Breakdown

Finding N+1 in New Relic

Page 11: 6 tips for improving ruby performance

Missing Indexes == Slow Queries

Page 12: 6 tips for improving ruby performance

# db/migrate/20120201040247_add_index_for_shop_id_on_orders.rb

class AddIndexForShopIdOnOrders < ActiveRecord::Migration

def change

add_index :orders, :shop_id

end

end

Adding an Index is Simple

Index Protips:

•  Searching an index on a table with 1,000 rows is 100x faster than searching a table without an index.

•  Put an index on any columns you will likely query against, it's better to have too many than too few indexes.

•  Adding an index to a table will lock the table!

Page 13: 6 tips for improving ruby performance

RUBY WEB SERVERS

Page 14: 6 tips for improving ruby performance

•  Simple to operate •  Simple configuration •  Handles worker management •  Great for multi-application environments •  Great for low resource environments •  Attached to Nginx/Apache HTTPD

Passenger 3

Page 15: 6 tips for improving ruby performance

Passenger Request Queue

solo i-c3f2d8a2 ~ # passenger-status ----------- General information ----------- max = 3 count = 3 active = 0 inactive = 3 Waiting on global queue: 0 ----------- Application groups ----------- /data/john_yerhot_org/current: App root: /data/john_yerhot_org/current * PID: 19802 Sessions: 0 Processed: 3 Uptime: 3h 10m 13s /data/scalingrails/current: App root: /data/scalingrails/current * PID: 28726 Sessions: 0 Processed: 3 Uptime: 59m 22s /data/sites/clmeisinger/current: App root: /data/sites/clmeisinger/current * PID: 22147 Sessions: 0 Processed: 70 Uptime: 10h 45m 57s

Page 16: 6 tips for improving ruby performance

•  Independent of front end web server •  More configuration options •  Master process will reap children on timeout •  Great for single application environments •  Allows for zero downtime deploys

Unicorn

Page 17: 6 tips for improving ruby performance

Unicorn Request Queue?

Raindrops solo i-5b74313d ~ # gem install raindrops Fetching: raindrops-0.10.0.gem (100%) Building native extensions. This could take a while... Successfully installed raindrops-0.10.0 1 gem installed solo i-5b74313d ~ # ruby -rubygems -e "require 'raindrops'; puts Raindrops::Linux.unix_listener_stats(['/var/run/engineyard/unicorn_appname.sock']).inspect" {"/var/run/engineyard/unicorn_appname.sock"=>#<struct Raindrops::ListenStats active=0, queued=0>}

Page 18: 6 tips for improving ruby performance

Request Queuing in New Relic

Page 19: 6 tips for improving ruby performance

Request Queuing in New Relic

NOT COOL

Page 20: 6 tips for improving ruby performance

Request Queuing in New Relic

•  Time between first ActionContoller hit - X-Queue-Start = Time spent in queuing.

Internet => LB inserts X-Queue-Start => Nginx => Ruby Webserver => Rack => Application

Track Rack Middleware as well def call(env) env["HTTP_X_MIDDLEWARE_START"] = "t=#{(Time.now.to_f * 1000000).to_i}" @app.call(env) end

Page 21: 6 tips for improving ruby performance

CACHING

Page 22: 6 tips for improving ruby performance

Cache Everything

Rails makes it stupid easy to

cache everything. Do it.

Page 23: 6 tips for improving ruby performance

Static Files & Nginx

The best cache is a static file served by Nginx.

# create it on #index, #show, etc.. caches_page :index # expire it on #creates, #updates, #destory, etc... expire_page :action => :index

Page 24: 6 tips for improving ruby performance

A Note About Static Files:

Use the front end server. upstream upstream_enki { server unix:/var/run/engineyard/unicorn_enki.sock fail_timeout=0; } location ~ ^/(images|assets|javascripts|stylesheets)/ { try_files $uri $uri/index.html /last_assets/$uri /last_assets/$uri.html @app_enki; expires 10y; } location / { if (-f $document_root/system/maintenance.html) { return 503; } try_files $uri $uri/index.html $uri.html @app_enki; }

Page 25: 6 tips for improving ruby performance

Memcached: The Standard

# config/initializers/memcached.rb config.cache_store =:mem_cache_store,

"server-1:11211", "server-2:11211", "server-3:11211",

"server-4:11211"

Page 26: 6 tips for improving ruby performance

Next Best: ActionCaching

Will still go through Rack/Rails, but the action gets cached. before_filter :make_sure_youre_ok caches_action :all_the_things def all_the_things @all_things = Thing.all_in_a_complex_way end def expire expire_action :action => :all_the_things end

Page 27: 6 tips for improving ruby performance

Fragment Caching

<% cache('my_cache_key') do %> <%= render_large_tag_cloud %> <% end %> ... def update_large_tag_cloud TagCloud.update expire_fragment('my_cache_key') end

Page 28: 6 tips for improving ruby performance

Baremetal

Rails.cache.write("john", "yerhot") Rails.cache.read("john")# => "yerhot" # execute a block on miss and cache it. Rails.cache.fetch("miss") do "yerhot" end Rails.fetch("miss")# => "yerhot" Rails.cache.exists("john") # => true Rails.cache.delete("john") # => true Rails.cache.exists("john") # => false

Page 29: 6 tips for improving ruby performance

Background Processing

Page 30: 6 tips for improving ruby performance

•  send email •  process images •  grab feeds and cache them •  complex computations/reports •  create/expire caches/pages (like Reddit)

Why Background Processing?

Page 31: 6 tips for improving ruby performance

Best Practice:

Use a utility server for background jobs and cron.

Page 32: 6 tips for improving ruby performance

Resque to the Rescue

Page 33: 6 tips for improving ruby performance

Resque in New Relic

Page 34: 6 tips for improving ruby performance

Delayed Job Too

Page 35: 6 tips for improving ruby performance

Background Processing baked in.

•  Allow an application to switch job systems with minimal code change due to common API

•  Very basic queuing system built in •  Roll your own wrapper class that responds to push & pop

Rails 4

# application.rb config.queue = QueueName Rails.queue.push(Job.new)

Page 36: 6 tips for improving ruby performance

•  You need to be monitoring your application.

•  Performance has to be reviewed on a regular basis.

•  Database indexes are cheap, make lots of them.

•  Every application can take advantage of some level of caching: page, action or fragment.

•  Background any work that you can.

•  Don't neglect front-end performance.

Review

Page 37: 6 tips for improving ruby performance

New Relic Standard is Free at Engine Yard

1.  If you’re an Engine Yard Customer, select your plan in your Engine Yard Account Settings

2.  Add newrelic_rpm to your Gemfile

3.  Enable monitoring in the Engine Yard Dashboard

Full Installation Details: http://ey.io/install-newrelic

How to Install New Relic

Page 38: 6 tips for improving ruby performance

Questions?

Page 39: 6 tips for improving ruby performance

Chris Kelly @amateurhuman www.newrelic.com

John Yerhot @yerhot www.engineyard.com

Thanks for Watching!