【问题标题】:Possible to avoid ActiveRecord::ConnectionTimeoutError on Heroku?可以避免 Heroku 上的 ActiveRecord::ConnectionTimeoutError 吗?
【发布时间】:2013-09-24 19:41:14
【问题描述】:

在 Heroku 上,我有一个 Rails 应用程序运行它,其中有几个 web dyno 和一个 worker dyno。我全天在 Sidekiq 上运行数千个工作任务,但偶尔会引发 ActiveRecord::ConnectionTimeoutError(每天大约 50 次)。我已经按如下方式设置了我的独角兽服务器

worker_processes 4
timeout 30
preload_app true

before_fork do |server, worker|
    # As suggested here: https://devcenter.heroku.com/articles/rails-unicorn
    Signal.trap 'TERM' do
        puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
        Process.kill 'QUIT', Process.pid
    end

    if defined?(ActiveRecord::Base)
        ActiveRecord::Base.connection.disconnect!
    end
end

after_fork do |server,worker|
    if defined?(ActiveRecord::Base)
        config = Rails.application.config.database_configuration[Rails.env]
        config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
        config['pool']            = ENV['DB_POOL'] || 10
        ActiveRecord::Base.establish_connection(config)
    end

    Sidekiq.configure_client do |config|
        config.redis = { :size => 1 }
    end

    Sidekiq.configure_server do |config|
        config = Rails.application.config.database_configuration[Rails.env]
        config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
        config['pool']            = ENV['DB_POOL'] || 10
        ActiveRecord::Base.establish_connection(config)
    end
end

在 heroku 上,我将 DB_POOL 配置变量设置为 2,即 recommended by Heroku。这些错误应该发生吗?似乎很奇怪,不可能避免这样的错误,不是吗?你有什么建议?

【问题讨论】:

    标签: ruby-on-rails activerecord heroku unicorn sidekiq


    【解决方案1】:

    默认情况下,sidekiq 服务器(在您的服务器上运行的进程实际上正在执行延迟的任务)将最多拨出 25 个线程来处理其队列之外的工作。如果您的任务需要,这些线程中的每一个都可能通过 ActiveRecord 请求连接到您的主数据库。

    如果您只有 5 个连接的连接池,但您有 25 个线程尝试连接,则 5 秒后,如果线程无法从池中获得可用连接,则线程将放弃,您将获得连接超时错误。

    将 Sidekiq 服务器的池大小设置为更接近并发级别(在启动进程时使用 -c 标志设置)将有助于缓解此问题,但代价是打开更多与数据库的连接。例如,如果您在 Heroku 上使用 Postgres,他们的一些计划限制为 20,而其他计划的连接限制为 500 (source)。

    如果您正在运行像 Unicorn 这样的多进程服务器环境,您还需要监控每个分叉进程建立的连接数。如果您有 4 个独角兽进程,并且默认连接池大小为 5,那么您的独角兽环境在任何给定时间都可能有 20 个活动连接。您可以在Heroku's docs 上阅读更多相关信息。另请注意,数据库池的大小并不意味着每个 dyno 现在将有那么多打开的连接,而只是如果需要一个新连接,它将被创建,直到创建了最多的连接。

    话虽如此,这就是我要做的。

    # config/initializers/unicorn.rb
    
    if ENV['RACK_ENV'] == 'development'
      worker_processes 1
      listen "#{ENV['BOXEN_SOCKET_DIR']}/rails_app"
      timeout 120
    else
      worker_processes Integer(ENV["WEB_CONCURRENCY"] || 2)
      timeout 29
    end
    
    # The timeout mechanism in Unicorn is an extreme solution that should be avoided whenever possible. 
    # It will help catch bugs in your application where and when your application forgets to use timeouts,
    # but it is expensive as it kills and respawns a worker process.
    # see http://unicorn.bogomips.org/Application_Timeouts.html
    
    # Heroku recommends a timeout of 15 seconds. With a 15 second timeout, the master process will send a 
    # SIGKILL to the worker process if processing a request takes longer than 15 seconds. This will 
    # generate a H13 error code and you’ll see it in your logs. Note, this will not generate any stacktraces 
    # to assist in debugging. Using Rack::Timeout, we can get a stacktrace in the logs that can be used for
    # future debugging, so we set that value to something less than this one
    
    preload_app true # for new relic
    
    before_fork do |server, worker|
      Signal.trap 'TERM' do
        puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
        Process.kill 'QUIT', Process.pid
      end
    
      if defined?(ActiveRecord::Base)
        ActiveRecord::Base.connection.disconnect!
      end
    
    end
    
    after_fork do |server, worker|
      Signal.trap 'TERM' do
        puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to sent QUIT'
      end
    
      Rails.logger.info("Done forking unicorn processes")
    
      #https://devcenter.heroku.com/articles/concurrency-and-database-connections
      if defined?(ActiveRecord::Base)
    
        db_pool_size = if ENV["DB_POOL"]
          ENV["DB_POOL"]
        else
          ENV["WEB_CONCURRENCY"] || 2
        end
    
        config = Rails.application.config.database_configuration[Rails.env]
        config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
        config['pool']              = ENV['DB_POOL'] || 2
        ActiveRecord::Base.establish_connection(config)
    
        # Turning synchronous_commit off can be a useful alternative when performance is more important than exact certainty about the durability of a transaction
        ActiveRecord::Base.connection.execute "update pg_settings set setting='off' where name = 'synchronous_commit';"    
    
        Rails.logger.info("Connection pool size for unicorn is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
      end
    
    end
    

    对于 sidekiq:

    # config/initializers/sidekiq.rb
    
    Sidekiq.configure_server do |config|
    
      sidekiq_pool = ENV['SIDEKIQ_DB_POOL'] || 20
    
      if defined?(ActiveRecord::Base)
        Rails.logger.debug("Setting custom connection pool size of #{sidekiq_pool} for Sidekiq Server")
        db_config = Rails.application.config.database_configuration[Rails.env]
        db_config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
        cb_config['pool']              = sidekiq_pool
        ActiveRecord::Base.establish_connection(db_config)
    
        Rails.logger.info("Connection pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
      end
    end
    

    如果一切顺利,当您启动进程时,您会在日志中看到类似的内容:

    Setting custom connection pool size of 10 for Sidekiq Server
    Connection pool size for Sidekiq Server is now: 20
    Done forking unicorn processes
       (1.4ms)  update pg_settings set setting='off' where name = 'synchronous_commit';
    Connection pool size for unicorn is now: 2
    

    来源:

    【讨论】:

    • 这是配置两台服务器的一个很好的解释。我有一些非常接近这个的东西,会尝试你的建议。为了清楚起见,唯一可以做的评论是 Unicorn 和 Sidekiq 是独立的服务器,将与其 config.rb 文件分开启动。这绝对是问题的正确答案。
    • 在 sidekiq.rb 中,您正在使用局部变量隐藏块变量 config。如果你想在 sidekiq 的 config 上调用任何东西,那么你真的应该将本地 var 重命名为 db_config 或其他东西。
    • @mpoisot:感谢您的评论。我们在我们的应用程序中使用了这个配置,我刚刚检查了它,结果发现我们在某个时候将本地 config 重命名为 db_config。我刚刚更新了答案以反映这一点。再次感谢。
    【解决方案2】:

    对于 Sidekiq 服务器配置,建议将 db_pool 数字与您的并发性相同,我假设您已将其设置为大于 2。

    假设设置您的db_poolunicorn.rb 中工作(我没有这样做的经验),一个潜在的解决方案是设置另一个环境变量来直接控制Sidekiq db_pool

    如果您的 sidekiq 并发数为 20,则类似于:

    配置变量 - SIDEKIQ_DB_POOL = 20

    Sidekiq.configure_server do |config|
      config = Rails.application.config.database_configuration[Rails.env]
      config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
      config['pool']            = ENV['SIDEKIQ_DB_POOL'] || 10
      ActiveRecord::Base.establish_connection(config)
    end
    

    这可确保您有两个独立的池,分别针对您的网络工作者 DB_POOL 和后台工作者 SIDEKIQ_DB_POOL 进行优化

    【讨论】:

    • 为什么我要将 Sidekiq DB 池设置为 20?它仍在 Unicorn 上运行。
    • Siddkiq.configure_server --> 实际执行工作的进程,默认情况下最多可以旋转 25 个线程。 Sidekiq.configure_client --> 你的 rails 进程(还有你的 sidekiq 工作人员,因为他们也可以添加工作!)。因此,您将使用接近 sidekiq 并发级别大小的数据库池配置您的 Sidekiq“服务器”(执行工作的进程)。
    • "db_pool number 与你的并发数相同"。在死连接的情况下,db_pool 实际上应该稍高一些,例如并发 + 2 (github.com/mperham/sidekiq/issues/503#issuecomment-33547209)
    • 您正在使用局部变量隐藏块变量config。如果你想在 sidekiq 的 config 上调用任何东西,那么你真的应该将本地 var 重命名为 db_config 或其他东西。
    猜你喜欢
    • 2016-12-26
    • 1970-01-01
    • 2017-04-14
    • 2014-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-10
    • 1970-01-01
    相关资源
    最近更新 更多