odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

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!を呼ぶと直ると思います。
  • SQL自体がゲロ重な場合
    • SLBやDBのtimeoutを伸ばすしかないでしょう。

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

実行環境

ruby (1.8.6)
rails (2.3.8)