ActiveRecordのコネクションプーリングをやめてみる

Tsukasa OISHI

関連 : RailsでMysqlスレーブ群をロードバランサ経由で使用できる、FreshConnectionを作り始めました

RailsActiveRecordは、DBとのコネクションがプールされます。
アクセスごとにコネクションをはりなおすよりは、オーバーヘッドがない分、理にかなっているようにも思えます。
ただ、比較的大きめなサイトになってくると、はりっぱなしのコネクションが多くなりすぎちゃって大変なことになってきます(1サーバ1万コネクションとかなりかねない)。リソースは食うし、たくさんのスレーブを抱えているときにActsAsReadonlyableなどでちまちまやっていたらとても運用できません。スレーブなんてLVS+keepalivedでバランシングしちゃいたいところ。でもコネクションがはりっぱなしだとそれもできないのです。

というわけで他に誰か同じ問題に取りかかっている人はいないのかとググってみたら、おいしい料理のサイトのインフラをなさっている方が1年も前にすでにやっておられました( Rails(ActiveRecord)でデータベースへのコネクションプーリングをさせなくする)。

以下は紹介されていた Nick Siegerさんのコードです。

なるほど、checkoutとcheckinをオーバーライドしちゃって、それぞれ単純に接続と切断だけをするようにしてあげています。

さっそく以下のようなアクションを作ってみて、production環境で/articles/showにアクセスして確かめてみます。スレッドの中でActiveRecordにアクセスしているのは、コネクションをたくさん使うためです。config/database.ymlで最大プール数を15くらいにしちゃっています。

class ArticlesController < ApplicationController
  def show
    10.times{|n| Thread.new{ Article.find(n) }}
    sleep(15)
    render :text => "ok"
  end
end

まずは、素のままのRailsです。sleepで待っている間にDBをのぞいてみると、確かにコネクションが複数はられています。当然ですが、これはRailsの処理が終わってもこのままでした。

mysql> show processlist;
+-----+------+-----------+------------------------+---------+------+-------+------------------+
| Id  | User | Host      | db                     | Command | Time | State | Info             |
+-----+------+-----------+------------------------+---------+------+-------+------------------+
| 116 | root | localhost | NULL                   | Query   |    0 | NULL  | show processlist |
| 164 | root | localhost | kaeruspoon_development | Sleep   |   14 |       | NULL             |
| 165 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 166 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 167 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 168 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 169 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 170 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 171 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 172 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 173 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
| 174 | root | localhost | kaeruspoon_development | Sleep   |   13 |       | NULL             |
+-----+------+-----------+------------------------+---------+------+-------+------------------+
12 rows in set (0.00 sec)

次にNick Siegerさんのコードを適用してためしてみました。ところでRailsの処理が終わっても、コネクションがひとつ切断されるだけで残りは接続されたままです。どういうことなのか調べてみました。

Rackに積まれたActiveRecord::ConnectionAdapters::ConnectionManagementの中で、レスポンスを返すときにActiveRecord::Base.clear_active_connections!が呼ばれています。これはactive_record/connection_adapters/abstract/connection_pool.rbの中で定義されています。

      def clear_active_connections!
        @connection_pools.each_value {|pool| pool.release_connection }
      end

コネクションプールに対してrelease_connectionが投げられています。このメソッドも同じファイル内に定義されています。

      def release_connection(with_id = current_connection_id)
        conn = @reserved_connections.delete(with_id)
        checkin conn if conn
      end

current_connection_idは、カレントスレッドのオブジェクトIDです。つまり、カレントスレッドが使用しているコネクションに対してのみ、checkinがコールされていることになります。これが、コネクションがひとつしか切断されなかった理由です。

スレッドを使わないかぎりは問題なさそうですが、どうも気持ち悪いので他のアプローチを考えてみましょう。
今の調査でわかるように、Rack上ですべてのコネクションに対して切断処理が行われれば問題ないように思えます。
そして、すべてのコネクションを切断するメソッドはすでに用意されています。ActiveRecord::Base.clear_all_connections!です。これも上記と同じファイル内で定義されています。

      def clear_all_connections!
        @connection_pools.each_value {|pool| pool.disconnect! }
      end

コネクションプールに対するdisconnect!メソッドも同ファイル内に存在します。

      def disconnect!
        @reserved_connections.each do |name,conn|
          checkin conn
        end
        @reserved_connections = {}
        @connections.each do |conn|
          conn.disconnect!
        end
        @connections = []
      end

使用したコネクションすべてに対してcheckin処理とdisconnect!を行っています。コネクションに対するdisconnect!メソッドは、切断処理を行います。これを使えば、Railsのコードにパッチをあてることもなく、アクションごとのコネクションの接続/切断が可能になります。

まず、libの下などでRackクラスを定義します。

class ThrowawayActiveRecordConnection
  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env)
  ensure
    unless env.key?("rack.test")
      ActiveRecord::Base.clear_all_connections!
    end
  end
end

次に、config/application.rbで、lib配下もautoloadの対象にし、Rackの入れ替えを行います。

class Application < Rails::Application
  config.autoload_paths += %W(#{config.root}/lib)
  config.middleware.swap ActiveRecord::ConnectionAdapters::ConnectionManagement, "::ThrowawayActiveRecordConnection"
end

config.middleware.swapで、Rackのmiddlewareを入れ替えすることができます。ThrowawayActiveRecordConnectionの指定を文字列にしているのは、この段階ではまだlib配下がロードされていないためです。文字列を指定しても、Railsがよきにはからってくれます。

この実装を行って、再度Railsのアクションをたたいてみました。結果、Railsの処理が終わるたびに全コネクションが切断されるようになりました。