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。