Базы данных

PostgreSQL Connection Pooling в 2026 году: PgBouncer, pgpool-II и встроенные механизмы под нагрузкой

Ruslan Ismailov Опубликовано 18 мин чтения
P

Введение: почему каждое соединение 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_connections PostgreSQL = 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. Подробнее обо мне →