Режимы пула

pg_doorman поддерживает два режима пула: transaction и session. Режим задаётся для пула, при необходимости переопределяется для конкретного пользователя.

Режима statement нет. В statement-пулинге серверное соединение ротируется после каждого оператора — это вынуждает клиентов отказаться от мульти-statement транзакций и полностью ломает протокол prepared statements. Все оптимизации pg_doorman (кеш prepared statements, прямая передача, строгий FIFO-планировщик) рассчитаны на транзакционный режим. PgBouncer оставляет statement для обратной совместимости; Odyssey его не реализует.

Транзакционный режим (рекомендуется)

pools:
  mydb:
    pool_mode: "transaction"

Backend-соединение удерживается в течение транзакции и возвращается в пул при COMMIT, ROLLBACK или неявном завершении.

Именно этот режим даёт ту самую эффективность соединений, ради которой существует pg_doorman: pool_size равный 40 обслуживает тысячи клиентов, пока транзакции остаются короткими.

Что работает в транзакционном режиме (там, где большинство пулеров проваливаются):

  • Prepared statements. pg_doorman кеширует их в рамках пула, переименовывает имена statement между backend-соединениями и прозрачно повторно готовит запрос. Драйверы, привязанные к unnamed-statement (Go pgx, .NET Npgsql, Python asyncpg), работают без настройки.
  • Pipelined-батчи и асинхронный поток Flush.
  • Cancel-запросы поверх TLS.
  • LISTEN / NOTIFY — но только внутри транзакции. LISTEN после COMMIT отпускает backend, и любые уведомления, доставленные ему после, попадут к тому клиенту, который следующим заберёт это соединение, а не к исходному LISTEN-еру. PgBouncer ведёт себя так же; если вам нужен LISTEN через несколько транзакций, для этого клиента используйте сессионный режим.

Что в транзакционном режиме не работает:

  • SET и RESET вне транзакции. Используйте сессионный режим для клиентов, опирающихся на изменение GUC уровня сессии (SET TIME ZONE, SET search_path один раз на соединение).
  • advisory-блокировки, удерживаемые между транзакциями. Используйте сессионный режим.
  • курсоры, удерживаемые вне транзакций (WITH HOLD). Используйте сессионный режим.
  • SET LOCAL работает как ожидается — он ограничен транзакцией.

Сессионный режим

pools:
  legacy_app:
    pool_mode: "session"

Backend-соединение удерживается в течение клиентской сессии. В пул возвращается только при отключении клиента.

Используйте, когда:

  • Приложение использует состояние уровня сессии (SET search_path, SET TIME ZONE).
  • Приложение использует курсоры WITH HOLD.
  • Приложение использует advisory-блокировки между транзакциями.
  • Вы переезжаете с немодифицированного PgBouncer-развёртывания, работавшего в сессионном режиме, и хотите подмену один-в-один.

В сессионном режиме pool_size фактически равен максимальному числу одновременных клиентов. Размер пула подбирается так, чтобы соответствовать max_connections PostgreSQL минус резервы.

Переопределение для конкретного пользователя

Режим пула можно переопределить для конкретного пользователя:

pools:
  mydb:
    pool_mode: "transaction"
    users:
      - username: "app"
        password: "md5..."
        pool_size: 40
      - username: "admin_tools"
        password: "md5..."
        pool_size: 4
        pool_mode: "session"

Полезно, когда одному пользователю (инструменты эксплуатации, миграции) нужна семантика сессии, а основное приложение остаётся в транзакционном режиме.

Очистка при возврате в пул

Очистка в транзакционном режиме отслеживает мутации, а не выполняется безусловно. pg_doorman следит за каждой транзакцией на предмет SET, PREPARE и DECLARE CURSOR, и только когда backend уходит в пул с одним из этих флагов, отправляет соответственно RESET ALL, DEALLOCATE ALL или CLOSE ALL. Транзакция только на чтение пропускает очистку целиком — это измеримый выигрыш на горячих OLTP-путях.

Что сбрасывается, когда сработал флаг:

  • Флаг SETRESET ALL сбрасывает session-level GUCs и неявно вызывает pg_advisory_unlock_all.
  • Флаг PREPAREDEALLOCATE ALL удаляет PostgreSQL-side prepared statements, которые драйвер именовал явно. Собственный кеш prepared statements pg_doorman сохраняется после сброса: он индексируется текстом запроса, а не backend-именем.
  • Флаг DECLARE CURSORCLOSE ALL закрывает курсоры.

DEALLOCATE ALL и DISCARD ALL со стороны клиента очищают prepared-statement-кеш именно этого клиента (следующий Parse зарегистрируется заново). Pool-level shared cache не затрагивается; у других клиентов их записи сохраняются.

Полностью отключить очистку (ради производительности в жёстко контролируемых развёртываниях):

pools:
  mydb:
    pool_mode: "transaction"
    cleanup_server_connections: false

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

Справочник