Обмен технологиями

[ruby onrails] Причины и решения ошибок ActiveRecord::PreparedStatementCacheExpired во время развертывания

2024-07-12

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

1. Вопрос:

  • Иногда при развертывании приложения Rails в Postgres вы можете увидеть ошибку ActiveRecord::PreparedStatementCacheExpired. Это происходит только при выполнении миграции в развертывании.
  • Это происходит потому, что Rails использует функцию кэша подготовленных операторов Postgres (PreparedStatementCache) для повышения производительности. Эта функция включена в рельсах по умолчанию.

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После этого отправьте его в базу данных,Сначала выполните ПОДГОТОВКУ Подготовленные операторы, операторы 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 после добавления или удаления полей:база данных pgбросит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-код из подготовленного кэша операторов и выполняет команду excute напрямую. Однако, поскольку в это время структура таблицы изменяется, подготовленный кэш операторов станет недействительным.база данных pgброситcached plan must not change result typeошибка
  • Посмотреть исходный код active_recordexec_cacheМетод, я обнаружил, что метод обработки ошибок рельсов для pg:
    1. В транзакции он будет брошен напрямую raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Те, кто находится за пределами транзакции, будут кэшировать @statements.Удалите это предложение и попробуйте, после повторной попытки оператор 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 и выше могут отключить эту функцию, установив для подготовленных_выражений в базе данных значение 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, nameТакие специфические поля в рельсах7Официальное решениеВот и все

  • Установите для enumerate_columns_in_select_statements значение true в рельсах 7.
# 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
  • Ниже рельсов 7 такой конфигурации нет, для ее достижения вы можете использовать ignore_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

Вывод: проблема с этим решением заключается в том, что добавление полей может быть прекрасно решено, но удаление полей все равно будет вызывать ошибки. Например, после удаления поля имени, если имя в подготовленном операторе выбирает id, имя из пользователей не существует. будет сообщено об ошибке. Официальное решение Rails7 также имеет эту проблему.

3. Перезапустите приложение Rails.

  • Жизненный цикл кэша подготовленных операторов существует только в одном сеансе базы данных. Если соединение с базой данных закрыто (перезапуск приложения приведет к закрытию исходного соединения и повторному установлению нового соединения), исходный кэш подготовленных операторов будет очищен, а файл кэша подготовленных операторов будет очищен. 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 или выполняете другие операции, которые взаимодействуют с внешним миром в рамках транзакции, это может привести к тому, что некоторые из этих операций иногда могут выполняться дважды. Вот почему Rails официально не выполняет повторные попытки автоматически, а оставляет это на усмотрение разработчиков приложений.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Когда я сам тестирую этот метод, у меня все равно возникают ошибки.

5. Вручную очистите кэш подготовленных операторов.

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Окончательный ответ

Не нашел идеального решения