特别感谢:我实现的主从分离,基于 tumayun ( https://github.com/tumayun )的 master_slave ( https://github.com/tumayun/master_slave )项目。该项目是 rails 4 的。由于 Rails4 和 Rails3 在数据库访等方面的接口变化,导致该项目不适合 rails3,因此我将其改造成 rails3,并写了这篇笔记分析其实现。
( 我的Blog http://blog.yuaz.net 暂时无法无法创建文章,因此将这篇笔记贴在这里)
默认情况下,Rails 项目仅支持一个数据库访存。当出现数据库访问瓶颈时,就要考虑主从数据库分离:在主库上写,在从库上读。只要从库的延迟在可接受的范围内,这样的分离可以大大提高性能。本文分析Rails的源码,介绍添加从库的方式。
在程序上,如何访问从库?
大部分情况下,项目从一开始都不考虑主从分离。因此,对从库的支持,通常是在对现有代码的重构基础上进行。原则是,在不大改现有代码的前提下,支持从库;并且能很方便地调用。这里希望通过Ruby语言的代码块来切库。
假设现有的读库程序为
posts = Post.recent.limit(10)
那么使用如下方式,在代码块使用从库,在代码块之外访问使用原有逻辑。
Post.using(:slave) do
posts = Post.recent.limit(10)
end
这样,Post.using
方法应该实现:将数据库连接切换到从库,调用代码块,将数据库连接还原。
查阅 Rails 的源码可知,所有的数据库操作,都落在了 ActiveRecord::Base.connection
方法上。因此,可以修改这个方法,在 Post.using(:slave) {}
代码块里,使用从库;在代码外,使用原来的方式。
伪代如下
def connection_with_master_slave
if using_slave? # 是否使用从库
# connection_with_slave # 返回从库连接
else
# connection_without_slave # 返回主库的连接
end
end
通常可以使用 ActiveRecord::Base.establish_connection
来建立连接。源码如下
# activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +128
def self.establish_connection(spec = ENV["DATABASE_URL"])
resolver = ConnectionSpecification::Resolver.new spec, configurations
spec = resolver.spec
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
remove_connection
connection_handler.establish_connection name, spec
end
ActiveRecord::Base
会持有一个类变量 connection_handler
,它是 ActiveRecord::ConnectionAdapters::ConnectionHandler
的实例 ,从字面上就能看出 ActiveRecord::Base
通过它来管理数据库连接,并通过它来真正创建连接。
ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection
实现如下
def establish_connection(name, spec)
@connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec)
@class_to_pool[name] = @connection_pools[spec]
end
从这里可以看出 spec 是连接池的标识符。通过阅读更多源码可知,ActiveRecord
将类名(即这里的name
)和连接池,通过 @class_to_pool
对应起来。这样不同的类,可以采用不同的连接池了。
但增加从库,并不是实现不同的类,使用不同的连接池。而是同一个类,在不同场景下,使用不同的连接池。因此,要对每个从库建立一个连接池。并且有唯一标识。只要参考 ActiveRecord::Base.establish_connection
的实现,来创建连接即可。
另外在 ActiveRecord::Base.establish_connection
里,调用了 remove_connection
方法,其实现为
def remove_connection(klass = self)
connection_handler.remove_connection(klass)
end
即 ActiveRecord::ConnectionAdapters::ConnectionHandler#remove_connection
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +373
def remove_connection(klass)
pool = @class_to_pool.delete(klass.name)
return nil unless pool
@connection_pools.delete pool.spec
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
可以看出这里移除了类和连接池的对应关系。方法里调用了 klass.name
。这里看起来有些不一致, ActiveRecord::ConnectionAdapters::ConnectionHandler(name, spec)
在创建连接时,直接使用了参数 name
,而在 ActiveRecord::ConnectionAdapters::ConnectionHandler.remove_connection(klass)
时,又调用了 klass.name
。
因此,这里需要一个拥有 #name
方法的对象,返回连接池的标识。这里定义新的类来做
module MasterSlave
module ConnectionHandler
class ArProxy
attr_reader :name
def initialize(name)
@name = name
end
end
end
end
创建连接池的代码如下
module MasterSlave
class ConnectionHandler
def self.setup_connection
# activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +128
configuration = {"adapter"=>"sqlite3", "database"=>"db/slave.sqlite3", "pool"=>5, "timeout"=>5000}
resolver = ActiveRecord::Base::ConnectionSpecification::Resolver.new(configuration, nil)
spec = resolver.spec
unless ActiveRecord::Base.respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
ar_proxy = ArProxy.new(connection_pool_name(slave_name))
# activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +179
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +424
# remove_connection 时会调用方法内部 ar_proxy.name
ActiveRecord::Base.remove_connection(ar_proxy)
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +373
# establish_connection 时,使用 ar_proxy.name
ActiveRecord::Base.connection_handler.establish_connection ar_proxy.name, spec
end
end
end
以上创建连接的代码,应该在rails启动时执行,且执行一次。
ActiveRecord::Base.connection
的实现如下:
class << self
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
retrieve_connection
end
def retrieve_connection
connection_handler.retrieve_connection(self)
end
end
即
module ActiveRecord
module ConnectionAdapters
def retrieve_connection(klass) #:nodoc:
pool = retrieve_connection_pool(klass)
(pool && pool.connection) or raise ConnectionNotEstablished
end
def retrieve_connection_pool(klass)
pool = @class_to_pool[klass.name]
return pool if pool
return nil if ActiveRecord::Base == klass
retrieve_connection_pool klass.superclass
end
end
end
可以看出,获取连接最终使用的是 ActiveRecord::ConnectionAdapters#retrieve_connection_pool(klass)
,注意使用了 klass.name
方法。参考建立连接的代码,应该使用如下代码获取合适的连接池。
ActiveRecord::Base.connection_handler.retrieve_connection(ar_proxy)
最终,切换数据库连接的代码如下
module MasterSlave
module Base
extend ActiveSupport::Concern
included
# rails 3 弃用了 alias_method_chain,所以这里用 alias_method 实现类似功能
alias_method :connection_without_master_slave, :connection
alias_method :connection, :connection_with_master_slave
end
module ClassMethods
def connection_with_master_slave
slave_block = Thread.current["[MasterSlave]slave_block"]
current_slave_name = Thread.current["[MasterSlave]current_slave_name"]
if slave_block && current_slave_name
pool_name = MasterSlave::ConnectionHandler.connection_pool_name(current_slave_name)
ar_proxy = MasterSlave::ConnectionHandler::ArProxy.new(pool_name)
ActiveRecord::Base.connection_handler.retrieve_connection(ar_proxy)
else
connection_without_master_slave
end
end
def using(slave_name, &block)
# 用thread local 变量来保存当前的数据库主从状态
Thread.current["[MasterSlave]current_slave_name"] = slave_name
Thread.current["[MasterSlave]slave_block"] = true
yield
ensure
Thread.current["[MasterSlave]current_slave_name"] = nil
Thread.current["[MasterSlave]slave_block"] = false
end
end
end
end
end
将上面的创建连接和切换数据库代码的模块,混入到 ActiveRecord::Base
中,就能达到同一个类,在不同场景下,使用不同数据库的目的。
使用 Passenger 并开启 Spawning 模式时(参见 http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning_methods_explained ),通常需要在工作进程fork之后,重新打开文件描述符,即重新连接数据库。那么这里添加了从库后,是否需要在fork后重新连接?
首先,通常使用 Passenger 时,我们并不会处理 ActiveRecord 的连接,是否意味着 Passenger 已经帮我们处理了?以 ActiveRecord 为关键词搜索 Passenger 的源码,可以找到代码(见 https://github.com/phusion/passenger/blob/release-3.0.21/lib/phusion_passenger/utils.rb#L383 )
# If we were forked from a preloader process then clear or
# re-establish ActiveRecord database connections. This prevents
# child processes from concurrently accessing the same
# database connection handles.
if forked && defined?(::ActiveRecord::Base)
if ::ActiveRecord::Base.respond_to?(:clear_all_connections!)
::ActiveRecord::Base.clear_all_connections!
elsif ::ActiveRecord::Base.respond_to?(:clear_active_connections!)
::ActiveRecord::Base.clear_active_connections!
elsif ::ActiveRecord::Base.respond_to?(:connected?) &&
::ActiveRecord::Base.connected?
::ActiveRecord::Base.establish_connection
end
end
而 ActiveRecord::Base.clear_all_connections!
的实现为:
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
def clear_all_connections!
@connection_pools.each_value {|pool| pool.disconnect! }
end
可见 Passenger 已经对 ActiveRecord 的连接做了特殊处理。会在创建工作进程后,断开所有的连接池中的连接。