由于数据库重连失败,恢复操作失败

情况描述:

一个由两个容器组成的 Discourse 安装环境,Web 和数据分别部署在不同的主机上。

问题

从非常旧的 Discourse 版本(即需要运行许多耗时的迁移任务)执行一个 22 GB 的大型命令行恢复操作时,在数据库恢复完成后立即失败,耗时 45 分钟。

Reconnecting to the database...
EXCEPTION: PQconsumeInput() could not receive data from server: Connection timed out
SSL SYSCALL error: Connection timed out
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'

…

from /var/www/discourse/app/models/backup_metadata.rb:16:in 'BackupMetadata.update_last_restore_date'
from /var/www/discourse/lib/backup_restore/database_restorer.rb:31:in 'BackupRestore::DatabaseRestorer#restore'
from /var/www/discourse/lib/backup_restore/restorer.rb:61:in 'BackupRestore::Restorer#run'
from script/discourse:242:in 'DiscourseCLI#restore'

…

Trying to rollback...
Cleaning stuff up...
Dropping functions from the discourse_functions schema...
Something went wrong while dropping functions from the discourse_functions schema
PQsocket() can't get socket descriptor

理论分析

Discourse 使用第二个数据库连接进行实际的恢复操作,并使用另一个连接执行迁移。
当这些操作完成后,它会在主连接上重新连接到数据库并执行 BackupMetadata.update_last_restore_date,但这会立即失败。

导致此失败的原因似乎是数据库重连 实际上并没有重新连接。
它复用了缓存的 ConnectionHandler。参见 此处。而在 45 分钟后,该连接已经断开。

handler = connection_handlers[handler_key(spec)]

unless handler
  handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
  handler.establish_connection(spec.config)
  connection_handlers[handler_key(spec)] = handler
end

ActiveRecord::Base.connection_handler = handler

临时解决方案

Postgres 的 tcp_keepalives_idle = 0,这意味着回退到操作系统设置。
操作系统的 net.ipv4.tcp_keepalive_time = 7200(2 小时)。

ALTER SYSTEM SET tcp_keepalives_idle = 60; 
ALTER SYSTEM SET tcp_keepalives_interval = 30; 
ALTER SYSTEM SET tcp_keepalives_count = 5;

这可以防止连接被关闭,并解决了该问题。

建议的修复方案

让重连代码在重新建立连接之前执行 ActiveRecord::Base.connection_handler.clear_all_connections! 或类似操作。

或者,更通用的做法是,为 establish_connection 添加一个 reconnect 参数,以绕过缓存的 handler。

1 個讚