Scale database reads to replicas in Rails
Distribute Reads 1.0 was recently released - see how to upgrade
🍊 Battle-tested at Instacart with Makara
Add this line to your application’s Gemfile:
gem "distribute_reads"ActiveRecordProxyAdapters does most of the work. First, update config/database.yml to use it:
default: &default
primary:
adapter: postgresql_proxy
url: <%= ENV["DATABASE_URL"] %>
replica:
adapter: postgresql
url: <%= ENV["REPLICA_DATABASE_URL"] %>
replica: true
development:
<<: *default
production:
<<: *defaultNote: You can use the same instance for the primary and replica in development.
Then add connects_to to app/models/application_record.rb:
class ApplicationRecord < ActiveRecord::Base
connects_to database: {writing: :primary, reading: :replica}
endBy default, all reads go to the primary instance. To use the replica, do:
distribute_reads { User.count }Works with multiple queries as well.
distribute_reads do
User.find_each do |user| # replica
user.orders_count = user.orders.count # replica
user.save! # primary
end
endDistribute all reads in a job with:
class TestJob < ApplicationJob
distribute_reads
def perform
# ...
end
endYou can pass any options as well.
Active Record uses lazy evaluation, which can delay the execution of a query to outside of a distribute_reads block. In this case, the primary will be used.
users = distribute_reads { User.where(orders_count: 1) } # not executed yetCall to_a or load inside the block to ensure the query runs on a replica.
users = distribute_reads { User.where(orders_count: 1).to_a }You can automatically load relations returned from distribute_reads blocks by creating an initializer with:
DistributeReads.eager_load = trueRaise an error when replica lag is too high (specified in seconds)
distribute_reads(max_lag: 3) do
# raises DistributeReads::TooMuchLag
endInstead of raising an error, you can also use primary
distribute_reads(max_lag: 3, lag_failover: true) do
# ...
endIf you have multiple databases, this only checks lag on ActiveRecord::Base connection. Specify connections to check with
distribute_reads(max_lag: 3, lag_on: [ApplicationRecord, LogRecord]) do
# ...
endNote: If lag on any connection exceeds the max lag and lag failover is used, all connections will use their primary.
If no replicas are available, primary is used. To prevent this situation from overloading the primary, you can raise an error instead.
distribute_reads(failover: false) do
# ...
endChange the defaults for distribute_reads blocks
DistributeReads.default_options = {
lag_failover: true,
failover: false
}Messages about failover are logged to the Active Record logger by default. Set a different logger with:
DistributeReads.logger = Logger.new(STDERR)Or use nil to disable logging.
At some point, you may wish to distribute reads by default.
DistributeReads.by_default = trueTo make queries go to primary, use:
distribute_reads(primary: true) do
# ...
endGet replication lag in seconds
DistributeReads.replication_lagMost of the time, ActiveRecordProxyAdapters does a great job automatically routing queries to replicas. If it incorrectly routes a query to primary, you can use:
distribute_reads(replica: true) do
# send all queries in block to replica
endRails 6+ has native support for replicas 🎉
ActiveRecord::Base.connected_to(role: :reading) do
# do reads
endHowever, it’s not able to do automatic statement-based routing yet.
Thanks to Nasdaq for ActiveRecordProxyAdapters, TaskRabbit for Makara, Sherin Kurian for the max lag option, and Nick Elser for the write-through cache.
ActiveRecordProxyAdapters is now used instead of Makara. Update config/database.yml and app/models/application_record.rb to use it.
View the changelog
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features
To get started with development and testing:
git clone https://github.com/ankane/distribute_reads.git
cd distribute_reads
createdb distribute_reads_test_primary
createdb distribute_reads_test_replica
bundle install
bundle exec rake test