Как 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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.