Technologieaustausch

[Ruby on Rails] Gründe und Lösungen für ActiveRecord::PreparedStatementCacheExpired-Fehler während der Bereitstellung

2024-07-12

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

1. Frage:

  • Beim Bereitstellen von Rails-Anwendungen auf Postgres werden manchmal ActiveRecord::PreparedStatementCacheExpired-Fehler angezeigt. Dies geschieht nur, wenn Migrationen in einer Bereitstellung ausgeführt werden.
  • Dies liegt daran, dass Rails die PreparedStatementCache-Funktion von Postgres nutzt, um die Leistung zu verbessern. Diese Funktion ist in Rails standardmäßig aktiviert.

2. Wiederauftreten des Problems:

  • Wir können rspec verwenden, um diesen Fehler zu reproduzieren
 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

Fügen Sie hier eine Bildbeschreibung ein

3. Produktionsprinzip:

  • Rails-Abfrageanweisungen wieUser.allvon active_record In SQL-Anweisung analysierenSenden Sie es anschließend an die Datenbank.Führen Sie zuerst PREPARE aus Vorbereitete Anweisungen, SQL-Anweisungen werden geparst, analysiert, optimiert und neu geschrieben.Wenn das Follow-upGeben Sie einen EXECUTE-Befehl aus, wird die vorbereitete Stellungnahme geplant und ausgeführt.
  • Rails speichert die Abfrageanweisung unterpg_prepared_statementsum den nächsten Aufruf ähnlicher Aussagen zu erleichtern.direkt ausführen Anweisungen statt Parsen, Analysieren und Optimieren, wodurch Doppelarbeit vermieden und die Effizienz verbessert wird.
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
  • In Postgres wird der Cache für vorbereitete Anweisungen ungültig, wenn sich das Schema der Tabelle ändert und sich auf die zurückgegebenen Ergebnisse auswirkt. Konkret handelt es sich um DDL-Operationen wie das Hinzufügen und Löschen von Feldern zur Tabelle oder das Ändern des Typs und der Länge von Feldern.

Wie im folgenden Beispiel: Wenn Sie SELECT nach dem Hinzufügen oder Löschen von Feldern ausführen,pg-Datenbankwird werfencached plan must not change result type, in SchienenAktiver RekordErhalten Sie diesen Fehler und werfen Sie ihn dannActiveRecord::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
  • Wenn Sie die Migration zum Hinzufügen, Entfernen oder Ändern von Feldern im Bereitstellungsdienst ausführen, ruft die vom Benutzer ausgegebene Abfrageanweisung die SQL direkt aus dem Cache der vorbereiteten Anweisung ab und führt Execute jedoch direkt aus, da sich die Tabellenstruktur zu diesem Zeitpunkt ändert. Der Cache für vorbereitete Anweisungen wird ungültig.pg-Datenbankwird werfencached plan must not change result typeFehler
  • Active_record-Quellcode anzeigenexec_cacheMethode: Ich habe festgestellt, dass die Fehlerbehandlungsmethode von Rails für pg wie folgt lautet:
    1. In der Transaktion wird es direkt ausgelöst raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Diejenigen außerhalb der Transaktion speichern @Anweisungen zwischenLöschen Sie diesen Satz und versuchen Sie esNach dem erneuten Versuch wird die SQL-Anweisung erneut analysiert, analysiert, optimiert und ausgeführt.prepare_statement-Methode in den vorbereiteten Anweisungscache
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
  • Daher führt dieser Fehler, der in der Transaktion auftritt, dazu, dass die Transaktion zurückgesetzt wird. Für das Unternehmen bedeutet dies, dass die Anfrage fehlgeschlagen ist und wir sie selbst bearbeiten müssen.

4. Lösung:

1. Deaktivieren Sie die Cache-Prepared-Statement-Funktion (nicht empfohlen).

Rails6 und höher können diese Funktion deaktivieren, indem vorbereitete_Anweisungen in der Datenbank auf „false“ gesetzt werden.

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

Rails6 und niedriger wurden nicht getestet. Wenn das oben Genannte nicht funktioniert, können Sie versuchen, eine neue Initialisierungsdatei zu erstellen.

# 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

verifizieren:

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

Fazit: Bei kleinen Projekten spielt es keine Rolle, ob Sie diese Funktion deaktivieren, und die Leistung bleibt nahezu unverändert. Bei großen Projekten sind jedoch die Vorteile dieser Funktion umso größer, je mehr Benutzer und je komplexer die Abfrageanweisungen sind. So können Sie entsprechend der tatsächlichen Situation entscheiden, ob Sie es deaktivieren möchten.

2. machenselect * werdenselect id, nameSolche spezifischen Felder in Rails7Offizielle LösungDas ist es

  • Setzen Sie enumerate_columns_in_select_statements in Rails7 auf 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
  • Unter Rails7 gibt es keine solche Konfiguration. Sie können dies mit „ignored_columns“ erreichen
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

Fazit: Das Problem bei dieser Lösung besteht darin, dass das Hinzufügen von Feldern perfekt gelöst werden kann, das Löschen von Feldern jedoch immer noch Fehler verursacht. Wenn beispielsweise nach dem Löschen des Namensfelds der Name in der vorbereiteten Anweisung „select id“ (Name von Benutzern) nicht vorhanden ist, Bei der offiziellen Rails7-Lösung wird dieses Problem ebenfalls gemeldet

3. Starten Sie die Rails-Anwendung neu

  • Der Lebenszyklus des Caches für vorbereitete Anweisungen existiert nur in einer Datenbanksitzung. Wenn die Datenbankverbindung geschlossen wird (durch einen Neustart der Anwendung wird die ursprüngliche Verbindung geschlossen und eine neue Verbindung wiederhergestellt), wird der ursprüngliche Cache für vorbereitete Anweisungen gelöscht und die Nach dem Neustart wird die SQL-Anfrage neu zwischengespeichert und Sie können die Daten normal abrufen.

Fazit: Ein Neustart der Anwendung führt dazu, dass der Dienst vorübergehend 502 nicht verfügbar ist. Natürlich muss der Dienst beim Bereitstellen neu gestartet werden und es wird auch 502 angezeigt. Daher ist es am besten, ihn bereitzustellen, wenn niemand darauf zugreift (in der Mitte). in der Nacht?), damit möglichst wenige Fehler auftretenPreparedStatementCacheExpiredMelden Sie einen Fehler

4. Umschreiben transaction Methode

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
  • Nach dem Umschreiben wird die Stelle, an der Transaktionen im Code geschrieben werden, zur Verwendung geändert ApplicationRecord.transaction do ... end oderMyModel.transaction

Fazit: Wichtiger Hinweis: Wenn Sie E-Mails senden, an APIs posten oder andere Vorgänge ausführen, die im Rahmen einer Transaktion mit der Außenwelt interagieren, kann dies dazu führen, dass einige dieser Vorgänge gelegentlich zweimal ausgeführt werden. Aus diesem Grund führt Rails offiziell Wiederholungsversuche nicht automatisch durch, sondern überlässt dies den Anwendungsentwicklern.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Wenn ich diese Methode selbst teste, erhalte ich immer noch Fehler.

5. Löschen Sie den Cache für vorbereitete Anweisungen manuell

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Endgültige Antwort

Habe keine perfekte Lösung gefunden