Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Rails4.2のコネクションプールの実装を理解する

tl;dr

Railsではコネクションプール数を設定していても、1スレッド辺り1コネクションしか持ちません。

発端

アカツキではRails + Unicorn + Nginx + MySQLの構成をAWSで運用しており、c3.4xlargeインスタンス上で1台辺り64のUnicornワーカープロセスが実行される設定になっています。

ソーシャルゲームでは時にたくさんのアプリケーションサーバを並列稼働される必要がでてきます。特に年末年始の時期は平時の2-3倍のトラフィックが予想され、アプリケーションサーバを最大100台で稼働させる必要がありました。

Railsのdatabase.ymlのpool設定は5だったので、単純に考えると最大 100台 * 64プロセス * 5接続 = 32,000個の接続が常時貼られるのでは?MySQLmax_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_rackActiveRecord::ConnectionAdapters::ConnectionManagementがコネクションプールを管理していることしか分かりません。 configuringを読むと、以下の記載があります。

Active Recordのデータベース接続はActiveRecord::ConnectionAdapters::ConnectionPoolによって管理されます。これは、接続数に限りのあるデータベース接続にアクセスする際のスレッド数と接続プールが同期するようにするものです。最大接続数はデフォルトで5ですが、database.ymlでカスタマイズ可能です。...snip... 接続プールはデフォルトではActive Recordで取り扱われるため、アプリケーションサーバーの動作は、ThinやmongrelUnicornなどどれであっても同じ振る舞いになります。最初はデータベース接続のプールは空で、必要に応じて追加接続が作成され、接続プールの上限に達するまで接続が追加されます。

これだけを読むと、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::RuntimeRegistoryActiveSupport::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_poolThreadSafe::Cacheownerは実行元のモデルクラスなので、実行元モデル名ごとに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

ここで分かることは、以下のとおりです。

  1. Reaperの(別スレッドでの)実行を開始している
  2. スレッドセーフなKey:Valueキャッシュである@reserved_connectionsを作っている
  3. コネクションのキューを作っている
  4. 設定にしたがって接続情報の初期化をしているだけで、実際に接続はされていない

ここではドキュメントの記載どおり実際の接続は行わず、接続の管理情報を初期化しているだけなので、特に気にすることは無さそうです。

接続の処理

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!