Ruby, PostgreSQL и Rails — мощный стек для разработки современных веб-приложений, но без грамотного DevOps-подхода даже опытные разработчики могут столкнуться с проблемами масштабирования. В этой статье разберем ключевые аспекты работы с базами данных, пулерами соединений и многопоточностью, которые помогут избежать типичных ошибок в продакшене. Особое внимание уделим взаимодействию Puma, ActiveRecord и PgBouncer — трио, которое требует тонкой настройки для стабильной работы под нагрузкой.
🧠 TL;DR
Если ты используешь PgBouncer с Rails, и у тебя Puma в режиме многопоточности —
внимание: ActiveRecord может открыть слишком много соединений!
🔁 Как работает соединение в Puma?
Puma запускает несколько воркеров, каждый из которых — отдельный процесс.
Внутри каждого — несколько потоков (по threads в puma.rb).
И каждый поток может запросить соединение к БД через ActiveRecord.
🧮 Сколько соединений будет?
Формула простая:
connection_count = workers * max_threads
Допустим:
workers 4
threads 1, 8
Тогда потенциально — 4 × 8 = 32 соединения к PgBouncer от Rails.
🧩 А PgBouncer не справляется?
PgBouncer по умолчанию в режиме session — и это плохо.
Каждое соединение из Rails → держится за backend до тех пор, пока клиент не отключится. А Puma не отключается — это long-lived процессы.
🩹 Что делать?
- Переключи PgBouncer в режим
transaction:
pool_mode = transaction
Теперь backend соединение освобождается сразу после коммита.
- В
database.ymlдобавь:
production:
adapter: postgresql
...
variables:
application_name: myapp
prepared_statements: false
⚠️ prepared_statements: false — обязательно, иначе PostgreSQL выдаст ошибки.
📉 А что если не отключить?
ActiveRecord будет думать, что держит соединение долго —
а PgBouncer отдаст его другому —
и вы получите server closed the connection unexpectedly.
💡 Вывод
Если у тебя PgBouncer и Puma — обязательно разбери, как и где работают соединения.
pool_mode = transactionprepared_statements = falsemax connections = workers * threadsPgBouncer pool_size ≥ sum of all clients per DB