技術共有

[ruby on Rails] デプロイメント中の ActiveRecord::PreparedStatementCacheExpired エラーの理由と解決策

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1. 質問:

  • Rails アプリケーションを Postgres にデプロイするときに、ActiveRecord::PreparedStatementCacheExpired エラーが表示される場合があります。これは、デプロイメントで移行を実行する場合にのみ発生します。
  • これは、Rails が Postgres の PreparedStatementCache 機能を利用してパフォーマンスを向上させるために発生します。この機能は、rails ではデフォルトで有効になっています。

2. 問題の再発:

  • rspec を使用してこのエラーを再現できます
 it 'not raise ActiveRecord::PreparedStatementCacheExpired' do
    create(:user)
    User.first
    User.find_by_sql('ALTER TABLE users ADD new_metric_column integer;')
    ActiveRecord::Base.transaction { User.first }
  end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ここに画像の説明を挿入します

3. 生産原理:

  • 次のような Rails クエリステートメントUser.all作成者: active_record SQL文に解析するその後、それをデータベースに送信し、最初に PREPARE を実行するプリペアドステートメント、SQL ステートメントは解析、分析、最適化、および書き換えられます。フォローアップのときEXECUTEコマンドを発行する、準備されたステートメントが計画され、実行されます。
  • Rails はクエリステートメントを次の場所に保存します。pg_prepared_statements次回同様のステートメントを呼び出すときに便利です。直接実行する 解析、分析、最適化の代わりにステートメントを実行することで、作業の重複を回避し、効率を向上させます。
User.first
User.all
# 执行上面的2个查询后,用connection.instance_variable_get(:@statements)就可以看到缓存的准备语句
ActiveRecord::Base.connection.instance_variable_get(:@statements)
==> <ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::StatementPool:0x00000001086b13c8 
@cache={78368=>{""$user", public-SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 
$1"=>"a7", ""$user", public-SELECT "users".* FROM "users" /* loading for inspect */ LIMIT $1"=>"a8"}},
@statement_limit=1000, @connection=#<PG::Connection:0x00000001086b31a0>, @counter=8>

# 这个也可以看到,会在数据库中去查询
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
(0.5ms) select * from pg_prepared_statements
==> [["a7", "SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1", "2024-07-
11T07:03:06.891+00:00", "{bigint}", false], ["a8", "SELECT "users".* FROM "users" /* loading for inspect 
*/ LIMIT $1", "2024-07-11T07:04:47.772+00:00", "{bigint}", false]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • Postgres では、返された結果に影響を与えるテーブルのスキーマが変更された場合、プリペアド ステートメント キャッシュは無効になります。具体的には、テーブルへのフィールドの追加や削除、フィールドの型や長さの変更などの DDL 操作です。

以下の例のように、フィールドを追加または削除した後にSELECTを実行すると、ページデータベース投げますcached plan must not change result type、レール内アクティブレコードこのエラーが発生してスローされるActiveRecord::PreparedStatementCacheExpired

ALTER TABLE users ADD COLUMN new_column integer;
ALTER TABLE users DROP COLUMN old_column;
添加或删除列,然后执行 SELECT *
删除 old_column 列然后执行 SELECT users.old_column
  • 1
  • 2
  • 3
  • 4
  • デプロイメントサービスでフィールドの追加、削除、変更の移行を実行する場合、ユーザーが発行したクエリステートメントはプリペアドステートメントキャッシュから直接SQLを取得して直接実行しますが、このときテーブル構造が変更されるため、プリペアドステートメントキャッシュは無効になります。ページデータベース投げますcached plan must not change result type間違い
  • active_record のソース コードを表示するexec_cache方法、rails for pgのエラー処理方法は次のとおりであることがわかりました。
    1. トランザクションでは直接スローされます raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. トランザクション外のものは @statement をキャッシュしますこの文を削除して試してください、再試行後、SQL ステートメントは再解析、分析、最適化されて実行されます。prepare_statementメソッドをプリペアドステートメントキャッシュに入れる
module ActiveRecord
  module ConnectionHandling
    def exec_cache(sql, name, binds)
      materialize_transactions
      mark_transaction_written_if_write(sql)
      update_typemap_for_default_timezone

      stmt_key = prepare_statement(sql, binds)
      type_casted_binds = type_casted_binds(binds)

      log(sql, name, binds, type_casted_binds, stmt_key) do
        ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
          @connection.exec_prepared(stmt_key, type_casted_binds)
        end
      end
    rescue ActiveRecord::StatementInvalid => e
      raise unless is_cached_plan_failure?(e)

      # Nothing we can do if we are in a transaction because all commands
      # will raise InFailedSQLTransaction
      if in_transaction?
        raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
      else
        @lock.synchronize do
          # outside of transactions we can simply flush this query and retry
          @statements.delete sql_key(sql)
        end
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • したがって、トランザクションに表示されるこのエラーは、ビジネスにとって、リクエストが失敗したことを意味し、自分で処理する必要があります。

4. 解決策:

1. 準備済みステートメントのキャッシュ機能を無効にする (非推奨)

Rails6 以降では、データベース内の prepare_statements を false に設定することで、この機能を無効にできます。

default: &default
  adapter: postgresql
  encoding: unicode
  prepared_statements: false
  • 1
  • 2
  • 3
  • 4

Rails6 以下はテストされていません。上記の方法が機能しない場合は、新しい初期化ファイルを作成してみてください。

# config/initializers/disable_prepared_statements.rb:
db_configuration = ActiveRecord::Base.configurations[Rails.env]
db_configuration.merge!('prepared_statements' => false)
ActiveRecord::Base.establish_connection(db_configuration)
  • 1
  • 2
  • 3
  • 4

確認する:

User.all
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
==> []
  • 1
  • 2
  • 3

結論: 小規模なプロジェクトでは、この機能を無効にしても問題はなく、パフォーマンスにはほとんど影響がありません。ただし、大規模なプロジェクトでは、ユーザーが増え、クエリ ステートメントが複雑になるにつれて、この機能がもたらすメリットは大きくなります。実際の状況に応じて無効にするかどうかを決定できます。

2.作るselect * なるselect id, nameRails7 のこのような特定のフィールド公式ソリューションそれでおしまい

  • Rails7 で enumerate_columns_in_select_statements を true に設定します
# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • Rails7 以下ではそのような設定はありません。それを実現するには、ignored_columns を使用できます。
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  #__fake_column__是自定义的,不要是某个表中的字段就行,如果是[:id],那么 User.all就会被解析为select name from users,没有id了
  self.ignored_columns = [:__fake_column__] 
end
  • 1
  • 2
  • 3
  • 4
  • 5

結論: このソリューションの問題は、フィールドの追加は完全に解決できますが、フィールドの削除では引き続きエラーが発生することです。たとえば、準備済みステートメントの select id、name from users の名前が存在しない場合、name フィールドを削除した後でもエラーが発生します。 Rails7 の公式ソリューションにもこの問題が報告されます。

3. Rails アプリケーションを再起動します

  • プリペアド ステートメント キャッシュのライフ サイクルは、1 つのデータベース セッション内にのみ存在します。データベース接続が閉じられた場合 (アプリケーションを再起動すると、元の接続が閉じられ、新しい接続が再確立されます)、元のプリペアド ステートメント キャッシュはクリアされ、再起動後のSQLリクエストはプリペアドステートメントをキャッシュし、正常にデータを取得できるようになります。

結論: アプリケーションを再起動すると、一時的なサービス 502 Unavailable が表示されます。 もちろん、アプリケーションをデプロイするときにサービスを再起動する必要があり、502 も表示されるため、誰もアクセスしていないとき (途中で) デプロイするのが最善です。夜?)、できるだけエラーが少なくなるように表示しますPreparedStatementCacheExpiredエラーを報告する

4.書き換える transaction 方法

class ApplicationRecord < ActiveRecord::Base
  class << self
    def transaction(*args, &block)
      retried ||= false
      super
    rescue ActiveRecord::PreparedStatementCacheExpired
      if retried
        raise
      else
        retried = true
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 書き換え後は、コード内でトランザクションを記述する箇所を を使用するように変更します。 ApplicationRecord.transaction do ... end またはMyModel.transaction

結論: 重要な注意事項: 電子メールの送信、API への投稿、またはトランザクションの一部として外部とやり取りするその他の操作を実行する場合、これらの操作の一部が 2 回発生することがあります。これが、Rails が公式に再試行を自動的に実行せず、アプリケーション開発者に任せている理由です。

&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;このメソッドを自分でテストすると、やはりエラーが発生します。

5. Prepared Statement キャッシュを手動でクリアする

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. 最終的な答え

完璧な解決策は見つかりませんでした