PRODСуверенная европейская платформа BaaSОткрыть панель управления →

Производительность · 10 минута чтения

Объяснение пула транзакций PgBouncer

Affane Daylami · Fondateur · 12 июня 2026 г.

Вернуться в блог

Режим транзакций PgBouncer освобождает соединение PostgreSQL в конце каждой транзакции, а не при отключении клиента. Это то, что позволяет обслуживать тысячи HTTP-клиентов с несколькими десятками реальных подключений к серверу, и это рекомендуемый режим для любого REST API с короткими запросами.

Этот текст на английском языке был создан автоматически на основе французского оригинала и еще не проверялся.
Эта страница была переведена автоматически. Английская версия является авторитетной.

У этого выигрыша есть определенная цена: режим транзакций незаметно разрывает все, что предполагает стабильное соединение Postgres, от одного запроса к другому. Сеанс SET, LISTEN/NOTIFY, консультативные блокировки, курсоры, сохраняющие транзакцию, именованные подготовленные операторы. В этой статье подробно описан механизм, перечислены эти ограничения с их точными симптомами, а затем показано, как работающий бэкэнд (наш, проверенный непосредственно в его репозитории) настраивает его, не попадая в его ловушку. Метод измерения производительности, приведенный здесь, см. в нашей методике тестирования .

Самое необходимое

  • Режим транзакции: соединение с сервером разъединяется в конце каждой транзакции, а не при отключении клиента. Это наиболее эффективный режим для совместного использования коротких соединений типа REST API.
  • По конструкции несовместим с: сеансами SET/RESET, LISTEN/NOTIFY, консультативными блокировками сеанса, курсорами с HOLD, временными таблицами, повторно используемыми от одного запроса к другому.
  • Самая распространенная ошибка на практике: именованные подготовленные операторы, которые несколько драйверов (sqlx, asyncpg, драйвер JDBC pgjdbc) активируют по умолчанию, могут воспроизводиться при другом соединении с сервером и вызывать ошибку типа prepared statement does not exist под нагрузкой.
  • Начиная с версии 1.21, PgBouncer может следовать инструкциям, подготовленным протоколом, в режиме транзакций (кэш LRU через соединение с сервером). Это не предотвращает отключение кэша на стороне клиента, если ваше приложение меняет search_path при каждом запросе.
  • Проверено в коде Aurabase: пулы клиентов работают с statement_cache_capacity(0) и PgBouncer в pool_mode=transaction, а PostgREST добровольно остается в прямом соединении для перезагрузки своей схемы через LISTEN/NOTIFY.
#
Концепции

3 режима объединения PgBouncer

PgBouncer предлагает три режима, которые отличаются только тем, когда соединение с сервером Postgres возвращается в общий пул. В официальной документации они называются session, transaction и statement (pgbouncer.org/features.html, раздел «Режимы объединения», по состоянию на 24 августа 2026 г.).

МодаСоединение с сервером потеряноСовместимость сеансов
сеанс (по умолчанию)При отключении клиентаИтого: SET, LISTEN, курсоры, все работает как живое
сделкаВ конце каждой транзакции (COMMIT/ROLLBACK)Частичный: только то, что остается локальным для транзакции.
заявлениеПосле каждого отдельного запросаМинимальный: явные транзакции с несколькими запросами запрещены.

Сеансовый режим является наиболее разрешающим, но наименее эффективным с точки зрения масштабируемости: соединение Postgres остается зарезервированным для клиента до тех пор, пока оно остается подключенным, даже если между двумя запросами оно ничего не делает. Режим операторов зарезервирован для очень специфических случаев (прокси-сервер только для чтения, проверки работоспособности) и даже нарушает классические явные транзакции. Режим транзакций — это компромисс, который на практике доминирует в REST API: каждый HTTP-запрос обычно соответствует одной короткой транзакции Postgres.

#
Механизм

Как работает режим транзакций, соединение за соединением

В режиме транзакции PgBouncer подключает соединение с сервером к клиенту только тогда, когда последний открывает транзакцию, и возвращает ее в пул при COMMIT или ROLLBACK. Между двумя транзакциями один и тот же клиент может быть переназначен на совершенно другое соединение с сервером.

Конкретно, с default_pool_size, равным 20, PgBouncer может одновременно обрабатывать несколько сотен клиентов, у которых в любой момент времени фактически выполняются только несколько транзакций. Именно это соотношение оправдывает режим транзакций для REST API с высоким трафиком, но короткими транзакциями: редкий ресурс (соединение Postgres, требующее больших затрат памяти на стороне сервера) занят только на строго необходимое время.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

Этот последний комментарий суммирует суть: режим транзакций работает, потому что он намеренно разрывает связь между «сеансом моего приложения» и «моим соединением Postgres». Все, что основано на этой ссылке, ломается. В следующем разделе перечислено именно то, что именно.

#
Пределы

Что ломается в режиме пула транзакций

В официальной документации PgBouncer явно перечислены функции PostgreSQL, которые теряют свое значение, как только соединение с сервером может быть перезапущено между двумя запросами от одного и того же клиента.

Затронутая функциональностьПочему оно ломаетсяТипичный симптом
УСТАНОВКА / УСТАНОВКА СЕССИИЭтот параметр применяется к соединению, которое можно перезапустить сразу послеКажется, что параметр случайно забыт между двумя запросами
СЛУШАЙТЕ / УВЕДОМЛЯЙТЕПредполагается постоянное соединение для получения уведомлений.Клиент никогда не уведомляется или только время от времени
Консультативные блокировки сеансаБлокировка удерживается соединением с сервером, а не с логическим клиентом.Блокировка снимается до ожидаемого завершения или никогда не снимается.
С ползунками HOLDДолжен выжить после транзакции, которая его открылаОшибка «курсор не существует» на следующей итерации
Временные таблицыСвязано с сеансом Postgres, а не с транзакцией.Таблица «исчезает» при следующем запросе
Подготовленные заявления по имениПодготовлено на одном соединении с сервером, воспроизведено на другом«подготовленный оператор... не существует» под нагрузкой
Ловушка не всегда срабатывает мгновенно

Большинство этих ограничений не проявляются при локальной разработке, где обычно весь трафик обслуживается одним соединением. Они появляются при реальной нагрузке, когда несколько клиентов фактически делят пул и соединение с сервером фактически переходит из рук в руки между двумя запросами от одного и того же логического клиента. Дым-тест почти никогда их не выявляет.

#
Обычная ловушка

Готовые заявления: самый непонятый лимит

Большинство современных драйверов Postgres по умолчанию готовят именованные запросы на стороне протокола, без явного запроса кода приложения. Именно поэтому эту ловушку трудно предвидеть.

Подготовленный протоколом оператор получает имя и кэшируется на определенном соединении с сервером во время Parse. В режиме транзакции это соединение можно переназначить другому клиенту между двумя запросами от одного и того же логического клиента. Если драйвер затем воспроизводит то же имя оператора в соединении, где оно никогда не было подготовлено, Postgres отвечает явной ошибкой, обычно prepared statement "sqlx_s_N" does not exist для клиента sqlx. Поведение прерывистое: оно зависит от того, как соединения работают под нагрузкой, а не от детерминированной ошибки, воспроизводимой при каждом вызове.

Исправление на стороне клиента одинаково, независимо от языка: отключите кеш именованных подготовленных операторов или принудительно задайте неименованные запросы для любого пула, который пересекает средство объединения в режиме транзакций. В Rust с sqlx в параметрах подключения он проходит через statement_cache_capacity(0).

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// Эквиваленты: asyncpg ->statement_cache_size=0, pgjdbc ->preparThreshold=0.

Начиная с версии 1.21, PgBouncer частично устраняет проблему на стороне сервера: он может следовать подготовленным протоколом операторам в режиме транзакций и готовить их на лету в назначенном соединении с LRU-кешем для каждого соединения, размер которого настраивается с помощью max_prepared_statements. Это уменьшает количество промахов, но не освобождает вас от отключения кэша клиента в многотенантном пуле, где search_path меняется от одного запроса к другому: кэшированный план замораживает внутренний идентификатор (OID) разрешенной таблицы во время Parse, и его воспроизведение в другой схеме может вернуть данные не от того клиента, а не простую ошибку.

#
Проверено в коде

Как Aurabase настраивает PgBouncer в режиме транзакции

Репозиторий Aurabase развертывает PgBouncer как pool_mode=transaction перед общей плоскостью данных (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) и идентично настроенный Pooler CNPG перед каждым выделенным экземпляром Postgres клиента (deploy/cnpg/tenant-pooler.yaml). Оба пути применяют одну и ту же дисциплину, описанную выше.

В исходном коде указана конкретная причина этого выбора, связанная с безопасностью, а не только причина стабильности. Пулы Postgres, совместно используемые арендаторами, размещают разные search_path для каждого проекта в повторно используемых соединениях. Кэшированный подготовленный оператор замораживает OID разрешенной таблицы во время Parse; воспроизведение его для другого арендатора в том же соединении приведет к запуску запроса к схеме первого арендатора, что является обходом изоляции, а не просто ошибкой приложения. Таким образом, statement_cache_capacity(0) применяется без исключения, в том числе к выделенным экземплярам, ​​которые также проходят через пул CNPG в режиме транзакций.

Вторая мера гигиены сеанса: когда каждое соединение возвращается в пул, перехват выполняет DISCARD ALL (сброс настроек, освобождение подготовленных операторов на стороне сервера, снятие консультативных блокировок, очистка курсоров и временных таблиц). Без этого перехватчика остаток сеанса, созданный предыдущим запросом, может просочиться при следующем запросе от другого клиента, повторно использующего то же переработанное соединение.

Принимается исключение: выделенные экземпляры PostgREST остаются в прямом соединении с основным, минуя пул. Перезагрузка схемы PostgREST опирается на LISTEN/NOTIFY, который предполагает постоянное соединение, а это именно та функциональность, которую нарушает режим транзакций (подробности уже описаны в нашей статье о совместимости PostgREST на Aurabase). Параметры RLS для каждого запроса передаются в SET LOCAL в рамках явной транзакции. Это единственный способ сохранить совместимость с пулом, который может изменять подключения к серверу при любом COMMIT (см. нашу статью о мультитенантной изоляции RLS).

#
Практическое руководство

Включите режим транзакций, не нарушая работу вашего приложения.

Краткий контрольный список, применимый к любому бэкэнду, переходящему с прямого соединения Postgres на PgBouncer в режиме транзакции.

  1. Проведите аудит кода приложения. Ищите нетранзакционные SET, LISTEN/NOTIFY, консультативные блокировки сеанса, курсоры WITH HOLD и временные таблицы, повторно используемые между запросами.
  2. Замените сеансовые SET на LOCAL SET в рамках явной транзакции. Это единственный параметр, который правильно выдерживает перезапуск соединения, поскольку он очищается при COMMIT/ROLLBACK, а не теряется при следующем соединении.
  3. Отключите кэш подготовленных операторов на стороне драйвера, если ваш пул проходит через средство объединения и схема или роль изменяются от одного запроса к другому. Затраты на производительность реальны, но измеримы и намного ниже, чем риск утечки информации между арендаторами.
  4. Изолируйте соединения, которым действительно нужен режим сеанса (миграции, сценарии администратора и все, что зависит от LISTEN/NOTIFY), прямым соединением без объединения в пул, а не отказывайтесь от режима транзакций для всего остального трафика.
  5. Размер default_pool_size и max_client_conn относительно фактического max_connectionsPostgres, а не произвольной цифры, скопированной из другого проекта.
  6. Тестируйте под реальной нагрузкой, а не просто дымовым тестом. Ошибки подготовленных операторов и утечки настроек сеанса практически никогда не возникают при одном локальном соединении.
  7. Мониторинг SHOW POOLS и SHOW STATS из консоли администрирования PgBouncer после запуска в производство, чтобы определить насыщение пула до того, как оно станет видимым на стороне клиента.
#
Решение

Следует ли всегда выбирать режим транзакции, а не сеанса?

Нет, но это правильный выбор по умолчанию для подавляющего большинства REST API. Режим сеанса остается предпочтительным для устаревшего приложения, сильно зависящего от функций сеанса, которые невозможно быстро реорганизовать, или для низкого трафика, когда выигрыш от объединения не компенсирует усилия по миграции.

PgBouncer — не единственная реализация этой модели объединения: Supavisor (Supabase) и PgCat — две недавние альтернативы с разными компромиссами в распределении нагрузки и кластеризации. См. наше подробное сравнение PgBouncer, Supavisor и PgCat, чтобы выбрать один из трех в зависимости от вашей топологии.

#
Часто задаваемые вопросы

Часто задаваемые вопросы

Вопросы, которые возникают чаще всего после активации режима транзакций в рабочей среде.

Что такое режим пула транзакций PgBouncer?+
Это один из трех режимов PgBouncer (с сеансом и оператором), в котором соединение Postgres переназначается другому клиенту в конце каждой транзакции, а не при отключении клиента. Это позволяет обслуживать гораздо больше конкурирующих клиентов, чем фактически открыто соединений Postgres.
Почему мои подготовленные операторы аварийно завершают работу в режиме транзакции?+
Именованный подготовленный оператор готовится для конкретного соединения с сервером. В режиме транзакции это соединение можно переназначить другому клиенту между двумя запросами. Если ваш драйвер воспроизводит имя оператора в соединении, где он никогда не был подготовлен, Postgres возвращает ошибку, например, подготовленный оператор не существует. Исправление заключается в отключении кэша подготовленных операторов на стороне драйвера (statement_cache_capacity(0) для sqlx, Statement_cache_size=0 для asyncpg).
Можем ли мы использовать LISTEN/NOTIFY за PgBouncer в режиме транзакции?+
Нет, не достоверно. LISTEN/NOTIFY предполагает постоянное соединение для получения уведомлений, чего режим транзакции не гарантирует. Стандартная практика — передавать компоненты, которые зависят от LISTEN/NOTIFY (например, PostgREST), через прямое соединение с Postgres, вне пула.
Должны ли мы использовать SET LOCAL, а не SET в режиме транзакции?+
Да, систематически для любых настроек, которые должны применяться к данному запросу. SET LOCAL автоматически очищается при COMMIT или ROLLBACK, что делает его безопасным при соединении с сервером, которое может меняться между двумя транзакциями. Классический SET может просочиться к следующему клиенту, который восстановит то же переработанное соединение с сервером.
Работает ли режим транзакций с безопасностью на уровне строк (RLS)?+
Да, при условии, что утверждения JWT или переменные сеанса, используемые вашими политиками RLS, установлены в LOCAL SET внутри транзакции, а не в SET сеанса. Этот шаблон описан в нашей статье о мультитенантной изоляции RLS.
PgBouncer, Supavisor, PgCat: какой выбрать?+
Все три реализуют схожую модель пула с различиями в кластеризации, распределении нагрузки и экосистеме (Supavisor разработан Supabase, PgCat написан на Rust). Выбор зависит, прежде всего, от топологии вашего развертывания и существующих эксплуатационных ограничений: подробности см. в нашем специальном сравнении.

ГОТОВЫ К РАЗВЕРТЫВАНИЮ?

Ваш бэкэнд за пять минут.

Кредитная карта не требуется · 500 МБ бесплатно · 50 000 MAU