when rails hits the fan

115
Proprietary and Confidential When Rails hits the fan Eric Saxby @sax @ecdysone @sax Thursday, June 6, 13

Upload: s4xx4s

Post on 20-Aug-2015

1.987 views

Category:

Technology


1 download

TRANSCRIPT

Proprietary and Confidential

When Rails hits the fan

Eric Saxby@sax @ecdysone @sax

Thursday, June 6, 13

This talk is about:

Proprietary and Confidential

User traffic increasing weekover week

Thursday, June 6, 13

Proprietary and Confidential

Databases outgrowing RAM

Thursday, June 6, 13

Proprietary and Confidential

Avg site response time over same period

Thursday, June 6, 13

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

What is Wanelo?

Proprietary and Confidential

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

Thursday, June 6, 13

Proprietary and Confidential

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

Thursday, June 6, 13

Proprietary and Confidential

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

Thursday, June 6, 13

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

How do you know whenyou are entering the

bad place?

Proprietary and ConfidentialThursday, June 6, 13

Let's go back to this for a sec

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

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

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

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

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

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

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

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

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

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

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

Know the limits of your data

Proprietary and Confidential

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

Thursday, June 6, 13

Know the limits of your data

Proprietary and Confidential

Outliers can cause severe problemseven when average is great

Thursday, June 6, 13

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

Proprietary and Confidential

pg_stat_statements

Thursday, June 6, 13

Ok, so back to thebad place

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and Confidential

Hit ratio of File System Cache ondatabase server degrading over time

Thursday, June 6, 13

Proprietary and Confidential

Average response latencyincreasing over time, ties up CPU resources

Thursday, June 6, 13

Step 1: Cache all the things!

Proprietary and Confidential

■ Reduce load on DB(s)

■ Reduce rendering time

Thursday, June 6, 13

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

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

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

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

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

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

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

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

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

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

Proprietary and Confidential

Increasing your cache hit ratio meansless queries against your database

Thursday, June 6, 13

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

count(*)

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

TOO MANY COUNTS

Proprietary and ConfidentialThursday, June 6, 13

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

But...

Proprietary and Confidential

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

■ DEADLOCK errors

Thursday, June 6, 13

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

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

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

STILL TOO MANY COUNTS

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and Confidential

Pagination gems run counts

Thursday, June 6, 13

Proprietary and Confidential

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

Pagination gems run counts

Thursday, June 6, 13

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

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

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

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

How do we know whatRails is really doing?

Proprietary and ConfidentialThursday, June 6, 13

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

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

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

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and Confidential

module UrlHelpers

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

end

Thursday, June 6, 13

The importance offast JSON rendering

Proprietary and ConfidentialThursday, June 6, 13

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

Proprietary and Confidential

Removing RABL

Thursday, June 6, 13

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

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

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

AsynchronousCommits

Proprietary and ConfidentialThursday, June 6, 13

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

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

i.e. PostgreSQL

Proprietary and ConfidentialThursday, June 6, 13

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

What happens when datagrows larger than a

single database?

Proprietary and ConfidentialThursday, June 6, 13

Proprietary and Confidential

Growth of one key DB table over 3 months

Thursday, June 6, 13

Service OrientedArchitecture

Proprietary and ConfidentialThursday, June 6, 13

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

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

Services are hard (the first time)

Proprietary and Confidential

■ Synchonous vs asynchronous data persistence

■ Message passing

■ Testing

■ Iterative development

Thursday, June 6, 13

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

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend

Thursday, June 6, 13

Proprietary and Confidential

class Product < ActiveRecord::Base has_many :savesend X

Thursday, June 6, 13

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Takeaways

Proprietary and ConfidentialThursday, June 6, 13

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

Thanks!

Proprietary and Confidential

@sax

@ecdysone

@sax

https://github.com/wanelo

https://github.com/wanelo-chef

Thursday, June 6, 13