9

在 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 上,我按照Heroku 的建议将 DB_POOL 配置变量设置为 2 。这些错误应该发生吗?似乎很奇怪,不可能避免这样的错误,不是吗?你有什么建议?

4

2 回答 2

14

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

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

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

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

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

# 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

资料来源:

于 2013-09-21T00:58:59.473 回答
0

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

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

如果您的 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

于 2013-09-20T13:21:19.820 回答