ActiveRecordのDBコネクションの接続切れと再接続について。reconnectオプションは危険だなーとかも
ActiveRecordは基本ずっと接続をはりっぱなしにします。
なので長時間接続をはりっぱなしにするため
タイムアウト等で接続がきれると問題がでます。
MySQLでDBの接続がきれるタイミング
状況別
- Railsの場合
- HTTPリクエストのあるたびに接続が切れてないかを確認します。切れてる場合は再接続するようになっているので問題なさそうです。
- バッチ、デーモン等
- ActiveRecordを使った場合はずっとつなぎっぱなしになるため、途中で接続が切れるとエラーになります。
reconnectオプションは危険
じゃあバッチ等の場合自動で再接続してくれればと思うと
MySqlのクライアントライブラリレベルでリコネクトの概念がありました。
railsで使うにはdatabase.ymlに:reconnect = trueで指定できます。
が!!
railsの実装ではデフォルトはOFFになっています。
なんでOFFかなとおもったら下記のページを見たら納得しました。
http://dev.mysql.com/doc/refman/5.1/ja/mysql-reconnect.html
変数を使ったりした場合等の接続セッションに依存するような場合のコードを書くと、途中で接続がきれて変数等がリセットされてるのにきづかずに次の変数等に依存したsqlを実行してしまう可能性があるからです。
トランザクション周りも危険っぽいですね。ロールバックしてるのに気づかず自動コミットに戻って実行されちゃうとかかなー 怖い怖い。
なのでこの自動再接続は使わないほうがよいかと。
バッチ等の場合での対策
切れる可能性は
まぁー普通はLANなのでそんな接続は切れないだろうという前提で気にしないでエラーになってよいかと思います。
但し、バッチによっては重い処理等をすることもあるでしょうから
MySQLでDBの接続がきれるタイミングに書いたように
重い集計処理をしてからインサートする場合等や、SQL自体がゲロ重な場合等々で接続が切れる可能性があります(特にSLB経由のDBアクセスの場合)。
あんまりないとは思いますがVPN経由でDB接続してて回線が不安定で瞬断がよくおきるとかもあるかもしれません。
対策
- VPN経由でDB接続の場合等の瞬断
- このような場合にはrescueで拾ってやりなおしにするのが良いかもしれません。
- 注意点としてやり直しする前にActiveRecord::Base.clear_active_connections!を呼ぶ必要があります。
- このメソッドを呼ぶことで接続しっぱなしのコネクションを一旦コネクションプールに返却して次のコネクション使用時に再接続してもらえます。
- 重い集計処理をしてからインサートする場合
- インサート直前でActiveRecord::Base.clear_active_connections!を呼ぶと直ると思います。
Railsでリクエストのたびに再接続される部分のソースおっかけ
rails初期化時にリクエスト毎に実行されるrackのミドルウェアとしてActiveRecord::ConnectionAdapters::ConnectionManagementを追加している。
ConnectionManagementの中ではActiveRecord::Base.clear_active_connections!が呼ばれるのでコネクションが再接続してもらえる。
# rails-2.3.8\lib\initializer.rb def initialize_database_middleware if configuration.frameworks.include?(:active_record) if configuration.frameworks.include?(:action_controller) && ActionController::Base.session_store.name == 'ActiveRecord::SessionStore' configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache else configuration.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement configuration.middleware.use ActiveRecord::QueryCache end end end
# activerecord-2.3.8\lib\active_record\connection_adapters\abstract\connection_pool.rb class ConnectionManagement def initialize(app) @app = app end def call(env) @app.call(env) ensure # Don't return connection (and peform implicit rollback) if # this request is a part of integration test unless env.key?("rack.test") ActiveRecord::Base.clear_active_connections! end end end
ActiveRecord::Base.clear_active_connections!が呼ばれるとコネクションがコネクションプールに返却(checkin)される。
コネクション自体はプールされてて接続は維持されたままである。あくまで現スレッドで使うプールからもらってたコネクションをチェックインするだけ。それがコネクションプーリングってもんだ。
# activerecord-2.3.8\lib\active_record\connection_adapters\abstract\connection_pool.rb def clear_active_connections! @connection_pools.each_value {|pool| pool.release_connection } end def release_connection conn = @reserved_connections.delete(current_connection_id) checkin conn if conn end
じゃあなんでコネクションプーリングに返却されると再接続されるかというと
コネクションプーリングから接続をもらう際(checkout)に再接続がされるためである。
def connection if conn = @reserved_connections[current_connection_id] conn else @reserved_connections[current_connection_id] = checkout end end
(少し脱線:ちなみに@reserved_connectionsはチェックアウトしたコネクションをキャッシュしておくhashっぽい。current_connection_idはThreadIDみたいなものなのでスレッドごとに接続がはられてキャッシュされる仕組み(マルチスレッド対応のため)。)
def current_connection_id #:nodoc: Thread.current.object_id end
ちょい省略するがcheckoutを辿ってくとcheckout_and_verifyが呼ばれる。ここでコネクションに対してverifyが呼ばれる。
def checkout_and_verify(c) c.verify! c.run_callbacks :checkout @checked_out << c c end
verifyの中でactive?を呼んで接続されているかを確認し、切れてる場合はreconnectしている。
# activerecord-2.3.8\lib\active_record\connection_adapters\abstract_adapter.rb def verify!(*ignored) reconnect! unless active? end
なおabstract_adapter.rbで定義されてるactive?は
def active? @active != false end def disconnect! @active = false end
というチープなものだが
mysql_adapterできちんと実装されなおしている。
# activerecord-2.3.8\lib\active_record\connection_adapters\mysql_adapter.rb def active? if @connection.respond_to?(:stat) @connection.stat else @connection.query 'select 1' end # mysql-ruby doesn't raise an exception when stat fails. if @connection.respond_to?(:errno) @connection.errno.zero? else true end rescue Mysql::Error false end