Каждое соединение с PostgreSQL это отдельный процесс на сервере, со своей памятью, своими дескрипторами и своей ценой при установке. Приложений становится больше, пулы в них держат соединения открытыми на всякий случай, и штатный лимит max_connections упирается в оперативную память задолго до реальной нагрузки. PgBouncer встаёт тонким слоем между приложением и базой: тысячи клиентских подключений сходятся к нему, а серверу он показывает десяток-другой аккуратно переиспользуемых соединений. Главный вопрос при внедрении не сколько, а какой режим выбрать: session, transaction или statement. От ответа зависит, что заработает сразу, а что сломается молча. Ниже механика всех трёх режимов, границы совместимости и рабочая конфигурация.

Цена каждого соединения PostgreSQL и смысл лёгкого пулера

PostgreSQL запускает отдельный процесс на каждое клиентское подключение. Установка сессии это TLS-рукопожатие, проверка пароля, порождение процесса и инициализация памяти, а простаивающее соединение продолжает занимать дескрипторы, страницы памяти и строку в pg_stat_activity. Сотни таких процессов напрягают не только лимиты: планировщику приходится обслуживать их все, хотя реально работает единицы. Подключение и так дорогая операция, а частые переподключения оборачиваются самостоятельной нагрузкой.

PgBouncer работает иначе: один компактный процесс на событийном вводе-выводе держит тысячи клиентских соединений и горстку серверных. Клиенты подключаются к нему, как к обычному PostgreSQL, сервер для них невидим. Пулер мультиплексирует: берёт свободное серверное соединение, отдаёт его клиенту на время сессии, транзакции или одиночного запроса и забирает обратно. Что именно считать этим временем, задаёт параметр pool_mode, и здесь живут три режима. Развёртывание незатейливое: пулер слушает отдельный порт, обычно 6432, в строке подключения меняется только порт, сервер и приложение трогать не обязательно.

Проект развивается живо: актуальная версия 1.26.0 вышла в конце сентября 2026 и закрыла три уязвимости разом, весной до неё успела выйти 1.25.2 с исправлениями безопасности. Об обновлениях ниже, сначала механика.

Режим session с закреплением сервера за клиентом до отключения

Session это режим по умолчанию и самый честный: клиенту выделяется серверное соединение, и тот держит его до собственного отключения. Внутри сессии доступно всё: команда SET живёт сколько нужно, временные таблицы переживают транзакции, пара LISTEN и NOTIFY работает, консультативные блокировки держатся часами. Приложение не отличает работу через пулер от прямого подключения, кроме адреса и порта.

Типичный кандидат на session: старое приложение, которое после подключения выставляет часовой пояс и локаль, а потом живёт с ними весь день, либо интерактивные psql-сессии аналитиков. Session проглотит такое без жалоб, transaction начнёт терять настройки на границах транзакций, если их нет в отслеживаемом списке.

Платой становится сама идея пулинга. Неактивный клиент не отдаёт серверное соединение, оно просто висит. Число серверных подключений равно числу одновременных клиентов и ограничено размером пула, по умолчанию 20 на каждую пару из пользователя и базы данных. Постоянно открытые, но ленивые соединения приложения занимают серверные слоты, и очередь растёт из-за клиентов, которые ничего не делают. Выгода session в другом: клиентские подключения стоят дёшево, их можно открывать и рвать без страха исчерпать лимит сервера, а рукопожатия и аутентификация уходят с тяжёлого узла на пулер.

Режим transaction с передачей сервера после каждого COMMIT

Transaction ломает связь между клиентом и сервером на границе транзакции. Началась транзакция, пулер выдал свободное серверное соединение, транзакция закончилась, соединение ушло в пул и ждёт следующего клиента. Сто тысяч клиентов легко обслуживаются пятьюдесятью серверными подключениями, если транзакции короткие. Именно этот режим выжимает из PostgreSQL главную выгоду, ведь лишний процесс на сервере стоит дорого.

Арифметика режима радует. Пул из 20 серверных соединений при средней транзакции в 50 миллисекунд пропускает до 400 транзакций в секунду, каждое соединение делает по 20. Клиентов при этом может быть хоть тысяча, лишние просто ждут в очереди свои сотни миллисекунд, зато сервер не захлёбывается сотнями процессов.

Внутри транзакции всё предсказуемо: все её запросы попадают в одно серверное соединение, SET внутри BEGIN сохраняет смысл, временные таблицы доживают до COMMIT. После COMMIT сервер меняется, и всё, что не уместилось в транзакцию, пропадает: настройки через SET, подготовленные выражения, объявленные вне транзакции, строки во временных таблицах.

Пулер смягчает углы. Настройки из списка track_extra_parameters он запоминает и восстанавливает на новом серверном соединении: application_name, client_encoding, DateStyle, TimeZone и другие. В версии 1.26.0 список пополнили search_path, для него нужна ветка PostgreSQL 18, и default_transaction_read_only. Подготовленные выражения протокольного уровня берёт на себя max_prepared_statements: пулер держит их кэш на каждом серверном соединении и перед выдачей сервера клиенту воспроизводит нужные. Ноль выключает поддержку целиком, а раздувать кэш без меры не стоит, память соединения не резиновая.

Сбросом сессии между клиентами занимается server_reset_query со значением DISCARD ALL по умолчанию: команда смахивает подготовленные выражения, временные таблицы и настройки, оставляя следующему клиенту чистую сессию. В режиме transaction сброс не выполняется вовсе, клиентам там не положено пользоваться сессионными состояниями, поэтому и чистить нечего. Тонкость спрятана в стартовых параметрах: их пулер применяет при каждом подключении клиента, но правки через SET после подключения не отслеживает, и без принудительного сброса параметром server_reset_query_always такие настройки перетекают к соседу по серверному соединению. Ещё один аргумент держать SET внутри транзакций.

Режим statement для одиночных запросов без явных транзакций

Statement отдаёт серверное соединение после каждого запроса. Это максимум плотности: одно серверное соединение обслуживает сотни клиентов подряд, каждый запрос живёт сам по себе. Плата жёсткая: транзакции из нескольких запросов запрещены, пара BEGIN и COMMIT теряет смысл, клиент получает ошибку. Автокоммитные одиночные запросы работают безупречно.

Ниша режима узкая: кеширующие слои, панели с одиночными SELECT, простые однотабличные операции без транзакционной логики. На практике между session и transaction почти всегда выигрывает второй, statement остаётся экзотикой там, где нужна предельная экономия, а запросы действительно атомарны.

Пределы режимов transaction и statement в реальном приложении

Ограничения не каприз пулера, а прямое следствие переиспользования соединений. Перечисленные ниже потребности приложения выполнимы только в режиме session:

  1. команда SET обязана переживать несколько транзакций и попадать в одну и ту же серверную сессию;
  2. временные таблицы со временем жизни сессии используются между транзакциями;
  3. пара LISTEN и NOTIFY обслуживает мгновенные уведомления между сессиями;
  4. консультативные блокировки уровня сессии держат захват дольше одной транзакции;
  5. курсоры с удержанием после COMMIT читаются следующей транзакцией.

Часть пунктов лечится конфигурацией: track_extra_parameters спасает большинство настроек уровня сессии, max_prepared_statements закрывает подготовленные выражения протокола, ignore_startup_parameters пропускает неизвестные параметры в стартовом пакете подключения. Остальное лечится дисциплиной кода: SET переезжает внутрь транзакций, временные таблицы заменяются обычными с очисткой по расписанию, уведомления переводятся на опрос таблицы. Хороший тон перед переключением: снять лог запросов приложения на тестовом контуре и поискать в нём SET вне транзакций и прочие следы сессионного состояния. Пулер честно вернёт ошибку там, где прямой сервер молчал, и лучше встретить её на тесте.

Рабочая конфигурация от pool_mode до лимитов файловых дескрипторов

Минимальный файл настройки умещается на экран:

;; pgbouncer.ini
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb pool_mode=transaction
reports = host=192.168.1.60 port=5432 dbname=reports pool_mode=session

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 5
server_lifetime = 3600
server_idle_timeout = 600
query_wait_timeout = 120
max_db_connections = 60
admin_users = dbadmin

Режим задаётся глобально в секции [pgbouncer] и при желании отдельно для каждой базы, как выше с отчётной. Ключевые лимиты стоят пояснений. max_client_conn, по умолчанию 100, ограничивает клиентские подключения; при тысячах клиентов его поднимают, заодно проверяя запас файловых дескрипторов: верхняя теоретическая планка равна max_client_conn плюс произведение максимального размера пула на число баз данных и число пользователей. default_pool_size держит 20 серверных соединений на каждую пару из пользователя и базы, значение подбирают по наблюдениям за очередью. Параметры server_lifetime в 3600 секунд и server_idle_timeout в 600 закрывают серверные соединения по возрасту и по простою, не давая им копить историю. query_wait_timeout в 120 секунд отключает клиента, не дождавшегося сервера, вместо вечного зависания в очереди. Параметр reserve_pool_size, по умолчанию выключенный нулём, добавляет экстренные слоты, когда клиент ждёт дольше reserve_pool_timeout, стандартные 5 секунд.

Аутентификацию задаёт auth_type, историческое значение md5, для современной связки берут scram-sha-256. Пароли кладут в auth_file или запрашивают прямо из базы через auth_query, запрос обязан вернуть две колонки, имя пользователя и пароль. Консоль администратора открывают только доверенным ролям через admin_users и держат на локальном адресе.

Мониторинг пула командой SHOW POOLS и своевременное обновление

Управление живёт в служебной базе pgbouncer, к которой подключаются обычным клиентом:

psql -h 127.0.0.1 -p 6432 -U dbadmin pgbouncer

Главная команда мониторинга выглядит лаконично:

SHOW POOLS;

Колонки рассказывают сразу всю историю: cl_active считает клиентов с сервером, cl_waiting застрявших в очереди, sv_active серверы в работе, sv_idle свободные, maxwait показывает, сколько секунд ждёт самый старый клиент. Растущий maxwait значит, что пул мал либо транзакции подорожали. Соседняя команда SHOW STATS копит транзакции, запросы и байты с момента старта процесса, из неё удобно строить графики. За отдельными подключениями следят командами SHOW CLIENTS и SHOW SERVERS: первая перечисляет клиентские сессии с именами пользователей и состояниями, вторая показывает серверные соединения, их адреса и время жизни. SHOW DATABASES выводит настроенные базы и адреса за ними, удобно проверять, куда реально уходит трафик. Изменённую конфигурацию перечитывает команда RELOAD без разрыва клиентских соединений.

Обновление пулера не стоит откладывать в долгий ящик. Версия 1.26.0, вышедшая 23 сентября 2026, закрывает три уязвимости: сбой от некорректного SCRAM-сообщения неаутентифицированного клиента, зацикливание из-за переполнения в росте буфера пакетов и неограниченный счётчик итераций SCRAM при подключении к злонамеренному серверу. Первые две доступны любому, кто умеет открывать соединения, то есть почти всем. Обновление приносит и приятности: отслеживание search_path и default_transaction_read_only по умолчанию, новый параметр pool_idle_timeout и настройку query_wait_timeout на уровне пользователя и базы. Устаревший запуск онлайн-рестарта флагом -R убрали вовсе, повседневные изменения теперь покрывает RELOAD.

Выбор режима это зеркало поведения приложения. Если оно живёт сессиями с SET и уведомлениями, подходит session. Если транзакции короткие, а код честно открывает их явно, выигрывает transaction, рабочая лошадка веб-нагрузки. Если запросы атомарны и история им не нужна, остаётся statement. И любой выбор проверяют на тестовом контуре с настоящим трафиком: пулер прощает многое, но на границе транзакции молча меняет правила игры.