1

デッドロック後のデータ損失 - SQL Server 2008、Ruby on Rails、Phusion Passenger、Linux、FreeTDS

私が担当している Ruby on Rails イントラネット アプリケーションで、データ損失を引き起こす原因となった謎の問題に直面しています。これがプログラミングの問題について厳密に話しているわけではない場合はお詫びします - 少なくとも私はアプリケーションの Ruby コードを保守しています。この問題は、これまで 2 年間で 3 回発生しています。

環境:

  • Linux - RedHat エンタープライズ サーバー 5.2
  • Apache 2 Web サーバー (httpd-2.2.3-11.el5_2.4.rpm)
  • Phusion Passenger 2.2.15
  • Ruby 1.8.7、Rails 2.3.8、gem:
    • アクションメーラー (2.3.8)
    • アクションパック (2.3.8)
    • アクティブレコード (2.3.8)
    • activerecord-sqlserver-adapter (2.3.8)
    • アクティブリソース (2.3.8)
    • アクティブサポート (2.3.8)
    • あかみ(1.2.0)
    • ビルダー (3.0.0)
    • exception_notification (2.3.3.0)
    • ファストスレッド (1.0.7)
    • ぎょく (0.4.6)
    • httpi (1.1.1)
    • MIME タイプ (1.16)
    • ノコギリ (1.4.4)
    • 海苔 (1.1.3)
    • 乗客 (2.2.15)
    • ラック (1.1.0)
    • レール (2.3.8)
    • レーキ (0.8.7)
    • ruby-net-ldap (0.0.4)
    • rubyjedi-actionwebservice (2.3.5.20100714122544)
    • サボン (1.1.0)
    • わさび(2.5.1)
    • will_paginate (2.3.14)
  • SQL Server 2008 データベース サーバー
  • ActiveRecord によるデータベース アクセス
  • データベースドライバー: freetds-0.82、unixODBC-2.3.0.tar.gz、ruby-odbc-0.99991.tar.gz

症状:

  • データベース リソースのロックを要求するユーザー アクションは、デッドロック状態に関係していました。
  • SQL Server は、デッドロックに関係するプロセスを強制終了することでデッドロックを解決し、少なくとも一部のプロセスが正常に完了できるようにしました。
  • Rails アプリケーション側では、デッドロックにより未処理の例外が発生しました (exception_notification gem を通じて通知されました)。
  • デッドロックの後、アクティブな Rails プロセスの数が増加し (監視システムの別の通知がトリガーされました)、プロセスがハングしているように見えました
  • これが起こった理由は不明です。プロセスはデータベース操作でハングしているように見えました (Rails ログによると)。通常、SQL サーバーのデッドロック解決機能によって、ブロッキング プロセスが放置されないことを期待していました。
  • 最初の 2 つのケースでは、例外/ハング プロセスへの対応として Web サーバーを再起動しました。3 番目のケース (私は休暇中でした) では、誰も通知に反応しませんでしたが、週末に実行されていた cron ジョブがプロセスも停止していたようです (「restart.txt」にタッチして Passenger をソフト再起動しても、同じ効果が得られました)。
  • Web サーバーの再起動後、ユーザーはデータ損失を報告しました。Web サーバーが再起動する前に、ユーザーの観点から、データは期待どおりに処理されました。私たちと通信する他のシステムの Rails ログとデータは、トランザクションが適切にコミットされたことを示しているようです。Web サーバーの再起動後、デッドロックが発生してからのすべてのデータベース変更が突然失われました。たとえば、ユーザー アクションごとに更新される「last_access」列を持つ「users」テーブルがあります。Web サーバーの再起動後、最新の「last_access」値は 1 日経過していました。すべてのトランザクションが欠落しているように見え、@@IDENTITY 値のみがデータ損失前に設定されていた値で継続されました。
  • 私たちの IT (データベース サーバーを管理している) から、失われた DB 操作のすべてが 1 つの巨大なトランザクションの一部であり、最後の COMMIT が欠落していたことを示すと思われる情報を受け取りました。もちろん、Rails のすべてのユーザー アクションが 1 つ以上の個別のトランザクションを実行することを期待していますが、SQL Server のトランザクション ログには、すべての操作が 1 つの巨大なトランザクションの一部として表示されます。

このようなことが起こったかのように私には見えます:

  • 関連するコンポーネントの 1 つ (Phusion Passenger、FreeTDS、SQL Server など) のバグが原因で、並行して実行されていた Rails プロセスがデータベース接続を共有し、プロセスの停止も引き起こした可能性があります。
  • 関連するプロセスの 1 つがトランザクション中で、COMMIT の前のどこかでハングしていました。
  • 他のプロセスは同じ接続を共有していたので(私が推測しているように)、それらも同じトランザクションにありました
  • プロセスは接続を共有しているため、COMMIT が保留中であっても、ユーザーは (Web サーバーが再起動する前に) データの変更を確認できました。
  • Web サーバーの再起動により、接続が中止され、トランザクションがロールバックされました。

それは理にかなっていますか?誰かが同様の経験や、さらに詳しく調べることができるヒントを持っているかどうか疑問に思っています. データベース接続のファイル記述子をフォークした可能性がある Passenger のバグを疑いましたが、再現できませんでした。Passenger は、すべてのフォークで新しい DB 接続を適切に作成しているようです。

デッドロックの数を減らすために、データベースの分離モデルを「コミットされたスナップショットの読み取り」に変更することを検討していますが、これでは根本的な原因が修正されず、他の問題が発生する可能性があることを認識しています。

4

2 に答える 2

3

私は自分で問題を追跡できるようになりました。現在または将来、同様の問題に直面している可能性のある人と解決策を共有したいと思います.

次のことが起こっていました。

  • トランザクションの一部として実行されている UPDATE 操作がデッドロックに関係していました
  • SQL Server は UPDATE 操作をデッドロックの犠牲者として選択し、トランザクションをロールバックしました
  • ActiveRecord::StatementInvalid次のような例外が発生しました。

    A ActiveRecord::StatementInvalid occurred in (...):
    
    ODBC::Error: 37000 (1205) [FreeTDS][SQL Server]Transaction (Process ID 55) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.: UPDATE [(....tablename....)] SET [position] = 1 WHERE [id] = 795419
    /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.8/lib/active_record/connection_adapters/abstract_adapter.rb:221:in `log'
    
  • ではActiveRecord::ConnectionAdapaters::DatabaseStatements.transaction()、例外は次の方法で処理されます。

    rescue Exception => database_transaction_rollback
      if transaction_open && !outside_transaction?
        transaction_open = false
        decrement_open_transactions
        if open_transactions == 0
          rollback_db_transaction
        else
          rollback_to_savepoint
        end
      end
      raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
    end
    
  • transaction_opentrueこの時点でです。メソッドoutside_transaction?は、SQL Server アダプターで次のように実装されます。

    def outside_transaction?
      info_schema_query { select_value("SELECT @@TRANCOUNT") == 0 }
    end
    
  • @@TRANCOUNT私のデバッグ出力に示されているように、SQL Serverはすでにトランザクションをロールバックしているため、この時点では0です。

    SQL (1.0ms)   SELECT @@TRANCOUNT
    => TRANCOUNT=0
    
  • したがって、outside_transaction?が返さtrueれ、上記の例外処理コードはロールバックを実行しません。ここまでは順調ですね。

  • 上記のコードによって例外が再スローされ、次のApplicationController理由でmy によってキャッチされます。

    class ApplicationController < ActionController::Base
      rescue_from Exception, :with => :render_error
    
  • このrender_errorメソッドは、例外メッセージをフラッシュ変数に格納します。

    flash[:exception_message] = exception.message
    
  • フラッシュ変数はセッションに保存されます。を使用しているactive_record_storeため、セッション データはデータベース テーブルに格納されますsessions。(私は実際に を使用してsmart_session_storeいますが、これはこの点で違いはありません)。それで、別のトランザクションが開始されます...

    EXECUTE (1.2ms)   BEGIN TRANSACTION
    SQL (1.1ms)   SELECT session_id, data,id FROM sessions WHERE id=150091
    EXECUTE (1.3ms)   UPDATE sessions SET updated_at=CURRENT_TIMESTAMP, data='BAh7FDoWdW9faGlk(........)' WHERE id=150091
    CACHE (0.0ms)   SELECT @@TRANCOUNT
    => TRANCOUNT=0
    
  • トランザクションは開始されていますが、 SELECT @@TRANCOUNT0 を返します - 値はキャッシュから取得されます! ここから災害が進行します。

  • このtransactionメソッドは、トランザクションがもうアクティブではないと判断したため、COMMITを実行しません。

    if outside_transaction? # (this does the SELECT @@TRANCOUNT)
      @open_transactions = 0      # Getting here!
    elsif transaction_open
      decrement_open_transactions
      begin
        if open_transactions == 0
          commit_db_transaction   # NOT getting here!
        else
          release_savepoint
        end
      rescue Exception => database_transaction_rollback
        if open_transactions == 0
          rollback_db_transaction
        else
          rollback_to_savepoint
        end
        raise
      end
    end
    

    終わり

  • データベース接続は開いたままなので (これは開発モードではなく本番モードでのみ行われるようです)、同じワーカー プロセスによって処理される後続の Rails アクションはすべて、ここで開いたままのトランザクションに追加されます。ユーザーには、トランザクションの目を通してすべてを見るため、データが正常に処理されているように見えます。この 1 つのワーカー プロセスだけがアクティブなままです。開いているトランザクションがすべての種類のデータベース リソースをロックするため、他の開始されたワーカー プロセスはハングします。Web サーバーを再起動すると、1 つの応答ワーカー プロセスが停止し、そのトランザクションがロールバックされました。これは、アプリケーションでデータ損失が目に見えるようになった場所です。

上記のコードの新しい (Rails 3.x) バージョンを簡単に調べたところ、問題が発生しなくなったようです。このtransactionメソッドは呼び出しを行っていないように見えますoutside_transaction?が、内部で維持されているトランザクション状態に依存しています。ただし、現時点ではアップグレードできないtransactionため、Rails 3.x の場合と同様の方法で方法を変更して、ローカルで問題を修正します。

于 2013-04-12T08:04:10.743 に答える
1

最初に、私はこの問題を完全に読んでいないことを認めますが、直感と提案があります。私はこのような問題を見てきましたが、それらは通常、多くのモデルを持つ大規模なレガシー アプリケーションで発生し、その一部には別のデータベースへの接続プールがあります。上記のシナリオでは、ActiveRecord のコールバック フェーズ中に別の接続を使用するモデルを簡単に参照できます。これにより、そのままでは ActiveRecord でサポートされていないクロス データベース トランザクションが発生します。

これが原因であると思われる場合は、次のコードを確認してください。# Establish_connection は絶対に使用しないでください。使用する場合は、他の継承元の基本クラスまたは接続クラスで 1 回だけ使用してください。私が話していることを示すgithub gist ここ ( https://gist.github.com/metaskills/4065702 ) があります。次に、保存/トランザクション中に、モデルが別の接続プール内の別のモデルと通信していないことを確認してください。

于 2013-04-15T11:41:47.273 に答える