Ruby on RailsMiddleTechnical

Как Puma concurrency, database pool и Sidekiq pool связаны между собой?

Puma workers×threads определяет нагрузку на БД: каждый поток держит одно соединение. Размер database pool должен быть ≥ Puma threads, а Sidekiq требует отдельный pool равный своему concurrency.

Как Puma, database pool и Sidekiq pool связаны

Puma — многопоточный веб-сервер. Каждый живой поток Puma может одновременно выполнять запрос к БД, поэтому ActiveRecord держит пул соединений размером не меньше числа потоков.

Формула расчёта

  • Puma: WEB_CONCURRENCY (воркеры) × RAILS_MAX_THREADS (потоки на воркер).
  • database pool в config/database.yml: должен быть равен RAILS_MAX_THREADS на процесс.
  • Sidekiq: запускается как отдельный процесс с собственным concurrency (количество потоков). Для него нужен отдельный pool того же размера.
# config/database.yml
production:
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
# config/sidekiq.yml
:concurrency: 10
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV["REDIS_URL"] }

  # Переопределяем pool под concurrency Sidekiq
  database_count = ActiveRecord::Base.configurations
    .find_db_config(Rails.env).configuration_hash[:pool].to_i
  sidekiq_count  = Sidekiq.options[:concurrency]

  if sidekiq_count > database_count
    ActiveRecord::Base.connection_pool.disconnect!
    config_hash = ActiveRecord::Base.configurations
      .find_db_config(Rails.env).configuration_hash
      .merge(pool: sidekiq_count + 2)
    ActiveRecord::Base.establish_connection(config_hash)
  end
end

Пример полной конфигурации

# .env / Heroku config vars
WEB_CONCURRENCY=2
RAILS_MAX_THREADS=5
# Итого Puma держит 2*5=10 соединений с БД
# Sidekiq concurrency=10 → ещё 10 соединений
# Итого PostgreSQL получит до 20 соединений

На Heroku Postgres Hobby лимит — 25 соединений. При WEB_CONCURRENCY=2, RAILS_MAX_THREADS=5 и Sidekiq concurrency=10 суммарно будет 20 соединений — это нормально. Если превысить лимит, Postgres вернёт ошибку too many connections.

PgBouncer как буфер

При большом масштабе между Rails и Postgres ставят PgBouncer в режиме transaction. Тогда каждому Rails-пулу достаточно 1–2 соединений, а PgBouncer мультиплексирует их на меньшее число реальных соединений PostgreSQL.

Подводные камни

  • Установка pool ниже RAILS_MAX_THREADS приводит к ActiveRecord::ConnectionTimeoutError при пиковой нагрузке.
  • Sidekiq запускает свой процесс независимо от Puma — забыть пересчитать pool для него очень легко.
  • При использовании preload_app: true в Puma пул открывается до форка; после форка каждый воркер должен переустановить соединения (ActiveRecord::Base.establish_connection в on_worker_boot).
  • PgBouncer в режиме session не даёт выигрыша — только transaction режим позволяет мультиплексирование.
  • Слишком большой pool держит соединения впустую, тратя память PostgreSQL (каждое соединение ~5–10 МБ).
  • ENV-переменная DATABASE_URL переопределяет database.yml целиком; явный pool: в ней имеет приоритет (?pool=10 в URL).
  • Active Job адаптеры (Delayed Job, GoodJob) тоже используют БД-соединения — их тоже нужно учитывать в суммарном лимите.
  • Puma before_fork/on_worker_boot хуки нужно прописывать явно в config/puma.rb, иначе соединения не сбрасываются при форке.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics