Top Banner
Proprietary and Confidential When Rails hits the fan Eric Saxby @sax @ecdysone @sax Thursday, June 6, 13
115
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: When rails hits the fan

Proprietary and Confidential

When Rails hits the fan

Eric Saxby@sax @ecdysone @sax

Thursday, June 6, 13

Page 2: When rails hits the fan

This talk is about:

Proprietary and Confidential

User traffic increasing weekover week

Thursday, June 6, 13

Page 3: When rails hits the fan

Proprietary and Confidential

Databases outgrowing RAM

Thursday, June 6, 13

Page 4: When rails hits the fan

Proprietary and Confidential

Avg site response time over same period

Thursday, June 6, 13

Page 5: When rails hits the fan

Who am I? Why should you care?

Proprietary and Confidential

■ Application developerMany and various technologies. Worked with Rails for ~5 years.

■ Recent focus has been operationalChef, PostgreSQL, SmartOS, monitoring

■ TDD, BDD, Agile, DevOps, SOA, etcI care about how code is organized, and always want to learn how to do that better.

■ Now I’m at Wanelo

Thursday, June 6, 13

Page 6: When rails hits the fan

What is Wanelo?

Proprietary and Confidential

■ Wanelo (“Wah-nee-lo” from Want, Need Love) is a global platform for shopping.

Thursday, June 6, 13

Page 7: When rails hits the fan

Proprietary and Confidential

Marketing-free shopping across 100s of thousands of unique stores

Thursday, June 6, 13

Page 8: When rails hits the fan

Proprietary and Confidential

Personal feed of products from any store on the internetNo Il8n or l10n... yet!

Thursday, June 6, 13

Page 9: When rails hits the fan

Technology overview

Proprietary and Confidential

■ MRI Ruby 1.9.3 & Rails 3.2

■ PostgreSQL 9.2.4, Solr 3.6

■ Joyent Cloud, SmartOSZFS, ARC, raw IO performance, SmartOS, dTrace

■ Circonus, NewRelic, BoundaryMonitoring, graphing, alerting

■ Chef + Opscode

■ Amazon S3 + Fastly CDN

■ statsd, Graphite, nagios

Thursday, June 6, 13

Page 10: When rails hits the fan

How do you know whenyou are entering the

bad place?

Proprietary and ConfidentialThursday, June 6, 13

Page 11: When rails hits the fan

Let's go back to this for a sec

Proprietary and ConfidentialThursday, June 6, 13

Page 12: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 13: When rails hits the fan

What is the bad place?

Proprietary and Confidential

green: disk reads, red: disk writes on DB server

Is this actually a problem?

Thursday, June 6, 13

Page 14: When rails hits the fan

What is the bad place?

Proprietary and Confidential

Can be difficult to predict, using most of thedefault metrics we track

green: disk reads, red: disk writes on DB server

Thursday, June 6, 13

Page 15: When rails hits the fan

Utilization

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 16: When rails hits the fan

Utilization

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 17: When rails hits the fan

Utilization

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 18: When rails hits the fan

Utilization

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Develop spidey senses over time, but problemsare highly anecdotal

Thursday, June 6, 13

Page 19: When rails hits the fan

Saturation

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 20: When rails hits the fan

Saturation

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 21: When rails hits the fan

Saturation

Proprietary and Confidential

> iostat -xM 3 extended device statisticsdevice r/s w/s Mr/s Mw/s wait actv svc_t %w %bsd0 11.3 48.8 0.2 0.5 0.0 0.5 8.2 0 11sd1 13.9 42.2 0.2 0.6 0.0 0.3 5.8 0 11sd2 12.0 24.9 0.2 0.5 0.0 0.2 5.5 0 10sd3 12.3 27.2 0.2 0.5 0.0 0.4 9.6 0 10sd4 7.3 15.3 0.1 0.3 0.0 0.1 6.2 0 6sd5 13.0 14.6 0.2 0.3 0.0 0.1 4.8 0 7sd6 9.6 50.5 0.2 0.5 0.0 0.4 5.9 0 10sd7 7.0 46.5 0.1 0.6 0.0 0.4 7.5 0 11sd8 9.3 33.5 0.1 0.4 0.0 0.3 6.3 0 9sd9 7.6 32.5 0.1 0.4 0.0 0.2 6.1 0 7sd10 6.3 52.1 0.1 0.6 0.0 0.3 6.0 0 10sd11 7.6 50.2 0.1 0.6 0.0 0.5 8.5 0 12

Thursday, June 6, 13

Page 22: When rails hits the fan

Both are important!

Proprietary and Confidential

■ Tracking saturation helps you predict problems

■ Tracking utilization can tell you how to solve that problem

■ Basically, watch every video by Brendan Gregg on YouTubehttp://www.youtube.com/results?search_query=brendan+gregg

Thursday, June 6, 13

Page 23: When rails hits the fan

Know the limits of your data

Proprietary and Confidential

Averages can be extremely useful, butdo not give a complete picture

Thursday, June 6, 13

Page 24: When rails hits the fan

Know the limits of your data

Proprietary and Confidential

Outliers can cause severe problemseven when average is great

Thursday, June 6, 13

Page 25: When rails hits the fan

This is why we ❤ PostgreSQL

Proprietary and Confidential

■ pg_stat_activity

■ pg_stat_user_indexes

■ pg_stat_user_tables

■ pg_stat_statements

■ PostgreSQL gives you tools to monitor it and operate at scale

Thursday, June 6, 13

Page 26: When rails hits the fan

Proprietary and Confidential

pg_stat_statements

Thursday, June 6, 13

Page 27: When rails hits the fan

Ok, so back to thebad place

Proprietary and ConfidentialThursday, June 6, 13

Page 28: When rails hits the fan

Proprietary and Confidential

Hit ratio of File System Cache ondatabase server degrading over time

Thursday, June 6, 13

Page 29: When rails hits the fan

Proprietary and Confidential

Average response latencyincreasing over time, ties up CPU resources

Thursday, June 6, 13

Page 30: When rails hits the fan

Step 1: Cache all the things!

Proprietary and Confidential

■ Reduce load on DB(s)

■ Reduce rendering time

Thursday, June 6, 13

Page 31: When rails hits the fan

Cache invalidation?

Proprietary and Confidential

■ Use model updated_at in the cache key

■ Changing DB record invalidates all caches for that record

Thursday, June 6, 13

Page 32: When rails hits the fan

What were the goals of caching?

Proprietary and Confidential

■ Using model attributes as cache keys means you need to fetch DB records

■ At high scale, fetching DB records for every page becomes problematic

■ Cache sweepers are painful, but they're the only thing we've found to be scalable and reliable

Thursday, June 6, 13

Page 33: When rails hits the fan

Action cache all the things!

Proprietary and Confidential

■ Cache hits skip all rendering

■ Still able to run before filters, for instance when doing A/B testing

■ Some cached pages can be put behind a CDN

■ Requires page personalization to be added via Ajax

Thursday, June 6, 13

Page 34: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 35: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 36: When rails hits the fan

Fragment cache the rest!

Proprietary and Confidential

■ Fragments can be shared between pages

■ Can reduce rendering time

■ Can remove queries for related records

Thursday, June 6, 13

Page 37: When rails hits the fan

Proprietary and Confidential

■ Difficult to remove top level query

■ Joins/eager loading means some related records are still queried

■ How many trips to memcached?

Thursday, June 6, 13

Page 38: When rails hits the fan

Proprietary and Confidential

def multi_get_on_collection(objects, cache_options = {}) cache_keys = objects. inject(ActiveSupport::OrderedHash.new) do |key_map, obj| key_map[obj] = cache_options[:cache_key_proc].call obj key_map end

pre_rendered_objects = Rails.cache.read_multi *cache_keys.values

cache_keys.map do |object, cache_key| cached_html = pre_rendered_objects[cache_key]

if cached_html.present? && caching_enabled? cached_html else cache_options[:render_proc].call(object).tap do |fragment| Rails.cache.write cache_key, fragment, cache_options end end endend

Thursday, June 6, 13

Page 39: When rails hits the fan

Proprietary and Confidential

def multi_get_on_collection(objects, cache_options = {}) cache_keys = objects. inject(ActiveSupport::OrderedHash.new) do |key_map, obj| key_map[obj] = cache_options[:cache_key_proc].call obj key_map end

pre_rendered_objects = Rails.cache.read_multi *cache_keys.values

cache_keys.map do |object, cache_key| cached_html = pre_rendered_objects[cache_key]

if cached_html.present? && caching_enabled? cached_html else cache_options[:render_proc].call(object).tap do |fragment| Rails.cache.write cache_key, fragment, cache_options end end endend

Thursday, June 6, 13

Page 40: When rails hits the fan

Proprietary and Confidential

def multi_get_on_collection(objects, cache_options = {}) cache_keys = objects. inject(ActiveSupport::OrderedHash.new) do |key_map, obj| key_map[obj] = cache_options[:cache_key_proc].call obj key_map end

pre_rendered_objects = Rails.cache.read_multi *cache_keys.values

cache_keys.map do |object, cache_key| cached_html = pre_rendered_objects[cache_key]

if cached_html.present? && caching_enabled? cached_html else cache_options[:render_proc].call(object).tap do |fragment| Rails.cache.write cache_key, fragment, cache_options end end endend

Thursday, June 6, 13

Page 41: When rails hits the fan

MultiGet is your friend

Proprietary and Confidential

■ Turns 100+ memcached calls into a single request

cache_key = ->(product) do "products_thumb_#{product.id}" endrenderer = ->(product) do render "products/thumb", model: product end

multi_get_on_collection(@products, cache_key_proc: cache_key, render_proc: renderer, expires_in: 6.hours).join("\n").html_safe

Thursday, June 6, 13

Page 42: When rails hits the fan

Proprietary and Confidential

Increasing your cache hit ratio meansless queries against your database

Thursday, June 6, 13

Page 43: When rails hits the fan

Proprietary and Confidential

device r/s w/s Mr/s Mw/s wait actv svc_t %w %b

sd1 384.0 1157.5 48.0 116.8 0.0 8.8 5.7 2 100 sd1 368.0 1117.9 45.7 106.3 0.0 8.0 5.4 2 100 sd1 330.3 1357.5 41.3 139.1 0.0 9.5 5.6 2 100

DB latency increases

■ Even with highly efficient caches, iostat shows 100% disk busy

Thursday, June 6, 13

Page 44: When rails hits the fan

count(*)

Proprietary and ConfidentialThursday, June 6, 13

Page 45: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 46: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 47: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 48: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 49: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 50: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 51: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 52: When rails hits the fan

TOO MANY COUNTS

Proprietary and ConfidentialThursday, June 6, 13

Page 53: When rails hits the fan

Rails has the answer!

Proprietary and Confidential

■ counter_cache column on table

■ Adding records executes INCR

■ Removing records executes DECR

class Product < ActiveRecord::Base belongs_to :store, counter_cache: trueend

Thursday, June 6, 13

Page 54: When rails hits the fan

But...

Proprietary and Confidential

■ On very write heavy applications, multiple requests will update the same record

■ DEADLOCK errors

Thursday, June 6, 13

Page 55: When rails hits the fan

Use background jobs

Proprietary and Confidential

■ Stop updating counter caches on save

■ Queue a delayed job for the near future

■ Job performs a complete recalculation of the counter cache and is idempotent

■ The higher the count, the further we delay the job (less likely users will notice)

Thursday, June 6, 13

Page 56: When rails hits the fan

Deduplicate delayed jobs

Proprietary and Confidential

■ Sidekiq with UniqueJob plugin

■ Updates are serialized via a fixed number of workers

■ Workers can be stopped to alleviate DB load in an emergency

■ Same pattern can be applied elsewhere, like updating Solr indexes

Thursday, June 6, 13

Page 57: When rails hits the fan

Deduplicate delayed jobs

Proprietary and Confidential

class UpdateProductCountWorker < WaneloWorker sidekiq_options queue: :product_counts, unique: true wait 10.minutes

def perform!(params) id = params[:id].to_i ActiveRecord::Base.connection.execute %Q{ update stores set product_count = (select count(*) from products where store_id = stores.id) where stores.id = #{id} } endend

Thursday, June 6, 13

Page 58: When rails hits the fan

STILL TOO MANY COUNTS

Proprietary and ConfidentialThursday, June 6, 13

Page 59: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 60: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 61: When rails hits the fan

Proprietary and Confidential

Pagination gems run counts

Thursday, June 6, 13

Page 62: When rails hits the fan

Proprietary and Confidential

■ Kaminari executes a count(*) to determine total page count

Pagination gems run counts

Thursday, June 6, 13

Page 63: When rails hits the fan

Proprietary and Confidential

■ Kaminari executes a count(*) to determine total page count SELECT "stores".* FROM "stores" WHERE (state = 'approved') LIMIT 20 OFFSET 0

SELECT COUNT(*) FROM "stores" WHERE (state = 'approved')

Pagination gems run counts

Thursday, June 6, 13

Page 64: When rails hits the fan

Proprietary and Confidential

■ Kaminari executes a count(*) to determine total page count SELECT "stores".* FROM "stores" WHERE (state = 'approved') LIMIT 20 OFFSET 0

SELECT COUNT(*) FROM "stores" WHERE (state = 'approved')

Pagination gems run counts

■ We paginate EVERYTHING

■ We often already know total count from counter cache (or can hard code 10,000)

Thursday, June 6, 13

Page 65: When rails hits the fan

Proprietary and Confidential

module Kaminari module ActiveRecordRelationMethods # a workaround for AR 3.0.x that returns 0 for #count when page > 1 # if +limit_value+ is specified, load all the records and count them if ActiveRecord::VERSION::STRING < '3.1' def count(column_name = nil, options = {}) #:nodoc: limit_value ? length : super(column_name, options) end end

def total_count(column_name = nil, options = {}) #:nodoc: @total_count ||= begin c = except(:offset, :limit, :order)

# Remove includes only if they are irrelevant c = c.except(:includes) unless references_eager_loaded_tables?

# .group returns an OrderdHash that responds to #count c = c.count(column_name, options) if c.is_a?(ActiveSupport::OrderedHash) c.count else c.respond_to?(:count) ? c.count(column_name, options) : c end end end endend

Thursday, June 6, 13

Page 66: When rails hits the fan

Proprietary and Confidential

Time for some monkey patches!

module ActiveRecord class Relation def custom_counter(count) @total_count ||= count self end endend

module Sunspot module Search class AbstractSearch def custom_counter(count) @total ||= count self end end endend

@products = @store.products. custom_counter(@store.products_count). page(params[:page])

Thursday, June 6, 13

Page 67: When rails hits the fan

How do we know whatRails is really doing?

Proprietary and ConfidentialThursday, June 6, 13

Page 68: When rails hits the fan

ruby-prof and pilfer

Proprietary and Confidential

■ Profile the entire stack trace of an action or a set of classes

https://github.com/eric/pilfer

https://github.com/ruby-prof/ruby-prof

Thursday, June 6, 13

Page 69: When rails hits the fan

Proprietary and Confidential

■ Can work as Rack middleware

■ Outputs HTML that will tell you...

if Rails.env.profile? use Rack::RubyProf, :path => '/tmp/profile'end

Thursday, June 6, 13

Page 70: When rails hits the fan

Proprietary and Confidential

■ Can work as Rack middleware

■ Outputs HTML that will tell you...

if Rails.env.profile? use Rack::RubyProf, :path => '/tmp/profile'end

■ How long are we spending generating URLs???

Thursday, June 6, 13

Page 71: When rails hits the fan

Proprietary and ConfidentialThursday, June 6, 13

Page 72: When rails hits the fan

Proprietary and Confidential

module UrlHelpers

def product_path(product) "/p/#{product.id}/#{product.slug}" end

end

Thursday, June 6, 13

Page 73: When rails hits the fan

The importance offast JSON rendering

Proprietary and ConfidentialThursday, June 6, 13

Page 74: When rails hits the fan

HTML vs JSON

Proprietary and Confidential

■ Since launching native iOS and Android apps, the majority of Wanelo traffic is served via a JSON API

■ The longer we spend rendering JSON, the more CPUs are tied up, the more servers we need

Thursday, June 6, 13

Page 75: When rails hits the fan

Proprietary and Confidential

Removing RABL

Thursday, June 6, 13

Page 76: When rails hits the fan

Things we did not expect

Proprietary and Confidential

■ RABL serializes, then deserializes JSON to do merges!

■ ActiveSupport defines inefficient :to_json on every object

Thursday, June 6, 13

Page 77: When rails hits the fan

Proprietary and Confidential

■ Rendering JSON partials should hash.merge!

■ Fragment caching should marshal hashes, not JSON

■ Caching should allow for MultiGet

■ Should allow for arbitrary composition

■ JSON conversion should use OJ to call :to_json ONCE

https://github.com/wanelo/compositor

Thursday, June 6, 13

Page 78: When rails hits the fan

Proprietary and Confidential

require 'active_support/json'require 'active_support/core_ext/object/to_json'

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| klass.class_eval do def to_json(options = nil) Oj.dump(self, options) end endend

Thursday, June 6, 13

Page 79: When rails hits the fan

AsynchronousCommits

Proprietary and ConfidentialThursday, June 6, 13

Page 80: When rails hits the fan

Do you read, or do you write?

Proprietary and Confidential

■ Tools like iostat, vmstat, kstat, collectd pg_stat_user_tables can show you utilization

■ Reads much easier to scale than writes. Read/write splitting, caching.

■ What do you do when writes become the bottleneck?

Thursday, June 6, 13

Page 81: When rails hits the fan

Writes committed to disk?

Proprietary and Confidential

■ Some workloads are more lenient for delay/possible data loss

■ Applicable to many technologies

■ Even microsecond delays can reduce load

■ Waiting for Solr to respond can keep DB transactions open longer

Thursday, June 6, 13

Page 82: When rails hits the fan

i.e. PostgreSQL

Proprietary and ConfidentialThursday, June 6, 13

Page 83: When rails hits the fan

i.e. Solr

Proprietary and Confidential

<!-- Perform a <commit/> automatically under certain conditions --> <autoCommit> <!-- number of updates since last commit --> <maxDocs>1000</maxDocs>

<!-- oldest uncommited update (in ms) long ago --> <maxTime>30000</maxTime></autoCommit>

Thursday, June 6, 13

Page 84: When rails hits the fan

What happens when datagrows larger than a

single database?

Proprietary and ConfidentialThursday, June 6, 13

Page 85: When rails hits the fan

Proprietary and Confidential

Growth of one key DB table over 3 months

Thursday, June 6, 13

Page 86: When rails hits the fan

Service OrientedArchitecture

Proprietary and ConfidentialThursday, June 6, 13

Page 87: When rails hits the fan

The only real way to scale

Proprietary and Confidential

■ Tune code, infrastructure for a very particular workload

■ Hide sharding from other codebases

■ Allow small teams to manage small(er) codebases

Thursday, June 6, 13

Page 88: When rails hits the fan

The only real way to scale

Proprietary and Confidential

■ Tune code, infrastructure for a very particular workload

■ Hide sharding from other codebases

■ Allow small teams to manage small(er) codebases

■ Everyone talks about why, no-one talks about how in the Rails world

Thursday, June 6, 13

Page 89: When rails hits the fan

Services are hard (the first time)

Proprietary and Confidential

■ Synchonous vs asynchronous data persistence

■ Message passing

■ Testing

■ Iterative development

Thursday, June 6, 13

Page 90: When rails hits the fan

Iteration is the key

Proprietary and Confidential

■ Isolate data

■ Isolate interface

■ Extract interface with an database adapter

■ Launch service layer

■ Switch interface to user service adapter

Thursday, June 6, 13

Page 91: When rails hits the fan

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend

Thursday, June 6, 13

Page 92: When rails hits the fan

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend X

Thursday, June 6, 13

Page 93: When rails hits the fan

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend Xclass Product < ActiveRecord::Base def saves Save.where(product_id: self.id) endend

Thursday, June 6, 13

Page 94: When rails hits the fan

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend Xclass Product < ActiveRecord::Base def saves Save.where(product_id: self.id) endend

Do this everywhere (you have tests, right?)

Thursday, June 6, 13

Page 95: When rails hits the fan

Proprietary and Confidential

class Save < ActiveRecord::Base establish_connection "saves_#{Rails.env}"end

■ Set up a read replica

■ Take down site

■ Promote replica to be a master

■ Restart unicorns with new config

■ Bring up site

■ Clean up unnecessary tables on each DB

Thursday, June 6, 13

Page 96: When rails hits the fan

Proprietary and Confidential

class Save < ActiveRecord::Base establish_connection "saves_#{Rails.env}"

def self.by_product(product) where(product_id: product.id) endend

class Product < ActiveRecord::Base def saves Save.by_product(self) endend

■ Reduce Ruby interface to minimum possible

■ Easy to deploy this

Thursday, June 6, 13

Page 97: When rails hits the fan

Proprietary and Confidential

class Save include SavesClientend

module SavesClient def self.included(other) other.send(:attr_accessor, :id, :product_id) other.extend ClientClassMethods end

module ClientClassMethods def by_product(*args) adapter.new(self).by_product(*args) end

def adapter @adapter ||= SavesClient::DbAdapter end endend

Thursday, June 6, 13

Page 98: When rails hits the fan

Proprietary and Confidential

class Save include SavesClientend

module SavesClient def self.included(other) other.send(:attr_accessor, :id, :product_id) other.extend ClientClassMethods end

module ClientClassMethods def by_product(*args) adapter.new(self).by_product(*args) end

def adapter @adapter ||= SavesClient::DbAdapter end endend

Thursday, June 6, 13

Page 99: When rails hits the fan

Proprietary and Confidential

class Save include SavesClientend

module SavesClient def self.included(other) other.send(:attr_accessor, :id, :product_id) other.extend ClientClassMethods end

module ClientClassMethods def by_product(*args) adapter.new(self).by_product(*args) end

def adapter @adapter ||= SavesClient::DbAdapter end endend

Thursday, June 6, 13

Page 100: When rails hits the fan

Proprietary and Confidential

module SavesClient class DbAdapter def self.close_connections # Check in the database connection, # since we're shutting down this thread SavesService::Save.clear_active_connections! end

def by_product(*args) relation :by_product, *args end

def relation(method, *args) SavesClient::AdapterRelation.new(self, SavesService::Save.send(method, *args)) end

def all(scope) # Scope is an AR Relation instance returned from # SavesService::Save.by_product(product) scope.all.map { |m| client_class.new save_attrs_from(m) } end endend

Thursday, June 6, 13

Page 101: When rails hits the fan

Proprietary and Confidential

module SavesClient class DbAdapter def self.close_connections # Check in the database connection, # since we're shutting down this thread SavesService::Save.clear_active_connections! end

def by_product(*args) relation :by_product, *args end

def relation(method, *args) SavesClient::AdapterRelation.new(self, SavesService::Save.send(method, *args)) end

def all(scope) # Scope is an AR Relation instance returned from # SavesService::Save.by_product(product) scope.all.map { |m| client_class.new save_attrs_from(m) } end endend

Thursday, June 6, 13

Page 102: When rails hits the fan

Proprietary and Confidential

module SavesClient class DbAdapter def self.close_connections # Check in the database connection, # since we're shutting down this thread SavesService::Save.clear_active_connections! end

def by_product(*args) relation :by_product, *args end

def relation(method, *args) SavesClient::AdapterRelation.new(self, SavesService::Save.send(method, *args)) end

def all(scope) # Scope is an AR Relation instance returned from # SavesService::Save.by_product(product) scope.all.map { |m| client_class.new save_attrs_from(m) } end endend

Thread safety is EXTREMELY importantThursday, June 6, 13

Page 103: When rails hits the fan

Proprietary and Confidential

module SavesClient class AdapterRelation attr_reader :adapter, :scope

def initialize(adapter, scope) @adapter, @scope = adapter, scope end

def limit(num); end def order(order); end def page(num); end def first; end

def all adapter.all(scope) end endend

■ Calling :by_product instantiates a Relation

■ Calling :all executes the query

Thursday, June 6, 13

Page 104: When rails hits the fan

What executes the query?

Proprietary and Confidential

■ The ActiveRecord model moves into the gem

■ The adapter translates the Save methods into AR calls, maps columns into attributes on our class

■ Important to deploy this at this stage, as there many problems to solve, like:

■ Getting your tests green, fixtures consistent

■ Figuring out whether you really covered all access patterns

module SavesService class Save < ActiveRecord::Base endend

Thursday, June 6, 13

Page 105: When rails hits the fan

Minimize the Ruby access

Proprietary and Confidential

■ We were able to reduce everything to 7 scopes, i.e. :by_product, :by_user, etc

■ Reduced Relation methods to these:

■ limit

■ page

■ order

■ count

■ all

■ first

■ last

■ pluck

■ find_in_batches

Thursday, June 6, 13

Page 106: When rails hits the fan

Launch the service layer

Proprietary and Confidential

■ Now that we have a small public Ruby interface, we can pair that to a Sinatra app

■ Sinatra can serve as a long-term fake for Selenium, even after service is re-written

Thursday, June 6, 13

Page 107: When rails hits the fan

module SavesService class Web < Sinatra::Base set :environment, ENV['RACK_ENV'] || "development" PAGE_SIZE = 50

ActiveRecord::Base.include_root_in_json = false register Sinatra::ActiveRecordExtension

configure do set :database_file, SavesService.config.db_config set :root, File.expand_path("../../../", __FILE__) disable :raise_errors disable :show_exceptions set :logger, nil end

error do e = env['sinatra.error'] ActiveRecord::Base.logger.error ["#{e.class}: #{e.message}", *e.backtrace].join("\n ") status 500 body '{"errors":":("}' end

before { content_type 'application/json' } endend

Proprietary and ConfidentialThursday, June 6, 13

Page 108: When rails hits the fan

Proprietary and Confidential

get '/products/:pid/saves' do paginate SavesService::Save.by_product(params[:pid])end

private

def paginate(scope) return count(scope) if params[:count].present?

order = params[:order] if params[:order] =~ /\Adesc|asc\Z/i scope = scope.order("created_at #{order || 'desc'}")

limit = params[:limit] ? params[:limit].to_i : PAGE_SIZE page_number = [params[:page].to_i - 1, 0].max * limit scope = scope.offset(page_number).limit(limit)

scope = scope.pluck(params[:pluck]) if params[:pluck]

body Oj.dump(saves: scope)end

Thursday, June 6, 13

Page 109: When rails hits the fan

Proprietary and Confidential

#!/usr/bin/env rubyrequire 'optparse'

options = { :environment => 'development', :port => 3000}

OptionParser.new do |opts| opts.banner = "Usage: saves_service [options]"

opts.on("-d", "--dbconfig OPT", "path to database.yml") do |opt| options[:db_config] = File.expand_path(opt, Dir.pwd) end

opts.on("-E", "--environment OPT", "RACK_ENV to use") do |opt| options[:environment] = opt end

opts.on("-p", "--port OPT", "port to use") do |opt| options[:port] = opt endend.parse!

cmd_env = { 'RACK_ENV' => options[:environment], 'DB_CONFIG' => options[:db_config],}.delete_if{|k,v| v.nil? }

rackup_file = File.expand_path('../../config.ru', __FILE__)

exec cmd_env, "unicorn -p #{options[:port]} #{rackup_file}"

Thursday, June 6, 13

Page 110: When rails hits the fan

DbAdapter vs HTTPAdapter

Proprietary and Confidential

■ Maps Ruby interface to Net::HTTP::Persistent

■ Deserializes JSON into values on class

■ Scope becomes a mapby_product(1) => "/products/1/saves"

■ Finder methods map paramslimit(10) => "?limit=10"

■ AdapterRelation uses Adapter to fetch records, does not change at all

Thursday, June 6, 13

Page 111: When rails hits the fan

Proprietary and Confidential

module SavesClient class AdapterRelation attr_reader :adapter, :scope

def initialize(adapter, scope) @adapter, @scope = adapter, scope end

def limit(num); end def order(order); end def page(num); end def first; end

def all adapter.all(scope) end endend

Thursday, June 6, 13

Page 112: When rails hits the fan

Deploy as a setting change

Proprietary and Confidential

Rails.application.config.after_initialize do |app|

if Settings.saves_service.enabled Save.saves_base_url = Settings.saves_service.url else require 'saves_client/db_adapter' Save.adapter = SavesClient::DbAdapter

require 'saves_service/save' saves_db = "saves_#{Rails.env}" SavesService::Save.establish_connection saves_db end

end

Thursday, June 6, 13

Page 113: When rails hits the fan

Takeaways

Proprietary and ConfidentialThursday, June 6, 13

Page 114: When rails hits the fan

Proprietary and Confidential

■ Choose technologies that are easy to operate and monitor

■ Don't immediately break when they hit resource thresholds

■ Sound replication strategies

■ Assume that data should be tracked, even if you don't yet understand the relevance

■ Small iterative performance improvements can have massive payoff over time

Thursday, June 6, 13

Page 115: When rails hits the fan

Thanks!

Proprietary and Confidential

@sax

@ecdysone

@sax

https://github.com/wanelo

https://github.com/wanelo-chef

Thursday, June 6, 13