Rails 7.2连接池变更可能会拖慢你的应用

升级到Rails 7.2后,你可能会注意到应用变慢了一些。不是很明显,但可以测量——在实际基准测试中大约5-6%。这归结于ActiveRecord现在管理数据库连接的方式。

变更内容

在Rails 7.1中,每个线程在请求期间持有其数据库连接。

Rails 7.2改变了这一点:连接现在为每个查询检出,并在之后立即归还。目标是在连接稀缺的多线程环境中更好地共享连接。

该变更在PR #50793中引入,将ActiveRecord::Base.connection替换为用于临时访问的ActiveRecord::Base.with_connection和用于较长时间持有的lease_connection

为什么这会导致变慢

检入/检出循环不是免费的。每次连接返回池时,都会运行checkin回调,回调系统会分配对象。一个有10个查询的请求现在要做10次检入而不是零次。

issue #55728的基准测试显示了影响:

Rails 7.1:              98.55 req/sec
Rails 7.2:              92.45 req/sec  (慢6%)
Rails 7.2 + 解决方案:   99.99 req/sec  (恢复正常)

如果你运行像Unicorn这样的单线程worker,有本地或低延迟数据库,并且每个请求执行多个查询,你会最明显地感受到这一点。

解决方法

对于不需要连接共享的单线程worker,在每个请求开始时租用一个连接:

class ApplicationController < ActionController::Base
  before_action :lease_database_connection

  private

  def lease_database_connection
    ActiveRecord::Base.lease_connection
  end
end

这会在整个请求期间持有连接,跳过检入/检出循环。

后台作业也是同样的思路:

class ApplicationJob < ActiveJob::Base
  before_perform do
    ActiveRecord::Base.lease_connection
  end
end

何时跳过这个

如果你运行多线程Puma且线程数多于数据库连接数,新行为实际上是你想要的——它让线程共享连接。长时间运行但不太访问数据库的请求也是如此。持有不需要的连接会让其他线程挨饿。

底层原理

大部分开销在run_callbacks(:checkin)中。即使没有自定义回调,Rails也会运行完整的回调机制——对象分配、方法派发等等。

Jean Boussier(byroot)一直在为Rails main进行优化工作,但这些不会回移到7.2,因为它们是性能调整而不是bug修复。

测量你的应用

在改变任何东西之前,检查这是否影响你:

# config/initializers/connection_benchmark.rb
if Rails.env.development?
  ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    Rails.logger.debug "SQL (#{event.duration.round(2)}ms)"
  end
end

比较有无lease_connection的请求时间。如果看不到差异,就不用费心解决了。

对于升级后响应变慢的Unicorn或单线程Puma部署,试试lease_connection。这是安全的,会让你回到7.1的行为。

如果你想跟踪正在进行的优化工作,请关注issue #55728