SlavePools

Easy Single Master/ Multiple Slave Setup for use in Ruby/Rails projects

Build
Status

Overview

SlavePools replaces ActiveRecord's connection with a proxy that routes database interactions to the proper connection. Safe (whitelisted) methods may go to the current replica, and all other methods go to the master connection.

SlavePools also provides helpers so you can customize your replica strategy. You can organize replicas into pools and cycle through them (e.g. in a before_filter). You can make the connection default to the master, or the default replica pool, and then use block helpers to temporarily change the behavior (e.g. in an around_filter).

  • Uses a naming convention in database.yml to designate replica pools.
  • Defaults to a given replica pool, but may also be configured to default to master.
  • Routes database interactions (queries) to the right connection
    • Whitelisted queries go to the current connection (might be a replica).
    • All queries inside a transaction run on master.
    • All other queries are also sent to the master connection.
  • Supports ActiveRecord's in-memory query caching.
  • Helper methods can be used to easily load balance replicas, route traffic to different replica pools, or run directly against master. (examples below)

Not Supported

  • Sharding.
  • Automatic load balancing strategies.
  • Replica weights. You can accomplish this in your own load balancing strategy.
  • Whitelisting models that always use master.
  • Blacklisting poorly performing replicas. This could cause load spikes on your master. Whatever provisions your database.yml should make this choice.

Installation and Setup

Add to your Gemfile:

gem 'slave_pools'

Adding Replicas

Add entries to your database.yml in the form of <environment>_pool_<pool_name>_name_<db_name>

For example:

# Master connection for production environment
production:
  adapter: mysql
  database: myapp_production
  username: root
  password:
  host: localhost

# Default pool for production environment
production_pool_default_name_replica1:
  adapter: mysql
  database: replica_db1
  username: root
  password:
  host: 10.0.0.2
production_pool_default_name_replica2:
  ...

# Special pool for production environment
production_pool_admin_name_replica1:
  ...
production_pool_admin_name_another_replica:
  ...

Simulating Replicas

If you don't have any replicas (e.g. in your development environment), SlavePools will create a default pool containing only master. But if you want to mimic your production environment more closely you can create a read-only mysql user and use it like a replica.

# Development connection
development: &dev
  adapter: mysql
  database: myapp_development
  username: root
  password:
  host: localhost

development_pool_default_name_replica1:
  username: readonly
  <<: &dev

Don't do this in your test environment if you use transactional tests though! The replica connections won't be able to see any fixtures or factory data.

Configuring

Add a config/initializers/slave_pools.rb if you want to change config settings:

SlavePools.config.defaults_to_master = true

Usage

Toggle to next replica:

SlavePools.next_slave!

Specify a pool besides the default:

SlavePools.with_pool('other_pool') { #do stuff }

Specifically use the master for a call:

SlavePools.with_master { #do stuff }

Load Balancing

If you have multiple replicas in a pool and you'd like to load balance requests between them, you can easily accomplish this with a before_filter:

class ApplicationController < ActionController::Base
  after_filter    :switch_to_next_slave

  protected

  def switch_to_next_slave
    SlavePools.next_slave!
  end
end

Specialty Pools

If you have specialized replica pools and would like to use them for different controllers or actions, you can use an around_filter:

class ApplicationController < ActionController::Base
  around_filter   :use_special_replicas

  protected

  def use_special_replicas
    SlavePools.with_pool('special'){ yield }
  end
end

Replica Lag

By default, writes are sent to the master and reads are sent to replicas. But replicas might lag behind the master by seconds or even minutes. So if you write to master during a request you probably want to read from master in that request as well. You may even want to read from the master on the next request, to cover redirects.

Here's one way to accomplish that:

class ApplicationController < ActionController::Base

  around_filter   :stick_to_master_for_updates
  around_filter   :use_master_for_redirect #goes with above

  def stick_to_master_for_updates
    if request.get?
      yield
    else
      SlavePools.with_master { yield }
      session[:stick_to_master] = 1
    end
  end

  def use_master_for_redirect
    if session[:stick_to_master]
      session[:stick_to_master] = nil
      SlavePools.with_master { yield }
    else
      yield
    end
  end
end

Running specs

If you haven't already, install the rspec gem, then set up your database with a test database and a read_only user.

To match spec/config/database.yml, you can run:

rake bootstrap

From the plugin directory, run:

rspec spec

Authors

Author: Dan Drabik, Lance Ivy

Copyright (c) 2012-2013, Kickstarter

Released under the MIT license

See also

MultiDb

The project is based on:

Masochism

The original master/slave plugin: