Перейти к содержанию
Innovialabs

Пул соединений PgBouncer: настройка и подводные камни

Продвинутое4 мин

Коротко

PgBouncer позволяет тысячам клиентов делить небольшое число реальных соединений с PostgreSQL; для веба и serverless берите режим transaction, но откажитесь от состояния сессии и явно решите вопрос с prepared statements.

Зачем вообще нужен пулер

PostgreSQL создаёт отдельный серверный процесс на каждое соединение. Это fork, аутентификация (часто с TLS) и собственная память под кэши и выполнение запросов. Даже простаивающие соединения не бесплатны: они занимают слоты max_connections и добавляют накладные расходы на внутренний учёт.

Обычному веб-приложению не нужно много соединений одновременно — ему нужно много клиентов, каждый из которых держит соединение несколько миллисекунд. PgBouncer стоит между приложением и PostgreSQL, принимает большое число дешёвых клиентских подключений и распределяет их по небольшому пулу реальных серверных соединений.

Режимы session и transaction

У PgBouncer три режима, на практике важны два.

SessionTransaction
Серверное соединение занятовсю клиентскую сессиютолько на время транзакции
Переиспользованиенизкоевысокое
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.

Частые вопросы

Пул в приложении ограничивает соединения одного процесса. Когда процессов, контейнеров или функций несколько, их пулы складываются. PgBouncer ограничивает общее число для всего парка, поэтому при горизонтальном масштабировании обычно нужны оба, причём пул в приложении — маленький.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.