Rails4.2のコネクションプールの実装を理解する
tl:dr;
Railsではコネクションプール数を設定していても、1スレッド辺り1コネクションしか持ちません。
発端
アカツキではRails + Unicorn + Nginx + MySQLの構成をAWSで運用しており、c3.4xlarge
のインスタンス上で1台辺り64のUnicornワーカープロセスが実行される設定になっています。
ソーシャルゲームでは時にたくさんのアプリケーションサーバを並列稼働される必要がでてきます。特に年末年始の時期は平時の2-3倍のトラフィックが予想され、アプリケーションサーバを最大100台(6400プロセス!)で稼働させる必要がありました。
そこで、MySQLのmax_connections
の設定は大丈夫か?という議論があり、Railsのコネクションプールの実装をきちんと理解しようと思い、調査しました。
動きの確認
まずは実行してみます。
- Unicornのworker_processesを2に、database.ymlのpoolを5にして動作させてみる -> コネクション数:2
- Unicornのworker_processesを2に、database.ymlのpoolを1にして動作させてみる -> コネクション数:2
- Unicornのworker_processesを4に、database.ymlのpoolを1にして動作させてみる -> コネクション数:4
どうやらpoolの設定にかかわらず、ワーカープロセスの数 = コネクション数
となるようです。
ドキュメントの確認
さて、Railsのドキュメントといえば、RailsGuideです。
https://github.com/yasslab/railsguides.jp をクローンし、
git grep --name-only 'プール' guides/source
を実行すると、以下がヒットします。
guides/source/ja/configuring.md
guides/source/ja/rails_on_rack.md
rails_on_rackはActiveRecord::ConnectionAdapters::ConnectionManagement
がコネクションプールを管理していることしか分かりません。
configuringを読むと、以下の記載があります。
Active Recordのデータベース接続はActiveRecord::ConnectionAdapters::ConnectionPoolによって管理されます。これは、接続数に限りのあるデータベース接続にアクセスする際のスレッド数と接続プールが同期するようにするものです。最大接続数はデフォルトで5ですが、database.ymlでカスタマイズ可能です。…snip… 接続プールはデフォルトではActive Recordで取り扱われるため、アプリケーションサーバーの動作は、ThinやmongrelやUnicornなどどれであっても同じ振る舞いになります。最初はデータベース接続のプールは空で、必要に応じて追加接続が作成され、接続プールの上限に達するまで接続が追加されます。
これだけを読むと、database.ymlで設定された分のコネクションプールが「必要に応じて」確保され、各スレッドはそのコネクションプールを使う動きをしているようです。
実際の動きと、設定された分のコネクションプールが確保されるというドキュメントの記載内容に、違和感を感じます。
ここでの「必要に応じて」とは、どういう意味でしょうか?曖昧なので、より深く実装を確認する必要がありそうです。
実装の確認
API Documentation
ソースコードを読む前に、APIドキュメントを確認してみましょう。
RailsGuideには、ActiveRecord::ConnectionAdapters::ConnectionPoolによって管理されるとあるので、その部分をみてみます。
ActiveRecord::ConnectionAdapters::ConnectionPool
A connection pool synchronizes thread access to a limited number of database connections. The basic idea is that each thread checks out a database connection from the pool, uses that connection, and checks the connection back in. ConnectionPool is completely thread-safe, and will ensure that a connection cannot be used by two threads at the same time, as long as ConnectionPool’s contract is correctly followed. It will also handle cases in which there are more threads than connections: if all connections have been checked out, and a thread tries to checkout a connection anyway, then ConnectionPool will wait until some other thread has checked in a connection.
Introductionを読むと、コネクションプールの実装は完全にスレッドセーフであり、各スレッドはコネクションプールから接続をチェックアウトして使う、という実装のようです。
スレッドセーフということは「ひとつの接続が、同タイミングで複数のスレッドにより使われることはない」ということなので、ワーカープロセスの数 = コネクション数
となるのは分かる気がします。
しかし、リクエストのたびに接続をし、コネクションプールの最大設定値までプールする様に実装されているならば、その限りではありません。
Introduction以下を読み進めても、直接答えに結びつくような記載は見つかりません。ここまできたら、ソースコードを読むほうが早いでしょう。
ActiveRecord::Base
接続に関する内容については、sonotsさんが書かれた
や、kotaroitoさんが書かれた
等の記事を見たほうが早いかもしれませんが、ここでも読み進めていきます。
ドキュメント
ActiveRecord::Base APIドキュメントの、”Connection to multiple databases in different models”の項には、このように書かれています。
Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.
ということで、ActiveRecord::Base.establish_connection
が接続情報の作成、ActiveRecord::Base.connection
が接続の処理として見ていきます。
接続情報の作成
ActiveRecord::ConnectionHandling
以下、ActiveRecord::Base.establish_connectionの実装を見ると、connection_handler.establish_connection
を実行しています。
def establish_connection(spec = nil)
spec ||= DEFAULT_ENV.call.to_sym
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
remove_connection
connection_handler.establish_connection self, spec
end
connection_handler
は以下、ActiveRecord::Coreで実装されており、ConnectionAdapters::ConnectionHandlerのインスタンスをキャッシュしています。
def self.connection_handler
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
end
def self.connection_handler=(handler)
ActiveRecord::RuntimeRegistry.connection_handler = handler
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
※ ちなみに、ActiveRecord::RuntimeRegistoryはActiveSupport::PerThreadRegistry
モジュールを利用しており、これはスレッドローカル(スレッドセーフなグローバル変数)値を安全にキャッシュするための機能です。クラス名をキーに含めることで、同じキーを別々のクラスで定義していても大丈夫な設計になっています。
ActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection
ConnectionAdapters::ConnectionHandler#establish_connectionの実装を見ると、以下の行でコネクションプールを生成しています。
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
owner_to_pool[owner.name]
のowner_to_pool
はThreadSafe::Cache、owner
は実行元のモデルクラスなので、実行元モデル名ごとにConnectionAdapters::ConnectionPoolのインスタンスをキャッシュしています。
実装の先を追ってみましょう。
ActiveRecord::ConnectionAdapters::ConnectionPool
大枠を捉えながら、ConnectionPool内の処理を見てみます。
ActiveRecord::ConnectionAdapters::ConnectionPool::Queue
コネクションを格納するFIFOキューです。
ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper
database.ymlファイルのreaping_frequency
に設定された秒数ごとに、reap
を実行しています。
ActiveRecord::ConnectionAdapters::ConnectionPool#initialize
ConnectionPool.newされるときに実行される、initializeメソッドです。
def initialize(spec)
super()
@spec = spec
@checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
@reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f))
@reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
# The cache of reserved connections mapped to threads
@reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
@connections = []
@automatic_reconnect = true
@available = Queue.new self
end
ここで分かることは、以下のとおりです。
- Reaperの(別スレッドでの)実行を開始している
- スレッドセーフなKey:Valueキャッシュである
@reserved_connections
を作っている - コネクションのキューを作っている
- 設定にしたがって接続情報の初期化をしているだけで、実際に接続はされていない
ここではドキュメントの記載どおり実際の接続は行わず、接続の管理情報を初期化しているだけなので、特に気にすることは無さそうです。
接続の処理
ActiveRecord::ConnectionAdapters::ConnectionPool#connection
実際に接続されるときに実行される、connectionメソッドです。
def connection
# this is correctly done double-checked locking
# (ThreadSafe::Cache's lookups have volatile semantics)
@reserved_connections[current_connection_id] || synchronize do
@reserved_connections[current_connection_id] ||= checkout
end
end
@reserved_connections[current_connection_id]
にすでにキャッシュされていればその情報を返します- そうでなければ
synchronize
(mutexロック)した上で、checkout
により貼られたDBとの接続情報を@reserved_connections[current_connection_id]
に格納しています。
ここで着目するのは、@reserved_connections
のキーに、current_connection_id
を使っていることです。
current_connection_id
の実装は以下のとおりです。
def current_connection_id #:nodoc:
Base.connection_id ||= Thread.current.object_id
end
Thread.current.object_id
を使っています
object_id
はスレッドを一意に識別するIDなので、同スレッド上で何度connection
を実行しようとも、同じ接続が使われます。
ここではじめて、1スレッド辺り1コネクションとなる動きが実装レベルで理解できました!
まとめ
ActiveRecord::Base.establish_connection
で接続管理情報が作成され、ActiveRecord::Base.connection
で接続が行われますActiveRecord::ConnectionAdapters::ConnectionHandler#establish_connection
ではコネクションプールのインスタンスをモデル名ごとにキャッシュしていますが、これはそれほど重要ではありません- 実際の接続は
ActiveRecord::ConnectionAdapters::ConnectionPool
が持つ@reserved_connections
により、スレッドID単位にキャッシュされています - Railsではコネクションプール数を設定していても、1スレッドあたり1コネクションしか使いません。つまり、シングルスレッドのUnicornでは、
1ワーカープロセス = 1コネクション
となります。
あとがき
Rails4.2のコネクションプールに関するソースコードを、APIドキュメントをきっかけにして追ってみました。
使っているフレームワークの動作を実装レベルで知ることは、未来の自分が書くソースコードの品質向上に直結すると思いますし、なにより様々な実装のアイデアを知ることは楽しいです。
そして、2016/01/16に Rails v5.0.0.beta1.1 が公開されましたね。
Rails5では @thedarkone さんのコミットによりコネクションプールの実装が大きく変わっており、Biasable queueなどのアイデアが追加され、コメントも増えて分かりやすくなりました。
この記事を見て、ちょっと興味が出てきた人はConnectionPoolのコミットログを読んでみると、面白い発見があるかもしれません。
Enjoy code reading!