PostgreSQL под каждое соединение поднимает отдельный серверный процесс. Стартует он с 5-10 МБ ОЗУ ещё до первого запроса. Умножьте на 100 клиентов - минимум полгигабайта памяти улетает в никуда. На 200+ соединениях PostgreSQL начинает ощутимо тормозить: latency растёт, планировщик больше занят переключением контекста между процессами, чем самими запросами. pgBouncer прослойкой между приложением и базой собирает все входящие соединения и отдаёт их в небольшой пул реальных коннектов к PostgreSQL.
Как pgBouncer работает
pgBouncer - это легковесный прокси-сервер, который располагается между приложением и PostgreSQL. Приложение подключается к pgBouncer так же, как к обычной базе данных, - по TCP на указанный порт. pgBouncer поддерживает собственный пул соединений к PostgreSQL и выдаёт их клиентам по запросу.
Ключевая идея: реальных соединений к PostgreSQL намного меньше, чем клиентских. Если к pgBouncer подключено 200 воркеров Django, а пул к PostgreSQL настроен на 20 соединений - в базе открыто ровно 20 процессов. Остальные 180 запросов ждут в очереди и получают соединение, как только оно освобождается.
В отличие от connection pooling внутри самого приложения (например, SQLAlchemy pool), pgBouncer работает на уровне сети и не зависит от языка или фреймворка. Один экземпляр pgBouncer обслуживает любое количество приложений и сервисов.
Три режима пулинга
Три режима, и выбор между ними - ключевое решение при настройке.
Session pooling - соединение с PostgreSQL выдаётся клиенту на всю сессию. Пока клиент подключён к pgBouncer - он держит одно реальное соединение в базе. Безопасно, поддерживает всё что умеет PostgreSQL, но если клиент сидит без активных запросов - ресурс тратится впустую. Экономия небольшая.
Transaction pooling - то, что нужно для большинства случаев. Соединение с базой занято только на время транзакции. После COMMIT или ROLLBACK оно идёт обратно в пул. 200 клиентов через 20 реальных соединений - и все довольны, если транзакции достаточно короткие.
Statement pooling - соединение возвращается после каждого отдельного оператора. Агрессивный режим с жёстким ограничением: BEGIN/COMMIT блоки не работают. Подходит разве что для аналитических SELECT без явных транзакций. В продакшне встречается редко.
Установка pgBouncer на Ubuntu/Debian
pgBouncer есть в стандартных репозиториях. Установка без лишних шагов:
sudo apt update
sudo apt install pgbouncer -y
После установки проверяем версию и статус службы:
pgbouncer --version
sudo systemctl status pgbouncer
Конфигурационные файлы:
- /etc/pgbouncer/pgbouncer.ini - основной конфиг
- /etc/pgbouncer/userlist.txt - список пользователей и паролей для аутентификации
По умолчанию pgBouncer слушает порт 6432. PostgreSQL продолжает работать на стандартном порту 5432. Приложение переключается на порт 6432 - больше никаких изменений в коде не требуется.
Базовая настройка pgbouncer.ini
Открываем основной конфиг:
sudo nano /etc/pgbouncer/pgbouncer.ini
Минимальная рабочая конфигурация:
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
log_file = /var/log/postgresql/pgbouncer.log
pid_file = /var/run/postgresql/pgbouncer.pid
Блок [databases] - маппинг имён баз данных. Клиент подключается к myapp на pgBouncer, pgBouncer проксирует запрос на реальную базу myapp по адресу 127.0.0.1:5432.
Файл userlist.txt содержит учётные данные в формате:
"username" "password"
Пароль можно хранить в открытом виде или в формате MD5-хеша PostgreSQL. Для получения MD5-хеша:
echo -n "passwordusername" | md5sum
Результат вставляется с префиксом md5:
"appuser" "md5a1b2c3d4e5f6..."
После изменения конфига перезапускаем службу:
sudo systemctl restart pgbouncer
sudo systemctl enable pgbouncer
Ключевые параметры производительности
default_pool_size - сколько реальных соединений к PostgreSQL открывается на каждую пару база+пользователь. Главный параметр. Одна база, один пользователь - это число и есть максимум соединений в PostgreSQL от pgBouncer. Начинайте с 20-30 для продакшна.
max_client_conn - лимит клиентских соединений к самому pgBouncer. Это не коннекты к базе, а то, сколько приложений может ломиться в pgBouncer одновременно. pgBouncer лёгкий - 1000 клиентских соединений для него не нагрузка, ставьте с запасом.
reserve_pool_size - резерв соединений на пиковые моменты. Если все слоты в основном пуле заняты и клиенты ждут дольше reserve_pool_timeout секунд - pgBouncer открывает дополнительные соединения из этого резерва. Хватит 5-10% от default_pool_size.
server_idle_timeout - через сколько секунд неактивное соединение к PostgreSQL закрывается. По умолчанию 600 сек. Снижение до 300 помогает освободить ресурсы базы в часы низкой нагрузки.
client_idle_timeout - то же для клиентских соединений. Клиент молчит дольше указанного времени - соединение разрывается. Защита от зависших коннектов.
server_connect_timeout - сколько ждать при открытии нового соединения к PostgreSQL. Если база не ответила за это время - pgBouncer считает попытку неудачной.
Расширенный пример конфига с этими параметрами:
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 300
client_idle_timeout = 60
server_connect_timeout = 10
log_file = /var/log/postgresql/pgbouncer.log
pid_file = /var/run/postgresql/pgbouncer.pid
Практический пример
Рассмотрим реальный сценарий: Django-приложение развёрнуто в 200 воркерах Gunicorn на одном VPS с 8 ГБ ОЗУ.
До pgBouncer - прямые соединения с PostgreSQL:
- 200 воркеров × 1 соединение каждый = 200 соединений в PostgreSQL
- Каждое соединение занимает ~8 МБ
- Итого: 200 × 8 МБ = 1,6 ГБ ОЗУ только под соединения
- Реальная нагрузка на базу при этом - пиковые 30-40 одновременных запросов
- 170 соединений большую часть времени простаивают, но ресурс потребляют
После pgBouncer в режиме transaction pooling:
- 200 воркеров подключаются к pgBouncer - для pgBouncer это 200 лёгких клиентских соединений
- pgBouncer держит 20 реальных соединений к PostgreSQL
- 20 × 8 МБ = 160 МБ ОЗУ под соединения
- Экономия: 1,44 ГБ оперативной памяти
- Throughput при равномерной нагрузке не снижается - транзакции Django короткие
Конфиг Django для работы через pgBouncer:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myapp',
'USER': 'appuser',
'PASSWORD': 'yourpassword',
'HOST': '127.0.0.1',
'PORT': '6432', # порт pgBouncer, не PostgreSQL
'OPTIONS': {
'connect_timeout': 10,
},
'CONN_MAX_AGE': 0, # важно: отключить persistent connections в Django
}
}
Параметр CONN_MAX_AGE = 0 критически важен. Django по умолчанию держит соединения открытыми между запросами. При transaction pooling это нарушает логику пула - соединение не возвращается в pgBouncer после транзакции. Устанавливайте CONN_MAX_AGE = 0 при использовании transaction mode.
Частые проблемы и решения
Prepared statements ломаются в transaction mode. PostgreSQL именованные prepared statements привязаны к сессии. В transaction mode одна сессия PostgreSQL может обслуживать разных клиентов, и prepared statements предыдущего клиента недоступны следующему. Решение: отключить prepared statements на уровне ORM. Для SQLAlchemy:
create_engine(url, execution_options={"no_parameters": True})
Для Django с psycopg2 - использовать параметр prepared_statements = False или переключиться на psycopg3, который умеет работать с pgBouncer в transaction mode без отключения prepared statements.
Команды SET и временные таблицы. В transaction mode SET-команды (SET search_path, SET timezone и т.д.) и временные таблицы не переживают окончание транзакции - соединение уходит другому клиенту. Если приложение использует SET для смены схемы - либо включайте эти настройки через ALTER ROLE, либо переходите на session mode.
Ошибка аутентификации: password authentication failed. pgBouncer проверяет пользователя по userlist.txt, а не напрямую через PostgreSQL. Если пользователь есть в базе, но не добавлен в userlist.txt - получите отказ. Добавьте пользователя в оба места. После изменения userlist.txt перезагрузка не нужна - достаточно:
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "RELOAD;"
pg_hba.conf не разрешает соединения от pgBouncer. Если pgBouncer работает на том же сервере, что и PostgreSQL, в pg_hba.conf должна быть строка для 127.0.0.1:
host all all 127.0.0.1/32 md5
После изменения pg_hba.conf:
sudo systemctl reload postgresql
Мониторинг текущего состояния. pgBouncer предоставляет служебную базу pgbouncer для мониторинга:
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW STATS;"
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "SHOW CLIENTS;"
Команда SHOW POOLS показывает, сколько клиентов ждут соединения (cl_waiting). Если это число постоянно больше нуля - увеличивайте default_pool_size.
Часто задаваемые вопросы
В чём разница между pgBouncer и PgPool-II?
pgBouncer - специализированный пулер соединений. Делает одно, но делает хорошо: минимальная задержка, низкое потребление ресурсов. PgPool-II - более тяжёлое решение с дополнительными функциями: репликация, балансировка нагрузки, кеширование запросов. Если нужен только пулинг соединений - pgBouncer быстрее и проще в настройке. PgPool-II оправдан, если нужна ещё и балансировка между несколькими репликами.
Сколько соединений в PostgreSQL можно поставить в max_connections?
Рекомендуемая формула: (RAM в ГБ × 100) / work_mem в МБ. При 8 ГБ ОЗУ и work_mem = 64MB получается около 200. Но с pgBouncer реальных соединений намного меньше, поэтому можно снизить max_connections в PostgreSQL до 100-150 и направить сэкономленную память на shared_buffers и work_mem.
Как мониторить pgBouncer в Prometheus?
Используйте pgbouncer_exporter. Он подключается к служебной базе pgBouncer и экспортирует метрики в формате Prometheus: количество клиентов, размер пулов, время ожидания, статистику запросов. Установка:
docker run -d \
-e DATA_SOURCE_NAME="postgresql://pgbouncer:password@127.0.0.1:6432/pgbouncer?sslmode=disable" \
-p 9127:9127 \
prometheuscommunity/pgbouncer-exporter
Поддерживает ли pgBouncer SSL?
Да. pgBouncer поддерживает SSL как на стороне клиентов, так и на стороне PostgreSQL. Для включения SSL в конфиге:
[pgbouncer]
client_tls_sslmode = require
client_tls_key_file = /etc/pgbouncer/server.key
client_tls_cert_file = /etc/pgbouncer/server.crt
server_tls_sslmode = require
pgBouncer работает в контейнере - нужны ли изменения?
В контейнерной среде pgBouncer обычно разворачивают как отдельный sidecar-контейнер или как отдельный сервис в Docker Compose. Конфигурация идентична, меняется только адрес PostgreSQL. В Kubernetes pgBouncer часто запускают как DaemonSet или как отдельный Deployment с Service - это зависит от того, нужен ли общий пул для всего кластера или локальный на каждом узле.