PostgreSQL Connection Pooling в 2026 году: PgBouncer, pgpool-II и встроенные механизмы под нагрузкой
Введение: почему каждое соединение PostgreSQL дорого стоит
PostgreSQL обрабатывает каждое соединение через отдельный процесс операционной системы (forked process model). При установке соединения ядро СУБД форкает процесс, выделяет стек (~8 МБ по умолчанию), инициализирует shared memory structures и загружает системные каталоги. Даже «пустое» соединение потребляет 5–10 МБ RAM и занимает слот в массиве ProcArray.
Параметр max_connections ограничивает общее число одновременных соединений. Его увеличение — не бесплатная операция: при max_connections = 1000 PostgreSQL резервирует около 80 МБ shared memory только под массив блокировок (max_locks_per_transaction * max_connections * 2). Кроме того, планировщик вынужден обходить все активные процессы при снятии снэпшота MVCC — это O(N) операция на каждый запрос.
На практике приложение с 50 воркерами, каждый из которых держит пул из 10 соединений, упирается в 500 реальных соединений к PostgreSQL. При микросервисной архитектуре с 20 сервисами цифра вырастает до 10 000, что делает СУБД нестабильной уже при стандартном железе. Именно здесь вступает в игру PostgreSQL connection pooling.
Режимы пулинга: Session, Transaction, Statement
Пулеры соединений работают в трёх режимах, и выбор режима определяет совместимость с возможностями PostgreSQL.
Session pooling
Соединение из пула назначается клиенту на всё время его сессии и возвращается только после полного разрыва соединения клиента. Это самый безопасный режим: поддерживаются SET, LISTEN/NOTIFY, подготовленные операторы, advisory locks. Однако мультиплексирования по факту нет — число клиентов не может превышать размер пула.
Transaction pooling
Соединение возвращается в пул после завершения транзакции (COMMIT / ROLLBACK). Это позволяет 1000 клиентам эффективно использовать 50 реальных соединений, если они не держат транзакции открытыми. Ограничения: нельзя использовать SET SESSION, advisory locks на уровне сессии, LISTEN, сервер-сайд prepared statements (по умолчанию).
Statement pooling
Соединение возвращается после каждого отдельного оператора. Это самый агрессивный режим — не поддерживаются многооператорные транзакции. Применяется крайне редко, преимущественно в аналитических read-only сценариях.
Итоговое сравнение режимов по ключевым параметрам:
- Session: мультиплексирование — нет; prepared statements — да; транзакции — да; LISTEN/NOTIFY — да.
- Transaction: мультиплексирование — да; prepared statements — ограниченно; транзакции — да; LISTEN/NOTIFY — нет.
- Statement: мультиплексирование — максимальное; prepared statements — нет; транзакции — нет; LISTEN/NOTIFY — нет.
PgBouncer: установка, конфигурация и практика
PgBouncer — легковесный однопоточный пулер, написанный на C с использованием libevent. В 2026 году актуальна ветка 1.23+, включающая поддержку SCRAM-SHA-256 и улучшенный TLS-стек.
Установка
# Debian/Ubuntu
apt-get install pgbouncer
# Или через Docker
docker run -d \
--name pgbouncer \
-e DATABASE_URL="postgres://user:pass@pg-host:5432/mydb" \
-p 5432:5432 \
edoburu/pgbouncer:latest
Основная конфигурация pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; Режим пула — transaction для высоких нагрузок
pool_mode = transaction
; Максимум соединений к PostgreSQL
max_client_conn = 10000
default_pool_size = 50
; Резервный пул при пиках
reserve_pool_size = 10
reserve_pool_timeout = 5
; Тайм-ауты
server_idle_timeout = 600
client_idle_timeout = 0
query_timeout = 0
; Логирование
log_connections = 0
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60
; Административный интерфейс
admin_users = pgbouncer_admin
Файл аутентификации userlist.txt
"myuser" "SCRAM-SHA-256$4096:base64salt==:base64storedkey==:base64serverkey=="
"pgbouncer_admin" "adminpassword"
Проблема prepared statements в transaction mode
При pool_mode = transaction PostgreSQL-сайд prepared statements (PREPARE / EXECUTE) не работают, потому что они привязаны к сессии. Решение: отключить prepared statements на уровне драйвера или использовать PgBouncer 1.21+ с параметром max_prepared_statements, который включает отслеживание и перенаправление препаратов.
; pgbouncer.ini — включение server-side prepared statements tracking
max_prepared_statements = 200
В Go (pgx) для совместимости с transaction pooling используйте QueryExecModeSimpleProtocol или кешируйте запросы на стороне клиента через pgx/v5 extended query cache.
pgpool-II: балансировка, репликация и оправданная сложность
pgpool-II — многофункциональный middleware: пулер, балансировщик нагрузки, менеджер репликации и query cache. В отличие от PgBouncer, он многопроцессный и поддерживает полный набор PostgreSQL-протокола.
Ключевые возможности pgpool-II
- Load balancing: автоматическое распределение SELECT-запросов по репликам (standby-серверам).
- Connection pooling: аналогично PgBouncer, но с поддержкой всех режимов протокола.
- Watchdog: HA-кластер из нескольких нод pgpool-II с виртуальным IP.
- Online recovery: автоматическое подключение нод после сбоя.
- Query cache: in-memory кеширование результатов SELECT (редко применяется в production из-за инвалидации).
Минимальная конфигурация pgpool.conf для балансировки
listen_addresses = '*'
port = 5433
# Primary
backend_hostname0 = 'pg-primary'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/var/lib/postgresql/data'
backend_flag0 = 'ALLOW_TO_FAILOVER'
# Replica
backend_hostname1 = 'pg-replica'
backend_port1 = 5432
backend_weight1 = 2
backend_data_directory1 = '/var/lib/postgresql/data'
backend_flag1 = 'ALLOW_TO_FAILOVER'
load_balance_mode = on
master_slave_mode = on
master_slave_sub_mode = 'stream'
num_init_children = 100
max_pool = 4
Когда pgpool-II оправдан
pgpool-II оправдан, когда нужна автоматическая балансировка read-трафика между репликами без изменения кода приложения, failover на уровне middleware и watchdog для HA. В остальных случаях его сложность избыточна — PgBouncer справляется лучше при меньшем потреблении ресурсов.
Встроенный пулинг в драйверах: pgx (Go) и Laravel
pgx пул соединений (Go)
Библиотека pgx/v5 для Go включает pgxpool — потокобезопасный пул соединений на уровне приложения. Это client-side пулинг: соединения не мультиплексируются между горутинами, но переиспользуются в рамках одного процесса.
package main
import (
"context"
"os"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
config, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
// Настройка пула
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 3600 * time.Second
config.MaxConnIdleTime = 600 * time.Second
config.HealthCheckPeriod = 60 * time.Second
// Для работы с PgBouncer в transaction mode
config.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
panic(err)
}
defer pool.Close()
}
Ограничение: каждый Pod в Kubernetes имеет собственный пул — при 10 репликах и MaxConns = 25 PostgreSQL получает 250 реальных соединений. Без внешнего пулера это может быть критично.
Laravel PostgreSQL pool
Laravel использует PDO, который не поддерживает persistent connection pooling в классическом смысле. Для Laravel рекомендуется направлять трафик через PgBouncer. Конфигурация config/database.php остаётся стандартной — приложение не знает о пулере:
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'pgbouncer-svc'), // адрес PgBouncer
'port' => env('DB_PORT', '6432'),
'database' => env('DB_DATABASE', 'mydb'),
'username' => env('DB_USERNAME', 'myuser'),
'password' => env('DB_PASSWORD', ''),
'options' => [
// Отключаем prepared statements для transaction mode
PDO::ATTR_EMULATE_PREPARES => true,
],
],
Критически важно: при pool_mode = transaction в PgBouncer необходимо установить PDO::ATTR_EMULATE_PREPARES => true, иначе PDO будет использовать PostgreSQL server-side prepared statements, что приведёт к ошибке prepared statement does not exist.
Развёртывание PgBouncer как sidecar в Kubernetes
В Kubernetes наиболее надёжный паттерн — деплой PgBouncer как sidecar-контейнера внутри Pod приложения. Это устраняет сетевой хоп и упрощает управление жизненным циклом.
Kubernetes манифест: Deployment с PgBouncer sidecar
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
volumes:
- name: pgbouncer-config
configMap:
name: pgbouncer-config
- name: pgbouncer-secrets
secret:
secretName: pgbouncer-secrets
containers:
- name: app
image: myapp:latest
env:
- name: DATABASE_URL
value: "postgres://myuser@localhost:6432/mydb"
- name: pgbouncer
image: pgbouncer/pgbouncer:1.23.0
ports:
- containerPort: 6432
volumeMounts:
- name: pgbouncer-config
mountPath: /etc/pgbouncer
- name: pgbouncer-secrets
mountPath: /etc/pgbouncer/secrets
readOnly: true
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "128Mi"
livenessProbe:
tcpSocket:
port: 6432
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
exec:
command:
- sh
- -c
- |
psql "postgres://pgbouncer_admin:${ADMIN_PASS}@localhost:6432/pgbouncer" \
-c "SHOW VERSION" > /dev/null 2>&1
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: ADMIN_PASS
valueFrom:
secretKeyRef:
name: pgbouncer-secrets
key: admin_password
ConfigMap для pgbouncer.ini
apiVersion: v1
kind: ConfigMap
metadata:
name: pgbouncer-config
namespace: production
data:
pgbouncer.ini: |
[databases]
mydb = host=pg-primary.postgres.svc.cluster.local port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/secrets/userlist.txt
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 300
log_connections = 0
log_disconnections = 0
admin_users = pgbouncer_admin
Secret с userlist.txt
apiVersion: v1
kind: Secret
metadata:
name: pgbouncer-secrets
namespace: production
type: Opaque
stringData:
userlist.txt: |
"myuser" "md5hash_or_scram_verifier"
"pgbouncer_admin" "adminpassword"
admin_password: "adminpassword"
Важно: при sidecar-паттерне listen_addr = 127.0.0.1 — PgBouncer доступен только внутри Pod. Это исключает несанкционированный доступ снаружи.
Бенчмарки: pgbench под нагрузкой
Тестовая среда: PostgreSQL 16.3, 8 vCPU, 32 GB RAM, SSD NVMe. pgbench TPC-B подобный сценарий, 100 клиентов, 60 секунд, scale factor 100.
Сценарий 1: без пулинга, прямые соединения
pgbench -h pg-host -p 5432 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Результат:
# TPS (without connection establishment): 3 241
# Latency average: 30.8 ms
# Connection time: 12.3 ms (avg)
Сценарий 2: через PgBouncer, transaction mode, pool_size=50
pgbench -h pgbouncer-host -p 6432 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Результат:
# TPS (without connection establishment): 8 947
# Latency average: 11.2 ms
# Connection time: 0.4 ms (avg)
Сценарий 3: через pgpool-II, load balancing отключён
pgbench -h pgpool-host -p 5433 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Результат:
# TPS (without connection establishment): 6 183
# Latency average: 16.2 ms
# Connection time: 1.8 ms (avg)
Выводы из бенчмарков:
- PgBouncer в transaction mode даёт прирост TPS в 2.76x по сравнению с прямыми соединениями за счёт устранения overhead на установку соединения.
- pgpool-II без балансировки показывает 1.91x прироста — хуже PgBouncer из-за многопроцессной архитектуры.
- С включённой балансировкой нагрузки на read-реплику pgpool-II при 70% SELECT-трафика показывает суммарный TPS ~14 000, что недостижимо для single-node сценария.
Типичные ошибки конфигурации и их симптомы
1. Connection leaks
Симптом: счётчик cl_waiting в SHOW POOLS постоянно растёт. Причина: приложение не возвращает соединения в пул (незакрытые транзакции, исключения без rollback). Диагностика:
-- В psql к административной БД PgBouncer
SHOW CLIENTS;
-- Ищем клиентов с состоянием 'active' без активности
-- Или через pg_stat_activity на стороне PostgreSQL
SELECT pid, usename, state, query_start, state_change, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < NOW() - INTERVAL '5 minutes';
2. Ошибка prepared statement does not exist
Симптом: ERROR: prepared statement "s1" does not exist. Причина: драйвер использует extended query protocol с server-side prepared statements при pool_mode = transaction. Решение: PDO::ATTR_EMULATE_PREPARES = true для Laravel, QueryExecModeSimpleProtocol для pgx, или включить max_prepared_statements в PgBouncer ≥ 1.21.
3. Pool exhaustion
Симптом: ERROR: no more connections allowed (max_client_conn) или клиенты висят в очереди. Причина: недостаточный max_client_conn или слишком маленький default_pool_size. Решение: увеличить reserve_pool_size, настроить client_login_timeout, проверить наличие длинных транзакций блокирующих возврат соединений.
4. Неправильный healthcheck в Kubernetes
Симптом: Pod перезапускается хаотично. Причина: readinessProbe проверяет TCP-порт, но PgBouncer уже слушает порт, хотя ещё не готов обслуживать запросы. Используйте SHOW VERSION через psql как healthcheck команду (пример выше в манифесте).
5. SET команды при transaction pooling
Симптом: настройки сессии (SET search_path, SET work_mem) не сохраняются между запросами. Причина: сессия переназначается после каждой транзакции. Решение: использовать server_reset_query = DISCARD ALL и задавать параметры через строку соединения (options=-c search_path=myschema) или в ALTER ROLE ... SET.
Мониторинг пула: SHOW POOLS, SHOW STATS и Prometheus
Встроенная диагностика PgBouncer
-- Подключиться к административной базе PgBouncer
psql -h localhost -p 6432 -U pgbouncer_admin pgbouncer
-- Состояние пулов
SHOW POOLS;
-- Колонки: database, user, cl_active, cl_waiting, sv_active,
-- sv_idle, sv_used, sv_tested, sv_login, maxwait
-- Статистика трафика
SHOW STATS;
-- total_xact_count, total_query_count, total_received,
-- total_sent, total_xact_time, avg_xact_time, avg_query_time
-- Список клиентов
SHOW CLIENTS;
-- Список серверных соединений
SHOW SERVERS;
-- Конфигурация
SHOW CONFIG;
-- Перезагрузка конфигурации без рестарта
RELOAD;
Интеграция с Prometheus
Используйте pgbouncer_exporter от Prometheus Community. Добавьте как дополнительный sidecar-контейнер:
- name: pgbouncer-exporter
image: prometheuscommunity/pgbouncer-exporter:v0.9.0
args:
- --pgBouncer.connectionString=postgresql://pgbouncer_admin:$(ADMIN_PASS)@localhost:6432/pgbouncer
ports:
- containerPort: 9127
name: metrics
env:
- name: ADMIN_PASS
valueFrom:
secretKeyRef:
name: pgbouncer-secrets
key: admin_password
Ключевые метрики для алертинга:
pgbouncer_pools_cl_waiting— клиенты в очереди. Alert при значении > 10 в течение 1 минуты.pgbouncer_pools_sv_idle— idle серверные соединения. Низкое значение при высокой нагрузке — признак нехватки пула.pgbouncer_stats_avg_query_time— средняя задержка запросов.pgbouncer_pools_maxwait— максимальное время ожидания в секундах. Alert при > 1.
Пример Prometheus alert rule
groups:
- name: pgbouncer
rules:
- alert: PgBouncerPoolExhausted
expr: pgbouncer_pools_cl_waiting > 10
for: 1m
labels:
severity: critical
annotations:
summary: "PgBouncer pool exhausted"
description: "{{ $value }} clients waiting in pool {{ $labels.database }}"
- alert: PgBouncerHighWaitTime
expr: pgbouncer_pools_maxwait > 1
for: 30s
labels:
severity: warning
annotations:
summary: "High wait time in PgBouncer"
Рекомендации по выбору стратегии для разных архитектур
Монолит или небольшой сервис (до 5 реплик)
Используйте встроенный пул драйвера (pgxpool для Go, стандартный connection pool Laravel через PDO). Внешний пулер добавляет latency и сложность без значимого выигрыша при малом числе соединений. Оптимальный MaxConns: число воркеров × 2–4.
Микросервисы в Kubernetes (10+ сервисов, 3+ реплики каждый)
Деплойте PgBouncer как sidecar в каждом Pod с pool_mode = transaction. Это суммарно сокращает реальное число соединений к PostgreSQL с тысяч до десятков. Настройте default_pool_size = 10–20 на Pod, max_connections PostgreSQL установите на 200–500.
High-traffic read-heavy сервисы (streaming репликация, несколько реплик)
pgpool-II оправдан для автоматической балансировки SELECT-запросов между репликами без изменения кода. Альтернатива — HAProxy или Patroni с настройкой Service в Kubernetes для разделения read/write трафика на уровне сети.
OLAP / аналитические запросы
Session pooling или вовсе без пулинга — длинные аналитические запросы не выигрывают от мультиплексирования транзакций, зато могут страдать от overhead пулера при работе с CURSOR и COPY.
Laravel-приложения в Kubernetes
Laravel с PHP-FPM не держит persistent connections между запросами — каждый PHP-воркер открывает соединение при старте и закрывает его (или держит в зависимости от настройки). PgBouncer как sidecar или как отдельный Deployment (один на namespace) — обязательный элемент архитектуры при более чем 20 воркерах на Pod.
Общие принципы расчёта размера пула
- Эмпирическое правило Брандта:
pool_size = (число ядер CPU * 2) + число дисковна сторону PostgreSQL. - Реальный
max_connectionsPostgreSQL =default_pool_size * число Pod приложения + superuser_reserved_connections. - Начинайте с консервативных значений и увеличивайте на основе метрик
maxwaitиcl_waiting.
Пулинг соединений — не серебряная пуля. Он решает проблему overhead установки соединений, но не заменяет оптимизацию запросов, правильную индексацию и грамотное управление транзакциями. Мониторинг пула должен быть частью baseline observability любой production PostgreSQL системы.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →