Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → PgBouncer: пулинг соединений Postgres

Пул соединений к Postgres: PgBouncer, лимиты и режимы работы

Зачем Postgres нужен пулер соединений: цена соединения, режимы PgBouncer (session/transaction), настройка под приложение, prepared statements и диагностика лимитов.

Прямой ответ

Каждое соединение с Postgres — отдельный процесс с своей памятью (несколько МБ + work_mem на операции): max_connections=200 — не «200 бесплатных слотов», а потолок, за которым сервер деградирует (контекст-переключения, память, падение скорости вакуума); приложение на 50 воркеров с пулом по 10 соединений на процесс уже требует 500 — упирается. PgBouncer решает: лёгкий прокси (один процесс, тысячи клиентских соединений), который держит к БД маленький пул реальных соединений и переиспользует их между клиентами; приложение подключается к PgBouncer как к Postgres (тот же протокол, меняется только порт/хост). Ключевой выбор — pool_mode: session (соединение клиенту на всю сессию — совместимо со всем, выигрыш мал), transaction (соединение возвращается в пул после COMMIT — 50 воркеров обслуживаются 10–20 реальными соединениями; стандарт де-факто, но несовместим с prepared statements, сессионными переменными и advisory locks вне транзакции), statement (ниша). Практика: PgBouncer в transaction-режиме рядом с БД (default_pool_size ≈ 2× ядер БД), пулы приложений уменьшены ( SQLAlchemy pool_size=2–5 — сессии), prepared statements переведены на серверные с поддержкой пулера (или отключены), и мониторинг очередей пулера (SHOW POOLS) — очередь к пулу означает «пул мал или запросы длинные».

Почему соединений «слишком много»: механика проблемы

Postgres — process-per-connection (fork на каждое подключение): 300 соединений = 300 процессов, у каждого кеш каталога и контекст; измеримые эффекты — CPU на переключениях, память (сотни МБ просто на существования соединений), замедление recovery/vacuum. Ошибка «лечить поднятием max_connections»: больше процессов — хуже каждому; разумный верх без пулера — десятки и низкая сотня. Откуда берётся лавина: каждый воркер приложения держит свой пул (gunicorn 8 воркеров × SQLAlchemy pool 10 + overflow 20 = до 240 — воркеры); микросервисов несколько; плюс миграции, cron-задачи, админы в psql — лимит исчерпан, новые подключения получают FATAL: too many connections; а «починили» обычно max_connections=500 — сервер начинает тормозить целиком. Альтернативы пулеру: встроенный пулинг Postgres 17+ (перспективен, но молод), пулеры в облачных managed-БД (RDS Proxy и подобные — тот же PgBouncer по сути), и дисциплина пулов приложений (маленькие пулы + короткий acquire timeout — лучше очередь в приложении с явной ошибкой, чем 300 полуживых соединений в БД).

PgBouncer: режимы и их последствия

Конфиг-минимум: [databases] app = host=127.0.0.1 port=5432 dbname=app + pool_mode = transaction, default_pool_size = 20, max_client_conn = 500 (клиентских — дёшево; реальных к БД — default_pool_size), reserve_pool_size/timeout (всплеск). Режимы: session — 1:1 на сессию клиента (прозрачен полностью, смысл только в multiplexing редких сессий); transaction — рабочий стандарт: между транзакциями соединение уходит другому клиенту — следствия, которые надо знать: SET/SET LOCAL вне транзакции утеряется (сессионные настройки приложения не работают — настраивать в самой транзакции или через server_reset_query/параметры подключения), prepared statements в классическом виде ломаются (готовятся на одном физическом соединении, исполняются на другом) — PgBouncer 1.21+ умеет отслеживать prepared statements (max_prepared_statements) — включить и проверить, либо отключить prepare в драйвере (asyncpg statement_cache_size=0, psycopg — зависит от настроек), advisory locks и LISTEN/NOTIFY — только в session-пуле (выделить отдельную базу-вход в конфиге с pool_mode=session для таких задач — один прокси, два режима); statement — каждое утверждение отдельно (autocommit-мир, транзакции запрещены — редко нужен). Отдельная фича — PAUSE/RESUME базы: плановые работы на БД без обрыва клиентов (запросы стоят в очереди пулера, не падают).

Настройка под приложение и диагностика

Связка чисел: ядра БД 8 → default_pool_size 16–25 (правило «2–3× ядер» для смешанной нагрузки; CPU-bound аналитика — меньше), max_client_conn 500–1000 (память на клиента мизерная); пул SQLAlchemy: pool_size=2, max_overflow=3 на воркер (через пулер больше не нужно — очередь уже есть в PgBouncer); connect_timeout/pool_timeout в приложении — явные (зависшее подключение должно падать ошибкой, а не вечно ждать). Диагностика через админ-консоль: SHOW POOLS (cl_active/sv_active/sv_idle/waiting — очередь клиентов к пулу: растёт — пул мал или долгие транзакции), SHOW SERVERS/SHOW CLIENTS (кто держит), SHOW STATS (запросы/секунду, байты); на стороне БД — pg_stat_activity показывает только реальные соединения пулера (картина «20 соединений при 500 клиентах» — норма и цель всей затеи). Важная связка с транзакциями: transaction-режим не терпит открытых транзакций между запросами (дисциплина транзакций) — idle in transaction клиента держит реальное соединение намертво: idle_transaction_timeout на пулере + короткий — на БД. Мониторинг: метрики очереди и времени в ней (Prometheus-экспортер для PgBouncer есть) — «p95 времени acquire» — главный пользовательский симптом нехватки пула.

Практический сценарий: утро после релиза — база легла

Симптом: после выката (воркеров стало 16 вместо 8) — шквал FATAL: sorry, too many clients, сайт недоступен. Экспресс-анализ: pg_stat_activity — 198 соединений, почти все idle (пулы приложений держат соединения про запас, сервер задыхается от процессов); временная мера — рестарт приложения с pool_size=3, max_overflow=2 и pool_timeout=5 (часть запросов теперь честно ждёт/падает таймаутом — лучше деградация, чем смерть); постоянное решение — PgBouncer в transaction-режиме рядом с БД: 16 воркеров × 5 = 80 клиентских соединений в пулер, к БД — 20 реальных; prepared statements: asyncpg-кеш отключён (statement_cache_size=0) — запросы чуть дороже на парсинг, зато совместимо; проверка под нагрузкой (locust): 300 RPS, очередь пулера ноль, p95 acquire — 1 мс, соединений к БД стабильно 20. Дополнительно: cron-миграции и админский psql — через отдельный session-вход пулера; PAUSE app использован через неделю для ночного обслуживания — клиенты пережили его очередью без единой ошибки. Вывод: лимит соединений — архитектурная граница process-per-connection, а не «настройка, которую надо поднять».

FAQ

PgBouncer или пул в драйвере/ORM — не дублирование? Разные уровни: пул драйвера экономит reconnect внутри одного процесса (нужен, маленький); PgBouncer агрегирует пулы всех процессов и сервисов перед БД (нужен при >1 воркере/сервисе); вместе — пул приложения 2–5 + пулер 20 к БД; без пулера при мультипроцессности — лимиты БД.

Transaction-режим сломает мои LISTEN/NOTIFY и temp-таблицы? Да — сессионная семантика в нём не держится: temp-таблицы, SET, LISTEN, advisory-локи вне транзакций требуют session-режима; решение — отдельная logical-база в конфиге PgBouncer с session-пулом для таких воркеров (воркер фоновых уведомлений ходит в session-вход, основной API — в transaction).

Нужен ли PgBouncer для одного приложения с 4 воркерами? Часто нет: 4×5=20 соединений — в пределах нормы БД; пулер оправдан от ~50+ агрегированных соединений, нескольких сервисов, или как задел (дешёв и прозрачен — подключить раньше лимитов проще, чем в момент аварии).

Как обновлять/рестартов PgBouncer без обрыва? RELOAD (конфиг на лету) покрывает большинство правок; рестарт бинаря — с PAUSE+ожидание стекания или быстрый restart с reconnect на стороне клиентов (драйверы ретраят подключение — секундная пауза вместо ошибок); два пулера за балансировщиком — схема без единой точки простоя.

Источники: документация PgBouncer (config, FAQ), PostgreSQL connection settings — ссылки в шапке.