Пул соединений PgBouncer: настройка и подводные камни
Продвинутое4 минРедакция Innovialabs
Коротко
PgBouncer позволяет тысячам клиентов делить небольшое число реальных соединений с PostgreSQL; для веба и serverless берите режим transaction, но откажитесь от состояния сессии и явно решите вопрос с prepared statements.
Зачем вообще нужен пулер
PostgreSQL создаёт отдельный серверный процесс на каждое соединение. Это fork, аутентификация (часто с TLS) и собственная память под кэши и выполнение запросов. Даже простаивающие соединения не бесплатны: они занимают слоты max_connections и добавляют накладные расходы на внутренний учёт.
Обычному веб-приложению не нужно много соединений одновременно — ему нужно много клиентов, каждый из которых держит соединение несколько миллисекунд. PgBouncer стоит между приложением и PostgreSQL, принимает большое число дешёвых клиентских подключений и распределяет их по небольшому пулу реальных серверных соединений.
Режимы session и transaction
У PgBouncer три режима, на практике важны два.
| Session | Transaction | |
|---|---|---|
| Серверное соединение занято | всю клиентскую сессию | только на время транзакции |
| Переиспользование | низкое | высокое |
SET, временные таблицы, LISTEN | работают | ломаются или «протекают» к другим клиентам |
| Сессионные advisory-блокировки | работают | небезопасны |
| Где применять | старые приложения, админ-инструменты | веб-API, воркеры, serverless |
Session безопасен, но выигрыш небольшой: клиент, который подключился и молчит, всё равно держит серверное соединение. В основном этот режим защищает базу от постоянного открытия и закрытия соединений.
Transaction — режим, где пулинг действительно окупается. После COMMIT или ROLLBACK серверное соединение возвращается в пул, и следующий запрос того же клиента может попасть на другой backend. Всё, что привязано к сессии, — SET search_path, SET timezone, временные таблицы, LISTEN/NOTIFY, pg_advisory_lock — перестаёт быть надёжным.
Statement запрещает транзакции из нескольких запросов и подходит редко.
Минимальный рабочий конфиг
[databases]
app = host=127.0.0.1 port=5432 dbname=app
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 300
Ключевые параметры:
- max_client_conn — сколько клиентов принимает PgBouncer. Может быть большим: клиентские соединения дешёвые.
- default_pool_size — число реальных соединений на пару «пользователь + база». Именно оно нагружает PostgreSQL.
- reserve_pool_size — дополнительные соединения, если клиенты ждут слишком долго.
- Сумма всех пулов всех экземпляров PgBouncer должна быть меньше
max_connectionsс запасом для админских сессий и миграций.
Переключите приложение с порта 5432 на 6432 и загляните в админ-консоль:
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
Следите за cl_waiting и maxwait. Если клиенты постоянно ждут, значит пул мал или транзакции слишком длинные — чаще второе.
Prepared statements
Многие драйверы и ORM используют prepared statements на уровне протокола: запрос готовится на одном backend и потом выполняется по имени. В режиме transaction следующее выполнение может попасть на другой backend, и вы получите ошибку вида prepared statement "s1" does not exist.
Варианты, от простого к сложному:
- Новые версии PgBouncer умеют отслеживать протокольные prepared statements в режиме transaction через
max_prepared_statements. Проверьте, что ваша версия это поддерживает. - Отключить именованные prepared statements в драйвере: например,
prepareThreshold=0в JDBC-драйвере PostgreSQL,statement_cache_size=0в asyncpg,pgbouncer=trueв строке подключения Prisma. - SQL-команда
PREPAREв режиме transaction небезопасна всегда — она живёт в сессии.
Serverless и много мелких инстансов
Serverless-функции и автомасштабируемые контейнеры — классический случай для пулинга. Каждый инстанс открывает свои соединения, и всплеск трафика превращается в шторм подключений, который упирается в max_connections задолго до нехватки CPU.
Что работает:
- Поставьте PgBouncer (или управляемый пулер вашего провайдера) перед базой и используйте transaction.
- Держите пул на стороне приложения минимальным — часто одно соединение на инстанс функции. Два слоя больших пулов только маскируют проблему.
- Миграции,
pg_dumpи долгие админские задачи запускайте через прямое соединение, а не через пулер. Например, в Prisma для этого есть отдельныйdirectUrl. - Параметры сессии задавайте на уровне роли или базы (
ALTER ROLE app SET search_path = ...), а не черезSETв коде.
Частые ошибки
- Длинные транзакции. Транзакция, которая ждёт внешний HTTP-запрос, всё это время держит серверное соединение. Делайте транзакции короткими, сетевые вызовы — вне их.
- Сессии
idle in transaction. ЗабытыйBEGINбез коммита блокирует слот пула. Настройтеidle_in_transaction_session_timeoutв PostgreSQL. - Слишком большой пул. Больше серверных соединений не значит больше пропускной способности: в какой-то момент они начинают конкурировать за CPU и блокировки. Начинайте с малого и увеличивайте, измеряя задержки.
- Один PgBouncer как единая точка отказа. Он однопоточный и лёгкий, но для продакшена планируйте минимум два экземпляра или управляемое решение.
- Уверенность, что сессионные функции продолжат работать. Перед переходом на transaction проверьте код на
SET, временные таблицы,LISTENи advisory-блокировки.
Полный список параметров: документация PgBouncer.