Condivisione della tecnologia

[Ruby on rails] Motivi e soluzioni per gli errori ActiveRecord::PreparedStatementCacheExpired durante la distribuzione

2024-07-12

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

1. Domanda:

  • A volte, durante la distribuzione di applicazioni Rails su Postgres, potresti visualizzare errori ActiveRecord::PreparedStatementCacheExpired. Ciò accade solo quando si eseguono migrazioni in una distribuzione.
  • Ciò accade perché Rails sfrutta la funzionalità PreparedStatementCache di Postgres per migliorare le prestazioni. Questa funzione è abilitata per impostazione predefinita nei binari.

2. Ricorrenza del problema:

  • Possiamo usare rspec per riprodurre questo errore
 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

Inserisci qui la descrizione dell'immagine

3. Principio di produzione:

  • Istruzioni di query su Rails comeUser.allda active_record Analizzare nell'istruzione SQLSuccessivamente, invialo al database,Eseguire prima PREPARE Dichiarazioni preparate, istruzioni SQL verranno analizzate, analizzate, ottimizzate e riscritte.Quando il seguitoEmettere un comando EXECUTE, la dichiarazione preparata sarà pianificata ed eseguita.
  • Rails salverà l'istruzione della query inpg_prepared_statementsper facilitare la prossima volta che chiamerai affermazioni simili.eseguire direttamente dichiarazioni, invece di analizzare, analizzare e ottimizzare, evitando la duplicazione del lavoro e migliorando l'efficienza.
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, la cache delle istruzioni preparate verrà invalidata se lo schema della tabella cambia influenzando i risultati restituiti. Nello specifico, si tratta di operazioni DDL come l'aggiunta e l'eliminazione di campi nella tabella o la modifica del tipo e della lunghezza dei campi.

Come nell'esempio seguente, quando si esegue SELECT dopo aver aggiunto o eliminato campi,banca dati pglanceràcached plan must not change result type, su rotaieregistrazione_attivaOttieni questo errore e poi lanciaActiveRecord::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
  • Quando si esegue la migrazione aggiungendo, sottraendo o modificando campi nel servizio di distribuzione, l'istruzione di query emessa dall'utente recupererà direttamente l'SQL dalla cache delle istruzioni preparate ed eseguirà excute direttamente. Tuttavia, poiché la struttura della tabella cambia in questo momento, la cache delle istruzioni preparate diventa non valida.banca dati pglanceràcached plan must not change result typeerrore
  • Visualizza il codice sorgente active_recordexec_cacheMetodo, ho scoperto che il metodo di gestione degli errori di rails per pg è:
    1. Nella transazione, verrà lanciato direttamente raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Quelli al di fuori della transazione memorizzeranno nella cache le istruzioni @Elimina questa frase e prova, dopo aver riprovato, l'istruzione SQL verrà nuovamente analizzata, analizzata, ottimizzata ed eseguita.prepare_statementmetodo nella cache delle istruzioni preparate
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
  • Pertanto, questo errore visualizzato nella transazione causerà il rollback della transazione. Per l'azienda, significa che la richiesta non è riuscita e dobbiamo gestirla da soli.

4. Soluzione:

1. Disabilitare la funzione di istruzione preparata della cache (non consigliata)

Rails6 e versioni successive possono disabilitare questa funzionalità impostando ready_statements nel database su false.

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

Rails6 e versioni precedenti non sono state testate. Se quanto sopra non funziona, puoi provare a creare un nuovo file di inizializzazione.

# 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

verificare:

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

Conclusione: nei piccoli progetti non importa se disabiliti questa funzione e le prestazioni rimarranno quasi inalterate. Tuttavia, nei progetti di grandi dimensioni, maggiore è il numero degli utenti e le istruzioni di query più complesse, maggiori saranno i vantaggi che questa funzione porterà. quindi puoi decidere se disabilitarlo in base alla situazione reale.

2. fareselect * diventareselect id, nameTali campi specifici, in rails7Soluzione ufficialeQuesto è tutto

  • Imposta enumerate_columns_in_select_statements su true in rails7
# 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
  • Non esiste una configurazione di questo tipo sotto rails7, puoi utilizzare ignore_columns per ottenerla
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

Conclusione: il problema con questa soluzione è che l'aggiunta di campi può essere risolta perfettamente, ma l'eliminazione dei campi causerà comunque errori. Ad esempio, dopo aver eliminato il campo del nome, se il nome nell'istruzione preparata select id, il nome degli utenti non esiste. verrà segnalato un errore. Anche la soluzione ufficiale di Rails7 presenta questo problema

3. Riavviare l'applicazione delle rotaie

  • Il ciclo di vita della cache delle istruzioni preparate esiste solo in una sessione del database. Se la connessione al database viene chiusa (il riavvio dell'applicazione chiuderà la connessione originale e ristabilirà una nuova connessione), la cache delle istruzioni preparate originale verrà cancellata e il file verrà cancellato. La richiesta SQL dopo il riavvio verrà riavviata. Memorizza nella cache l'istruzione preparata e sarà possibile ottenere i dati normalmente.

Conclusione: il riavvio dell'applicazione causerà un servizio temporaneo 502 Non disponibile Naturalmente, quando si distribuisce l'applicazione, il servizio deve essere riavviato e verrà visualizzato anche 502, quindi è meglio eseguire la distribuzione quando nessuno vi accede (nel mezzo di). la notte?), in modo che vengano visualizzati meno errori possibiliPreparedStatementCacheExpiredSegnala un errore

4. Riscrivi transaction metodo

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
  • Dopo la riscrittura, il luogo in cui vengono scritte le transazioni nel codice viene modificato ApplicationRecord.transaction do ... end OMyModel.transaction

Conclusione: Nota importante: se invii e-mail, pubblichi su API o esegui altre operazioni che interagiscono con il mondo esterno come parte di una transazione, ciò potrebbe far sì che alcune di queste operazioni si ripetano occasionalmente due volte. Questo è il motivo per cui Rails ufficialmente non esegue i tentativi automaticamente, ma li lascia agli sviluppatori dell'applicazione.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Quando provo personalmente questo metodo, ricevo ancora errori.

5. Svuotare manualmente la cache delle istruzioni preparate

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Risposta finale

Non ho trovato una soluzione perfetta