pgBouncer

Для чего нужен пулер соединений
Количество соединений, которое поддерживает сервер БД PostgreSQL, ограничено. Поэтому нормальная логика работы клиентского приложения с БД предполагает, что приложение:
Устанавливает соединение с сервером БД.
Отправляет SQL-запрос и получает результат.
Закрывает соединение, освобождая его для следующих клиентов.
Проблема в том, что PostgreSQL выделяет отдельный процесс на каждое установленное соединение. Постоянное открытие и закрытие множества соединений означает запуск и остановку множества процессов, что создает большие накладные расходы ресурсов сервера и приводит к снижению производительности СУБД.
Чтобы решить эту проблему, в DBaaS Postgres для всех инстансов установлен менеджер соединений PgBouncer (пулер). Логика его работы следующая:
Пулер устанавливает с сервером БД фиксированное количество соединений (пул), которые держит открытыми все время. В DBaaS Postgres размер пула по умолчанию (default_pool_size pgbouncer) составляет 200 соединений.
Клиентское приложение подключается не напрямую к БД, а к пулеру. Максимальное количество поддерживаемых соединений между приложением и пулером составляет 6000.
Пулер коммутирует клиентское соединение на сервер, используя открытое соединение из пула.
Когда приложение закрывает соединение с пулером, соединение между пулером и сервером возвращается в пул и может быть переиспользовано следующим клиентским соединением.
Режимы работы PgBouncer
Менеджер соединений PgBouncer в DBaaS Postgres работает в двух режимах:
Сессионный — в этом режиме клиентское соединение устанавливается при первом запросе к базе данных и поддерживается до тех пор, пока клиент не разорвет сессию. Такой режим поддерживается всеми клиентами PostgreSQL, но является менее производительным, чем транзакционный режим. PgBouncer в сессионном режиме доступен на порту
5432
.Транзакционный — в этом режиме клиентское соединение устанавливается при первом запросе к базе данных и поддерживается до завершения транзакции. Транзакционный режим обеспечивает высокую производительность и позволяет максимально эффективно нагрузить СУБД. PgBouncer в транзакционном режиме доступен на порту
6432
.
Однако транзакционный режим поддерживается не всеми клиентами PostgreSQL, и в нем недоступно использование некоторых функций PostgreSQL:
временных таблиц (temporary tables);
курсоров (cursors);
рекомендательных блокировок (advisory locks), которые существуют дольше одной транзакции;
подготовленных операторов (prepared statements).
Experiment
Preparation
Having following docker-compose file:
services:
postgres:
image: postgres
environment:
POSTGRES_PASSWORD: mysecretpassword
ports:
- "5437:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
pgbouncer:
image: edoburu/pgbouncer
depends_on:
- postgres
ports:
- "6432:6432"
volumes:
- ./pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini
- ./userlist.txt:/etc/pgbouncer/userlist.txt
volumes:
postgres_data:
# pgbouncer.ini file content
[databases]
maindb = host=postgres port=5432 dbname=postgres
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 100
default_pool_size = 20
# userlist.txt
"pgbouncer" "pgbouncer"
"postgres" "mysecretpassword"
# prepare for benchmark
pgbench -i -h 127.0.0.1 -p 5437 -U postgres -d postgres
This creates the test tables (pgbench_branches
, pgbench_accounts
, etc.) and populates them with sample data.
Without pgBouncer
# start benchmark
pgbench -j 4 -c 50 -t 1000 -h 127.0.0.1 -p 5437 -U postgres -d postgres
-c 50
: Use 10 concurrent clients.-j 4
: Use 4 threads.
result may look like
pgbench (17.6)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 50
number of threads: 4
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 50000/50000
number of failed transactions: 0 (0.000%)
latency average = 83.820 ms
initial connection time = 1491.121 ms
tps = 596.515427 (without initial connection time)
With pgBouncer
# start benchmark
pgbench -j 4 -c 50 -t 1000 -h 127.0.0.1 -p 6432 -U postgres -d maindb
-d maindb (this comes from pgbouncer.ini where I define an alias
maindb which leads to actual postgres
database in postgres container)
pgbench (17.6)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 50
number of threads: 4
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 50000/50000
number of failed transactions: 0 (0.000%)
latency average = 93.732 ms
initial connection time = 76.382 ms
tps = 533.434535 (without initial connection time)
Session vs Transaction modes
Scenario 1: pgBouncer
in session
Mode
pgBouncer
in session
Mode📌 Behavior
A single PostgreSQL session is assigned to a client connection for its entire lifetime.
All transactions from the same client use the same backend PID.
Session-scoped features (like temporary tables, GUCs,
SET LOCAL
, etc.) persist across transactions.
✅ Use Cases
Applications that rely on:
Temporary tables
Session variables (
SET LOCAL app.user_id = '123'
)Listeners (
LISTEN
/NOTIFY
)Extensions like
pg_trgm
with session-local settings
🧠 Spring Boot + Hibernate Behavior
One
EntityManager
may reuse the same connection (session) across multiple@Transactional
methods.Session state (e.g.,
SET LOCAL
) persists across transactions.Temporary tables created in one transaction are visible in subsequent transactions.
⚠️ Drawbacks
Less scalable: Each client connection holds a backend connection.
Risk of connection leaks if clients don’t close connections.
🧪 Scenario 2: pgBouncer
in transaction
Mode
pgBouncer
in transaction
Mode📌 Behavior
A backend connection is assigned only for the duration of a single transaction.
After
COMMIT
orROLLBACK
, the connection is returned to the pool and can be reused by another client.Session-scoped features do not persist across transactions.
✅ Use Cases
Stateless applications
High concurrency with many short transactions
Applications that don’t rely on session state
🧠 Spring Boot + Hibernate Behavior
Each
@Transactional
method may get a different backend PID.Session variables (
SET LOCAL
) are lost between transactions.Temporary tables are not visible across transactions (since they’re session-scoped).
Hibernate and Spring can still work fine, as long as they don’t rely on session state.
⚠️ Drawbacks
Incompatible with:
Temporary tables
SET LOCAL
variablesLISTEN
/NOTIFY
Extensions that rely on session state
Last updated
Was this helpful?