PgDoorman

Многопоточный пулер соединений для PostgreSQL, написанный на Rust. Drop-in замена для PgBouncer, Odyssey и PgCat. В production в Ozon уже больше трёх лет под нагрузками Go (pgx), .NET (Npgsql), Python (asyncpg, SQLAlchemy) и Node.js.

Скачать PgDoorman 3.6.0 · Сравнение · Benchmarks

Что отличает PgDoorman

Три вещи, которых вы не найдёте в PgBouncer и Odyssey.

Pool Coordinator

Ограничение числа соединений на уровне базы с приоритетным вытеснением. max_db_connections задаёт суммарное число backend-соединений к одной базе; когда лимит исчерпан, idle-соединения вытесняются у пользователей с наибольшим избытком, ранжируя их по p95 времени транзакции — самые медленные пулы отдают соединения первыми. Резервный пул поглощает короткие всплески. Per-user min_guaranteed_pool_size защищает критичные нагрузки.

В PgBouncer max_db_connections есть, но без вытеснения и без честности распределения. В Odyssey аналога нет.

Подробнее →

Patroni-assisted Fallback

Когда PgDoorman работает рядом с PostgreSQL на одной машине и switchover Patroni убивает локальный бэкенд, PgDoorman опрашивает Patroni REST API эндпоинт /cluster, выбирает живого члена кластера (предпочтение отдаётся sync_standby) и направляет новые соединения туда за 1–2 TCP round trips. Локальный бэкенд остаётся в cooldown; fallback-соединения используют короткий lifetime, чтобы пул вернулся к локальному узлу после восстановления.

Одна строка в [general] включает функцию для всех пулов. Никакого внешнего HAProxy, никакого consul-template.

Подробнее →

Graceful Binary Upgrade

Замените бинарник, не потеряв ни одного клиента. Новый процесс сразу принимает новые соединения, а существующие клиенты завершают свои транзакции на старом. TLS, состояние соединения и cancel keys переносятся корректно.

PgBouncer требует SO_REUSEPORT с отдельными процессами (что приводит к разбалансировке пулов). В Odyssey аналогичного механизма нет.

Подробнее →

Почему PgDoorman

  • Drop-in замена. Прозрачно кэширует и переименовывает prepared statements в транзакционном режиме — никаких DISCARD ALL, DEALLOCATE или хаков в драйверах.
  • Многопоточность. Один общий пул на все рабочие потоки. PgBouncer однопоточен; запуск нескольких инстансов через SO_REUSEPORT приводит к разбалансировке пулов.
  • Подавление thundering herd. Когда 200 клиентов одновременно борются за 4 idle-соединения, PgDoorman ограничивает число параллельных создаваемых backend-соединений и направляет ожидающих на возвращаемые соединения через direct handoff — большинство получают соединение за микросекунды.
  • Ограниченная хвостовая задержка. Строгий FIFO через каналы direct-handoff удерживает p99 в пределах 10% от p50 независимо от числа клиентов. Опережающая замена при истечении server_lifetime — никаких всплесков при ротации соединений.
  • Обнаружение разорванных бэкендов. Когда клиент держит открытую транзакцию, а бэкенд умирает (failover, OOM kill), PgDoorman сразу возвращает ошибку. Другие пулеры ждут TCP keepalive и оставляют клиентов висеть на минуты.
  • Сделано для эксплуатации. Конфиг в YAML или TOML с человекочитаемыми длительностями ("30s", "5m"). pg_doorman generate --host your-db интроспектирует PostgreSQL и собирает конфиг. pg_doorman -t валидирует его перед деплоем. Prometheus-эндпоинт встроен.

Сравнение

PgDoormanPgBouncerOdyssey
МногопоточностьДаНетДа
Prepared statements в транзакционном режимеДаНачиная с 1.21Начиная с 1.3
Полная поддержка extended query protocolДаДаЧастично
Pool Coordinator с приоритетным вытеснениемДаНетНет
Patroni-assisted fallback (встроенный)ДаНетНет
Опережающая замена при истечении server_lifetimeДаНетНет
Обнаружение застрявших бэкендов (idle-in-transaction)ДаНетНет
Graceful binary upgradeДаОграниченноНет
Server-side TLS (mTLS, горячая перезагрузка)ДаНетНет
Auth: passthrough SCRAM (без пароля в открытом виде в конфиге)ДаНетДа
Auth: JWTДаНетНет
Auth: PAM / pg_hba.conf / auth_queryДаДаДа
Auth: LDAPНетНачиная с 1.25Да
Конфиг YAML / TOMLДаНет (INI)Нет (свой формат)
JSON структурированное логированиеДаНетДа
Перцентили задержки (p50/90/95/99)ДаНетДа
Режим проверки конфига (-t)ДаНетНет
Авто-конфиг из PostgreSQLДаНетНет
Встроенный Prometheus-эндпоинтДаВнешнийДа

Полная матрица фич →

Бенчмарки

AWS Fargate (16 vCPU), pool size 40, pgbench 30 с на тест:

Сценарийvs PgBouncervs Odyssey
Extended protocol, 500 клиентов + SSL×3.5+61%
Prepared statements, 500 клиентов + SSL×4.0+5%
Simple protocol, 10 000 клиентов×2.8+20%
Extended + SSL + Reconnect, 500 клиентов+96%~0%

Полные результаты →

Быстрый старт

Запуск через Docker:

docker run -p 6432:6432 \
  -v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
  ghcr.io/ozontech/pg_doorman

Минимальный конфиг (pg_doorman.yaml):

general:
  host: "0.0.0.0"
  port: 6432
  admin_username: "admin"
  admin_password: "change_me"

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "app"
        password: "md5..."   # хэш из pg_shadow / pg_authid
        pool_size: 40

server_username и server_password опущены намеренно — PgDoorman переиспользует MD5-хэш клиента или SCRAM ClientKey для аутентификации в PostgreSQL. Никаких паролей в открытом виде в конфиге.

Руководство по установке → · Справочник по конфигурации →

Куда дальше

PgDoorman vs PgBouncer vs Odyssey vs PgCat

Практическая матрица фич для выбора пулера соединений PostgreSQL. PgDoorman нацелен на нагрузки, где важны prepared statements в транзакционном режиме, многопоточная производительность и удобство эксплуатации.

Числа бенчмарков — см. Benchmarks.

Аутентификация

ФичаPgDoormanPgBouncerOdyssey
MD5 парольДаДаДа
SCRAM-SHA-256 (клиент)ДаДаДа
Passthrough SCRAM-SHA-256 (без пароля в открытом виде в конфиге)ДаНетДа
Passthrough MD5ДаДаДа
auth_query (динамические пользователи)ДаДаДа
Режим passthrough в auth_query (per-user идентичность бэкенда)ДаНетДа
Формат pg_hba.confДа (файл или inline)НетНачиная с 1.4
PAMДа (Linux)Да (HBA)Да
JWT (RSA-SHA256)ДаНетНет
Talos (кастомный JWT с извлечением роли)ДаНетНет
LDAPНетНачиная с 1.25Да
SCRAM channel binding (scram-sha-256-plus)НетДаДа
Маппинг имён пользователей (cert/peer → DB user)НетНачиная с 1.23Да
Настраиваемый scram_iterationsНетНачиная с 1.25Нет

См. Аутентификация.

TLS

ФичаPgDoormanPgBouncerOdyssey
Client-side TLS (4 режима: disable, allow, require, verify-full)ДаДаДа
Server-side TLS к PostgreSQL (6 режимов, включая verify-ca, verify-full)ДаДаНет
mTLS к PostgreSQL (клиентский сертификат)ДаДаНет
Горячая перезагрузка TLS-сертификатов по SIGHUPДа (server-side)НетНет
Минимум TLS 1.2 + список шифров MozillaДаДаНет (разрешает TLS 1.0)
Direct TLS handshake (PG17, без SSLRequest)НетНачиная с 1.25Нет
Управление шифрами TLS 1.3НетНачиная с 1.25Нет

См. TLS.

Маршрутизация и высокая доступность

ФичаPgDoormanPgBouncerOdyssey
Patroni-assisted fallback (встроенный поиск через /cluster)ДаНетНет
Встроенный TCP-прокси с маршрутизацией по ролям (patroni_proxy)ДаНетНет
Защита от отставания репликиДа (max_lag_in_bytes в patroni_proxy)НетДа (watchdog-запрос)
Round-robin / least-connections для нескольких хостовДа (patroni_proxy)Начиная с 1.24Да
target_session_attrs (read-write / read-only)Да (через роли в patroni_proxy)НетДа
Последовательная маршрутизация (правила по порядку)НетНетДа
Маршрутизация по типу соединения (TCP vs UNIX)НетНетДа
Выбор хоста с учётом availability zoneНетНетДа

См. Patroni-assisted fallback, patroni_proxy.

Пулинг

ФичаPgDoormanPgBouncerOdyssey
Режимы пула (transaction, session)ДаДа (+ statement)Да
Pool Coordinator (cross-user max_db_connections с приоритетным вытеснением)ДаНет (без вытеснения)Нет
Резервный пул с min_guaranteed_pool_sizeДаТолько reserveНет
Опережающая замена при истечении server_lifetimeДаНетНет
Опережающее создание / burst scaling (scaling_warm_pool_ratio, быстрые retry)ДаНетНет
Direct-handoff (ожидающий получает возвращаемое соединение за микросекунды)ДаНетНет
min_pool_size (прогретые соединения)ДаНетДа
Кэш prepared statements (двухуровневый, query interner, statement remap)ДаНачиная с 1.21Начиная с 1.3
Умный DISCARD при возвратеRESET ALL + сброс кэшаНетДа (авто)
Прикрепление LISTEN / NOTIFY в транзакционном режимеНетНетЭкспериментально
Cross-rule ограничение соединений (shared_pool)НетНетНачиная с 1.5.1
PAUSE / RESUME / RECONNECTДаДаДа (1.4.1+)

См. Pool Coordinator, Пул под нагрузкой.

Лимиты и таймауты

ФичаPgDoormanPgBouncerOdyssey
server_idle_check_timeout (проверка перед выдачей)ДаНетНет
idle_timeout (серверное соединение)ДаДаДа
server_lifetimeДаДаДа
query_wait_timeoutДаДаДа
client_idle_timeoutНетНачиная с 1.24Нет
transaction_timeoutНетНачиная с 1.25Нет
max_user_client_connectionsНетНачиная с 1.24Нет
Per-user query_timeoutНетНачиная с 1.24Нет
Per-user reserve_pool_sizeНетНачиная с 1.24Нет
query_wait_notify (NOTICE при ожидании бэкенда)НетНачиная с 1.25Да (pool_notice_after_waiting_ms)

См. Справочник по general settings, Справочник по pool settings.

Observability

ФичаPgDoormanPgBouncerOdyssey
Встроенный Prometheus-эндпоинтДаВнешний (pgbouncer_exporter)Да
Перцентили задержки на пул (p50, p90, p95, p99)Да (HDR Histogram)НетДа (TDigest)
Счётчики prepared statements в статистикеДаНачиная с 1.24Нет
JSON структурированное логированиеДа (--log-format Structured)НетДа
Управление уровнем логирования в рантайме (SET log_level)ДаНетНет
Admin SHOW POOL_COORDINATOR / SHOW POOL_SCALING / SHOW SOCKETSДаНетНет
Admin SHOW PREPARED_STATEMENTSДаНетНет
Admin SHOW HOSTS (CPU/память хоста)НетНетДа
Admin SHOW RULES (дамп маршрутизации)НетНетДа
Метрики TLS-соединений (длительность handshake, ошибки, активные)Да (server-side)НетНет
Метрики Patroni APIДаНетНет
Метрики fallback (флаг активности, текущий хост, попадания)ДаНетНет

См. Справочник по Prometheus-метрикам, Admin-команды.

Эксплуатация

ФичаPgDoormanPgBouncerOdyssey
Graceful binary upgrade (zero-downtime, in-flight клиенты сохраняются)ДаОграниченно (SO_REUSEPORT)Нет
YAML конфигДаНет (INI)Нет (свой формат)
TOML конфигДа (legacy)НетНет
Человекочитаемые длительности и размеры (30s, 1h, 256MB)ДаНетНет
Режим проверки конфига (pg_doorman -t)ДаНетНет
Авто-конфиг из PostgreSQL (pg_doorman generate --host)ДаНетНет
Перезагрузка по SIGHUPДа (включая server TLS-сертификаты)ДаДа
Интеграция с systemd sd-notifyДа (Type=notify)НетНет
Лимит памяти (max_memory_usage)ДаНетНет

См. Binary upgrade, Сигналы.

Протокол

ФичаPgDoormanPgBouncerOdyssey
Simple queryДаДаДа
Extended queryДаДаЧастично
Pipelined batchesДаДаЧастично
Async FlushДаДаНет
Cancel-запросы поверх TLSДаДаДа
COPY IN / COPY OUTДаДаДа
Проброс replication-соединений (replication=true в startup)НетНачиная с 1.23Нет
Поддержка версии протокола 3.2НетНачиная с 1.23Нет
server_drop_on_cached_plan_errorНетНетНачиная с 1.5.1

Когда PgDoorman не подходит

  • Нужна аутентификация LDAP. Используйте Odyssey или PgBouncer 1.25+.
  • Нужен SCRAM channel binding (scram-sha-256-plus) end-to-end. Используйте PgBouncer или Odyssey.
  • Нужна сквозная replication для инструментов логической репликации. Используйте PgBouncer 1.23+.
  • Нужна маршрутизация с учётом availability zone или последовательные правила в стиле pg_hba. Используйте Odyssey.
  • Нужно, чтобы transaction_timeout принудительно применялся пулером. Используйте PgBouncer 1.25+.

Если важны prepared statements в транзакционном режиме, Patroni HA без внешних прокси, многопоточный throughput и перезапуски без простоя — PgDoorman подходит ближе.

Обзор PgDoorman

Что такое PgDoorman?

PgDoorman — пулер соединений PostgreSQL на Rust. Изначально форк PgCat, теперь самостоятельный проект. Стоит между приложениями и PostgreSQL: переиспользует серверные соединения вместо того, чтобы открывать их под каждый клиентский запрос.

graph LR
    App1[Приложение A] --> Pooler(PgDoorman)
    App2[Приложение B] --> Pooler
    App3[Приложение C] --> Pooler
    Pooler --> DB[(PostgreSQL)]

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

Ключевые преимущества

  • Меньше накладных расходов на соединения: снижается влияние установки новых соединений к базе на производительность.
  • Оптимизация ресурсов: ограничивается число соединений к серверу PostgreSQL.
  • Улучшенная масштабируемость: больше клиентских приложений могут подключаться к одной базе данных.
  • Управление соединениями: инструменты для мониторинга и управления соединениями.

Режимы пулинга

Чтобы сохранить корректную семантику транзакций при эффективном пулинге соединений, PgDoorman поддерживает несколько режимов:

Транзакционный пулинг

Рекомендуется для большинства сценариев

В транзакционном режиме клиенту выдаётся серверное соединение только на время транзакции. Как только транзакция завершается, соединение возвращается в пул.

  • Соединения переиспользуются между клиентами, поэтому пул из 40 backend-соединений обслуживает тысячи клиентов, пока транзакции короткие.
  • Подходит большинству приложений, кроме тех, что полагаются на состояние сессии (SET, WITH HOLD курсоры, advisory-блокировки между транзакциями).

Сессионный пулинг

Полезно для специфических legacy-сценариев

В сессионном режиме каждому клиенту выделяется собственное серверное соединение на всё время его клиентского соединения.

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

Администрирование

PgDoorman предоставляет полный набор инструментов для мониторинга и управления:

  • Admin-консоль: PostgreSQL-совместимый интерфейс для просмотра статистики и управления пулером.
  • Параметры конфигурации: широкие возможности настройки поведения под конкретные требования.
  • Мониторинг: подробные метрики использования соединений и производительности.

Подробнее об управлении PgDoorman — см. документацию admin-консоли.

Установка PgDoorman

PgDoorman работает на Linux и macOS. Для production рекомендуем собирать самим — так вы контролируете версию Rust, целевую платформу и зависимости. Также доступны готовые пакеты из репозиториев и статические бинарники. Docker — только для тестов.

Системные требования

  • Linux (рекомендуется) или macOS
  • PostgreSQL 10 или новее (любая поддерживаемая версия)
  • Память пропорциональна размеру пулов (несколько МБ на пул + кэш prepared statements)
  • Rust 1.87 или новее, если собираете из исходников

Сборка из исходников (рекомендуется)

Соберите со своим toolchain — это даёт контроль над версией компилятора, целевой платформой и зависимостями:

git clone https://github.com/ozontech/pg_doorman.git
cd pg_doorman
cargo build --release
sudo install -m 0755 target/release/pg_doorman /usr/local/bin/pg_doorman

cargo build --release собирает оптимизированный бинарник в target/release/pg_doorman. Требования к окружению и процесс разработки описаны в Участие в проекте.

Cargo features

FeatureПо умолчаниюЭффект
tls-migrationвыклVendored OpenSSL 3.5.5 с патчем, позволяющим TLS-клиентам пережить обновление бинарника. Нужен для zero-downtime перезапуска TLS-клиентов.
pamвыклПоддержка аутентификации PAM (Linux).

Сборка с миграцией TLS-клиентов

По умолчанию TLS-клиенты не могут перейти на новый процесс при обновлении бинарника — они получают ошибку 58006 и переподключаются. Чтобы соединения переходили на новый процесс без разрыва, соберите с фичей tls-migration:

cargo build --release --features tls-migration

Сборка использует vendored OpenSSL 3.5.5 с патчем, который экспортирует и заново импортирует состояние TLS-шифров (ключи, IV, sequence numbers, TLS 1.3 traffic secrets) при передаче соединений между процессами. Зашифрованные клиенты остаются на том же TCP-соединении без повторного TLS handshake.

Требования:

  • Только Linux (macOS и Windows используют системный TLS, не OpenSSL).
  • Утилиты perl и patch в PATH.
  • Около 5 минут дополнительного времени сборки на компиляцию OpenSSL.

Офлайн-сборка (air-gapped среды):

curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz
OPENSSL_SOURCE_TARBALL=$(pwd)/openssl-3.5.5.tar.gz \
  cargo build --release --features tls-migration

Старый и новый процесс должны использовать одни и те же tls_certificate и tls_private_key. Полное описание upgrade-процесса, мониторинг и диагностика — в Graceful Binary Upgrade → TLS migration.

Для упаковки в deb/rpm смотрите каталоги debian/ и pkg/ в репозитории. Пример Dockerfile.ubuntu22-tls собирает образ с поддержкой TLS migration на Ubuntu 22.04.

Пакеты из репозиториев

Готовые deb- и rpm-пакеты публикуются с теми же релизными тегами. Используйте их, когда сборка из исходников нежелательна.

В пакетах нет поддержки TLS

Пакеты из Ubuntu PPA и Fedora COPR собираются без поддержки TLS. Если нужен TLS — для клиентских соединений, серверных соединений к PostgreSQL или для горячей миграции TLS при обновлении бинарника — собирайте из исходников с включённой TLS-фичей. См. Сборка из исходников выше.

Ubuntu / Debian (PPA)

sudo add-apt-repository ppa:vadv/pg-doorman
sudo apt update
sudo apt install pg-doorman

Поддерживаемые релизы: jammy (22.04 LTS), noble (24.04 LTS), questing (25.10), resolute (26.04 LTS).

Fedora / RHEL / CentOS / Rocky / AlmaLinux (COPR)

sudo dnf copr enable @pg-doorman/pg-doorman
sudo dnf install pg_doorman

Поддерживаемые цели: Fedora 39, 40, 41; EPEL 8 и 9 для семейства RHEL.

Пакет ставит systemd-юнит, конфиг по умолчанию и пользователя pg_doorman.

Готовые бинарники с GitHub Releases

Если ни сборка из исходников, ни пакеты из репозиториев не подходят, скачайте статический бинарник со страницы релизов:

# Замените VERSION и TARGET на нужные значения со страницы релизов.
curl -L -o pg_doorman \
  "https://github.com/ozontech/pg_doorman/releases/download/VERSION/pg_doorman-TARGET"
chmod +x pg_doorman
sudo mv pg_doorman /usr/local/bin/

Перед деплоем сверьте SHA-256, опубликованный вместе с релизом.

Docker (только для тестов)

Docker поддерживается для разработки, CI и быстрых демо. Для production не рекомендуется — упаковка и управление жизненным циклом проще через пакеты из репозиториев выше.

docker run -p 6432:6432 \
  -v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
  ghcr.io/ozontech/pg_doorman

docker-compose.yaml с PostgreSQL в качестве sidecar лежит в example/ — для smoke-тестов.

Проверка установки

pg_doorman --version
pg_doorman -t /etc/pg_doorman/pg_doorman.yaml   # проверяет конфиг
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c "SHOW VERSION;"

pg_doorman -t проверяет конфиг до деплоя — у PgBouncer и Odyssey такой возможности нет.

Куда дальше

Базовое использование PgDoorman

PgDoorman -- пулер соединений PostgreSQL, основанный на PgCat. В этом руководстве описаны конфигурация, эксплуатация и администрирование.

Параметры командной строки

PgDoorman поддерживает несколько параметров командной строки для настройки поведения при запуске:

$ pg_doorman --help

PgDoorman: Nextgen PostgreSQL Pooler (based on PgCat)

Usage: pg_doorman [OPTIONS] [CONFIG_FILE] [COMMAND]

Commands:
  generate  Generate configuration for pg_doorman by connecting to PostgreSQL and auto-detecting databases and users
  help      Print this message or the help of the given subcommand(s)

Arguments:
  [CONFIG_FILE]  [env: CONFIG_FILE=] [default: pg_doorman.toml]

Options:
  -l, --log-level <LOG_LEVEL>    [env: LOG_LEVEL=] [default: INFO]
  -F, --log-format <LOG_FORMAT>  [env: LOG_FORMAT=] [default: text] [possible values: text, structured, debug]
  -n, --no-color                 disable colors in the log output [env: NO_COLOR=]
  -d, --daemon                   run as daemon [env: DAEMON=]
  -h, --help                     Print help
  -V, --version                  Print version

Доступные параметры

ПараметрОписание
-d, --daemonЗапуск в фоне. Без этого параметра процесс работает на переднем плане.

В daemon-режиме обязательны daemon_pid_file и syslog_prog_name. После ухода в фон сообщения в stderr больше не пишутся.
-l, --log-levelУровень логирования: INFO, DEBUG или WARN.
-F, --log-formatФормат логов. Возможные значения: text, structured, debug.
-n, --no-colorОтключить цвета в выводе логов.
-V, --versionПоказать информацию о версии.
-h, --helpПоказать справку.

Установка и настройка

Структура файла конфигурации

PgDoorman поддерживает форматы YAML и TOML. Для новых установок рекомендуется YAML. Конфигурация разбита на несколько секций:

general:        # Глобальные настройки сервиса PgDoorman
pools:
  <name>:       # Настройки конкретного пула базы данных
    users:
      - ...     # Настройки пользователя для этого пула

Important

Некоторые параметры обязательно должны быть указаны в файле конфигурации, чтобы PgDoorman запустился, даже если у них есть значения по умолчанию. Например, для доступа к admin-консоли нужно указать admin username и password.

Минимальный пример конфигурации

Минимальный пример конфигурации для старта:

YAML (рекомендуется)

general:
  host: "0.0.0.0"         # Слушать на всех интерфейсах
  port: 6432               # Порт для клиентских соединений
  admin_username: "admin"
  admin_password: "admin"  # Поменяйте это в production!

pools:
  exampledb:
    server_host: "127.0.0.1"  # Адрес сервера PostgreSQL
    server_port: 5432          # Порт сервера PostgreSQL
    pool_mode: "transaction"   # Режим пулинга
    users:
      - pool_size: 40
        username: "doorman"
        password: "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="

TOML

[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "admin"

[pools.exampledb]
server_host = "127.0.0.1"
server_port = 5432
pool_mode = "transaction"

[pools.exampledb.users.0]
pool_size = 40
username = "doorman"
password = "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="

Полный список параметров конфигурации можно получить командой pg_doorman generate --reference --output ref.yaml -- она генерирует аннотированный конфиг со всеми параметрами и значениями по умолчанию.

Автоматическая генерация конфигурации

Команда generate создаёт файл конфигурации, подключаясь к серверу PostgreSQL и определяя базы данных и пользователей. По умолчанию сгенерированный конфиг содержит inline-комментарии, объясняющие каждый параметр.

# Посмотреть все доступные опции
pg_doorman generate --help

# Сгенерировать YAML-конфиг (рекомендуется)
pg_doorman generate --output pg_doorman.yaml

# Сгенерировать TOML-конфиг (для обратной совместимости)
pg_doorman generate --output pg_doorman.toml

# Сгенерировать reference-конфиг со всеми настройками (без подключения к PG)
pg_doorman generate --reference --output pg_doorman.yaml

# Сгенерировать reference-конфиг с русскими комментариями для быстрого старта
pg_doorman generate --reference --ru --output pg_doorman.yaml

# Сгенерировать конфиг без комментариев (plain serialization)
pg_doorman generate --no-comments --output pg_doorman.yaml

Команда generate поддерживает несколько опций:

ОпцияОписание
--hostХост PostgreSQL для подключения (если не указано, используется localhost)
--port, -pПорт PostgreSQL для подключения (по умолчанию: 5432)
--user, -uПользователь PostgreSQL для подключения (нужны привилегии superuser, чтобы читать pg_shadow)
--passwordПароль PostgreSQL для подключения
--database, -dБаза данных PostgreSQL для подключения (если не указано, используется имя пользователя)
--sslПодключение к серверу PostgreSQL по SSL/TLS
--pool-sizeРазмер пула в сгенерированной конфигурации (по умолчанию: 40)
--session-pool-mode, -sСессионный режим пулинга в сгенерированной конфигурации
--output, -oФайл для сгенерированной конфигурации (если не указан, используется stdout)
--server-hostПереопределить server_host в конфиге (если не указано, используется значение --host)
--no-commentsОтключить inline-комментарии в сгенерированной конфигурации (по умолчанию они включены)
--referenceСгенерировать полный reference-конфиг с примерами значений, без подключения к PG
--russian-comments, --ruСгенерировать комментарии на русском для быстрого старта
--format, -fФормат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение

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

Переменные окружения PostgreSQL

Команда generate также учитывает стандартные переменные окружения PostgreSQL: PGHOST, PGPORT, PGUSER, PGPASSWORD и PGDATABASE.

Passthrough Authentication (по умолчанию)

PgDoorman по умолчанию использует passthrough authentication: криптографическое доказательство клиента (MD5-хэш или SCRAM ClientKey) автоматически переиспользуется для аутентификации в backend-сервере PostgreSQL. Plaintext-пароли в конфиге не нужны -- достаточно записать в password хэш из pg_shadow / pg_authid.

Указывайте server_username и server_password только тогда, когда backend-пользователь отличается от username пула (например, маппинг username или JWT-аутентификация):

users:
  - username: "app_user"              # имя для клиента
    password: "md5..."                # хэш для аутентификации клиента
    server_username: "pg_app_user"    # другой backend-пользователь PostgreSQL
    server_password: "real_password"  # plaintext-пароль для этого пользователя

Подробнее см. описания полей server_username и server_password в сгенерированном reference-конфиге.

Привилегии superuser

Чтение информации о пользователях из PostgreSQL требует привилегий superuser, чтобы получить доступ к таблице pg_shadow.

Контроль доступа клиентов (pg_hba)

PgDoorman может применять правила доступа клиентов в стиле PostgreSQL pg_hba.conf через параметр general.pg_hba. Правила можно встроить прямо в конфиг или сослаться на путь к файлу. Полные примеры -- в reference-разделе.

Trust-режим: когда правило использует trust, PgDoorman принимает соединения без запроса пароля у клиента -- зеркально поведению PostgreSQL. Учитываются TLS-зависимые типы правил: hostssl требует TLS, hostnossl его запрещает.

Запуск PgDoorman

После создания файла конфигурации запустите PgDoorman из командной строки:

$ pg_doorman pg_doorman.toml

Если файл конфигурации не указан, PgDoorman будет искать pg_doorman.toml в текущей директории.

Подключение к PostgreSQL через PgDoorman

После запуска PgDoorman подключайтесь к нему вместо прямого подключения к PostgreSQL:

$ psql -h localhost -p 6432 -U doorman exampledb

Строку подключения вашего приложения нужно изменить так, чтобы она указывала на PgDoorman, а не напрямую на PostgreSQL:

postgresql://doorman:password@localhost:6432/exampledb

PgDoorman прозрачно обработает пулинг соединений, поэтому приложению не нужно знать, что оно подключается через пулер.

Администрирование

Admin-консоль

PgDoorman предоставляет административный интерфейс через специальную базу данных pgdoorman (или pgbouncer для обратной совместимости):

$ psql -h localhost -p 6432 -U admin pgdoorman

После подключения можно посмотреть доступные команды:

pgdoorman=> SHOW HELP;
NOTICE:  Console usage
DETAIL:
	SHOW HELP|CONFIG|DATABASES|POOLS|POOLS_EXTENDED|POOLS_MEMORY|POOL_COORDINATOR|POOL_SCALING
	SHOW CLIENTS|SERVERS|USERS|CONNECTIONS|STATS|PREPARED_STATEMENTS|AUTH_QUERY
	SHOW LISTS|SOCKETS|LOG_LEVEL|VERSION
	SET log_level = '<filter>'
	RELOAD
	SHUTDOWN
	UPGRADE
	PAUSE [db]
	RESUME [db]
	RECONNECT [db]

Совместимость протокола

Admin-консоль на данный момент поддерживает только simple query protocol. Некоторые драйверы баз данных используют extended query protocol для всех команд, что делает их непригодными для работы с admin-консолью. В таких случаях используйте клиент psql для администрирования.

Безопасность

Войти в admin-консоль может только пользователь, указанный в admin_username файла конфигурации. Если это разрешено правилами general.pg_hba, в admin-консоль можно входить методом trust (без запроса пароля), например:

# Разрешить только локальному admin доступ к admin-БД без пароля
host  pgdoorman  admin  127.0.0.1/32  trust

Используйте trust с предельной осторожностью. Всегда ограничивайте по адресу и, по возможности, требуйте TLS через hostssl. В production предпочитайте методы с паролями, если только не понимаете последствия полностью.

Мониторинг PgDoorman

Admin-консоль предоставляет несколько команд для мониторинга текущего состояния PgDoorman:

  • SHOW STATS -- посмотреть статистику производительности.
  • SHOW CLIENTS -- список текущих клиентских соединений.
  • SHOW SERVERS -- список текущих серверных соединений.
  • SHOW POOLS -- состояние пулов соединений.
  • SHOW DATABASES -- список настроенных баз данных.
  • SHOW USERS -- список настроенных пользователей.

Эти команды подробно описаны в разделе Команды admin-консоли ниже.

Перечитывание конфигурации

Если вы изменили pg_doorman.toml, изменения можно применить без перезапуска сервиса:

pgdoorman=# RELOAD;

При перечитывании конфигурации:

  1. PgDoorman читает обновлённый файл конфигурации.
  2. Обнаруживаются изменения параметров подключения к базам.
  3. Существующие серверные соединения закрываются при следующем возврате в пул (в соответствии с режимом пулинга).
  4. Новые серверные соединения сразу используют обновлённые параметры.

Это позволяет вносить изменения в конфигурацию с минимальным влиянием на приложения.

Команды admin-консоли

Admin-консоль предоставляет набор команд для мониторинга и управления PgDoorman. Команды используют SQL-подобный синтаксис и могут выполняться из любого PostgreSQL-клиента, подключённого к admin-консоли.

SHOW-команды

SHOW-команды отображают информацию о работе PgDoorman. Каждая команда даёт свой срез данных о производительности и текущем состоянии пулера.

SHOW STATS

SHOW STATS показывает развёрнутую статистику работы PgDoorman:

pgdoorman=> SHOW STATS;

Статистика приводится для каждой пары (database, user):

МетрикаОписание
databaseИмя базы данных
userИмя пользователя
total_xact_countВсего SQL-транзакций с момента запуска
total_query_countВсего SQL-команд с момента запуска
total_receivedВсего байт получено от клиентов
total_sentВсего байт отправлено клиентам
total_xact_timeВсего микросекунд в транзакциях (включая idle in transaction)
total_query_timeВсего микросекунд на выполнение запросов
total_wait_timeВсего микросекунд клиенты ждали серверного соединения
total_errorsВсего ошибок с момента запуска
avg_xact_countСреднее число транзакций в секунду за последние 15 секунд
avg_query_countСреднее число запросов в секунду за последние 15 секунд
avg_recvСреднее число байт в секунду, получаемых от клиентов
avg_sentСреднее число байт в секунду, отправляемых клиентам
avg_errorsСреднее число ошибок в секунду за последние 15 секунд
avg_xact_timeСредняя длительность транзакции в микросекундах
avg_query_timeСредняя длительность запроса в микросекундах
avg_wait_timeСреднее время ожидания сервера в микросекундах

Мониторинг производительности

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

SHOW SERVERS

SHOW SERVERS показывает детальную информацию обо всех серверных соединениях:

pgdoorman=> SHOW SERVERS;
КолонкаОписание
server_idУникальный идентификатор серверного соединения
server_process_idPID backend-процесса PostgreSQL (если доступен)
database_nameИмя базы данных, к которой подключено соединение
userUsername, под которым PgDoorman подключается к серверу PostgreSQL
application_nameЗначение параметра application_name, выставленное на серверном соединении
stateТекущее состояние соединения: active, idle или used
waitСостояние ожидания соединения: idle, read или write
transaction_countСколько транзакций обработало это соединение
query_countСколько запросов обработало это соединение
bytes_sentВсего байт отправлено серверу PostgreSQL
bytes_receivedВсего байт получено от сервера PostgreSQL
age_secondsВремя жизни текущего серверного соединения в секундах
prepare_cache_hitЧисло попаданий в кэш prepared statements
prepare_cache_missЧисло промахов кэша prepared statements
prepare_cache_sizeЧисло уникальных prepared statements в кэше

Состояния соединений

  • active: соединение прямо сейчас выполняет запрос.
  • idle: соединение свободно для использования.
  • used: соединение выделено клиенту, но прямо сейчас не выполняет запрос.

SHOW CLIENTS

SHOW CLIENTS показывает информацию обо всех клиентских соединениях к PgDoorman:

pgdoorman=> SHOW CLIENTS;
КолонкаОписание
client_idУникальный идентификатор клиентского соединения
databaseИмя базы данных (пула), к которой подключён клиент
userUsername, с которым клиент подключился
application_nameИмя приложения, заявленное клиентом
addrIP-адрес и порт клиента (IP:port)
tlsИспользует ли соединение TLS-шифрование (true или false)
stateТекущее состояние клиентского соединения: active, idle или waiting
waitСостояние ожидания клиентского соединения: idle, read или write
transaction_countСколько транзакций обработано для этого клиента
query_countСколько запросов обработано для этого клиента
error_countСколько ошибок было у этого клиента
age_secondsВремя жизни клиентского соединения в секундах

Мониторинг долгоживущих соединений

Колонка age_seconds помогает находить долгоживущие соединения, которые могут зря держать ресурсы. Подумайте о настройке таймаутов на idle-соединения в приложении.

SHOW POOLS

SHOW POOLS показывает информацию о пулах соединений. Для каждой пары (database, user) создаётся отдельная запись пула:

pgdoorman=> SHOW POOLS;
КолонкаОписание
databaseИмя базы данных
userUsername, ассоциированный с пулом
pool_modeИспользуемый режим пулинга: session или transaction
cl_idleЧисло idle-клиентов (вне транзакции)
cl_activeЧисло активных клиентских соединений (привязаны к серверу или idle)
cl_waitingЧисло клиентских соединений, ожидающих серверного соединения
cl_cancel_reqЧисло cancel-запросов от клиентов
sv_activeЧисло серверных соединений, привязанных к клиентам
sv_idleЧисло idle-серверных соединений, доступных для немедленного использования
sv_usedЧисло серверных соединений, недавно использованных, но ещё не ставших idle
sv_loginЧисло серверных соединений, которые сейчас в процессе логина
pool_sizeНастроенный максимальный размер пула для этой пары (database, user)
maxwaitМаксимальное время ожидания в секундах для самого старого клиента в очереди
maxwait_usМикросекундная часть максимального времени ожидания
avg_xact_timeСредняя длительность транзакции в микросекундах
pausedНа паузе ли пул: 1 (paused) или 0 (active)

Сигнал о производительности

Если значение maxwait начинает расти, серверный пул может не справляться с обработкой запросов. Это может быть вызвано перегруженным сервером PostgreSQL или недостаточным pool_size.

SHOW USERS

SHOW USERS показывает информацию обо всех настроенных пользователях:

pgdoorman=> SHOW USERS;
КолонкаОписание
nameUsername, как настроен в PgDoorman
pool_modeРежим пулинга, назначенный пользователю: session или transaction

SHOW DATABASES

SHOW DATABASES показывает информацию обо всех настроенных пулах баз данных:

pgdoorman=> SHOW DATABASES;
КолонкаОписание
nameИмя настроенного пула
hostHostname сервера PostgreSQL
portНомер порта сервера PostgreSQL
databaseРеальное имя базы на бэкенде (может отличаться от имени пула, если задан server_database)
force_userПользователь, форсированный для этого пула (если настроено)
pool_sizeМаксимальное число серверных соединений для этого пула
min_pool_sizeМинимальное число серверных соединений, которое поддерживается
reserve_poolМаксимальное число дополнительных reserve-соединений
pool_modeРежим пулинга по умолчанию для этого пула
max_connectionsМаксимально разрешённое число серверных соединений (из max_db_connections)
current_connectionsТекущее число серверных соединений для этого пула

Управление соединениями

Следите за соотношением current_connections и pool_size, чтобы убедиться, что пул адекватно размерен. Если current_connections часто доходит до pool_size, увеличьте размер пула.

SHOW SOCKETS

SHOW SOCKETS показывает количество сокетов в разных состояниях для TCP/TCP6/Unix (только Linux):

pgdoorman=> SHOW SOCKETS;

Показывает агрегированные счётчики состояний сокетов (ESTABLISHED, SYN_SENT и т.д.), полученные из /proc/net/tcp, /proc/net/tcp6 и /proc/net/unix.

SHOW VERSION

SHOW VERSION показывает информацию о версии PgDoorman:

pgdoorman=> SHOW VERSION;

Полезно, чтобы проверить, какая версия запущена, особенно после обновлений.

Управляющие команды

PgDoorman предоставляет управляющие команды, позволяющие управлять работой сервиса прямо из admin-консоли.

SHUTDOWN

Команда SHUTDOWN корректно завершает процесс PgDoorman:

pgdoorman=> SHUTDOWN;

При выполнении:

  1. PgDoorman перестаёт принимать новые клиентские соединения.
  2. Существующим транзакциям даётся время завершиться (в пределах настроенного таймаута).
  3. Все соединения закрываются.
  4. Процесс завершается.

Прерывание сервиса

Команда SHUTDOWN останавливает сервис PgDoorman, отключая всех клиентов. В production используйте её с осторожностью.

SET log_level

Изменить уровень логирования на лету, не перезапуская пулер:

-- Глобальный уровень
pgdoorman=> SET log_level = 'debug';

-- На модуль (синтаксис RUST_LOG)
pgdoorman=> SET log_level = 'warn,pg_doorman::pool::pool_coordinator=debug';

-- Посмотреть текущий уровень
pgdoorman=> SHOW LOG_LEVEL;

-- Сбросить на стартовое значение
pgdoorman=> SET log_level = 'default';

Изменения временные -- теряются при перезапуске. Допустимые уровни: error, warn, info, debug, trace, off.

RELOAD

Команда RELOAD обновляет конфигурацию PgDoorman без перезапуска сервиса:

pgdoorman=> RELOAD;

Эта команда:

  1. Перечитывает файл конфигурации.
  2. Обновляет все изменяемые настройки.
  3. Применяет изменения параметров соединений для новых соединений.
  4. Сохраняет существующие соединения, пока они не вернутся в пул.

Изменение конфигурации без простоя

RELOAD применяет большинство параметров конфигурации без разрыва существующих соединений — то, что нужно в production, где простой недопустим.

PAUSE

Команда PAUSE [db] блокирует получение новых backend-соединений для указанной базы (или всех баз, если аргумент не задан). Активные транзакции продолжают работать -- блокируются только запросы на новые соединения.

-- Поставить на паузу все пулы
pgdoorman=> PAUSE;

-- Поставить на паузу только пулы конкретной базы
pgdoorman=> PAUSE mydb;

Клиенты, которые запросят новое backend-соединение во время паузы, будут ждать RESUME или истечения query_wait_timeout -- что наступит раньше. Если истечёт таймаут, клиент получит ошибку timeout.

Используйте SHOW POOLS для проверки состояния паузы -- колонка paused покажет 1 для приостановленных пулов.

Когда использовать PAUSE

PAUSE полезен во время операций обслуживания, когда нужно не пускать новые запросы на бэкенд:

  • Failover базы: PAUSE → переключить бэкенд → RECONNECT → RESUME.
  • Полная ротация соединений: PAUSE → RECONNECT → RESUME гарантирует, что все соединения будут пересозданы.
  • Обслуживание бэкенда: PAUSE на время изменений схемы, затем RESUME.

RESUME

Команда RESUME [db] снимает PAUSE и немедленно разблокирует всех ожидающих клиентов:

-- Снять паузу со всех пулов
pgdoorman=> RESUME;

-- Снять паузу только с пулов конкретной базы
pgdoorman=> RESUME mydb;

Клиенты, которые ждали из-за PAUSE, немедленно продолжат получать backend-соединения.

RECONNECT

Команда RECONNECT [db] форсирует пересоздание всех backend-соединений:

-- Reconnect для всех пулов
pgdoorman=> RECONNECT;

-- Reconnect только для пулов конкретной базы
pgdoorman=> RECONNECT mydb;

При выполнении:

  1. Внутренний epoch-счётчик пула увеличивается.
  2. Все idle-соединения сразу закрываются.
  3. Активные соединения (которые сейчас обслуживают транзакцию) продолжают работать, но утилизируются при возврате в пул -- они не будут переиспользованы.

То есть RECONNECT не прерывает активные транзакции. Новые соединения создаются по запросу с текущим epoch, поэтому они будут приняты recycle().

Шаблоны ротации соединений

Постепенная ротация (минимум воздействия): Один RECONNECT -- idle-соединения сбрасываются сразу, активные -- по завершении текущей транзакции. Новые соединения создаются по мере необходимости.

Полная ротация (гарантированно все новые соединения): PAUSE → RECONNECT → RESUME -- сначала пауза не даёт стартовать новым транзакциям, потом RECONNECT помечает всё на утилизацию. После RESUME все последующие запросы получают свежие соединения.

RECONNECT и min_pool_size

После RECONNECT пулы с настроенным min_pool_size будут автоматически дозаполнены до минимума на следующем retain-цикле. У новых соединений будет текущий epoch.

Граничные случаи и поведение

В таблице ниже описано поведение PAUSE, RESUME и RECONNECT в граничных случаях:

СценарийПоведение
PAUSE для уже приостановленного пулаNo-op (идемпотентно). Ошибка не возвращается.
RESUME для не приостановленного пулаNo-op (идемпотентно). Ошибка не возвращается.
RECONNECT для приостановленного пулаРаботает: idle-соединения дренируются, epoch инкрементируется. После RESUME новые соединения создаются с новым epoch.
PAUSE/RESUME/RECONNECT для несуществующей базыВозвращает ошибку: No pool for database "xxx". Без аргумента-базы команда применяется ко всем пулам (ошибки нет, даже если пулов нет).
query_wait_timeout во время PAUSEКлиенты, ожидающие соединение, ожидаемо получают ошибку timeout. Пул остаётся на паузе.
RELOAD во время PAUSERELOAD пересоздаёт пулы из конфигурации, поэтому состояние паузы теряется. Это ожидаемое поведение -- новая конфигурация означает новые пулы.
GC приостановленных динамических пуловПриостановленные динамические пулы защищены от garbage collection даже при 0 соединений.
Replenish во время PAUSEПулы с min_pool_size не дозаполняются, пока на паузе -- новые соединения не создаются. Дозаполнение возобновляется после RESUME.
Время жизни соединений во время PAUSERetain-задача продолжает закрывать просроченные соединения (idle timeout, server lifetime). Соединения по-прежнему стареют.
Несколько вызовов RECONNECTКаждый вызов увеличивает epoch ещё. Только соединения, созданные после последнего RECONNECT, считаются валидными.

Обработка сигналов

PgDoorman реагирует на стандартные Unix-сигналы для управления и контроля. Сигналы посылаются через kill (например, kill -HUP <pid>).

СигналЭффект
SIGHUPПеречитывание конфигурации -- эквивалент admin-команды RELOAD.
SIGUSR2Binary upgrade и graceful shutdown. Валидирует новый бинарник флагом -t, запускает новый процесс, затем завершается. Рекомендуется для обновлений. См. Binary upgrade.
SIGINTForeground + TTY (Ctrl+C): только graceful shutdown (без binary upgrade). Daemon / без TTY: binary upgrade и graceful shutdown (legacy-поведение).
SIGTERMНемедленное завершение. Активные соединения обрываются.

Управление процессом

В окружениях на базе systemd unit-файл по умолчанию использует ExecReload=/bin/kill -SIGUSR2 $MAINPID, чтобы запускать binary upgrade при systemctl reload.

Аутентификация

pg_doorman аутентифицирует клиентов прежде чем перенаправить их к PostgreSQL. Поддерживается шесть методов; они выбираются в порядке приоритета на основании того, что присылает клиент и что задано в конфигурации пула.

Эта страница объясняет, как pg_doorman выбирает метод аутентификации. Подробности настройки смотрите по ссылкам каждого метода ниже.

Обзор методов

МетодКогда использоватьХранит ли секрет в конфиге?
Passthrough (MD5 / SCRAM)По умолчанию. Имя пользователя пула совпадает с пользователем PostgreSQL.Хеш MD5 или SCRAM ClientKey, никогда — открытый пароль
auth_queryМного пользователей, динамическое заведение. Учётные данные ищутся в самом PostgreSQL.Только секрет одного сервисного пользователя
PAMАутентификация на уровне OS (LDAP через pam_ldap, Kerberos, локальные учётки). Только Linux.Нет
JWTДоступ сервиса к базе по короткоживущим токенам, подписанным внешним IdP.Только публичный ключ
TalosJWT с встроенным извлечением роли. Используется в Ozon.Только публичный ключ
pg_hba.confОграничение того, кто откуда может подключаться (сетевой ACL), независимо от метода учётных данных.Нет

LDAP, Kerberos GSSAPI, аутентификация по сертификатам и SCRAM channel binding (scram-sha-256-plus) не поддерживаются. Смотрите Сравнение.

Порядок выбора метода

pg_hba.conf оценивается первым, до любой проверки учётных данных. Правило reject обрывает соединение; правило trust полностью пропускает проверку учётных данных.

После HBA pg_doorman выбирает метод проверки учётных данных в таком порядке:

  1. Talos. Активируется, когда клиент подключается с именем пользователя talos. Пароль клиента разбирается как JWT, из него извлекается роль (owner / read_write / read_only), и соединение продолжается под этой производной идентичностью.
  2. HBA Trust. Если pg_hba.conf совпал с правилом trust, проверки учётных данных не происходит.
  3. PAM. Если у совпавшего пользователя задан auth_pam_service, учётные данные уходят в PAM (только Linux). PAM приоритетнее статического пароля.
  4. SCRAM static. Если password пользователя в конфиге начинается с SCRAM-SHA-256$, pg_doorman запускает SCRAM-аутентификацию.
  5. MD5 static. Если password пользователя начинается с md5, pg_doorman запускает MD5-аутентификацию.
  6. JWT. Если password пользователя начинается с jwt-pkey-fpath:, пароль клиента проверяется как JWT по публичному ключу с диска.

auth_query не входит в этот список выбора — он выполняется до диспетчеризации, чтобы наполнить список пользователей пула хешами, полученными из PostgreSQL. После того как auth_query вернёт значение passwd, выбор метода идёт по префиксу этого значения (SCRAM-SHA-256$ или md5).

Если ни один метод не подошёл к формату пароля, pg_doorman возвращает «Authentication method not supported» и закрывает соединение.

Аутентификация на стороне PostgreSQL: passthrough vs configured

pg_doorman аутентифицируется дважды: один раз как шлюз (клиент → pg_doorman) и один раз как бэкенд (pg_doorman → PostgreSQL). Возможны три варианта:

  • Passthrough (по умолчанию). Хеш MD5 клиента или SCRAM ClientKey переиспользуется для аутентификации на PostgreSQL. В конфиге нет открытого пароля. Требует, чтобы server_username не был задан (или был равен имени клиента).
  • Заданный пользователь бэкенда. Установите server_username и server_password в блоке пользователя. pg_doorman будет аутентифицироваться на PostgreSQL под ними. Используйте, когда имя пользователя пула отвязано от пользователя базы (Talos, JWT, переименование).
  • auth_query в выделенном режиме. Установите server_user внутри блока auth_query. Все динамически найденные пользователи разделят один пул бэкенда, аутентифицированный как server_user. Это размен идентичности бэкенда на пользователя ради эффективности переиспользования пула.

Подробности смотрите в Passthrough, а про выделенный режим — в auth_query.

Ограничение подключений

pg_hba.conf применяется до проверки учётных данных. Распространённые шаблоны:

  • Отвергнуть всё кроме localhost: host all all 0.0.0.0/0 reject, а перед ним host all all 127.0.0.1/32 trust.
  • Требовать TLS для нелокальных соединений: hostssl all all 0.0.0.0/0 scram-sha-256 и hostnossl all all 127.0.0.1/32 trust.
  • ACL по базам: host mydb appuser 10.0.0.0/8 scram-sha-256.

Смотрите pg_hba.conf.

Куда дальше

  • Новая инсталляция? Прочтите Passthrough и Basic usage.
  • Много пользователей с ротируемыми учётными данными? Используйте auth_query.
  • Идентичность сервиса по токенам? Используйте JWT.
  • Аутентификация, интегрированная с OS? Используйте PAM.
  • Ограничения на сетевом уровне? Настройте pg_hba.conf.

Passthrough-аутентификация (по умолчанию)

pg_doorman переиспользует криптографическое доказательство клиента — хеш MD5 или SCRAM ClientKey — чтобы аутентифицироваться на PostgreSQL. Открытый пароль никогда не покидает клиента и никогда не хранится в конфигурации пула.

Это рекомендуемая настройка, когда имя пользователя пула совпадает с пользователем PostgreSQL.

Как это работает

MD5

Протокол MD5-пароля PostgreSQL хранит на сервере md5(password + username). Клиент хеширует пароль тем же способом и присылает md5(stored_hash + salt). pg_doorman:

  1. Получает хешированный ответ клиента.
  2. Ищет сохранённый хеш MD5 в своём конфиге (или через auth_query).
  3. Проверяет, что ответ клиента совпадает.
  4. Передаёт сохранённый хеш в PostgreSQL как пароль во время аутентификации бэкенда. PostgreSQL принимает его, потому что именно этот хеш и хранится в pg_authid.

Поле password в конфиге пула содержит сохранённый хеш в формате md5XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (32-символьный MD5 от password + username с буквальным префиксом md5).

SCRAM-SHA-256

SCRAM проверяет клиента, не пересылая ничего эквивалентного паролю. pg_doorman:

  1. Выполняет SCRAM handshake с клиентом, проверяя ClientProof.
  2. Извлекает ClientKey из успешного обмена.
  3. Выполняет SCRAM handshake с PostgreSQL, переиспользуя тот же ClientKey для вычисления свежего ClientProof под nonce бэкенда.

Поле password в конфиге пула содержит SCRAM-верификатор из pg_authid.rolpassword в формате SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey>.

pg_doorman не поддерживает SCRAM channel binding (scram-sha-256-plus).

Конфигурация

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "app"
        password: "md5d41d8cd98f00b204e9800998ecf8427e"
        pool_size: 40

Обратите внимание, чего здесь нет: ни server_username, ни server_password. pg_doorman распознаёт passthrough-режим по отсутствию этих полей.

Для SCRAM поле password выглядит так:

password: "SCRAM-SHA-256$4096:random_salt$stored_key:server_key"

Получение хеша

Подключитесь как суперпользователь к PostgreSQL и прочитайте pg_shadow (или pg_authid):

SELECT usename, passwd FROM pg_shadow WHERE usename = 'app';

Колонка passwd содержит либо хеш MD5 (md5...), либо SCRAM-верификатор (SCRAM-SHA-256$...) — в зависимости от значения password_encryption в момент установки пароля.

Чтобы принудительно сохранить MD5: SET password_encryption = 'md5'; ALTER ROLE app PASSWORD 'plaintext'; Чтобы принудительно SCRAM: SET password_encryption = 'scram-sha-256'; ALTER ROLE app PASSWORD 'plaintext';

Когда passthrough-режима недостаточно

Задавайте server_username и server_password явно, когда:

  • Пользователь пула отличается от пользователя бэкенда (переименование).
  • Клиент аутентифицируется через JWT — у него нет ни хеша MD5, ни ключа SCRAM, чтобы пробросить.
  • Клиент аутентифицируется через Talos, и вы хотите фиксированную идентичность бэкенда на роль.
  • Вы используете auth_query в выделенном режиме.
users:
  - username: "external_app"
    password: "jwt-pkey-fpath:/etc/pg_doorman/jwt.pub"
    server_username: "app"
    server_password: "md5..."
    pool_size: 40

Автоматически сгенерированный конфиг

pg_doorman generate --host your-pg-host --user your-admin-user интроспектирует PostgreSQL и собирает конфиг с автоматически подставленными хешами из pg_shadow. Используйте это для новых инсталляций, чтобы избежать ошибок копирования.

pg_doorman generate --host db.example.com --user postgres --output pg_doorman.yaml

Подробнее про команду generate смотрите в Basic Usage.

auth_query

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

Два режима

pg_doorman поддерживает два режима; оба настраиваются в одном блоке auth_query. Выбор зависит от того, задан ли server_user:

  • Режим passthrough (без server_user): каждый аутентифицированный пользователь получает собственный пул бэкенда, аутентифицированный под ним же. Сохраняет идентичность бэкенда на пользователя для current_user, row-level security и audit logs.
  • Выделенный режимserver_user): все динамические пользователи разделяют один пул бэкенда, аутентифицированный как server_user. Это размен идентичности бэкенда на более высокое переиспользование пула и меньшее число соединений.

auth_query в стиле PgBouncer — это выделенный режим. Odyssey поддерживает оба. В pg_doorman режим passthrough — по умолчанию.

Режим passthrough

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    auth_query:
      query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
      user: "postgres"
      password: "md5..."
      database: "postgres"
      cache_ttl: "1h"
      cache_failure_ttl: "30s"

Запрос должен возвращать колонку с именем passwd или password, содержащую хеш MD5 или SCRAM. Дополнительные колонки игнорируются.

user и password — это учётные данные, под которыми pg_doorman выполняет lookup-запрос. У них должно быть право читать колонку с учётными данными. Либо выдайте доступ к специально созданному представлению (рекомендуется), либо используйте пользователя из группы pg_read_server_files.

Когда клиент подключается как alice:

  1. pg_doorman выполняет запрос с $1 = 'alice' и получает её хеш.
  2. Кэширует хеш в памяти на cache_ttl секунд.
  3. Выполняет passthrough-аутентификацию MD5 или SCRAM (смотрите Passthrough).
  4. Открывает соединение с бэкендом, аутентифицированное как alice с тем же хешем.

Выделенный режим (dedicated)

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    auth_query:
      query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
      user: "auth_lookup"
      password: "md5..."
      database: "postgres"
      server_user: "app"
      server_password: "md5..."
      pool_size: 40
      min_pool_size: 5
      cache_ttl: "1h"

Установка server_user переключает режим. Теперь:

  1. Клиент аутентифицируется как alice против хеша, возвращённого запросом.
  2. Пул бэкенда аутентифицирован как app (значение server_user) и общий для всех динамических пользователей.
  3. current_user в PostgreSQL всегда будет app, независимо от того, какой клиент подключился.

Используйте этот режим, когда у вас много пользователей (тысячи) и пулы бэкенда на каждого исчерпали бы слоты соединений PostgreSQL.

Рекомендуемая настройка PostgreSQL

Не используйте суперпользователя для lookup. Создайте отдельную функцию с SECURITY DEFINER:

CREATE OR REPLACE FUNCTION pg_doorman_lookup(uname text)
RETURNS TABLE(passwd text)
LANGUAGE sql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
  SELECT passwd FROM pg_shadow WHERE usename = uname;
$$;

REVOKE ALL ON FUNCTION pg_doorman_lookup(text) FROM public;
GRANT EXECUTE ON FUNCTION pg_doorman_lookup(text) TO auth_lookup;

Затем в конфиге пула:

auth_query:
  query: "SELECT passwd FROM pg_doorman_lookup($1)"
  user: "auth_lookup"
  password: "md5..."

Кэширование

ПараметрПо умолчаниюНазначение
cache_ttl"1h"Сколько кэшируется успешный lookup.
cache_failure_ttl"30s"Сколько кэшируется неуспешный lookup. Защищает от усиления brute-force атаки.
min_interval"1s"Минимальный интервал между повторными lookup-запросами для одного пользователя.

Длительности — строки в кавычках: "1h", "30m", "300s". Голое целое число интерпретируется как миллисекунды — cache_ttl: 3600 будет кэшировать на 3.6 секунды, не на час.

Кэш — пер-пуловый, в памяти, сбрасывается при RELOAD. После ротации пароля пользователя сделайте перезапуск или RELOAD.

Observability

SHOW AUTH_QUERY показывает статистику по базам:

database | cache_entries | cache_hits | cache_misses | cache_refetches | rate_limited | auth_success | auth_failure | executor_queries | executor_errors

Метрики Prometheus: pg_doorman_auth_query_cache, pg_doorman_auth_query_auth, pg_doorman_auth_query_executor, pg_doorman_auth_query_dynamic_pools. Смотрите Admin commands.

Аутентификация PAM

pg_doorman делегирует аутентификацию клиента сервису PAM на хосте. Используйте это для аутентификации, интегрированной с OS (LDAP через pam_ldap, Kerberos, локальные модули PAM), без хранения учётных данных на каждого пользователя в конфиге пула.

PAM работает только под Linux. Готовые бинарники собираются с поддержкой PAM.

Конфигурация

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "alice"
        auth_pam_service: "pg_doorman"
        server_username: "alice"
        server_password: "md5..."
        pool_size: 20

auth_pam_service — имя файла сервиса PAM в /etc/pam.d/. pg_doorman не проверяет имя сервиса при старте — убедитесь, что файл существует.

Поле password опускается, потому что проверкой занимается PAM. server_username и server_password обязательны: PAM аутентифицирует только клиента в pg_doorman; pg_doorman всё равно нужны учётные данные для соединения с бэкендом.

Пример сервиса PAM

/etc/pam.d/pg_doorman:

auth     required pam_unix.so
account  required pam_unix.so

Для аутентификации через LDAP:

auth     required pam_ldap.so
account  required pam_ldap.so

Настройте pam_ldap в /etc/ldap.conf (или /etc/nslcd.conf) под своё окружение.

Порядок выбора метода

PAM проверяется после Talos и HBA Trust, но до любого метода на основе пароля. Если у пользователя одновременно заданы auth_pam_service и статический password (с префиксом MD5, SCRAM или JWT), выигрывает PAM.

Смотрите Обзор.

Оговорки

  • PAM блокирует поток-обработчик во время вызова аутентификации. Если ваш стек PAM делает сетевые вызовы (LDAP, Kerberos), ждите эпизодических всплесков задержки.
  • pam_unix.so требует доступ на чтение к /etc/shadow — обычно только для root. Запускайте pg_doorman под пользователем с нужным членством в группе или используйте другой модуль PAM.
  • PAM не поддерживает passthrough SCRAM. Соединение с бэкендом всегда использует server_username и server_password.
  • Для LDAP без машинерии PAM в pg_doorman нет нативной поддержки LDAP. Используйте Odyssey или PgBouncer 1.25+.

Аутентификация JWT

Аутентифицируйте клиентов JSON Web Token, подписанным внешним поставщиком идентификации. pg_doorman проверяет подпись токена RSA-SHA256 по публичному ключу с диска, сверяет claim preferred_username и пробрасывает соединение в PostgreSQL под заданной идентичностью бэкенда.

Этот метод подходит для доступа сервиса к базе, когда короткоживущие токены выпускает OIDC-провайдер, Vault или внутренний токен-сервис.

Конфигурация

Сгенерируйте (или получите) публичный RSA-ключ и сошлитесь на него в поле password пользователя через префикс jwt-pkey-fpath::

pools:
  mydb:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "billing-service"
        password: "jwt-pkey-fpath:/etc/pg_doorman/jwt-public.pem"
        server_username: "billing"
        server_password: "md5..."
        pool_size: 40

То, что клиент пришлёт как пароль, будет считаться JWT и проверяться по /etc/pg_doorman/jwt-public.pem. Токен должен:

  • Быть подписан RS256 (RSA-SHA256). HS256 и EC-варианты не поддерживаются.
  • Иметь claim preferred_username, равный заданному username (billing-service в примере выше).
  • Проходить стандартную валидацию exp и nbf.

Соединение с бэкендом открывается как billing с хешем из server_password. Идентичность клиента (billing-service) отвязана от идентичности базы (billing).

Генерация пары ключей

openssl genrsa -out jwt-private.pem 2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem

Храните jwt-private.pem у издателя токенов. Раздавайте jwt-public.pem в pg_doorman.

Выпуск токена

Подойдёт любая RS256 JWT-библиотека. Пример на Python (PyJWT):

import jwt
import time

private_key = open("jwt-private.pem").read()

token = jwt.encode(
    {
        "preferred_username": "billing-service",
        "iat": int(time.time()),
        "exp": int(time.time()) + 300,  # 5 минут
    },
    private_key,
    algorithm="RS256",
)

Клиент подключается к pg_doorman с user=billing-service и password=<token>. Большинство драйверов PostgreSQL принимают любую строку в поле пароля.

Ротация токенов

pg_doorman читает файл публичного ключа один раз при старте и по SIGHUP. Чтобы ротировать ключ:

  1. Добавьте новый публичный ключ во вторую запись пользователя с параллельным именем.
  2. Сделайте reload (kill -HUP).
  3. Переключите издателя на новый ключ.
  4. Удалите старую запись пользователя после grace-периода.

Или, проще, замените файл на месте и пошлите SIGHUP. Поддержки нескольких ключей на одного пользователя нет.

Порядок выбора метода

JWT — самый низкоприоритетный формат пароля: pg_doorman сначала проверяет префиксы SCRAM-SHA-256$ и md5, затем jwt-pkey-fpath:. На практике это важно только если вы используете пароль-заглушку — задайте auth_pam_service для PAM или используйте префикс jwt-pkey-fpath: исключительно для JWT-пользователей.

Если у одного и того же пользователя заданы и auth_pam_service, и пароль jwt-pkey-fpath:, выигрывает PAM.

Смотрите Обзор.

Оговорки

  • Claim preferred_username должен совпадать в точности. Сопоставлений или алиасов claim'ов нет.
  • Поддержки JWKS-эндпоинта нет: публичный ключ должен быть на диске.
  • Проверки издателя (iss) или аудитории (aud) нет. Если нужны — терминируйте JWT в sidecar и переводите в passthrough-аутентификацию.
  • Если идентичность клиента должна нести информацию о роли в базе (например, read_only против read_write), смотрите Talos.

Аутентификация Talos

Talos — схема аутентификации на основе JWT, разработанная в Ozon. Токен несёт в claim resource_access назначение роли на каждую базу, а pg_doorman извлекает наивысшую роль, чтобы выбрать идентичность бэкенда. Несколько ключей подписи поддерживаются через заголовок kid.

Если вы работаете внутри стека идентификации Ozon Talos — это нужная вам интеграция. Снаружи предпочитайте обычный JWT.

Как это работает

  1. Клиент подключается с именем пользователя talos и JWT в качестве пароля.
  2. pg_doorman читает поле kid из заголовка JWT и ищет соответствующий публичный ключ в general.talos.keys.
  3. Токен проверяется (RS256, exp, nbf).
  4. pg_doorman обходит ключи resource_access, разбивает каждый по : и сверяет часть после двоеточия с general.talos.databases. То есть ключ вида "postgres.stg:billing" совпадает с базой billing. Роли из всех совпавших записей собираются вместе; побеждает наивысшая (owner > read_write > read_only).
  5. Соединение аутентифицируется против пользователя пула, имя которого совпадает с ролью: owner, read_write или read_only. Этот пользователь должен существовать в пуле с заданными server_username и server_password.

Идентичность клиента (clientId из токена) сохраняется в application_name и audit logs.

Конфигурация

general:
  host: "0.0.0.0"
  port: 6432
  talos:
    keys:
      - "/etc/pg_doorman/talos/keys/abc123.pem"
      - "/etc/pg_doorman/talos/keys/def456.pem"
    databases:
      - "billing"
      - "inventory"

pools:
  billing:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "owner"
        server_username: "billing_owner"
        server_password: "md5..."
        pool_size: 20
      - username: "read_write"
        server_username: "billing_app"
        server_password: "md5..."
        pool_size: 40
      - username: "read_only"
        server_username: "billing_ro"
        server_password: "md5..."
        pool_size: 60

Имя файла каждого ключа без расширения (abc123, def456) — это kid, который сверяется с заголовком JWT.

databases — фильтр: только перечисленные базы допускаются для Talos. Токен без записи для запрошенной базы будет отвергнут.

Структура токена

{
  "kid": "abc123",
  "alg": "RS256"
}
.
{
  "exp": 1714500000,
  "nbf": 1714400000,
  "clientId": "billing-service",
  "resource_access": {
    "postgres.stg:billing": { "roles": ["read_write"] },
    "postgres.stg:inventory": { "roles": ["read_only", "read_write"] }
  }
}

Ключи resource_access обязаны содержать двоеточие. pg_doorman игнорирует всё до него и сверяет суффикс с general.talos.databases. Токен, собранный без префикса с двоеточием, не даст ни одной роли, и аутентификация провалится с сообщением «Token may not contain valid roles for the requested databases».

Клиент, подключающийся к inventory с этим токеном, попадает в пользователя read_write (максимум из двух перечисленных ролей).

Порядок выбора метода

У Talos наивысший приоритет. Если клиент подключается с именем пользователя talos и general.talos.keys непуст, никакой другой метод аутентификации не пробуется.

Смотрите Обзор.

Оговорки

  • Talos требует специального имени пользователя talos. Не-Talos клиенты используют другие методы аутентификации в обычном порядке.
  • Сопоставление роли с пользователем фиксированное: owner, read_write, read_only. Кастомные имена ролей требуют изменений в коде.
  • Несколько ролей в одной записи resource_access свёртываются в максимум. Семантики «deny» нет.
  • Публичные ключи загружаются один раз при старте и перечитываются по SIGHUP.

pg_hba.conf

Ограничивайте, кто может подключаться к pg_doorman, по адресу источника, базе, пользователю и типу соединения. Используется тот же формат правил, что и в pg_hba.conf PostgreSQL.

Это слой контроля доступа на сетевом уровне, который работает до проверки учётных данных. Соединение, отвергнутое pg_hba, никогда не доходит до проверки пароля.

Конфигурация

Три формата. Выбирайте тот, что подходит вашей инсталляции.

Inline-строка

general:
  hba: |
    hostssl all all 0.0.0.0/0 scram-sha-256
    host    all all 127.0.0.1/32 trust
    local   all all              trust
    host    all all 0.0.0.0/0    reject

Из файла

general:
  hba:
    path: "/etc/pg_doorman/pg_hba.conf"

Файл читается при старте и по SIGHUP.

Inline-содержимое под структурным ключом

general:
  hba:
    content: |
      hostssl all all 0.0.0.0/0 scram-sha-256
      host    all all 127.0.0.1/32 trust

То же, что и inline-строка; полезно, когда конфиг генерируется шаблонизатором.

Формат правил

Каждая строка:

<connection_type> <database> <user> [<source_cidr>] <method>

connection_type — один из:

ТипСовпадает с
hostTCP, с TLS или без
hostsslTCP только с активным TLS
hostnosslTCP только когда TLS не активен
localЛокальный Unix-сокет

databaseall, конкретное имя базы или список через запятую. replication не обрабатывается (pg_doorman не поддерживает проброс репликации).

userall, конкретный пользователь или список через запятую. Префикс +groupname (членство в роли PostgreSQL) не поддерживается.

source_cidr — IPv4- или IPv6-CIDR. Обязателен для host, hostssl, hostnossl. Неприменим к local.

method — один из:

МетодПоведение
trustПолностью пропустить проверку учётных данных. Клиент допускается под тем именем, которое заявил.
md5Принудительно требовать аутентификацию по паролю MD5.
scram-sha-256Принудительно требовать аутентификацию SCRAM-SHA-256.
rejectОтказать в соединении до любой проверки учётных данных.

Правила оцениваются сверху вниз. Побеждает первое совпавшее.

Примеры

Требовать TLS из сети, разрешить открытое локально

hostssl   all all 10.0.0.0/8     scram-sha-256
hostnossl all all 10.0.0.0/8     reject
host      all all 127.0.0.1/32   trust
local     all all                trust

ACL по базам

host billing  app_billing  10.0.0.0/8 scram-sha-256
host billing  all          0.0.0.0/0  reject
host inventory app_inv     10.0.0.0/8 scram-sha-256
host all       admin       10.1.1.0/24 scram-sha-256
host all       all         0.0.0.0/0  reject

Заблокировать legacy MD5 из открытого интернета

hostssl all all 0.0.0.0/0 scram-sha-256
host    all all 0.0.0.0/0 reject

Если в базе хранятся только хеши MD5, а клиент запрашивает SCRAM, аутентификация провалится с понятной ошибкой. Перед тем как ужесточать правила, переведите базу на SCRAM-SHA-256 (ALTER ROLE ... PASSWORD).

Отличия от pg_hba.conf PostgreSQL

  • Нет ключевого слова replication (pg_doorman не пробрасывает соединения репликации).
  • Нет методов peer, ident, cert, gss, sspi, pam. PAM настраивается на пользователя через auth_pam_service, не через HBA.
  • Нет префикса +groupname для пользователя.
  • Нет регулярных выражений (синтаксис /regex).
  • IPv6-CIDR поддерживается. IPv4-mapped IPv6 (::ffff:1.2.3.4) сверяется с правилами IPv4.

Перезагрузка

kill -HUP $(pidof pg_doorman)

Существующие соединения заново не оцениваются. Новые соединения используют новые правила.

Оговорки

  • Правила применяются к клиентам, подключающимся к pg_doorman, а не к PostgreSQL. Собственный pg_hba.conf PostgreSQL по-прежнему важен для соединения с бэкендом.
  • trust допускает клиента без какой-либо проверки учётных данных. Бэкенд всё равно должен аутентифицироваться как пользователь пула — но клиентская сторона не проверена. Используйте trust только в сетях, где адресу источника можно доверять (loopback, ограниченный Unix-сокет).
  • Поддержки LDAP, Kerberos и peer-аутентификации нет — смотрите Сравнение.

TLS

pg_doorman терминирует TLS на клиентской стороне (клиенты → pg_doorman) и инициирует TLS на серверной стороне (pg_doorman → PostgreSQL). Стороны настраиваются независимо.

Клиентский TLS

Шифрование соединений между клиентскими приложениями и pg_doorman.

Режимы

РежимПоведение
disableНе анонсировать TLS. Клиенты, отправляющие SSLRequest, получают 'N' (отказ).
allowАнонсировать TLS, но принимать и обычный TCP.
requireТребовать TLS. Обычные соединения разрываются после неудачного SSLRequest.
verify-fullТребовать TLS и валидный клиентский сертификат. Используется для mTLS.

verify-full — это mTLS: сервер проверяет сертификат клиента. Подготовьте набор клиентских CA через tls_ca_cert.

Конфигурация

general:
  tls_mode: "require"
  tls_certificate: "/etc/pg_doorman/tls/server.crt"
  tls_private_key: "/etc/pg_doorman/tls/server.key"
  tls_ca_cert: "/etc/pg_doorman/tls/client_ca.pem"   # только для verify-full
  tls_rate_limit_per_second: 100                       # необязательное ограничение скорости handshake

Для разработки сертификат может быть самоподписанным; в продакшене обычно используют Let's Encrypt или внутренний CA.

Перезагрузка (клиентская сторона)

Клиентские сертификаты загружаются при старте. Их смена требует рестарта процесса. Для клиентского TLS перезагрузки по SIGHUP нет.

Для ротации сертификатов без простоя смотрите Binary Upgrade.

Политика шифров

Минимальный TLS 1.2. Список шифров — Mozilla «intermediate»; настройке не подлежит. Direct TLS handshake (PG17, без SSLRequest) не поддерживается.

Для управления шифрами TLS 1.3 или direct TLS из PG17 используйте PgBouncer 1.25+.

Серверный TLS

Шифрование соединений между pg_doorman и бэкендами PostgreSQL. Появилось в 3.6.0.

Режимы

РежимПоведение
disableОбычный TCP.
allow (по умолчанию)Сначала пробовать обычный TCP; если сервер отказывает, повторить попытку на новом сокете с TLS. Соответствует libpq sslmode=allow.
preferОтправить SSLRequest; если сервер отвечает 'N', откатиться к обычному TCP.
requireТребовать TLS. Падать, если сервер его не поддерживает.
verify-caТребовать TLS и проверять серверный сертификат против настроенного CA.
verify-fullТребовать TLS, проверять CA и проверять hostname сервера против сертификата.

allow — режим по умолчанию ради обратной совместимости: существующие развёртывания, где у PostgreSQL настроен TLS, автоматически переходят на TLS без изменения конфигурации. Для новых развёртываний, которым нужны явные гарантии, выбирайте require или verify-full.

Конфигурация

general:
  server_tls_mode: "verify-full"
  server_tls_ca_cert: "/etc/pg_doorman/tls/pg_ca_bundle.pem"

# Необязательно: клиентский сертификат для mTLS к PostgreSQL
  server_tls_certificate: "/etc/pg_doorman/tls/pg_client.crt"
  server_tls_private_key: "/etc/pg_doorman/tls/pg_client.key"

server_tls_ca_cert принимает PEM-bundle (несколько CA-сертификатов, склеенных подряд). Загружаются все.

Горячая перезагрузка

По SIGHUP серверные сертификаты перечитываются с диска. Существующие соединения продолжают пользоваться исходным TLS-контекстом; новые соединения используют перезагруженные сертификаты. Перезагрузка lock-free через Arc<ArcSwap<...>> — без обрыва соединений, без задержек на handshake.

kill -HUP $(pidof pg_doorman)

Это единственный путь перезагрузки TLS. Клиентские сертификаты по SIGHUP не перезагружаются.

mTLS к PostgreSQL

Задайте server_tls_certificate и server_tls_private_key. PostgreSQL должен быть настроен с ssl_ca_file, соответствующим подписанту клиентского сертификата, а у роли в pg_hba.conf со стороны PostgreSQL должно быть clientcert=verify-ca (или verify-full).

Observability

Серверный TLS покрывают три серии Prometheus:

МетрикаТипНазначение
pg_doorman_server_tls_connectionsgauge на пулЧисло активных TLS-соединений к PostgreSQL.
pg_doorman_server_tls_handshake_duration_secondshistogram на пулБакеты продолжительности handshake.
pg_doorman_server_tls_handshake_errors_totalcounter на пулНеудавшиеся handshake. Алерт при ненулевой скорости.

Смотрите Справочник Prometheus.

Известные ограничения

  • Протокол COPY поверх серверного TLS не покрыт BDD-тестами. Поведение должно работать, но не верифицировано.
  • Cancel-запросы к бэкенду минуют серверный TLS — они идут по свежему обычному TCP-соединению. Это совпадает с дизайном протокола PostgreSQL (cancel отправляется по отдельному сокету).
  • Direct TLS handshake (быстрый handshake PG17 без SSLRequest) не поддерживается ни на одной из сторон.

Куда дальше

  • Настройка нового кластера? Смотрите Установку.
  • Ротация сертификатов? Смотрите Binary Upgrade и Сигналы.
  • Hardening существующего развёртывания? Сочетайте с pg_hba.conf: принудительный hostssl для нелокальных соединений.

Режимы пула

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

Режима statement нет. Покул на уровне отдельного оператора ломает больше драйверов, чем помогает, а pg_doorman агрессивно оптимизирован под транзакционный режим (кеш prepared statements, direct handoff, FIFO-планирование).

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

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 — но только внутри транзакции; кросс-транзакционные уведомления теряются (так же, как в PgBouncer).

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

  • 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 выполняет RESET ALL и ROLLBACK (если cleanup_server_connections: true, как по умолчанию). Это сбрасывает:

  • значения SET уровня сессии;
  • курсоры;
  • имена prepared statements, которые драйвер привязал к конкретным backend-именам (кеш prepared statements pg_doorman переживает — он индексируется текстом запроса, а не именем statement на бэкенде);
  • advisory-блокировки (pg_advisory_unlock_all неявно входит в RESET ALL).

DEALLOCATE ALL и DISCARD ALL со стороны клиента также инициируют сброс всего, что закешировано pg_doorman для этого клиента в кеше prepared statements. Кеш уровня пула не затрагивается.

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

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

Делайте так только если вы уверены, что приложение никогда не оставляет состояние сессии.

Справочник

Pool Coordinator

Pool Coordinator ограничивает суммарное число backend-соединений к одной базе по всем пользователям пула, а при достижении лимита применяет приоритетное вытеснение. Это то, чем должен был стать max_db_connections в PgBouncer: справедливое применение лимита, резерв на короткие всплески и минимум на пользователя, защищающий критичные нагрузки.

Эта страница объясняет концепцию и сценарии применения. Рецепты тюнинга и разбор вывода SHOW POOL_COORDINATOR смотрите в Пул под нагрузкой.

Какую задачу решает

Без координатора каждый user-пул независим. pool_size равный 40 у пяти пользователей означает до 200 backend-соединений — и PostgreSQL приходится бороться за свои собственные лимиты.

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

Pool Coordinator в pg_doorman ограничивает сумму и:

  • Вытесняет idle-соединения у пользователей с переизбытком, когда другому пользователю нужно вырасти.
  • Ранжирует пользователей по p95 времени транзакции, чтобы медленные пулы отдавали слоты первыми. Пользователи с быстрыми запросами сохраняют преимущество переиспользования; пользователи с длинными транзакциями отдают первыми, потому что их соединения и так дольше простаивали.
  • Резервирует небольшое переполнение для коротких всплесков. Настраивается отдельно от основного лимита.
  • Гарантирует минимум на пользователя, который никогда не вытесняется. Критичные нагрузки сохраняют опору при конкуренции.

Когда использовать

Включайте координатор, когда:

  • На одной базе сосуществуют разные нагрузки, и нужен верхний предел числа backend-соединений (max_connections PostgreSQL, RAM, файловые дескрипторы).
  • Одна нагрузка имеет всплеск нагрузки, и вы хотите, чтобы она забирала простаивающие слоты у других, не вытесняя их навсегда.
  • Вы работаете рядом с потолком соединений PostgreSQL и нуждаетесь в справедливой деградации, а не в обслуживании по принципу «кто первый встал, того и тапки».

Координатор не нужен, когда:

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

Конфигурация

pools:
  shared_db:
    server_host: "127.0.0.1"
    server_port: 5432
    pool_mode: "transaction"

    # Общий лимит на всех пользователей этого пула.
    max_db_connections: 80

    # Резерв сверх max_db_connections для коротких всплесков.
    # Захватывается, только если в течение reserve_pool_timeout не нашлось idle-соединения.
    reserve_pool_size: 16
    reserve_pool_timeout: "3s"

    # Страховка для пользователя: соединения никогда не вытесняются у него, даже под давлением.
    # Сумма по пользователям должна быть <= max_db_connections.
    min_guaranteed_pool_size: 5

    # Льготный период для вытеснения: соединения младше этого возраста не вытесняются.
    # Защищает от колебания, когда нагрузка кратковременно простаивает.
    min_connection_lifetime: "30s"

    users:
      - username: "fast_app"
        password: "md5..."
        pool_size: 40

      - username: "batch_job"
        password: "md5..."
        pool_size: 60

Эффективный потолок: max_db_connections + reserve_pool_size = 96. Резерв поглощает доли секундные пики; если пик затягивается, включается вытеснение.

Как выбирается донор

Когда пользователь запрашивает новый бэкенд, а лимит уже достигнут:

  1. Найти кандидатов с idle-соединениями. Пользователь, у которого все соединения активны, не может стать донором — его работа в полёте.
  2. Пропустить защищённых. Пользователь ниже min_guaranteed_pool_size исключается.
  3. Пропустить недавно созданные соединения. Соединения младше min_connection_lifetime не вытесняются (это снижает колебание при коротких idle-промежутках).
  4. Ранжировать по излишку. Пользователи с наибольшим числом idle-соединений сверх min_guaranteed_pool_size получают высший ранг.
  5. Tiebreaker — p95 времени транзакции. Среди пулов с одинаковым излишком первым отдаёт медленный. Их соединения, скорее всего, простаивали потому, что следующий запрос ещё готовится на стороне приложения.

Выбранное idle-соединение закрывается; запрашивающий пользователь получает свежее соединение из PostgreSQL.

Observability

SHOW POOL_COORDINATOR показывает текущее состояние по каждой базе:

database    | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
shared_db   | 80          | 78      | 16           | 2            | 142       | 18          | 0
  • evictions быстро растёт — один пользователь голодает раз за разом. Поднимите max_db_connections или задайте этому пользователю min_guaranteed_pool_size.
  • reserve_acq высокий — всплески нормальны, но размер занижен; рассмотрите повышение max_db_connections вместо опоры на резерв.
  • exhaustions ненулевой — резерв тоже был полным. Клиенты упёрлись в query_wait_timeout, ожидая бэкенд. Поднимите лимит.

Prometheus: pg_doorman_pool_coordinator{type="..."} (gauge) и pg_doorman_pool_coordinator_total{type="evictions|reserve_acquisitions|exhaustions"} (counter). Смотрите Команды администратора и Справочник Prometheus.

Оговорки

  • Координатор работает только внутри одного пула (одной базы). Кросс-пуловые / кросс-баз ограничения не поддерживаются.
  • Вытеснение выбирает idle-соединения; пользователь, удерживающий все соединения в длинных транзакциях, не может стать донором, и другие пользователи могут голодать. Если ваша форма нагрузки именно такая — поднимите max_db_connections или разделите нагрузки.
  • min_guaranteed_pool_size — это нижняя граница для вытеснения, а не min_pool_size для прогрева. Эти соединения пул всё равно создаёт по требованию.
  • Задавать max_db_connections без min_guaranteed_pool_size — это режим PgBouncer: работает, но мелкие пользователи голодают под давлением. Для общих баз всегда задавайте оба.

Куда дальше

Давление на пул

Pool pressure (давление на пул) описывает, как pg_doorman ведёт себя, когда множество клиентов одновременно запрашивают backend-соединение, а в idle-пуле (очереди свободных, готовых к выдаче серверных соединений; «idle» означает «открыто, но никем не используется прямо сейчас») пусто. Решение о том, кто получит соединение, кто подождёт, кто инициирует свежий connect() к PostgreSQL, а кому будет отказано, принимают два механизма. Локально в каждом пуле (database, user) работают упреждающее ожидание (anticipation) — ожидание возврата соединения от соседа прежде чем тратить ресурсы на новый connect() — и ограничитель всплесков (bounded burst gate) — жёсткий лимит на одновременные backend-connect() внутри одного пула. Поверх них при необходимости подключается координатор (coordinator), общий для всех пулов ограничитель суммарного числа backend-соединений к одной базе данных.

Аудитория: DBA или инженер эксплуатации, который уже знает PgBouncer и хочет понять, чем pg_doorman отличается и за чем нужно следить.

Зачем нужно давление на пул

Возьмём пул с pool_size = 40 и нагрузкой в 200 коротких транзакций, приходящих в одну и ту же миллисекунду. В пуле 4 idle-соединения. В наивном пулере первые 4 клиента забирают idle-соединения, а оставшиеся 196 независимо вызывают connect() к PostgreSQL. PostgreSQL получает 196 одновременных TCP connect-попыток, на каждую из которых нужно выполнить SCRAM-аутентификацию и согласование параметров, только чтобы обнаружить, что пул разрешает ещё 36 соединений. Backend-обращения к pg_authid взлетают всплеском, потолок max_connections пробивается, очередь accept() ядра насыщается, а tail latency (задержка в хвосте распределения, p99/p99.9, то есть редкие, но самые медленные запросы) уже подключённых клиентов растёт, потому что postmaster PostgreSQL занят порождением backend'ов вместо выполнения запросов. Это thundering herd (лавинообразный эффект): ситуация, когда множество независимых задач одновременно реагируют на одно состояние и устраивают шторм одинаковых запросов к общему ресурсу. В нашем сценарии это 196 одновременных connect() к одному PostgreSQL listener'у вместо размазанного по времени потока.

Time:  ----------------------------------------->

Client_1   -[idle hit]--[query]-----[done]
Client_2   -[idle hit]--[query]-----[done]
Client_3   -[idle hit]--[query]-----[done]
Client_4   -[idle hit]--[query]-----[done]
Client_5   -[connect]-[auth]-[query]-[done]
Client_6   -[connect]-[auth]-[query]-[done]
   .             ^
   .             196 backend connect()s
   .             fired in the same instant
Client_200 -[connect]-[auth]-[query]-[done]

PostgreSQL: 196 spawning backends + 4 running queries

Давление на пул подавляет это поведение. pg_doorman заставляет большинство из этих 196 вызовов переиспользовать соединение, которое другой клиент вот-вот вернёт, либо подождать несколько миллисекунд за небольшим числом уже идущих backend-connect-ов. Частота connect() к PostgreSQL остаётся ограниченной даже при всплесках клиентов.

Режим простого пула (plain pool mode)

Этот режим работает, когда max_db_connections не задан. Пулы независимы, общей координации между ними нет, давление управляется внутри каждого пула (database, user). Это режим по умолчанию, и большинство инсталляций находятся именно в нём.

Прогрев пула с холодного старта

Пул с pool_size = 40 и min_pool_size = 0 стартует с нулём соединений. Первый пришедший клиент не ждёт: pg_doorman сразу создаёт backend-соединение. Второй делает то же самое, третий тоже, пока пул не достигнет порога прогрева (warm threshold).

Порог прогрева равен pool_size × scaling_warm_pool_ratio / 100. При дефолтном значении 20% и pool_size = 40 порог равен 8 соединениям. Ниже этого порога pg_doorman создаёт соединения без раздумий: пул холодный, цена ожидания выше цены коннекта, и клиенты не могут конкурировать за idle-соединения, которых не существует.

Выше порога активируется зона упреждающего ожидания (anticipation zone): pg_doorman считает, что пул уже разогрет и кто-то из параллельных клиентов скоро вернёт занятое соединение, поэтому имеет смысл подождать его возврата вместо того, чтобы тратить ресурсы на новый connect(). Когда клиент не находит соединения в idle pool, pg_doorman сначала пытается перехватить такой возврат.

Третья зона накладывается поверх обеих: при любом размере пула, если число соединений, создаваемых прямо сейчас, достигает scaling_max_parallel_creates (по умолчанию 2), пул упирается в лимит ограничителя всплесков. Дополнительные вызовы ждут свободный слот независимо от того, сколько idle-соединений существует.

                        Three pressure zones
                        --------------------

Pool size:  0 ----------- 8 ---------------------------- 40
            ^             ^                              ^
            |             |                              |
            |  WARM ZONE  |  ANTICIPATION ZONE           |
            |             |                              |
            |  size <     |  size >= warm_threshold      |
            |  warm_thr   |                              |
            |             |                              |
            |  Пропускает |  Фаза 3: быстрый опрос       |
            |  фазы 3     |  Фаза 4: прямая передача     |
            |  и 4.       |   (ожидание возврата от       |
            |  Сразу к    |    соседа, дедлайн =          |
            |  фазе 5     |    wait_timeout − 500ms)      |
            |  (огранич.  |  Затем фаза 5                |
            |  + connect) |                              |

                  Ограничение параллельных создаваемых соединений
                  (ортогонально размеру пула)
                  -----------------------------------------------

Создаваемых сейчас: 0 ---- 1 ---- 2 (= scaling_max_parallel_creates)
                                   ^
                                   |  На лимите: новый вызов встаёт
                                   |  в очередь на получение
                                   |  возвращённого соединения и ждёт
                                   |  завершения чужого создания.

Зоны warm и anticipation отслеживают текущий размер пула. Отдельно работает bounded burst gate: жёсткий лимит на число одновременно идущих connect() к PostgreSQL внутри одного пула. Этот лимит срабатывает независимо от размера пула: пул может одновременно находиться в anticipation zone и иметь все слоты burst gate занятыми параллельными connect()-ами, под нагрузкой это типичная ситуация. Пул ниже порога прогрева тоже может упереться в этот лимит, если во время холодного заполнения одновременно приходит много клиентов.

Получение соединения

Когда клиент запрашивает соединение через pool.get(), pg_doorman проходит по следующим фазам. Каждая фаза либо возвращает соединение, либо передаёт управление следующей фазе.

Фаза 1 — горячий путь recycle. Берём первое соединение из idle-очереди (двусторонняя очередь свободных серверных соединений «голова очереди» — это то соединение, которое было возвращено раньше всех). Если оно проходит проверку пригодности (recycle), отдаём его клиенту. Проверка пригодности откатывает любые открытые транзакции, запускает проверку живости (liveness probe) если соединение простояло дольше server_idle_check_timeout, и сверяет поколение соединения с текущим поколением пула (reconnect-эпоха — счётчик, который увеличивается при admin-команде RECONNECT и при обнаруженных сбоях backend-а). Соединения, созданные до увеличения счётчика, эту проверку не проходят и удаляются вместо возврата клиенту. Здоровый пул в установившемся режиме идёт только этим путём.

Фаза 2 — warm zone gate. Если размер пула ниже порога прогрева, пропускаем anticipation и сразу переходим к созданию нового backend-соединения. Холодные пулы заполняются быстро.

Фаза 3 — anticipation spin. Выше порога прогрева повторяем проверку пригодности до 10 раз в плотном цикле без пауз (контролируется параметром scaling_fast_retries). Так перехватывается случай, когда другой клиент завершил свой запрос в том же микросекундном диапазоне и вот-вот вернёт соединение. Полная стоимость порядка 10–50 микросекунд. Без sleep, без блокирующего I/O.

Фаза 4 — direct handoff (прямая передача). Если spin не поймал возврат, задача встаёт в очередь ожидающих. Когда любой клиент возвращает соединение, оно отправляется напрямую старейшему ожидающему, минуя idle-очередь целиком. Получатель забирает соединение без конкуренции с другими задачами — соединение никогда не попадает в общую idle-очередь и достаётся ровно одному адресату.

Если передача удалась, соединение проходит проверку пригодности. При успехе соединение возвращается вызывающему. При ошибке (устаревший backend) пул уменьшает текущий размер, и вызов проваливается в путь создания нового соединения.

Если соединение не пришло до дедлайна, ожидающий снимается с очереди. При попытке доставки пул обнаруживает снятого ожидающего, пропускает его и пробует следующего. Таким образом устаревшие ожидающие вычищаются по мере обработки очереди, без отдельного прохода.

Дедлайн адаптивный: min(query_wait_timeout - 500 ms, adaptive_cap), где adaptive_cap вычисляется из реальной latency транзакций:

Состояние пулаБюджетПример
Холодный старт (нет данных)100ms ± 20% jitter80-120ms
Устойчивый режим (steady state)xact_p99 × 2 ± 20% jitterp99=0.7ms → 5ms (min); p99=50ms → 100ms
Высокая latencyОграничено 500msp99=300ms → 500ms

Фаза 1/2 ожидания семафора тратит из того же бюджета, поэтому суммарное ожидание не может увести клиента за его query_wait_timeout.

Случайный разброс (jitter) ±20% предотвращает обрыв таймаутов (timeout cliff): без него N клиентов, вошедших в Phase 4 одновременно, выходят одновременно и лавиной входят в burst gate, создавая N новых backend-соединений для пула, которому нужно значительно меньше. С jitter'ом клиенты выходят порциями — первые создают соединения, и к моменту выхода последних эти соединения уже использованы и возвращены в idle queue для переиспользования.

Если дедлайн истёк без получения соединения, переходим к фазе 5.

Фаза 5 — ограничитель всплесков (bounded burst gate). Перед тем как пойти на новый connect(), задача должна получить слот в ограничителе — механизме, который параллельно пропускает не больше scaling_max_parallel_creates (по умолчанию 2) задач на пул. Если слот свободен, задача забирает его и идёт вызывать connect(). Если все слоты заняты, задача встаёт в очередь на прямую передачу и одновременно ожидает завершения чужого создания. Приоритет отдаётся прямой передаче: если соединение вернулось, пока задача ждала освобождения слота, оно доставляется напрямую — задача проверяет его пригодность и возвращает клиенту. Если передачи не было, задача снова пробует проверку пригодности и захват слота. Так лимитируется скорость рождения новых backend-соединений в одном пуле, а не размер самого пула.

Адаптивный таймаут ограничителя всплесков (adaptive timeout burst gate). Цикл ограничителя лимитирован адаптивным бюджетом: xact_p99 × 2 ± 20% jitter (min 20 ms, max 500 ms). Если задача провела в цикле дольше бюджета, она прекращает ожидать прямую передачу и переходит к захвату слота ограничителя напрямую. Без этого механизма пул мог застрять на пороге прогрева навсегда: клиенты бесконечно получали переиспользованные соединения через прямую передачу, а до создания нового соединения дело не доходило. Счётчик burst_gate_budget_exhausted отслеживает срабатывания.

Фаза 6 — backend connect. Запускаем connect(), аутентифицируемся, отдаём соединение клиенту. Слот ограничителя освобождается автоматически по завершении этой фазы независимо от исхода.

                  Plain mode acquisition flow
                  ---------------------------

   pool.get()
       |
       v
   +--------------+
   |  Phase 1:    |  --- HIT ----> return idle connection
   |  recycle pop |
   +------+-------+
          | MISS
          v
   +--------------+
   |  Phase 2:    |  --- below warm ---> jump to phase 5
   |  warm gate   |
   +------+-------+
          | above warm
          v
   +--------------+
   |  Phase 3:    |  --- HIT ----> return idle connection
   |  fast spin   |
   +------+-------+
          | MISS
          v
   +--------------+
   |  Phase 4:    |  --- handoff  ----> return connection
   |  anticipate  |  --- timeout  ----> fall through
   |  direct h/o  |
   +------+-------+
          |
          v
   +--------------+
   |  Phase 5:    |  --- slot taken --> proceed to phase 6
   |  burst gate  |  --- slot full  --> wait, retry recycle
   +------+-------+
          |
          v
   +--------------+
   |  Phase 6:    |
   |  connect()   | ----> return new connection
   +--------------+

Подавление всплеска в действии

Тот же сценарий с 200 клиентами в формате thundering herd, но теперь в plain mode и с scaling_max_parallel_creates = 2:

Time:   t=0ms     t=5ms    t=10ms   t=15ms   t=20ms   t=25ms

C_1     [idle]--[query]-[done]
C_2     [idle]--[query]-[done]
C_3     [idle]--[query]-[done]
C_4     [idle]--[query]-[done]
C_5     [spin/wait]------[recycled C_1]--[query]-[done]
C_6     [spin/wait]------[recycled C_2]--[query]-[done]
C_7     [gate=1]-[connect]----[auth]--[query]-[done]
C_8     [gate=2]-[connect]----[auth]--[query]-[done]
C_9     [gate full, wait]---[recycled C_3]--[query]
C_10    [gate full, wait]---[recycled C_4]--[query]
  .
  .     [...196 clients use a mix of recycle, anticipation, and at
  .      most 2 in-flight connects...]
  .
C_200   [gate=2]-[connect]--[auth]--[query]--[done]

PostgreSQL: at most 2 spawning backends at any moment
            + the 4 connections that were already there

Тот же пул обслуживает все 200 клиентов, но PostgreSQL никогда не видит больше scaling_max_parallel_creates (по умолчанию 2) одновременных запусков backend-процессов из этого пула. Большинство клиентов попадают на переиспользованное соединение от соседа, который завершил работу мгновением раньше, а не на свежий connect().

Получение соединения без ожидания

Когда клиент устанавливает query_wait_timeout = 0, он просит либо сразу попасть на idle-соединение, либо свежий connect, без ожидания. Фаза anticipation и ожидание у burst gate пропускаются. pg_doorman выполняет recycle на горячем пути, один раз пробует burst gate, затем либо создаёт соединение, либо возвращает ошибку wait timeout.

Ограничение при включённом coordinator. Режим без ожидания пропускает только anticipation и ожидание у burst gate внутри пути в рамках одного пула. Если max_db_connections задан и фазы ожидания координатора (B–D) занимают время, такой вызов всё равно блокируется внутри coordinator.acquire() вплоть до reserve_pool_timeout (по умолчанию 3000 ms). Чтобы на базах под координатором соблюсти строгий дедлайн без ожидания, поставьте reserve_pool_timeout достаточно низким, чтобы он помещался в ваш бюджет.

Фоновый replenish

Когда задан min_pool_size, фоновая задача периодически дополняет пул до его минимума. Она использует тот же burst gate, что и клиентский трафик. Эта задача не встаёт в очередь за занятым gate: если gate занят, она немедленно сдаётся и повторяет попытку на следующем retain-цикле (по умолчанию каждые 30 секунд, контролируется параметром retain_connections_time).

Логика такова: во время всплеска нагрузки клиенты уже насыщают gate, создавая соединения, которые им нужны прямо сейчас. Если фоновый replenish будет конкурировать с ними за слоты, толку не будет: пул всё равно поднимется выше min_pool_size за счёт клиентских запросов на создание. При каждом таком отступлении фоновой задачи увеличивается счётчик replenish_deferred.

Следствие: под нагрузкой min_pool_size поддерживается в режиме best-effort (без гарантий: pg_doorman старается, но не обязуется держать прогретыми ровно столько соединений, если клиенты съедают весь бюджет). Если нужна жёсткая гарантия минимума, см. раздел troubleshooting.

Прямая передача при возврате (direct handoff)

Когда соединение возвращается, пул первым делом проверяет очередь ожидающих (direct-handoff). Если хотя бы один ожидающий зарегистрирован, соединение отправляется напрямую старейшему из них, минуя idle-очередь. Ожидающие, чей вызывающий уже отвалился по таймауту, пропускаются: пул обнаруживает недоступного получателя и пробует следующего в очереди.

Если зарегистрированных ожидающих нет (типичный случай при высоком throughput, когда каждый checkout попадает в горячий путь), соединение кладётся в idle-очередь и будит ближайшего клиента, ожидающего на Фазе 1/2.

В обоих случаях координатор (если настроен) уведомляется о возврате, чтобы ожидающие Фазы C из соседних пулов могли просканировать кандидатов для eviction. Ожидающие внутри того же пула получают соединения напрямую, а не через общее уведомление.

FIFO-честность и распределение латенси

Очередь ожидающих — двусторонняя очередь (FIFO): новые ожидающие добавляются в конец, соединения доставляются из начала. Старейший ожидающий всегда получает следующее возвращённое соединение.

Это даёт измеримо другой профиль латенси по сравнению с пулерами, использующими широковещательное пробуждение (broadcast-notify) или LIFO-планирование. При 500 клиентах на пул из 40 соединений (AWS Fargate):

Пулерp50 (мс)p95 (мс)p99 (мс)p99/p50
pg_doorman9.9310.5010.691.08
pgbouncer8.489.6210.451.23
odyssey0.8812.9322.4625.5

p50 у Odyssey в 11 раз ниже, чем у pg_doorman — большинство транзакций попадают на горячее соединение мгновенно. Но p99 в 2 раза выше. Одни клиенты ждут больше 22 мс, а другие завершаются меньше чем за 1 мс. При FIFO каждый клиент платит примерно одинаковую цену за очередь.

Что это значит для эксплуатации:

  • SLO compliance. SLO «p99 < 15 мс» достижим с pg_doorman при этой нагрузке. С Odyssey те же настройки пула его нарушают. Единственный выход — overprovisioning: увеличивать число соединений, пока даже невезучие клиенты укладываются.

  • Отсутствие голодания. При broadcast-notify клиент может проиграть гонку за wake-up многократно. При direct handoff соединение идёт ровно одному получателю, протухшие waiter'ы пропускаются. Нет thundering herd, нет повторных проигрышей.

  • Предсказуемое capacity planning. Когда p50 ≈ p99, удвоение числа клиентов примерно удваивает латенси. При соотношении хвоста 25x изменение нагрузки вызывает непредсказуемые всплески p99.

Теория очередей подтверждает: среди non-preemptive дисциплин планирования FIFO минимизирует дисперсию времени ожидания при том же среднем, что и LIFO. Среднее одинаково — разница только в хвосте.

Упреждающая замена при истечении lifetime (pre-replacement)

Когда настроен server_lifetime, backend-соединения закрываются по достижении индивидуального лимита (базовый ± 20% jitter). Закрытие означает, что в пуле на одно idle-соединение меньше — последующие checkout'ы могут попасть в зону упреждающего ожидания или путь создания нового соединения, добавляя несколько миллисекунд к p99 во время кластеров истечения lifetime.

Упреждающая замена (pre-replacement) убирает этот всплеск задержки. Когда checkout проверяет пригодность соединения и обнаруживает, что оно достигло 95% своего lifetime, фоновая задача создаёт замену и помещает её в idle-очередь. Когда старое соединение отклоняется при 100% lifetime, следующий checkout находит предсозданную замену через горячий путь — ноль ожидания.

Параллельно может работать до 3 упреждающих замен на пул. Во время окна перекрытия пул временно держит max_size + 3 соединений. Когда старые соединения умирают, текущий размер пула возвращается к max_size.

Условия, предотвращающие неконтролируемый рост:

УсловиеПредотвращает
Пул не под давлениемСоздание лишних при насыщении пула (старое соединение выживет, пропустив закрытие по lifetime)
Доля idle-соединений < 25%Замену в переразмеренном пуле, который должен сжаться
Запас координатора >= 2Захват последнего слота координатора у соседнего пула
Lifetime >= 60 sСрабатывание на коротких lifetime, где окно перекрытия слишком мало
Текущий размер <= max_size + capНакопление нескольких параллельных превышений
Лимит параллельных фоновых создаваемых (cap=3)Неограниченное число фоновых создаваемых соединений

Упреждающая замена срабатывает только на пути checkout (при проверке пригодности), не из фоновой задачи обслуживания. Idle-соединения, истекающие без checkout'а, закрываются фоновой задачей без замены — так пул естественно сжимается при падении нагрузки.

Согласование лимита с PostgreSQL

Перед чтением про координатор проверьте, что число backend-соединений в худшем случае умещается в PostgreSQL. Без max_db_connections верхняя граница для одной базы:

N pools (users) × pool_size  =  ceiling on backend connections

Пример с расчётом: три пула, у каждого pool_size = 40, без max_db_connections. В худшем случае получится 120 одновременных backend-соединений к этой базе, ограниченных только scaling_max_parallel_creates на пул (по умолчанию 2 на каждый, то есть до 6 одновременно идущих вызовов connect()). Если в PostgreSQL выставлен max_connections = 100, база начнёт отказывать в новых соединениях во время общего всплеска нагрузки, и клиенты получат FATAL: too many connections.

Два решения:

  • Понизить pool_size так, чтобы N × pool_size укладывалось ниже max_connections с запасом на superuser_reserved_connections, слоты репликации и любые прямые коннекторы в обход pg_doorman.
  • Задать max_db_connections как жёсткий лимит (см. следующий раздел).

Эмпирическое правило: суммарная потребность pg_doorman не должна превышать 80% от PostgreSQL max_connections - superuser_reserved_connections. Оставшиеся 20% оставьте под admin-соединения, репликацию и всплески.

Режим координатора (coordinator mode)

Режим координатора активируется, когда у пула задан max_db_connections. Он добавляет второй слой давления поверх того, что работает внутри каждого пула: общий семафор, который ограничивает суммарное число backend-соединений к базе по всем обслуживающим её user-пулам. Без него единственным ограничением остаётся потолок N × pool_size из предыдущего раздела. С max_db_connections = 80 к базе одновременно может существовать только 80 соединений независимо от конфигурации пулов, и координатор решает, какие пулы могут расти.

При max_db_connections = 0 (по умолчанию) координатор не создаётся. Когда параметр задан, все механизмы plain mode, описанные выше, продолжают работать; координатор добавляет один шаг получения permit (разрешения на удержание слота в общем лимите на базу: пока permit не получен, backend-соединение создавать нельзя) на пути создания нового соединения. Переиспользование idle координатора не касается.

Что добавляет координатор

Три вещи:

  1. Жёсткий лимит общего числа соединений к базе. Если 80 уже занято, 81-й запрос ждёт или падает независимо от того, какой пул его подал.

  2. Reserve pool (резервный пул). Когда общий лимит достигнут и у reserve_pool_size есть свободное место, координатор сразу выдаёт permit из резерва — небольшого дополнительного пула поверх max_db_connections, работающего как буфер под всплеск. Это Фаза R (reserve-first) в схеме ниже: ни одного соседнего backend не закрывается, ни одного ожидания не возникает. Резерв ограничен reserve_pool_size (по умолчанию 0, то есть выключен) и приоритизирован: голодающие пользователи (те, кто ниже своего эффективного минимума) и пользователи с большим числом ожидающих клиентов обслуживаются первыми через арбитра.

  3. Eviction (выселение). Fallback на случай, когда резерв выключен (reserve_pool_size = 0) или уже полностью занят: координатор закрывает idle-соединение из пула другого пользователя, чтобы освободить main-слот. Кандидаты сортируются по p95 времени транзакции (по убыванию): медленные пулы отдают первыми, потому что лучше переносят стоимость пересоздания (1 ms pool wait добавляет 6.7% к 15 ms p95, но 104% к 0.96 ms p95). Излишек над эффективным минимумом — tiebreaker среди пулов с похожим p95. Только соединения старше min_connection_lifetime (по умолчанию 30 000 ms) попадают в список. 30-секундный порог подавляет циклический reconnect между соседними пулами, которые по очереди воруют слоты друг у друга.

    Эффективный минимум для user-пула равен max(user.min_pool_size, pool.min_guaranteed_pool_size). Оба параметра защищают соединения от eviction; побеждает больший. Если снизить любой из них, эффективный минимум становится меньше и пользователь становится более доступен для eviction.

Фазы получения permit в координаторе

Когда путь внутри пула доходит до шага создания нового соединения, координатор проходит шесть фаз. Первая фаза, выдавшая permit, завершает последовательность.

Фаза A — Try-acquire (неблокирующая попытка: вернуть permit сразу, если есть свободный слот, иначе немедленно сообщить об отсутствии). Если лимит не достигнут, забираем слот и возвращаемся.

Фаза R — Reserve-first. Фаза A установила, что база заполнена. До того как закрыть хоть одно соседское backend-соединение, координатор проверяет, есть ли место в резервном пуле (reserve_in_use < reserve_pool_size). Если есть — сразу запрашивает permit у reserve arbiter. При успехе вызывающий получает reserve-permit без eviction, без закрытия соседнего backend и без ожидания на connection_returned. В обычных условиях арбитр отвечает за доли миллисекунды.

Это путь, который держит p99 латенси низкой: reserve-permit стоит одного round-trip с арбитром, тогда как старая последовательность (Фаза B + Фаза C) могла заблокировать клиента на полный reserve_pool_timeout, даже если в резерве было свободно. Фаза R не работает при reserve_pool_size = 0 и проваливается в Фазу B, если арбитр отказывает (все reserve permit-ы уже заняты, либо идёт гонка с другим вызывающим).

Фаза B — Eviction. Выполняется, когда Фаза R не выдала permit: reserve_pool_size = 0, либо резервный семафор был полностью занят на момент проверки (reserve_in_use == reserve_pool_size), либо арбитр отказал. Обходим все остальные user-пулы той же базы, сортируем по p95 времени транзакции (по убыванию, медленные первые) с излишком (spare) как tiebreaker, и закрываем одно idle-соединение старше min_connection_lifetime у верхнего кандидата. Permit выселенного соединения освобождается синхронно, слот становится доступен сразу. Повторяем захват семафора. Если два вызова конкурируют, проигравший переходит к следующей фазе. p95 кешируется каждые 15 секунд (stats cycle) — сканирование читает одно кешированное значение на кандидата без блокировки гистограммы.

Фаза C — Wait. Выполняется, когда резерв отключён или полностью занят и Фаза B не нашла что выселить. Регистрируется подписка на уведомления, которая срабатывает на двух событиях:

  1. Был освобождён permit координатора соседнего пула — серверное соединение физически закрыто (истёк server_lifetime, ошибка проверки пригодности, RECONNECT), и слот семафора теперь свободен.
  2. Соседний пул вернул соединение в свою idle-очередь — слот семафора НЕ освободился, но излишек над минимумом этого соседа только что вырос.

На каждое пробуждение Фаза C сначала пытается захватить свободный слот неблокирующей проверкой, и только если дешёвый путь не сработал — пробует выселение. Пробуждение от освобождения permit-а оставляет свободный слот в семафоре — дешёвый путь берёт его, и ни один соседний backend не закрывается. Пробуждение от idle-return не освобождает слот напрямую, но могло вырастить излишек соседа, поэтому повторная попытка eviction находит кандидата, которого мгновение назад не было, освобождает permit соседа, и следующая неблокирующая проверка срабатывает. Этот порядок (дешёвый путь сначала, выселение потом) закреплён регрессионным тестом: будущий рефакторинг не сможет случайно вернуть закрытия соседних backend-ов на пробуждениях от drop permit-а.

Ждём до reserve_pool_timeout (по умолчанию 3000 ms) либо пробуждения, либо дедлайна. Этот таймаут применяется даже при reserve_pool_size = 0: он задаёт бюджет всей фазы wait, а не только окно входа в резерв. Если ваш query_wait_timeout короче, чем reserve_pool_timeout, клиент сдаётся первым, и вы видите ошибки wait timeout вместо более наглядной all server connections to database 'X' are in use. Разбор симптома есть в разделе troubleshooting.

Фаза D — Reserve retry. Фаза R уже пробовала этот путь один раз. Фаза D повторяет попытку после того, как Фаза C исчерпала свой бюджет ожидания — на случай, если за время ожидания соседний reserve-держатель отпустил свой permit. Запросы ранжируются по паре (starving, queued_clients), где starving означает, что пул сейчас ниже своего эффективного минимума. Арбитр — это отдельный фоновый процесс, который раздаёт permit-ы резервного пула из приоритетной очереди.

Фаза E — Error. Если Фаза D тоже не выдала permit или резерв не настроен, клиент получает ошибку: all server connections to database 'X' are in use (max=N, ...).

Повышение из резерва в основной пул (reserve → main upgrade, retain task)

Reserve-permit — это буфер под всплеск, а не постоянное состояние. После того как всплеск прошёл, backend, получивший reserve-permit, продолжает жить как обычное idle-соединение, но его permit координатора по-прежнему учитывается в reserve_in_use — даже когда current < max_db_connections и в main-семафоре есть свободные слоты. Без активного обслуживания SHOW POOL_COORDINATOR показывал бы занятый резерв при том, что реальная ёмкость для всплеска пустая, и следующему всплеску некуда расти.

Retain task запускается каждые retain_connections_time (по умолчанию 30 секунд) и делает бухгалтерскую перестановку: для каждого пула, который не находится под давлением (см. определение ниже), он обходит idle-очередь и для каждого backend-а, удерживающего reserve-permit, пытается забрать permit из main-семафора.

Пул считается под давлением, когда его per-pool семафор имеет ноль свободных permit-ов. Одной колонки в SHOW POOLS, которая напрямую показывала бы состояние семафора, нет, и наблюдаемые колонки отстают от внутреннего состояния:

  • Строгий прокси: sv_active == pool_size. Каждое активное серверное соединение держит permit, поэтому когда все серверы в пуле активны, все permit-ы заняты. Это направление строгое.
  • Слабый прокси: cl_waiting > 0 означает, что как минимум один клиент находится внутри timeout_get — это часто означает, что семафор пуст, но клиент, который уже взял permit и припарковался в Фазе 4 anticipation или Фазе C координатора, тоже числится ожидающим. Используйте как индикатор, не как доказательство.

Retain task пропускает пулы под давлением по двум причинам: upgrade в такой момент просто отдаёт слот ожидающему клиенту (нет эффекта на reserve_used), а закрытие reserve-соединения заставит этого клиента делать свежий connect(). Очистка отработает на следующем цикле. При успехе reserve-permit возвращается в reserve-семафор, reserve_in_use уменьшается на единицу, а тип permit у этого backend переключается с reserve на main. Никакого переподключения, никакого дёргания соседа. Обход прерывается на первом неудачном upgrade в пуле: это доказывает, что main-семафор заполнен, и остальные reserve-permit-ы этого пула проверять бессмысленно. Тот же retain-цикл затем закрывает reserve-backend-ы, которые не удалось повысить до основного и которые простаивают дольше min_connection_lifetime.

При такой схеме reserve_in_use > 0 означает ровно одно: либо всплеск сейчас идёт, либо он закончился не более чем retain_connections_time назад. Исторический остаток reserve-ёмкости сходится к нулю, как только в main появляется свободное место.

Получение permit координатора по требованию (JIT coordinator permits, burst gate первым)

Внутри пути получения соединения burst gate работает до координатора. Это JIT (just-in-time) порядок: coordinator-permit берётся только когда вызов уже занял burst gate слот и готов вызвать connect().

Предыдущий порядок (координатор первый, потом gate) вызывал фантомные permit-ы (phantom permits): N вызовов захватывали по coordinator-permit и вставали в очередь за burst gate (cap=2). Реально создавали соединения только 2, но координатор видел N permit'ов в использовании и начинал выдавать reserve-permit'ы — хотя БД была далеко от предела.

С JIT-порядком в каждый момент coordinator-permit'ы держат не более max_parallel_creates вызовов. Остальные ждут gate слот без расходования бюджета координатора.

Блокировка головы очереди (head-of-line blocking) снимается разделением координатора на быстрый и медленный путь. Быстрый — неблокирующая проверка доступности слота координатора внутри слота ограничителя (мгновенно). Если не прошла — вызов освобождает слот ограничителя, ждёт координатора (eviction / возврат от соседа), и затем снова занимает слот ограничителя.

        Coordinator + plain mode acquisition flow (JIT)
        -----------------------------------------------

   pool.get()
       |
       v
   Phase 1: hot path recycle   --- HIT ---> return
       | MISS
       v
   Phase 2: warm gate          --- below ---+
       | above warm                         |
       v                                    |
   Phase 3: fast spin          --- HIT ---> return
       | MISS                               |
       v                                    |
   Phase 4: direct handoff     --- HIT ---> return
       | deadline                           |
       v                                    |
       | <----------------------------------+
       v
   Phase 5: bounded burst gate (scaling_max_parallel_creates)
              | slot acquired
              v
   +---------------------------+
   | JIT coordinator acquire   |  only when max_db_connections > 0
   |  fast: неблокир. проверка  |  мгновенный ответ
   |  slow: release gate slot  |  ожидание координатора (evict/return)
   |        → re-acquire slot  |  затем продолжить create
   +------------+--------------+
                | permit granted
                v
   Phase 6: server_pool.create()
                |
                v
                return new connection

Фазы пронумерованы так же, как в plain mode. Coordinator acquire работает внутри burst gate слота, когда max_db_connections > 0. В plain mode он не работает.

Когда координатор настроен, но лимит не достигнут

Если max_db_connections = 80, а текущее использование 30, фаза A координатора всегда успешна. Фазы B–E никогда не запускаются. Поведение идентично plain mode плюс одна атомарная инкрементация семафора на каждое новое соединение. Горячий путь (повторное использование idle) координатора вообще не касается, поэтому там у него нет измеримой стоимости. Платят только новые создания соединений, и платят ровно одной атомарной операцией.

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

Фоновый replenish под координатором

replenish берёт permit координатора через неблокирующую проверку доступности. Если база уже на лимите, replenish сдаётся и повторяет попытку на следующем retain-цикле. Та же логика, что и у burst gate: фоновая задача не должна бороться с клиентским трафиком за скудные permit-ы.

Параметры тюнинга

Scaling-параметры по умолчанию глобальные. Для scaling_warm_pool_ratio и scaling_fast_retries можно задать переопределение в каждом пуле. scaling_max_parallel_creates настраивается только глобально, переопределений на уровне пула для него нет.

ПараметрПо умолчаниюГдеЧто делает
scaling_warm_pool_ratio20 (процент)general, per-poolПорог, ниже которого соединения создаются без anticipation. Ниже pool_size × ratio / 100 каждый запрос нового соединения идёт сразу к connect().
scaling_fast_retries10general, per-poolЧисло быстрых повторных проверок пригодности в фазе anticipation перед переходом к прямой передаче (ожиданию возврата от соседа).
scaling_max_parallel_creates2generalЖёсткий лимит одновременно идущих backend-connect() на пул. Задачи сверх лимита ждут возврата idle-соединения или завершения чужого создания. Должен быть >= 1.
max_db_connectionsне задан (выключено)per-poolЛимит суммарного числа backend-соединений к базе по всем user-пулам. Когда не задан, координатор не создаётся.
min_connection_lifetime30000 (ms)per-poolМинимальный возраст idle-соединения, после которого координатор может выселить его в пользу другого пула. 30-секундный порог подавляет циклический reconnect между соседними пулами.
reserve_pool_size0 (выключено)per-poolДополнительные permit-ы координатора поверх max_db_connections, выдаваемые по приоритету при исчерпании основного пула.
reserve_pool_timeout3000 (ms)per-poolМаксимальное время ожидания координатора перед переходом к reserve pool.
min_guaranteed_pool_size0per-poolМинимум на пользователя, защищённый от eviction координатором. Соединения пользователя, у которого current_size <= min_guaranteed_pool_size, не могут быть выселены другими пользователями.

Когда повышать scaling_max_parallel_creates

Повышайте, если:

  • burst_gate_waits устойчиво растёт между скрейпами, и replenish_deferred тоже ненулевой: клиентский трафик и фоновая задача оба борются за слоты, которых нет;
  • connect() к бэкенду быстрый (< 50 ms), и у PostgreSQL есть запас по max_connections;
  • скачки задержки соединения совпадают с ростом частоты burst_gate_waits.

Жёсткий потолок. Не поднимайте scaling_max_parallel_creates выше ни одного из этих лимитов:

  • pool_size / 4 для самого маленького пула, в котором используется этот параметр. Выше этого значения лимит теряет смысл: одновременно создаётся половина пула, и сглаживание всплеска ломается.
  • (PostgreSQL max_connections - superuser_reserved_connections) / (10 × N pools), где N pools обозначает все пулы, делящие этот инстанс PostgreSQL. Выше этого значения суммарная частота одновременных коннектов превышает то, что бэкенд успевает обрабатывать, не переполняя очередь accept().

Понижайте, если:

  • connect() к PostgreSQL дорогой (> 200 ms, например, SSL с проверкой сертификата или медленный lookup pg_authid);
  • в логах PostgreSQL появляется конкуренция за pg_authid;
  • бэкенд жалуется на переполнение очереди accept().

Симптом слишком низкого значения: частота burst_gate_waits растёт быстрее, чем частота прихода клиентов. Симптом слишком высокого: задержка connect() к PostgreSQL растёт, и connection storm (шторм одновременных connect() к бэкенду, последствие thundering herd) возвращается.

Размер при множестве пулов. Суммарный потолок одновременных коннектов равен N pools × scaling_max_parallel_creates. Если за одним PostgreSQL стоит 10 пулов и в любой момент суммарно по ним всем вам нужно не более 8 одновременных backend-коннектов, поставьте scaling_max_parallel_creates около desired_aggregate / N pools, округляя вниз. Ниже 1 нельзя; если арифметика даёт меньше единицы, уменьшайте N pools, объединяя пользователей.

Когда повышать scaling_warm_pool_ratio

Повышайте, если:

  • пулы медленно прогреваются на старте, и min_pool_size не используется;
  • клиенты ждут anticipation в момент, когда пул в основном пуст (anticipation активируется только выше порога прогрева, поэтому такого быть не должно, но более высокий ratio дополнительно сужает окно, в котором это может случиться).

Понижайте, если:

  • пулы избыточны по размеру, и вы хотите, чтобы anticipation начинала подавлять создания на меньших значениях текущего размера.

Этот параметр редко требует вмешательства. Значение по умолчанию 20% подходит большинству нагрузок.

Когда задавать max_db_connections

Задавайте, если:

  • один хост PostgreSQL обслуживает несколько пулов (database, user), и сумма pool_size по всем пулам превышает max_connections базы;
  • нужен жёсткий потолок, который сохранится даже при неправильной конфигурации какого-то отдельного пула;
  • нужна общая честность между пулами через eviction.

Оставляйте незаданным, если:

  • один пул обслуживает одну базу, и pool_size остаётся единственным ограничением;
  • вам не нужна никакая eviction между пулами (некоторые нагрузки предпочитают жёсткую изоляцию между пользователями).

reserve_pool_size и reserve_pool_timeout

Резерв работает как временный клапан переполнения, а не как дополнительная постоянная ёмкость. Он предотвращает видимые клиенту ошибки исчерпания во время коротких всплесков. В нормальном режиме reserve_in_use большую часть времени должен быть равен нулю.

Эмпирическое правило размера: reserve_pool_size ≤ 0.25 × max_db_connections. За этим порогом резерв перестаёт вести себя как буфер. Если половина нагрузки постоянно лежит в резерве, поднимайте max_db_connections, а не расширяйте клапан переполнения.

reserve_pool_timeout задаёт, сколько клиент ждёт в фазе C координатора перед обращением к резерву. Значение 3000 ms по умолчанию консервативное. Понижайте его, если ваш query_wait_timeout короткий и вам выгоднее быстро переходить к резерву, чем блокировать клиентов на coordinator wait.

Рецепт тюнинга: снизить p99 checkout на базе под координатором

Профиль нагрузки: PostgreSQL отвечает за ~1 ms (низкий p99 длительности запроса), но клиенты видят p99 checkout latency 100–500 ms на базе под координатором. Задержка идёт от координатора, а не от PostgreSQL.

  1. Подтвердите фазу. Запустите SHOW POOL_COORDINATOR в момент всплеска latency. Вычислите main_used = current - reserve_used: current включает reserve-permit-ы, а рецепт зависит от того, заполнен ли именно main семафор.
    • main_used == max_db_conn и exhaustions не растёт → доминирует wait-фаза. Клиент тратит свой бюджет в Фазе C перед тем, как провалиться в Фазу D. Переходите к шагу 2.
    • main_used < max_db_conn без exhaustions → задержка идёт не от координатора. Смотрите SHOW POOL_SCALING create_fallback и troubleshooting plain-режима.
  2. Включите reserve-first, если он ещё не включён. Задайте reserve_pool_size как минимум max(2, 0.1 × max_db_connections). Reserve-first выдаёт permit за sub-ms, когда в резерве есть место, так что клиент, раньше сидевший в Фазе C, платит только один round-trip к арбитру.
  3. Уменьшите reserve_pool_timeout до 2 × p99 query latency, но не ниже. Для запроса в 1 ms нижняя граница обычно 20 ms; начните с 50 ms и неделю наблюдайте reserve_acq и evictions.
  4. Оставьте min_connection_lifetime на дефолте 30 000 ms, если у вас нет явной цели ускорить кросс-пуловую ребалансировку; понижение увеличивает частоту eviction и churn соединений.

За чем следить после каждого изменения (все в SHOW POOL_COORDINATOR):

ДоПослеВердикт
reserve_acq не растётreserve_acq растётReserve-first подхватил — checkout latency должен упасть; ожидаемо
evictions стабиленevictions падаетФаза B перестала срабатывать, потому что Фаза R ловит вызывающего раньше; ожидаемо
exhaustions 0exhaustions > 0Перетянули: reserve_pool_timeout ниже реального времени возврата от соседа
reserve_used колеблется > 0reserve_used возвращается к 0 за 30 сRetain upgrade-путь работает; делать ничего не надо

Если p99 checkout не упал после шагов 2–3, путь не ограничен координатором. Перечитайте SHOW POOL_SCALING по пострадавшему пулу: create_fallback > 0 означает, что сам пул не может обслужить нагрузку из возвратов, и лечить нужно pool_size, а не reserve_pool_size.

Нижняя граница. Не опускайте reserve_pool_timeout ниже 2 × ваш p99 query latency. Ниже этого порога фаза wait всегда истекает раньше, чем соседний пул вернёт соединение, и резерв превращается из клапана переполнения в обязательный permit для каждого нового соединения. Reserve-permit-ов по замыслу мало, и использовать их как постоянный источник означает сломать их назначение.

Ловушка: query_wait_timeout < reserve_pool_timeout. Когда дедлайн клиента короче фазы ожидания координатора, клиент сдаётся первым, и вы видите ошибки wait timeout вместо более наглядной all server connections to database 'X' are in use. Фазы wait и reserve координатора отрабатывают полностью, но к этому моменту клиента, которому нужен результат, уже нет. На старте валидатор конфига pg_doorman выдаёт предупреждение; реагируйте на него.

Observability

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

Admin: SHOW POOL_SCALING

Счётчики пути anticipation + bounded burst в разрезе каждого пула. Подключитесь к admin-базе pgdoorman и выполните:

pgdoorman=> SHOW POOL_SCALING;
КолонкаТипЗначение
usertextПользователь пула
databasetextБаза пула
inflightgaugeВызовы connect() к бэкенду, выполняемые в этом пуле прямо сейчас. Ограничено scaling_max_parallel_creates.
createscounterСколько всего backend-соединений пул начинал создавать с момента старта. В паре с gate_waits используется для расчёта частоты попаданий на gate.
gate_waitscounterСколько раз вызов наткнулся на заполненный burst gate и был вынужден ждать слот. Высокие значения говорят, что scaling_max_parallel_creates слишком низкий.
antic_notifycounterПопытки anticipation в Фазе 4, где прямая передача удалась. Инкрементируется один раз на успешное получение, до проверки пригодности. Высокий antic_notify при низком create_fallback — хороший признак: прямая передача ловит возвраты, клиенты не платят за connect().
antic_timeoutcounterПопытки anticipation в Фазе 4, где ожидание истекло без получения соединения, либо бюджет был нулевой. Инкрементируется ровно один раз при каждом провале Фазы 4 в путь создания. Высокий antic_timeout означает, что клиенты упираются в query_wait_timeout, не успев получить соединение через прямую передачу.
create_fallbackcounterФаза 4 не получила соединение через прямую передачу: дедлайн исчерпан или бюджет был нулевой. Именно эти ожидания превращаются в новый connect(). Стабильно ненулевой create_fallback значит, что клиентского бюджета не хватает на перехват возвратов: пул либо мал, либо запросы длиннее query_wait_timeout.
replenish_defcounterЗапуски фонового replenish, упёршиеся в лимит burst gate и отложенные до следующего retain-цикла. Устойчиво ненулевые значения означают, что min_pool_size нельзя поддержать при текущей нагрузке.

Все счётчики монотонные с момента старта. Считайте дельты между скрейпами; абсолютные значения полезны только для расчёта соотношений.

Admin: SHOW POOL_COORDINATOR

Состояние координатора в разрезе каждой базы. Присутствует только для баз с max_db_connections > 0.

pgdoorman=> SHOW POOL_COORDINATOR;
КолонкаТипЗначение
databasetextИмя базы
max_db_conngaugeСконфигурированное max_db_connections
currentgaugeСколько всего backend-соединений сейчас удерживается под этим координатором (по всем user-пулам)
reserve_sizegaugeСконфигурированное reserve_pool_size
reserve_usedgaugeСколько reserve-permit-ов используется прямо сейчас. Сходится обратно к 0, когда в main есть свободное место — retain task каждые retain_connections_time апгрейдит idle reserve-permit-ы в main. Устойчивое ненулевое значение означает либо активный всплеск, либо базу, постоянно упёртую в max_db_connections.
evictionscounterСколько раз координатор выселил idle-соединение соседнего пула, чтобы освободить слот. С включённым reserve-first этот счётчик растёт только при реальном кросс-пуловом давлении — когда резерв заполнен и у соседа есть что выселить.
reserve_acqcounterСколько всего reserve-permit-ов выдал arbiter (Фаза R быстрый путь плюс Фаза D fallback суммарно)
exhaustionscounterСколько раз координатор вернул клиенту ошибку исчерпания. Это главный сигнал на пейджер.

Чтение вывода SHOW POOL_COORDINATOR

Три снапшота и что каждый означает для оператора:

Здоровая спокойная база:

 database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
 mydb     |          80 |      24 |           10 |            0 |         0 |           0 |           0

Нормальное устойчивое состояние. Запас большой, резерв спит, evictions нет, exhaustions нет. Алерты здесь должны молчать.

После всплеска, upgrade в процессе:

 database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
 mydb     |          80 |      65 |           10 |            3 |         0 |          12 |           0

Всплеск занял большую часть max_db_connections и три соединения перелились в резерв. current < max_db_conn означает, что в main есть место, поэтому retain task повысит эти три permit-а до main на следующем цикле; reserve_used должен упасть до 0 в течение retain_connections_time (по умолчанию 30 секунд). Если не падает, смотрите раздел troubleshooting ниже. evictions = 0 и reserve_acq > 0 вместе подтверждают, что reserve-first поглотил всплеск без закрытия соседских backend-ов.

Устойчивая перегрузка:

 database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
 mydb     |          80 |      95 |           20 |           15 |       300 |         500 |           0

Main полностью занят (main_used = current - reserve_used = 80, равно max_db_conn), резерв использован на 75%, много eviction-ов, много reserve-грантов. База не просто иногда под давлением — она постоянно недоразмерена и выживает только за счёт того, что eviction ротирует соединения между пользователями, а reserve-first поглощает каждого нового вызывающего. exhaustions = 0 означает, что арбитр пока успевает, но любой переходный пик опрокинет ситуацию. Действие: поднимите max_db_connections (сначала проверьте запас PostgreSQL) или найдите жадный пул через SHOW POOLS и уменьшите его pool_size.

Метрики Prometheus

Два семейства метрик в разрезе пула и два в разрезе координатора. Все четыре живут в namespace pg_doorman_pool_scaling* и pg_doorman_pool_coordinator*.

МетрикаТипЛейблыИсточник
pg_doorman_pool_scaling{type="inflight_creates"}gaugeuser, databaseinflight из SHOW POOL_SCALING
pg_doorman_pool_scaling_total{type="creates_started"}counteruser, databasecreates
pg_doorman_pool_scaling_total{type="burst_gate_waits"}counteruser, databasegate_waits
pg_doorman_pool_scaling_total{type="burst_gate_budget_exhausted"}counteruser, databasegate_budget_ex — adaptive timeout сработал, клиент перешёл к созданию
pg_doorman_pool_scaling_total{type="anticipation_wakes_notify"}counteruser, databaseantic_notify
pg_doorman_pool_scaling_total{type="anticipation_wakes_timeout"}counteruser, databaseantic_timeout
pg_doorman_pool_scaling_total{type="create_fallback"}counteruser, databasecreate_fallback
pg_doorman_pool_scaling_total{type="replenish_deferred"}counteruser, databasereplenish_def
pg_doorman_pool_coordinator{type="connections"}gaugedatabasecurrent из SHOW POOL_COORDINATOR
pg_doorman_pool_coordinator{type="reserve_in_use"}gaugedatabasereserve_used
pg_doorman_pool_coordinator{type="max_connections"}gaugedatabasemax_db_conn
pg_doorman_pool_coordinator{type="reserve_pool_size"}gaugedatabasereserve_size
pg_doorman_pool_coordinator_total{type="evictions"}counterdatabaseevictions
pg_doorman_pool_coordinator_total{type="reserve_acquisitions"}counterdatabasereserve_acq
pg_doorman_pool_coordinator_total{type="exhaustions"}counterdatabaseexhaustions

Алерты для настройки

Алерты ниже покрывают режимы отказа, на которые стоит реагировать пейджером или варнингом. Они написаны на синтаксисе Prometheus; адаптируйте под свой стек. Все используют окна устойчивого условия, чтобы короткие всплески не будили дежурного.

Если вы часто перезагружаете pg_doorman, и пулы появляются и исчезают, ограничьте алерты недавно активными пулами (например, добавьте фильтр pg_doorman_pool_scaling_total{type="creates_started"} > 0).

Каждый алерт ниже содержит блок Runbook с одной диагностической командой и двумя-тремя ветвями, привязанными к конкретным значениям счётчиков.

Coordinator exhaustion (page). Клиент получил ошибку "database exhausted". Жёсткий отказ — резерв и eviction оба не сработали.

rate(pg_doorman_pool_coordinator_total{type="exhaustions"}[5m]) > 0

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'

current — это суммарное число соединений main + reserve (current == max_db_conn + reserve_size означает, что оба семафора пусты).

  • current == max_db_conn + reserve_size → оба семафора полностью пусты. Поднимите max_db_connections (сначала проверьте, что у PostgreSQL есть запас max_connections) или увеличьте резерв.
  • reserve_size == 0 и current == max_db_conn → резерв выключен, main полностью занят. Задайте reserve_pool_size, чтобы поглощать всплески, и, если exhaustions продолжит расти, поднимайте max_db_connections.
  • current < max_db_conn + reserve_size, но exhaustions растёт → гонка в Фазе R/D, такого не должно быть устойчиво; заведите баг со снапшотом SHOW POOL_COORDINATOR.
  • В SHOW POOLS у одного пользователя sv_idle сильно больше, чем у остальных → один пул захватывает соединения. Уменьшите его pool_size или задайте min_guaranteed_pool_size, чтобы защитить соседей.

Burst gate saturated (warn). Burst gate ждёт чужие create чаще, чем проходит напрямую. Короткие всплески выше порога при failover или рестарте нормальны. Устойчивые значения означают, что scaling_max_parallel_creates слишком низкий.

rate(pg_doorman_pool_scaling_total{type="burst_gate_waits"}[5m])
  > 0.5 * rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m])

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
  • inflight_creates сидит на лимите И в SHOW POOLS видны клиенты в cl_waitingconnect() медленный со стороны бэкенда. Смотрите troubleshooting "Burst gate как узкое место даже при низком трафике" перед тем, как поднимать лимит.
  • inflight_creates ходит ниже лимита, но gate_waits растёт → много коротких всплесков. Поднимайте scaling_max_parallel_creates, оставаясь в пределах потолка из раздела тюнинга.
  • Горит только один пул → рассмотрите min_guaranteed_pool_size для соседей или уменьшите pool_size у горячего.

Anticipation create fallback rate (warn). Фаза 4 anticipation сдаётся, не поймав возврат, и проваливается в свежий connect(). В устойчивом состоянии должно быть близко к нулю.

rate(pg_doorman_pool_scaling_total{type="create_fallback"}[5m]) > 0.1
  and
  rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m]) > 0.1

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
    -c 'SHOW STATS' | grep -E 'database|avg_xact_time|avg_query_time'
  • create_fallback высокий на одном пуле И avg_xact_time этой базы растёт → долгие запросы держат соединения вне ротации. Сначала лечите долгий запрос; пул рассчитан на обычную длительность, а не на эту транзакцию.
  • create_fallback высокий у всех пулов И creates_started тоже растёт → нагрузка превышает то, что возвраты могут обслужить в пределах дедлайна. Поднимайте pool_size.
  • create_fallback высокий, но query_wait_timeout короткий (< 1 секунды) → дедлайн anticipation (query_wait_timeout − 500 ms с верхней границей 500 ms) слишком короткий даже для нормальных возвратов. Поднимайте query_wait_timeout минимум до 2 × p99 длительности запроса.

Replenish deferred persistently (warn). Фоновая задача не может поддерживать min_pool_size, потому что burst gate занят клиентским трафиком.

increase(pg_doorman_pool_scaling_total{type="replenish_deferred"}[1h]) > 60

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
  • Пострадавший пул показывает sv_idle + sv_active < min_pool_size одновременно с растущим gate_waits → replenish проигрывает клиентскому трафику. Поднимайте scaling_max_parallel_creates, чтобы у фоновой задачи был запас, или принимайте defer как косметическую проблему (под нагрузкой клиенты сами поднимут пул выше min_pool_size).
  • inflight_creates стабильно стоит на лимите → gate полон по другой причине (медленный connect()); сначала это.

Reserve pool continuously in use (warn). Gauge reserve-permit не возвращался к нулю в течение 15 минут. Retain task апгрейдит idle reserve-permit-ы обратно в main каждые retain_connections_time (по умолчанию 30 секунд), поэтому алерт означает, что upgrade-путь не в состоянии отработать или получить успех, а не что он забыл запуститься.

min_over_time(pg_doorman_pool_coordinator{type="reserve_in_use"}[15m]) > 0

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'

Вычислите main_used = current - reserve_used из строки — current это суммарное число permit-ов main и reserve, а не только main.

  • main_used == max_db_conn → main полностью занят, upgrade-у негде забрать слот. База недоразмерена; поднимайте max_db_connections.
  • main_used < max_db_conn И каждый пул в SHOW POOLS имеет sv_active == pool_size (или cl_waiting > 0 как индикатор) → все пулы под давлением, retain task пропускает upgrade. Поднимайте pool_size у того пула, у которого самый высокий cl_waiting или самое плотное соотношение sv_active / pool_size.
  • main_used < max_db_conn И ни у одного пула нет ни того, ни другого признака, а gauge всё равно ненулевой → заведите баг со снапшотами SHOW POOL_COORDINATOR и SHOW POOLS; этого не должно быть.

Coordinator approaching cap (warn). Ранний сигнал: координатор подходит к потолку.

pg_doorman_pool_coordinator{type="max_connections"} > 0
  and
  pg_doorman_pool_coordinator{type="connections"}
    / pg_doorman_pool_coordinator{type="max_connections"} > 0.85

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
  • current монотонно растёт часами → проблема планирования ёмкости. Поднимайте max_db_connections (сначала проверьте запас PostgreSQL) до следующего всплеска.
  • current колеблется рядом с лимитом → burst-driven. Поднимайте reserve_pool_size, чтобы всплески поглощались без касания max_db_connections, потом следите за reserve_acq.
  • Один пул доминирует в SHOW POOLS (sv_active + sv_idle сильно больше, чем у соседей) → один пул захватывает соединения; уменьшите его pool_size или задайте min_guaranteed_pool_size соседям.

Inflight stuck at cap (warn). inflight_creates, сидящий на сконфигурированном лимите больше 5 минут, означает, что вызовы connect() не завершаются.

min_over_time(pg_doorman_pool_scaling{type="inflight_creates"}[5m])
  >= 2  # adjust to your scaling_max_parallel_creates value

Runbook:

time psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c 'SELECT 1'
psql -h $PG_HOST -p $PG_PORT -c \
    "SELECT state, count(*) FROM pg_stat_activity GROUP BY state"
  • time psql показывает connect() > 500 ms → backend-connect медленный. Проверьте pg_stat_ssl на стоимость SSL-handshake, pg_authid на конкуренцию за поиск роли и время DNS-резолва с хоста pg_doorman.
  • pg_stat_activity показывает много сессий в startup или authenticating → бэкенд спавнит процессы, но не разгружает очередь handshake. Вероятно, упёрлись в max_connections на стороне базы — выполните SELECT setting FROM pg_settings WHERE name = 'max_connections' и сравните с активными сессиями.
  • pg_stat_activity по пользователю pg_doorman пустой → сетевая проблема или firewall между pg_doorman и PostgreSQL.

Coordinator thrashing (warn). Лимит исчерпан и идут eviction-ы: координатор постоянно закрывает соседние соединения, чтобы освободить место. Недоразмеренный пул, а не "иногда под давлением".

pg_doorman_pool_coordinator{type="connections"}
    / pg_doorman_pool_coordinator{type="max_connections"} > 0.95
  and
  rate(pg_doorman_pool_coordinator_total{type="evictions"}[5m]) > 0

Runbook:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
  • evictions растёт И reserve_used == 0 → резерв выключен или исчерпан, eviction — единственный клапан сброса. Включите / поднимите reserve_pool_size, чтобы всплеск поглощался без закрытия соседних backend-ов.
  • evictions И reserve_acq растут одновременно → резерва тоже не хватает. Поднимайте max_db_connections или reserve_pool_size; сначала проверьте max_connections у PostgreSQL.

Чтение admin-вывода во время инцидента

Admin-консоль принимает только SHOW <subcommand>, SET, RELOAD, SHUTDOWN, UPGRADE, PAUSE, RESUME и RECONNECT. SHOW это не виртуальная таблица, поэтому к admin-базе нельзя выполнить SELECT. Чтобы запрашивать счётчики в shell-пайплайнах, запускайте SHOW из psql и обрабатывайте вывод.

Шаблоны ниже используют psql к admin-листенеру (по умолчанию учётные данные admin/admin):

# Highest burst-gate-wait ratio first (the hot pool).
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
     -c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
  | awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f  inflight=%d  defer=%d\n", $1, $2, $5/$4, $3, $9 }' \
  | sort -k3 -nr | head

# Highest create fallback ratio (cycle gave up on deadline and paid connect()).
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
     -c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
  | awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f  creates=%d  fallback=%d\n", $1, $2, $8/$4, $4, $8 }' \
  | sort -k3 -nr | head

# Coordinator: closest databases to exhaustion.
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
     -c 'SHOW POOL_COORDINATOR' --no-align --field-separator='|' \
  | awk -F'|' 'NR>1 && $2>0 { printf "%-30s %.3f  used=%d/%d  reserve=%d  exhaustions=%d\n", $1, $3/$2, $3, $2, $5, $8 }' \
  | sort -k2 -nr

Позиции полей в awk соответствуют порядку колонок, описанному выше: для POOL_SCALING это user|database|inflight|creates|gate_waits|gate_budget_ex|antic_notify|antic_timeout|create_fallback|replenish_def, для POOL_COORDINATOR это database|max_db_conn|current|reserve_size|reserve_used|evictions|reserve_acq|exhaustions.

Сравнение с PgBouncer

PgBouncer и pg_doorman оба пулят соединения, но давление обрабатывают по-разному.

АспектPgBouncerpg_doorman
Лимит размера в каждом пулеpool_sizepool_size
Лимит на уровне БД, общий для пуловmax_db_connections (жёсткий лимит, без eviction; для изоляции есть переопределения pool_size на базу или на пользователя)max_db_connections (жёсткий лимит плюс eviction между пулами и reserve pool)
Reserve poolreserve_pool_size, reserve_pool_timeoutreserve_pool_size, reserve_pool_timeout (плюс приоритизация в arbiter по starving/queued)
Eviction между пользователямиНе поддерживается. Пользователь, удерживающий idle-соединения, морит голодом соседа, которому они нужны.Координатор выселяет idle-соединения у пользователя с наибольшим излишком над эффективным минимумом (max(user.min_pool_size, min_guaranteed_pool_size)).
Одновременные connect() к бэкенду в одном пулеОднопоточный, обрабатывает события последовательно в пределах пула, вызовы connect() выпускаются по одному.Ограничено scaling_max_parallel_creates (по умолчанию 2 на пул): не больше N одновременных backend-коннектов в пуле, лишние задачи ждут на burst gate.
Anticipation возвратовНет. Клиенты ждут следующего доступного соединения в порядке прихода, в пределах wait_timeout.Event-driven anticipation: возвращающееся соединение будит ровно одного из ожидающих в очереди, часто ещё до того, как будет выпущен хоть один новый connect().
Прогрев min_pool_sizeПоддерживается на каждом такте event loop (без отдельной задачи replenish).Периодический фоновый replenish (retain_connections_time, по умолчанию 30 s), который отступает, когда burst gate занят.
Повторный логин после ошибкиserver_login_retry (по умолчанию 15 s) блокирует новые попытки логина после отказа бэкенда.Аналога нет. Ошибки логина бэкенда пробрасываются клиенту на каждую попытку.
Jitter на lifetimeНет. server_lifetime точный.±20% jitter на server_lifetime и idle_timeout, чтобы избежать одновременного массового закрытия.
Ключ поиска пула(database, user, auth_type)(database, user)
Честность между пользователями на общем лимитеFirst come first served на max_db_connections.Reserve arbiter оценивает запросы по (starving, queued_clients).
Observability давления на новые соединенияSHOW POOLS, SHOW STATS. Текущие inflight-коннекты и результаты anticipation не видны вовсе.SHOW POOL_SCALING и SHOW POOL_COORDINATOR показывают каждый счётчик нового кодового пути.

В production важнее всего два различия:

  1. Bounded burst gate. Размер пула в PgBouncer ограничивает, сколько соединений у вас есть, но не ограничивает, сколько вызовов connect() выпускается одновременно, когда в один момент приходит много клиентов. pg_doorman ограничивает частоту одновременных connect() к бэкенду независимо от размера пула, поэтому внезапный всплеск трафика не превращается в connection storm к PostgreSQL.

  2. Cross-pool eviction. max_db_connections в PgBouncer задаёт жёсткий потолок и не умеет перераспределять. Если пользователь A держит 80 idle-соединений, а пользователю B нужно одно, но лимит уже выбран, пользователь B ждёт или падает. Координатор pg_doorman может закрыть одно из соединений A (если оно старше min_connection_lifetime) и отдать слот B.

  3. FIFO direct handoff. PgBouncer ставит клиентов в порядке прихода и отдаёт следующее свободное соединение, но PgBouncer обрабатывает события на одном потоке — под нагрузкой порядок зависит от callback'ов libevent. pg_doorman отправляет возвращённые соединения напрямую каждому ожидающему в строгом FIFO-порядке. Результат — p99/p50 в пределах 1.1x при любом числе клиентов, тогда как пулеры без строгого FIFO показывают 10-25x раздувание хвоста при той же нагрузке.

Troubleshooting

В логах одновременно несколько backend connect

Симптом. В логах сервера (или в debug-логах pg_doorman) видно 5 или больше событий connect() к бэкенду в одну и ту же миллисекунду. Кажется, что burst gate не работает.

Причина. Либо scaling_max_parallel_creates выставлен слишком высоко (проверьте SHOW CONFIG или ваш pg_doorman.yaml), либо существует 5 или больше пулов, каждый из которых независимо открывает одновременные коннекты (gate работает в пределах одного пула, а не глобально).

Исправление. Понизьте scaling_max_parallel_creates. Значение по умолчанию 2 подходит большинству нагрузок. Когда пулов много, суммарная частота одновременных коннектов равна pools × scaling_max_parallel_creates, и это ожидаемо. Чтобы ограничить и сумму, задайте max_db_connections на базу; тогда координатор поставит в очередь создания сверх лимита.

min_pool_size не поддерживается

Симптом. Пул с min_pool_size = 10 показывает sv_idle = 4 в SHOW POOLS и держится так минутами.

Причина. Фоновый replenish откладывается, потому что burst gate занят клиентским трафиком. Проверьте replenish_def в SHOW POOL_SCALING. Если он продолжает расти, replenish пропускает каждый retain-цикл.

Исправление. Так задумано: под нагрузкой gate отдан клиентским запросам на создание. Пул дойдёт до min_pool_size, когда клиентский трафик ослабнет. Если нужна жёсткая гарантия минимума, поднимайте scaling_max_parallel_creates, чтобы у replenish была свободная ёмкость, либо сократите retain_connections_time, чтобы replenish запускался чаще.

Для transaction pooling (pool_mode = transaction) значение min_pool_size выше, чем pool_size / 2, обычно указывает на то, что пул мал: большинство соединений должно быть доступно для выдачи клиентам, а не пришпилено к минимуму. Для session pooling эвристика не работает: min_pool_size = pool_size это нормальная настройка, чтобы держать всё состояние сессий в горячем виде.

p99 latency растёт без видимой причины

Симптом. p99 клиентской задержки растёт, p50 остаётся ровным. Размер пула выглядит нормально, в логах нет ошибок.

Причина. Фаза 4 anticipation (direct handoff) держит клиентов в ожидании возврата до query_wait_timeout - 500 ms, но возвраты приходят медленнее, чем клиент готов ждать. Ожидающие либо получают соединение через прямую передачу, либо проваливаются в путь создания по истечении бюджета. Проверьте create_fallback в SHOW POOL_SCALING: если он ненулевой и растёт, клиенты уходят по таймауту.

Исправление. Сверьте query_wait_timeout с p95 длительности запросов, обслуживаемых этим пулом. Два случая.

  • query_wait_timeout сопоставим или короче p95: клиент не успевает дождаться освобождения соединения, каждый такой клиент платит полный бюджет ожидания. Либо поднимайте query_wait_timeout, либо увеличивайте pool_size, чтобы освобождения приходили чаще.
  • query_wait_timeout заметно длиннее p95, но create_fallback всё равно растёт: пул слишком мал для своей нагрузки, все соединения заняты дольше p95 из-за конкуренции. Поднимайте pool_sizemax_db_connections, если задан координатор).
  • create_fallback нулевой, а antic_notify растёт пропорционально обороту пула: direct handoff работает, возвраты перехватываются, лишних connect() нет. Задержка приходит откуда-то ещё: PostgreSQL, сеть, клиентская сторона. Проверьте SHOW STATS avg_wait_time.

max_db_connections исчерпан, клиенты получают ошибки

Симптом. Клиенты видят ошибки вида all server connections to database 'X' are in use (max=80, ...). pg_doorman_pool_coordinator_total{type="exhaustions"} растёт.

Причина. Все пять фаз координатора провалились: try-acquire не взял слот, выселять нечего, ожидание истекло, а резерв либо исчерпан, либо reserve_pool_size = 0.

Исправление. Пройдите по фазам по порядку.

  1. Сравните current и max_db_conn в SHOW POOL_COORDINATOR. Если current стабильно стоит на лимите, ваша нагрузка его превышает. Либо поднимайте max_db_connections, либо ищите разогнавшийся пул.
  2. Посмотрите на частоту evictions. Если она нулевая или близка к нулю, eviction не помогает: либо idle-соединения каждого пула моложе min_connection_lifetime (по умолчанию 30 000 ms), либо все остальные пулы стоят на своём min_guaranteed_pool_size. Понизьте min_connection_lifetime, если у вас очень короткие запросы и вы явно хотите более быстрый cross-pool rebalance, или увеличьте max_db_connections.
  3. Сравните reserve_used и reserve_size. Если резерв занят полностью, поднимите reserve_pool_size. Если резерв пустой, а exhaustions всё равно происходят, значит резерв не настроен (reserve_pool_size = 0). Задайте его, чтобы поглощать всплески.
  4. Посмотрите SHOW POOLS для базы. Если у одного пользователя sv_idle намного больше, чем у остальных, этот пользователь копит соединения; задайте min_guaranteed_pool_size, чтобы защитить мелких пользователей от вытеснения, либо понизьте pool_size накопителю.

Фаза ожидания координатора как узкое место

Симптом. Клиенты в среднем платят 3 секунды задержки, ровно совпадающие с reserve_pool_timeout.

Причина. Фаза C wait стабильно истекает по таймауту. Попадание в Фазу C при включённом reserve-first означает, что на момент прихода вызывающего резерв уже был полностью занят, — единственный путь вперёд это возврат от соседа. Либо база действительно стоит на лимите, и соединения не возвращаются; либо reserve_pool_size = 0, и wait отрабатывает до конца, прежде чем клиент получит хоть какой-то ответ.

Исправление. Понизьте reserve_pool_timeout, чтобы получать быстрый отказ, либо задайте reserve_pool_size > 0, чтобы Фаза R / Фаза D обрабатывала переполнение в рамках того же пути получения соединения, вообще не заходя в Фазу C.

reserve_used остаётся ненулевым, а пул выглядит пустым

Симптом. SHOW POOL_COORDINATOR показывает reserve_used = 4 (или любое ненулевое значение) при том, что SHOW POOLS показывает cl_waiting = 0, низкий cl_active и current < max_db_conn. Резервный пул выглядит занятым «призраками».

Причина. В сборках до reserve→main upgrade reserve-permit держался за своим backend, пока тот не доживёт до min_connection_lifetime и retain-цикл не поймает его idle. При устойчивом клиентском трафике last_used() на backend обновлялся быстрее, чем min_connection_lifetime, и permit никогда не отпускался.

Исправление. В текущих сборках это решается автоматически: retain task каждые retain_connections_time (по умолчанию 30 секунд) запускает повышение reserve-permit-ов до main. Для каждого reserve-backend в пуле без давления permit меняется с reserve на main, если в основном семафоре есть свободное место. Gauge reserve_used должен упасть до нуля в течение одного retain-цикла.

Если reserve_used всё равно не уходит, значит пул либо под устойчивым давлением (повышение пропускается, когда пул под давлением — и это правильно, иначе ожидающий клиент тут же заберёт освободившийся слот), либо current == max_db_connections (нет main-слота, который можно забрать). Оба случая означают, что база честно исчерпана; лечение — больше ёмкости, а не обход.

Burst gate как узкое место даже при низком трафике

Симптом. Частота gate_waits заметная, но частота creates низкая, а inflight_creates всё время стоит на лимите.

Причина. connect() к бэкенду медленный. Каждый create держит слот по нескольку секунд; даже с двумя слотами вы создадите всего около 2 / connect_seconds соединений в секунду.

Исправление. Разберитесь, почему connect() медленный со стороны PostgreSQL (слишком много SCRAM-итераций, конкуренция за блокировки pg_authid, медленный DNS, SSL handshake). Когда connect() станет быстрым, gate перестанет быть узким местом. Поднятие scaling_max_parallel_creates лишь маскирует проблему и перенесёт storm на PostgreSQL. Сначала разбирайтесь, потом поднимайте лимит.

Один пользователь забирает весь резерв

Симптом. reserve_acquisitions_total продолжает расти. Большую часть резервов забирает один и тот же небольшой пользователь.

Причина. Пользователь ниже своего эффективного минимума (max(user.min_pool_size, min_guaranteed_pool_size)), и координатор не может удовлетворить этот минимум, не выселяя соединения у соседей. Каждый клиентский запрос от этого пользователя попадает в Фазу R (reserve-first), как только база заполнена, и захватывает reserve-permit — арбитр даёт starving-пользователям наивысший балл, поэтому они выигрывают грант. Более глубокий вопрос: почему пользователю постоянно нужны свежие соединения? Либо его pool_size слишком мал, чтобы поглотить собственную нагрузку, либо его трафик всплесковый, и резерв делает ровно то, для чего нужен.

Исправление. Три варианта, выбор зависит от настоящей причины:

  • Если pool_size пользователя действительно мал для постоянной нагрузки, поднимите pool_size и (если нужно) max_db_connections, чтобы увеличенный пул туда поместился.
  • Если у пользователя высокий эффективный минимум, который координатор не может удовлетворить, понизьте тот параметр, который реально задаёт границу (проверьте оба: user.min_pool_size и min_guaranteed_pool_size).
  • Если трафик действительно всплесковый, а резервы ловят всплески, оставьте как есть. Кратковременное использование резерва это и есть его назначение.

Клиенты получают wait timeout, а не database exhausted

Симптом. Под давлением координатора клиенты видят ошибку wait timeout (во внутренних логах pg_doorman она записана как PoolError::Timeout(Wait)), но pg_doorman_pool_coordinator_total{type="exhaustions"} стоит на нуле. Координатор так и не объявил исчерпание, при этом каждый клиент уходит по таймауту.

Причина. query_wait_timeout короче, чем reserve_pool_timeout. Клиент сдаётся раньше, чем фаза ожидания координатора успевает завершиться. Счётчик exhaustions никогда не увеличивается, потому что координатор в итоге получает permit для запроса, у которого ожидающего клиента больше нет.

Исправление. Либо поднимите query_wait_timeout выше reserve_pool_timeout плюс типичное время connect(), либо понизьте reserve_pool_timeout (в пределах нижней границы из раздела тюнинга). На старте валидатор конфига выдаёт предупреждение для такой конфигурации; реагируйте на него.

Восстановление пула после рестарта PostgreSQL

Симптом. Мастер PostgreSQL перезапустился (failover, краш, плановое окно). Видна толпа клиентов, бьющих в burst gate, inflight_creates стоит на лимите, частота creates_started резко растёт.

Причина. Когда pg_doorman замечает непригодный бэкенд (через server_idle_check_timeout или упавший запрос), он повышает reconnect-эпоху пула и сразу сливает все idle-соединения. Горячий путь recycle становится пустым, и каждый пришедший после слива клиент идёт по маршруту anticipation, burst gate, connect. При scaling_max_parallel_creates = 2 каждый пул прирастает максимум двумя соединениями за раз, и скорость ограничена задержкой connect() к PostgreSQL.

Как выглядит здоровое восстановление. В первые несколько секунд inflight_creates = 2 непрерывно, creates_started быстро растёт, burst_gate_waits растёт вместе с ней. По мере того как новые соединения начинают циркулировать, anticipation_wakes_notify разгоняется, а create_fallback перестаёт расти: direct handoff ловит возвраты внутри клиентского query_wait_timeout и новые connect() уже не нужны. За время pool_size / 2 × connect() секунд пул возвращается в норму.

Исправление. Обычно никакого. Bounded burst gate делает свою работу: гасит connection storm к восстанавливающемуся primary, чтобы тот не получил сотню одновременных коннектов на и без того перегруженный postmaster. Если connect() действительно быстрый (< 50 ms), и у вашего max_connections есть запас, поднимите scaling_max_parallel_creates до 4 или 8, чтобы сократить восстановление, но оставайтесь в пределах жёсткого потолка из раздела тюнинга.

Глоссарий

  • ограничитель всплесков (bounded burst gate) — ограничитель на уровне пула, пропускающий не более scaling_max_parallel_creates одновременных вызовов connect() к бэкенду. Задачи сверх лимита встают в очередь на прямую передачу и ожидают завершения чужого создания, пока слот не освободится.
  • permit координатора (coordinator permit) — разрешение на удержание одного слота в общем лимите координатора. Может быть основным (main) или резервным (reserve). Освобождается, когда backend-соединение физически уничтожается (а не когда оно возвращается в idle-очередь); при освобождении слот возвращается либо в основной, либо в резервный семафор.
  • эффективный минимум — пол для eviction у user-пула, равный max(user.min_pool_size, pool.min_guaranteed_pool_size). Координатор защищает именно столько соединений на пользователя от выселения соседями.
  • прямая передача (direct handoff) — механизм доставки в Фазе 4. При возврате соединение отправляется напрямую старейшему зарегистрированному ожидающему, минуя idle-очередь. Соединение достаётся ровно одному адресату, конкуренции нет.
  • Фаза R (reserve-first) — короткое замыкание координатора, вставленное между Фазой A и Фазой B. Когда база заполнена, а в резерве есть место, Фаза R выдаёт reserve-permit напрямую через арбитра, вместо того чтобы закрывать соседний backend или парковать клиента в Фазе C.
  • жёсткий потолок Фазы 4 — каждый checkout выбирает случайный потолок в диапазоне 300–500 ms. Верхняя граница времени ожидания Фазы 4 anticipation, независимо от query_wait_timeout. Не настраивается. Случайный разброс предотвращает синхронные таймауты, вызывающие лавину запросов в ограничитель всплесков.
  • арбитр резерва (reserve arbiter) — отдельный фоновый процесс, владеющий reserve-permit-ами. Запросы на reserve ранжируются по паре (starving, queued_clients) и разгружаются из приоритетной очереди так, чтобы самые нуждающиеся пользователи получали permit первыми.
  • повышение из резерва в основной (reserve → main upgrade) — периодическая бухгалтерская перестановка. Когда idle-backend удерживает reserve-permit, а в основном семафоре есть запас, retain task забирает основной permit, возвращает резервный слот и переключает тип permit. Без переподключения.
  • излишек над минимумом (spare_above_min) — текущий размер пула минус эффективный минимум, где текущий размер — это количество всех соединений пула (активные + idle вместе, а не только idle). Координатор использует это значение, чтобы выбрать жертву eviction: пул с самым большим излишком теряет соединение первым. Само соединение, чтобы его можно было выселить, всё равно должно быть свободным — излишек выбирает пул, а не конкретное соединение.
  • голодающий пользователь (starving) — user-пул, у которого текущее число соединений ниже эффективного минимума. Арбитр резерва даёт starving-пользователям абсолютный приоритет перед обычными.
  • пул под давлением (under pressure) — состояние, при котором все permit-ы пула заняты, то есть каждый слот сейчас используется. Retain task пропускает повышение/закрытие на таких пулах, потому что иначе слот просто перейдёт ожидающему клиенту.
  • порог прогрева (warm threshold)pool_size × scaling_warm_pool_ratio / 100. Ниже этого размера пул пропускает упреждающее ожидание и идёт сразу в connect(). Выше — упреждающее ожидание активно, и пул пытается ловить возвраты, прежде чем создавать новые backend-ы.

Patroni-assisted fallback

Когда pg_doorman работает на одной машине с PostgreSQL и подключён через unix socket, switchover в Patroni или аварийное падение PG оставляют doorman без бэкенда. Пока Patroni не закончит promote реплики или не перезапустит локальный PostgreSQL, все клиентские запросы падают.

Patroni-assisted fallback перекрывает этот промежуток. Когда локальный PostgreSQL перестаёт отвечать, pg_doorman запрашивает Patroni REST API, выбирает другого члена кластера и направляет новые соединения туда. Существующие соединения к мёртвому бэкенду закрываются при штатном recycle.

Это краткосрочная мера. Она перекрывает 10-30 секунд, пока Patroni завершает свой failover. Когда Patroni восстановит локальный PostgreSQL (как реплику нового primary или как восстановленный primary), pg_doorman сам вернётся к локальному socket.

Быстрый старт

Рекомендуемая схема — pg_doorman рядом с PostgreSQL на одной машине, ходит к нему через unix socket. Patroni REST API тоже на localhost, поэтому fallback включается одной строкой в [general]:

general:
  patroni_api_urls: ["http://localhost:8008"]

Каждый пул подхватывает это автоматически. Когда unix socket перестаёт отвечать, pg_doorman запрашивает /cluster, выбирает кандидата по приоритету sync_standby > replica > leader и направляет новые соединения на выбранный хост, пока локальный PostgreSQL не вернётся в строй. Значения по умолчанию: cooldown 30s, HTTP-таймаут 5s, TCP-таймаут 5s, lifetime fallback-соединений 30s. Переопределить их можно через параметры настройки.

Когда это помогает

Плановый switchover. DBA запускает patroni switchover --candidate node2. Patroni промотирует node2, затем останавливает PostgreSQL на node1. Между остановкой и тем, как Patroni перезапустит node1 как реплику node2, doorman на node1 не имеет бэкенда. С включённым fallback doorman подключается к node2 за 1-2 TCP round trip.

Аварийное падение. PostgreSQL на node1 убит OOM killer. Patroni ещё не обнаружил сбой. doorman получает connection refused на unix socket, запрашивает Patroni API и подключается к sync_standby (вероятному следующему лидеру).

Когда это не помогает

Падение машины. Если вся машина недоступна, doorman мёртв вместе с ней. Для этого сценария нужна внешняя маршрутизация (HAProxy, patroni_proxy, DNS failover, VIP).

Ошибки аутентификации. Если PostgreSQL отклоняет credentials doorman, бэкенд жив. Fallback не активируется.

Как это работает

Штатный режим:
  клиент --unix--> doorman --unix--> PostgreSQL (локальный)

Fallback:
  клиент --unix--> doorman --TCP---> PostgreSQL (удалённый, из /cluster)
                      |
                      +-- GET /cluster --> Patroni API
  1. Doorman пробует локальный unix socket.
  2. Connection refused или socket error: doorman помечает локальный backend как недоступный на fallback_cooldown (по умолчанию 30 секунд).
  3. doorman отправляет GET /cluster ко всем Patroni URL из конфига параллельно и берёт первый успешный ответ.
  4. Из списка members doorman выбирает кандидата с наивысшим приоритетом: сначала sync_standby, потом replica, потом любой другой. TCP connect ко всем кандидатам запускается параллельно; если sync_standby отвечает, он выбирается немедленно, обходя replica.
  5. Новое соединение попадает в пул со сниженным lifetime (по умолчанию 30 секунд, совпадает с fallback_cooldown). На него действуют все обычные правила пула: лимиты coordinator, idle timeout, recycle.
  6. Последующие соединения в рамках cooldown идут к тому же fallback-хосту напрямую, без повторного запроса к Patroni API.
  7. Когда cooldown истекает, doorman снова пробует локальный socket. Если работает — штатный режим. Если нет — цикл повторяется.

Write-запросы на реплике

Если fallback-хост — реплика, которая ещё не промотирована, write-запросы получат ошибку от PostgreSQL:

ERROR: cannot execute INSERT in a read-only transaction

Read-запросы работают нормально. При типичном switchover sync_standby промотируется раньше, чем doorman обнаруживает отказ, поэтому большинство write-запросов проходит. В худшем случае write-ошибки длятся до истечения сниженного lifetime (30 секунд), после чего следующее соединение через свежий /cluster найдёт нового master.

Конфигурация

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

pools:
  mydb:
    pool_mode: transaction
    server_host: "/var/run/postgresql"
    server_port: 5432

    # Адреса Patroni REST API. Укажите минимум 2 для отказоустойчивости.
    # Первый ответивший URL выигрывает; порядок не важен.
    patroni_api_urls:
      - "http://10.0.0.1:8008"
      - "http://10.0.0.2:8008"
      - "http://10.0.0.3:8008"

TOML-эквивалент:

[pools.mydb]
pool_mode = "transaction"
server_host = "/var/run/postgresql"
server_port = 5432

patroni_api_urls = [
    "http://10.0.0.1:8008",
    "http://10.0.0.2:8008",
    "http://10.0.0.3:8008",
]

Параметры настройки

Все параметры опциональны и имеют разумные значения по умолчанию.

ПараметрПо умолчаниюОписание
fallback_cooldown"30s"Сколько локальный backend остаётся помеченным как недоступный после ошибки соединения. В течение этого окна все новые соединения идут на fallback-хост.
patroni_api_timeout"5s"HTTP-таймаут запросов к Patroni API. Действует на каждый URL; так как все URL опрашиваются параллельно, реальный таймаут равен этому значению, а не умноженному на количество URL.
fallback_connect_timeout"5s"Таймаут TCP connect к fallback-кандидатам. Действует на всю пачку параллельных connect, не на каждый member отдельно.
fallback_lifetime= fallback_cooldownLifetime fallback-соединений. Короче штатного server_lifetime, чтобы doorman быстро вернулся к локальному backend после восстановления.

Что указывать в patroni_api_urls

Перечислите адреса Patroni REST API ваших узлов кластера. Endpoint /cluster на любом узле Patroni возвращает полную топологию кластера, поэтому даже одного URL достаточно для перечисления всех members.

Два и более URL рекомендуется: если первый URL указывает на ту же машину что и мёртвый PostgreSQL, он тоже не ответит. doorman опрашивает все URL параллельно и берёт первый ответ.

Prometheus-метрики

МетрикаТипОписание
pg_doorman_patroni_api_requests_totalcounterКоличество запросов /cluster
pg_doorman_fallback_connections_totalcounterСоздано fallback-соединений
pg_doorman_patroni_api_errors_totalcounterНеудачные запросы /cluster (все URL недоступны)
pg_doorman_fallback_activegauge1, пока локальный backend в cooldown и пул использует fallback
pg_doorman_fallback_hostgaugeТекущий активный fallback-хост (1 = активен). Labels: pool, host, port
pg_doorman_fallback_cache_hits_totalcounterПовторное использование кешированного fallback-хоста без запроса к Patroni API
pg_doorman_patroni_api_duration_secondshistogramВремя запроса /cluster

Активные транзакции

Если PostgreSQL падает во время транзакции клиента, клиент получает ошибку соединения. doorman не переносит незавершённые транзакции на fallback-хост — клиент должен выполнить retry.

Новые запросы от этого и других клиентов автоматически идут через fallback.

Эксплуатационные заметки

Credentials. Все узлы кластера должны принимать те же username и password, которые использует doorman. Patroni-кластеры обычно разделяют pg_hba.conf через bootstrap-конфигурацию, но это не гарантировано. Убедитесь, что fallback-узлы принимают настроенные credentials.

TLS. Fallback-соединения используют тот же server_tls_mode, что и локальный backend. Если локальный backend идёт через unix socket (без TLS), fallback TCP-соединения тоже пойдут без TLS. Настройте server_tls_mode явно, если fallback-соединения должны быть зашифрованы.

DNS. Используйте IP-адреса в patroni_api_urls, а не hostname. Неудача DNS-резолва во время failover добавляет задержку и может привести к полному отказу запроса.

standby_leader. В standby-кластерах Patroni используется роль standby_leader. doorman обрабатывает её как «other» (наименьший приоритет, после sync_standby и replica). Для большинства развёртываний это корректно.

Связь с patroni_proxy

patroni_proxy и Patroni-assisted fallback решают разные задачи.

patroni_proxy — TCP-балансировщик, разворачивается рядом с клиентскими приложениями. Маршрутизирует соединения к нужному узлу PostgreSQL по роли (leader, sync, async). Не пулит соединения.

Patroni-assisted fallback — встроен в pooler doorman, который разворачивается рядом с PostgreSQL. Обрабатывает ситуацию, когда локальный backend умер и doorman нуждается во временной альтернативе. Пулит соединения.

В рекомендуемой архитектуре (patroni_proxy → pg_doorman → PostgreSQL) fallback сохраняет read-трафик на уровне doorman при падении локального backend, не затрагивая маршрутизацию patroni_proxy.

Patroni Proxy

patroni_proxy — TCP-прокси для кластеров PostgreSQL под управлением Patroni. Делает одну вещь: балансирует TCP-соединения и обрабатывает failover в Patroni-кластере.

Обзор

В отличие от HAProxy, patroni_proxy не рвёт существующие соединения при изменении топологии кластера. Когда реплика добавляется или удаляется, переключаются только затронутые соединения — остальные продолжают работать.

Возможности

Управление соединениями без простоя

patroni_proxy не разрывает существующие соединения при изменении upstream-конфигурации. Это важно для долгих транзакций и приложений с активным использованием connection pool.

Hot-обновления upstream

  • Автоматическое обнаружение членов кластера через Patroni REST API (endpoint /cluster).
  • Периодический опрос с настраиваемым интервалом (cluster_update_interval).
  • Немедленное обновление через HTTP API (endpoint /update_clusters).
  • Перечитывание конфигурации сигналом SIGHUP без перезапуска.

Маршрутизация по ролям

Маршрутизация соединений на основе ролей узлов PostgreSQL:

РольОписание
leaderУзел primary/master
syncСинхронные standby-реплики
asyncАсинхронные реплики
anyЛюбой доступный узел

Умная балансировка нагрузки

  • Стратегия Least Connections для распределения соединений между бэкендами.
  • Счётчики соединений сохраняются при обновлениях кластера.
  • Автоматическое исключение узлов с тегом noloadbalance.

Учёт лага репликации

  • Настраиваемый max_lag_in_bytes для каждого порта.
  • Автоматическое отключение клиентов, когда лаг реплики превышает порог.
  • Влияет только на соединения к репликам (у leader лага нет).

Фильтрация по состоянию members

  • В качестве бэкендов используются только члены со state: "running".
  • Члены в состояниях starting, stopped, crashed автоматически исключаются.
  • Динамические изменения состояния обрабатываются при периодических обновлениях.

Рекомендуемая архитектура развёртывания

Двухслойная схема:

graph TD
    App1[Приложение A] --> PP(patroni_proxy<br/>TCP-балансировка)
    App2[Приложение B] --> PP
    App3[Приложение C] --> PP

    PP --> D1(pg_doorman<br/>пулинг)
    PP --> D2(pg_doorman<br/>пулинг)
    PP --> D3(pg_doorman<br/>пулинг)

    D1 --> PG1[(PostgreSQL<br/>leader)]
    D2 --> PG2[(PostgreSQL<br/>sync-реплика)]
    D3 --> PG3[(PostgreSQL<br/>async-реплика)]
  • pg_doorman разворачивается рядом с серверами PostgreSQL -- он обеспечивает пулинг соединений, кэширование prepared statements и оптимизации на уровне протокола, которым выгодна низкая задержка до базы.
  • patroni_proxy разворачивается рядом с клиентскими приложениями -- он отвечает за TCP-маршрутизацию и failover, распределяя соединения по кластеру без накладных расходов пулинга.

Каждый компонент решает свою задачу: pg_doorman — пулинг и протокол, patroni_proxy — маршрутизация и failover.

Конфигурация

Пример patroni_proxy.yaml:

# Интервал обновления кластера в секундах (по умолчанию: 3)
cluster_update_interval: 3

# Адрес HTTP API для health checks и ручных обновлений (по умолчанию: 127.0.0.1:8009)
listen_address: "127.0.0.1:8009"

clusters:
  my_cluster:
    # Endpoint'ы Patroni API (несколько -- для отказоустойчивости)
    hosts:
      - "http://192.168.1.1:8008"
      - "http://192.168.1.2:8008"
      - "http://192.168.1.3:8008"
    
    # Опционально: TLS-конфигурация для Patroni API
    # tls:
    #   ca_cert: "/path/to/ca.crt"
    #   client_cert: "/path/to/client.crt"
    #   client_key: "/path/to/client.key"
    #   skip_verify: false
    
    ports:
      # Соединения к primary/master
      master:
        listen: "0.0.0.0:6432"
        roles: ["leader"]
        host_port: 5432
      
      # Read-only соединения к репликам
      replicas:
        listen: "0.0.0.0:6433"
        roles: ["sync", "async"]
        host_port: 5432
        max_lag_in_bytes: 16777216  # 16MB

Параметры конфигурации

ПараметрПо умолчаниюОписание
cluster_update_interval3Интервал в секундах между опросами Patroni API
listen_address127.0.0.1:8009Адрес для HTTP API
clusters.<name>.hosts--Список endpoint'ов Patroni API
clusters.<name>.tls--Опциональная TLS-конфигурация для Patroni API
clusters.<name>.ports.<name>.listen--Адрес для listener этого порта
clusters.<name>.ports.<name>.roles--Список разрешённых ролей
clusters.<name>.ports.<name>.host_port--Порт PostgreSQL на бэкенд-хостах
clusters.<name>.ports.<name>.max_lag_in_bytes--Максимальный лаг репликации (опционально)

Использование

Запуск patroni_proxy

# Запуск с файлом конфигурации
patroni_proxy /path/to/patroni_proxy.yaml

# С debug-логированием
RUST_LOG=debug patroni_proxy /path/to/patroni_proxy.yaml

Перечитывание конфигурации

Перечитать конфигурацию без перезапуска (добавить или удалить порты, обновить hosts):

kill -HUP $(pidof patroni_proxy)

Ручное обновление кластера

Запустить немедленное обновление всех членов кластера через HTTP API:

curl http://127.0.0.1:8009/update_clusters

HTTP API

EndpointМетодОписание
/update_clustersGETЗапустить немедленное обновление всех членов кластера
/GETHealth check (возвращает "OK")

Сравнение с HAProxy + confd

Возможностьpatroni_proxyHAProxy + confd
Сохранение соединений при обновленииДаНет (reload разрывает соединения)
Hot-обновления upstreamНативныеТребуется confd + reload
Учёт лага репликацииВстроенТребуются кастомные проверки
Сложность конфигурацииОдин YAMLНесколько конфигов
Потребление ресурсовЛёгкийПроцессы HAProxy + confd
Маршрутизация по ролямНативнаяТребуются кастомные шаблоны

Сборка

# Сборка release-бинарника
cargo build --release --bin patroni_proxy

# Запуск тестов
cargo test --test patroni_proxy_bdd

Диагностика

No backends available

Если видите предупреждения вроде no backends available, проверьте:

  1. Patroni API доступен с хоста patroni_proxy.
  2. У членов кластера state: "running".
  3. Роли в конфигурации совпадают с реальными ролями members.
  4. Если используется max_lag_in_bytes -- проверьте текущий лаг реплик.

Соединения разрываются после обновления

С patroni_proxy этого происходить не должно. Если соединения всё-таки разрываются:

  1. Проверьте, действительно ли бэкенд-хост был удалён из кластера.
  2. Убедитесь, что порог max_lag_in_bytes не превышается.
  3. Включите debug-логирование, чтобы увидеть детальный жизненный цикл соединений.

Binary upgrade

Обновление pg_doorman без разрыва клиентских соединений. Старый процесс передаёт idle-клиентов новому через Unix socket -- клиенты продолжают работу на том же TCP-соединении без reconnect.

Быстрый старт

# 1. Заменить бинарник на диске
cp pg_doorman_new /usr/bin/pg_doorman

# 2. Запустить upgrade
kill -USR2 $(pgrep pg_doorman)

# 3. Проверить: старый PID исчез, клиенты на месте
pgrep pg_doorman   # новый PID

Или через admin-консоль:

UPGRADE;

Как работает upgrade

                        SIGUSR2
                           |
                           v
               +-----------------------+
               | 1. Валидация конфига  |
               |    (pg_doorman -t)    |   -- fail --> отмена, продолжаем
               +-----------+-----------+
                           |
                           v
               +-----------------------+
               | 2. Запуск нового      |
               |    socketpair()       |
               |    inherit-fd         |
               |    readiness pipe     |   -- ожидание до 10с
               +-----------+-----------+
                           |
             +-------------+-------------+
             |                           |
             v                           v
  +---------------------+    +---------------------+
  | СТАРЫЙ процесс      |    | НОВЫЙ процесс       |
  |                     |    |                     |
  | 3. Idle-клиенты     |    | migration_receiver  |
  |    сериализация     +--->+    восстановление   |
  |    dup() + SCM_RIGHTS    |    запуск client    |
  |                     |    |    handle()         |
  | 4. Клиенты в tx     |    |                     |
  |    дождаться COMMIT +--->+ Принимает новые     |
  |    мигрировать      |    | соединения          |
  |                     |    |                     |
  | 5. Shutdown timer   |    +---------------------+
  |    опрос 250мс      |
  |    выход при 0      |
  +---------------------+

Фаза 1: Валидация конфига

Текущий бинарник перезапускается с флагом -t и конфигом. Если валидация проваливается -- upgrade отменяется, старый процесс продолжает обслуживать трафик. В логах баннер:

!!!  BINARY UPGRADE ABORTED - SHUTDOWN CANCELLED  !!!
!!!  FIX THE CONFIGURATION BEFORE ATTEMPTING BINARY UPGRADE AGAIN  !!!
!!!  THE SERVER WILL CONTINUE RUNNING WITH THE CURRENT BINARY  !!!

Фаза 2: Запуск нового процесса

Foreground mode:

  1. Создаётся Unix socketpair() для миграции клиентов.
  2. Listener fd передаётся дочернему процессу через --inherit-fd.
  3. Readiness pipe: родитель ждёт до 10 секунд байт от дочернего процесса. Дочерний пишет в pipe, когда начинает принимать соединения.
  4. Родитель закрывает свой listener -- новые соединения идут в дочерний процесс.

Daemon mode:

Запускается новый daemon-процесс. Старый закрывает listener. Миграция клиентов через socketpair не используется -- клиенты дренируются (получают error 58006 при истечении shutdown_timeout).

Фаза 3: Миграция idle-клиентов (foreground)

Когда установлен флаг MIGRATION_IN_PROGRESS, каждый idle-клиент (нет активной транзакции, нет pending deferred BEGIN, нет буферизованных данных на чтение) мигрирует:

  1. Сериализация: connection_id, secret_key, имя пула, username, server parameters, полный кэш prepared statements.
  2. dup() + SCM_RIGHTS: TCP socket fd дублируется и передаётся новому процессу через Unix socketpair.
  3. Восстановление: новый процесс пересоздаёт Client struct, подключает к нужному пулу и запускает handle().

Клиент не замечает миграции. Никакого reconnect, никакого error, никакой повторной аутентификации. TCP-соединение -- тот же физический socket.

Фаза 4: Дренирование in-transaction клиентов

Клиент внутри BEGIN ... COMMIT продолжает работать на старом процессе. Его серверное соединение остаётся живым. После завершения транзакции (COMMIT или ROLLBACK) клиент становится idle и мигрирует на следующей итерации цикла.

Deferred BEGIN (сервер ещё не выделен) тоже блокирует миграцию. Клиент должен отправить запрос (сбросив deferred BEGIN), затем COMMIT, и только потом мигрирует.

Фаза 5: Shutdown timer

Shutdown timer опрашивает CURRENT_CLIENT_COUNT каждые 250 мс. Когда все клиенты мигрировали или отключились -- старый процесс вызывает process::exit(0).

Если shutdown_timeout истекает раньше -- принудительный выход, оставшиеся соединения закрываются.

Во время миграции drain_all_pools() откладывается: in-transaction клиентам нужны их серверные соединения. Дренирование пулов начинается только после завершения миграции или сброса MIGRATION_IN_PROGRESS.

Prepared statements

Кэш prepared statements каждого клиента сериализуется при миграции:

  • Ключ statement (именованный или anonymous hash)
  • Hash запроса
  • Полный текст запроса
  • OID типов параметров

В новом процессе:

  1. Каждая запись регистрируется в pool-level shared cache (DashMap).
  2. Серверные бэкенды свежие -- на них нет prepared statements.
  3. При первом Bind к мигрированному statement pg_doorman прозрачно отправляет Parse на новый бэкенд. Клиент не видит дополнительного round-trip.

Ограничения:

  • Если client_prepared_statements_cache_size нового конфига меньше, чем количество entries у клиента -- лишние вытесняются (LRU). Оставшиеся работают нормально.
  • Anonymous prepared statements (Parse с пустым именем) переживают миграцию, но требуют повторного Parse перед Bind в новом процессе.
  • DEALLOCATE ALL после миграции очищает переданный кэш. Повторный Parse с тем же именем использует новый текст запроса.

TLS migration

По умолчанию TLS-клиенты не мигрируют -- зашифрованная сессия требует ключевой материал, который живёт внутри OpenSSL state machine. Такие клиенты дренируются при upgrade: соединение закрывается при истечении shutdown_timeout, клиент переподключается к новому процессу.

Opt-in фича tls-migration решает эту проблему. Патченный OpenSSL экспортирует symmetric cipher state, передаёт его вместе с fd через Unix socket, а новый процесс импортирует состояние и продолжает шифрование. Клиент не делает повторный TLS handshake.

Что экспортируется

Патч добавляет SSL_export_migration_state() и SSL_import_migration_state() в OpenSSL 3.5.5. Экспортируемые данные:

  • Версия TLS-протокола
  • ID cipher suite и tag length
  • Symmetric keys для чтения/записи (входные данные для AES key schedule, не развёрнутые)
  • IV (nonce) для чтения/записи
  • Sequence numbers для чтения/записи (по 8 байт)
  • Для TLS 1.3: server и client application traffic secrets

Этого достаточно для восстановления record layer в новом процессе и продолжения шифрования/дешифрования на том же TCP-соединении.

Сборка с TLS migration

cargo build --release --features tls-migration

Требует perl и patch в build-окружении. Vendored OpenSSL 3.5.5 собирается из исходников с наложенным патчем.

Offline-сборка (без доступа к интернету)

# Скачать tarball заранее
curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz

# Собрать с указанием пути
OPENSSL_SOURCE_TARBALL=./openssl-3.5.5.tar.gz \
  cargo build --release --features tls-migration

SHA-256 tarball'а проверяется автоматически.

Ограничения

  • Linux only. На macOS/Windows TLS migration не поддерживается (native-tls использует Security.framework / SChannel, не OpenSSL).
  • Одинаковые сертификаты. Старый и новый процесс должны использовать одни и те же tls_private_key и tls_certificate. Cipher state привязан к SSL_CTX, созданному из сертификата. Изменённые сертификаты приводят к ошибке импорта и отключению клиента.
  • FIPS несовместимо. Vendored OpenSSL не проходит FIPS-валидацию. Для FIPS используйте сборку без tls-migration (TLS-клиенты будут дренироваться вместо миграции).
  • Нет HSM/PKCS#11. Vendored OpenSSL собирается с no-engine.

Известные ограничения

  • TLS 1.3 KeyUpdate меняет ключи шифрования. Если любая сторона отправит KeyUpdate после экспорта cipher state, импортированные ключи станут невалидными — соединение упадёт с ошибкой AEAD.

    Поведение драйверов (проверено апрель 2026):

    ДрайверAuto KeyUpdate?Риск
    libpq (psql, pgbench)Нет — OpenSSL не отправляетНет
    asyncpg (Python)Нет — Python ssl = OpenSSLНет
    node-postgresНет — Node.js tls = OpenSSLНет
    Npgsql (.NET)Нет — SslStream без KeyUpdate APIНет
    pgjdbc (Java)Да — JSSE после ~128 GB (jdk.tls.keyLimits)Высокий
    tokio-postgres (rustls)Да — rustls при AEAD limitСредний
    PostgreSQL serverНет — renegotiation отключён, KeyUpdate не вызываетсяНет

    Java-клиенты: JSSE автоматически отправляет KeyUpdate после ~128 GB зашифрованных данных. Баг JDK-8329548 может вызвать шторм KeyUpdate сообщений. Для Java с долгоживущими high-throughput соединениями TLS migration может потерять соединения. Обходной путь: увеличить порог через jdk.tls.keyLimits в java.security, или отключить TLS между Java-клиентом и pg_doorman.

    Rust-клиенты с rustls: rustls ротирует ключи при лимитах AEAD (очень высокий порог, ~2^36 records для AES-GCM). Для PostgreSQL workloads практически недостижимо. Использование native-tls (OpenSSL) вместо rustls устраняет риск.

    Все OpenSSL-драйверы безопасны. OpenSSL явно не выполняет автоматический key update (openssl#23566).

  • SSL_pending данные не проверяются. Миграция происходит в idle point, где нет буферизованных данных приложения. Инвариант idle point это гарантирует, но явная проверка SSL_pending() не выполняется.

  • Привязка к OpenSSL 3.5.5. Патч модифицирует внутренние структуры OpenSSL (ssl_local.h, rec_layer_s3.c, ssl_lib.c). При обновлении OpenSSL нужно проверить и переложить патч на новую версию.

Сигналы

СигналПоведение
SIGUSR2Binary upgrade + graceful shutdown. Рекомендуемый для всех режимов.
SIGINTВ foreground + TTY (Ctrl+C): только shutdown, без upgrade. В daemon / non-TTY: binary upgrade (legacy-совместимость).
SIGTERMНемедленный выход. Транзакции обрываются. Все клиенты отключаются.
SIGHUPПеречитать конфигурацию без перезапуска. Без простоя.
UPGRADE (admin)Отправляет SIGUSR2 текущему процессу. Тот же эффект.

Legacy-поведение SIGINT: SIGINT запускает binary upgrade в daemon mode или без TTY (например, под systemd). В интерактивном терминале Ctrl+C останавливает процесс без запуска нового. Используйте kill -USR2 или UPGRADE в admin-консоли для binary upgrade в foreground mode.

Daemon vs foreground

ForegroundDaemon
Миграция клиентов через fd passingДа (socketpair)Нет
Idle-клиенты сохраняютсяДаНет (drain с 58006)
In-tx клиентыЗавершают tx, затем мигрируютЗавершают tx, затем 58006
Запуск нового процессаНаследует listener fdЗапускается независимо
Рекомендуется дляsystemd, контейнеры, k8sLegacy-установки

Для zero-downtime upgrade с миграцией клиентов запускайте в foreground mode. systemd управляет жизненным циклом процесса:

[Service]
Type=simple
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman.yaml
ExecReload=/bin/kill -SIGUSR2 $MAINPID

Конфигурация

shutdown_timeout

Максимальное время ожидания завершения транзакций перед принудительным закрытием соединений. Старый процесс завершается по истечении этого таймаута вне зависимости от оставшихся клиентов.

По умолчанию: 10 секунд.

Рекомендация для production с длинными аналитическими запросами: 30-60 секунд.

[general]
shutdown_timeout = 60000  # миллисекунды

Слишком маленькое значение -- риск убить активные транзакции. Слишком большое -- задержка выхода старого процесса при зависшем клиенте (например, idle-in-transaction). Выбирайте значение, покрывающее самую длинную ожидаемую транзакцию, с запасом.

tls_private_key / tls_certificate

Для TLS migration оба процесса (старый и новый) загружают одни и те же файлы. Если сертификат поменялся между версиями бинарника -- TLS-клиенты получат ошибку при импорте cipher state и будут отключены.

Ротацию сертификатов делайте через SIGHUP (reload конфига) до binary upgrade.

prepared_statements_cache_size

Pool-level кэш prepared statements. Напрямую на миграцию не влияет, но pool cache в новом процессе должен быть достаточного размера для entries от мигрированных клиентов.

client_prepared_statements_cache_size

Per-client кэш prepared statements. Клиентский кэш сериализуется полностью при миграции. Если новый конфиг имеет меньшее значение -- LRU вытесняет лишние записи.

Мониторинг

Логи

Ключевые строки в логах при миграции:

INFO  Got SIGUSR2, starting binary upgrade and graceful shutdown
INFO  Validating configuration with: /usr/bin/pg_doorman -t pg_doorman.yaml
INFO  Configuration validation successful
INFO  Starting new process with inherited listener fd=5
INFO  New process signaled readiness
INFO  Client migration enabled
INFO  [user@pool #c42] client 10.0.0.1:51234 migrated to new process
INFO  waiting for 3 clients in transactions
INFO  All clients disconnected, shutting down
INFO  Migration sender finished

В новом процессе:

INFO  migration receiver: listening for migrated clients
INFO  [user@pool #c42] migrated client accepted from 10.0.0.1:51234
INFO  migration receiver done: migration socket closed
INFO  migration receiver: stopped

Prometheus-метрики

МетрикаЗначение при upgrade
pg_doorman_pools_clients{status="active"}Должна упасть до 0 на старом процессе
pg_doorman_pools_clients{status="idle"}Падает по мере миграции клиентов
pg_doorman_connection_count{type="total"}Старый: убывает, новый: растёт
pg_doorman_clients_prepared_cache_entriesПодтверждает перенос кэша

Admin-консоль

-- На новом процессе (старый отклоняет не-admin соединения)
SHOW POOLS;
SHOW CLIENTS;

Troubleshooting

Клиент получил "pooler is shut down now" (58006) вместо миграции

Ctrl+C в foreground mode. SIGINT в TTY = shutdown без upgrade. Используйте kill -USR2 или UPGRADE в admin-консоли.

Daemon mode. Daemon mode не использует fd-based миграцию. Клиенты дренируются. Переключитесь на foreground mode.

PG_DOORMAN_CI_SHUTDOWN_ONLY=1 установлен. Эта переменная окружения принудительно включает shutdown-only mode (используется в CI-тестах). Уберите её.

Старый процесс не завершается

Длинная транзакция. Клиент застрял в BEGIN без COMMIT. Дождитесь shutdown_timeout или завершите транзакцию вручную.

Admin-соединения. Admin-соединения не мигрируются. Закройте admin-сессию на старом процессе.

Принудительный выход: kill -TERM <old_pid> отправляет SIGTERM.

TLS-соединение оборвалось после upgrade

Бинарник собран без --features tls-migration. TLS-клиенты дренируются вместо миграции. Пересоберите с feature flag.

Запуск не на Linux. TLS migration работает только на Linux.

Сертификат/ключ изменились. Старый процесс экспортировал cipher state, привязанный к старому сертификату. Используйте те же файлы для обоих процессов. Ротацию сертификатов делайте через SIGHUP до binary upgrade.

"TLS migration not available" в логах

Новый процесс получил миграционный payload с TLS-данными, но собран без --features tls-migration или запущен не на Linux. Клиент отключается. Пересоберите новый бинарник с feature flag.

"migration channel not ready" в логах

Канал MIGRATION_TX ещё не инициализирован. Новый процесс не завершил запуск, когда клиент попытался мигрировать. Клиент повторит попытку на следующей idle-итерации (через миллисекунды).

"migration channel send failed" в логах

Канал миграции переполнен (capacity: 4096). Возможно при одновременной миграции тысяч клиентов. Клиент повторит попытку на следующей idle-итерации.

"prepare_migration failed" в логах

Raw fd клиента недоступен или dup() не удался. Возможные причины: исчерпание файловых дескрипторов, или клиент подключился через code path, который не сохраняет raw fd. Проверьте ulimit -n.

Совместимость с клиентскими библиотеками: Библиотеки вроде github.com/lib/pq или Go database/sql могут потребовать настройки для обработки reconnect при получении error 58006 (для клиентов в daemon mode или застрявших дольше shutdown_timeout). См. issue.

Чек-лист перед production

Перед выкатом binary upgrade в production:

  • Запуск в foreground mode (не daemon) для fd-based миграции
  • shutdown_timeout покрывает самую длинную ожидаемую транзакцию (рекомендация: 30-60 секунд для OLTP, больше для аналитики)
  • Если используете TLS: сборка с --features tls-migration, оба процесса используют одинаковые файлы сертификата и ключа
  • Протестировать upgrade в staging: открыть сессию, отправить SIGUSR2, убедиться что сессия продолжает работать
  • В systemd unit есть ExecReload=/bin/kill -SIGUSR2 $MAINPID
  • Мониторинг логов на ошибки миграции после первого production upgrade
  • Подтвердить что старый процесс завершился (PID file или pgrep)
  • Проверить Prometheus-метрики: клиенты на новом процессе

Сигналы и перезагрузка

pg_doorman реагирует на четыре POSIX-сигнала: SIGHUP, SIGINT, SIGUSR2 и SIGTERM. Каждый делает одну конкретную вещь.

Краткая справка

СигналЭффектСуществующие соединенияКогда применять
SIGHUPПерезагрузить конфиг с диска.Сохраняются.Подкрутить пулы, ротировать серверные TLS-сертификаты, отредактировать pg_hba.conf.
SIGTERMПлавное завершение работы.Дренируются до простоя, затем закрываются.Остановка сервиса.
SIGUSR2Плавное обновление бинарника.Мигрируют в новый процесс.Замена бинарника без простоя.
SIGINTЗависит от TTY (см. ниже).По-разному.Ctrl+C при разработке; устарело для production.

Перезагрузка (SIGHUP)

kill -HUP $(pidof pg_doorman)

Перечитывает файл конфига и применяет изменения. Что перезагружается:

  • Определения пулов (добавлены, удалены, изменён размер).
  • Списки пользователей, пароли, блоки auth_query.
  • Правила pg_hba.conf (файл или встроенное содержимое).
  • Серверные TLS-сертификаты и CA-бандлы (подмена без блокировок; существующие TLS-соединения сохраняют исходный контекст).
  • Публичные ключи Talos и JWT.
  • Уровень логирования и формат логов.

Что не перезагружается:

  • general.host, general.port — слушающий сокет фиксируется при старте.
  • Клиентские TLS-сертификаты — нужен перезапуск процесса или используйте обновление бинарника.
  • Число рабочих потоков и параметры рантайма Tokio.

После перезагрузки SHOW CONFIG отображает новые значения. Существующие клиентские соединения не перепроверяются по новому pg_hba.conf — только новые соединения.

Плавное завершение работы (SIGTERM)

kill -TERM $(pidof pg_doorman)

pg_doorman:

  1. Прекращает принимать новые соединения.
  2. Закрывает простаивающие соединения с бэкендом.
  3. Логирует, сколько клиентов ещё в транзакциях.
  4. Ждёт до shutdown_timeout (по умолчанию 10 с), пока клиенты завершат работу.
  5. Выходит.

shutdown_timeout — жёсткий потолок. Клиенты, всё ещё находящиеся в транзакциях по его истечении, получают закрытие соединения.

general:
  shutdown_timeout: "30s"

Для systemd установите TimeoutStopSec больше, чем shutdown_timeout, чтобы systemd не отправил SIGKILL, пока pg_doorman ещё дренирует.

Обновление бинарника (SIGUSR2)

kill -USR2 $(pidof pg_doorman)

Рекомендуемый способ заменить бинарник, не теряя клиентов:

  1. Замените бинарник на диске новой версией.
  2. Отправьте SIGUSR2 работающему процессу.
  3. Текущий процесс порождает дочерний с новым бинарником, передаёт ему слушающий сокет и продолжает обслуживать существующих клиентов до их завершения.
  4. Новые клиенты сразу подключаются к дочернему процессу.
  5. Старый процесс выходит, когда последняя клиентская транзакция завершится (или по shutdown_timeout).

Дочерний процесс отправляет sd_notify MAINPID=<new_pid>, чтобы юниты systemd с Type=notify бесшовно обновили свой главный PID.

Полный протокол, миграцию TLS и откат смотрите в Binary Upgrade.

SIGINT (Ctrl+C)

SIGINT зависит от контекста:

  • На переднем плане с TTY (разработка, cargo run): только плавное завершение работы.
  • В режиме демона или без TTY (legacy production): запускает обновление бинарника и плавное завершение работы, как SIGUSR2.

Legacy-путь обновления через SIGINT существует ради обратной совместимости с инсталляциями, которые отправляют SIGINT из init-скриптов. Новые инсталляции должны явно использовать SIGUSR2 для обновления и SIGTERM для остановки.

Интеграция с systemd

pg_doorman поддерживает Type=notify. Поставляемый юнит pg_doorman.service запускает бинарник в основном процессе и уведомляет systemd через sd_notify:

[Service]
Type=notify
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.yaml
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=60
Restart=on-failure

sd_notify READY=1 отправляется после привязки слушающего сокета и инициализации пулов. sd_notify MAINPID=<child> отправляется при обновлении бинарника, чтобы systemd корректно отслеживал новый процесс.

Если переходите с Type=forking + --daemon, уберите --daemon и переключитесь на Type=notify — меньше движущихся частей и корректное отслеживание готовности. Старые инсталляции с --daemon продолжают работать, но не получают преимуществ sd_notify.

Режим демона

pg_doorman --daemon ответвляется в фон и пишет PID в daemon_pid_file (по умолчанию /tmp/pg_doorman.pid). Под systemd предпочитайте Type=notify вместо --daemon.

general:
  daemon_pid_file: "/var/run/pg_doorman.pid"

Куда дальше

  • Binary Upgrade — полный протокол обновления с миграцией TLS.
  • Troubleshooting — что проверить, когда перезагрузка не подхватывает изменения.
  • TLS — семантика перезагрузки серверных сертификатов по SIGHUP.

Диагностика

Это руководство помогает решить типичные проблемы при использовании PgDoorman.

Ошибки аутентификации при подключении к PostgreSQL

Симптом: PgDoorman запустился успешно, но клиенты получают ошибки аутентификации вроде password authentication failed при попытке выполнить запрос.

Если username пула совпадает с пользователем backend-PostgreSQL

PgDoorman по умолчанию использует passthrough authentication -- криптографическое доказательство клиента (MD5-хэш или SCRAM ClientKey) повторно используется для аутентификации в PostgreSQL. Убедитесь, что поле password в конфиге содержит именно тот хэш, что хранится в pg_authid / pg_shadow:

SELECT usename, passwd FROM pg_shadow WHERE usename = 'your_user';

Скопируйте хэш (например, md5... или SCRAM-SHA-256$...) в поле password конфига. Хэш должен совпадать с тем, что хранится в PostgreSQL (та же соль и количество итераций для SCRAM).

Если username пула отличается от backend-пользователя

Когда обращённый к клиенту username в PgDoorman не совпадает с реальной ролью PostgreSQL, passthrough работать не может -- нужны явные credentials:

users:
  - username: "app_user"              # имя для клиента
    password: "md5..."                # хэш для аутентификации клиента
    server_username: "pg_app_user"    # реальная роль в PostgreSQL
    server_password: "plaintext_pwd"  # plaintext-пароль для этой роли
    pool_size: 40

Это же касается JWT-аутентификации, где нет пароля для passthrough.

Как получить хэш пароля

Хэши паролей пользователей можно получить из PostgreSQL запросом: SELECT usename, passwd FROM pg_shadow;

Или используйте команду pg_doorman generate, которая получает их автоматически.

Файл конфигурации не найден

Симптом: PgDoorman завершается с ошибкой "configuration file not found".

Решение: укажите путь к файлу конфигурации явно:

pg_doorman /path/to/pg_doorman.yaml

По умолчанию PgDoorman ищет pg_doorman.toml в текущей директории.

Pool size слишком мал

Симптом: клиенты долго ждут или получают ошибки про слишком большое количество соединений.

Решение: увеличьте pool_size для затронутого пользователя или проверьте значения cl_waiting и maxwait в admin-команде SHOW POOLS. Если maxwait стабильно высокий, значит пул мал для вашей нагрузки.


Проблема осталась?

Если столкнулись с проблемой, которой здесь нет, откройте issue на GitHub.

Admin-команды

pg_doorman предоставляет admin-базу, совместимую с протоколом Postgres. Подключайтесь к тому же порту, что и обычные клиенты, но с dbname=pgdoorman и admin-учёткой из конфига:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman

Или через connection string psql:

psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman"

Admin-команды читаются через SHOW <subcommand> или выполняются голыми глаголами (PAUSE, RESUME, RECONNECT, RELOAD, SHUTDOWN, SET <param> = <value>).

Команды SHOW

КомандаНазначение
SHOW HELPСписок доступных команд.
SHOW CONFIGТекущая активная конфигурация. Только для чтения.
SHOW DATABASESПо одной строке на пул: host, port, database, размер пула, режим.
SHOW POOLSСнимок утилизации пула на пару user×database: idle/active/waiting клиенты, idle/active серверы.
SHOW POOLS_EXTENDEDSHOW POOLS плюс полученные/отправленные байты и среднее время ожидания.
SHOW POOLS_MEMORYУчёт памяти на пул для кэша prepared statements (клиентский и серверный).
SHOW POOL_COORDINATORСостояние Pool Coordinator на базу: текущие соединения, использование резерва, число вытеснений. См. Pool Coordinator.
SHOW POOL_SCALINGМетрики anticipation/burst: in-flight create-операции, ожидания на воротах, anticipation notifies/timeouts.
SHOW PREPARED_STATEMENTSЗакэшированные prepared statements на пул: hash, имя, текст запроса, число попаданий.
SHOW CLIENTSАктивные клиенты: ID, database, user, имя приложения, адрес, состояние TLS, счётчики transaction/query/error, возраст.
SHOW SERVERSАктивные соединения с бэкендом: ID сервера, PID бэкенда, database, user, TLS, состояние, счётчики transaction/query, попадания/промахи кэша prepare, байты.
SHOW CONNECTIONSЧисло соединений по типу: total, errors, TLS, plain, cancel.
SHOW STATSАгрегированная статистика на пару user×database: всего транзакций, запросов, времени, байт, средние.
SHOW LISTSСчётчики по категориям (databases, users, pools, clients, servers).
SHOW USERSСписок пользователей и их режимы пула.
SHOW AUTH_QUERYКэш auth_query: попадания/промахи/перезапросы, успехи/отказы аутентификации, ошибки исполнителя, счётчики динамических пулов.
SHOW SOCKETSСчётчики TCP- и Unix-сокетов по состоянию (только Linux — читает /proc/net/).
SHOW LOG_LEVELТекущий уровень логирования.
SHOW VERSIONВерсия pg_doorman.

SHOW POOL_COORDINATOR и SHOW POOL_SCALING не имеют аналогов в PgBouncer или Odyssey — они показывают внутренние механизмы pg_doorman.

Управляющие команды

КомандаЭффект
PAUSEПрекратить принимать новые клиентские запросы. Существующие клиенты завершают свои транзакции.
PAUSE <database>Поставить на паузу один пул.
RESUME / RESUME <database>Возобновить после PAUSE.
RECONNECT / RECONNECT <database>Принудительно пересоздать соединения с бэкендом (закрыть простаивающие, дренировать активные). Новые соединения берутся из PostgreSQL.
RELOADТо же, что и SIGHUP — перезагрузить конфиг с диска.
SHUTDOWNТо же, что и SIGTERM — плавное завершение работы.
KILL <database>Сбросить всех клиентов, подключённых к конкретному пулу.
SET log_level = '<level>'Изменить уровень логирования в рантайме (error, warn, info, debug, trace).

PAUSE/RESUME полезны при failover или окнах обслуживания. RECONNECT после ротации учётных данных в pg_authid гарантирует, что бэкенды используют новый пароль.

Чтение типового вывода

SHOW POOLS

database | user | cl_idle | cl_active | cl_waiting | sv_active | sv_idle | sv_used | maxwait
mydb     | app  | 12      | 4         | 0          | 4         | 36      | 0       | 0.0
  • cl_waiting > 0 означает, что клиенты застряли в ожидании бэкенда. Либо поднимите pool_size, либо проверьте медленные запросы.
  • sv_idle соответствует свободным бэкендам; sv_active — занятым; sv_used — зарезервированным координатором (см. ниже).
  • maxwait — самое долгое текущее ожидание в секундах. Если оно вырастает за query_wait_timeout, клиенты получают ошибки.

SHOW POOL_COORDINATOR

database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
mydb     | 80          | 78      | 16           | 2            | 142       | 18          | 0
  • evictions быстро растут: какой-то пользователь голодает раз за разом. Задайте или поднимите min_guaranteed_pool_size для этого пользователя.
  • reserve_acq высокий: всплески — норма, но возможно вы недооценили размер. Подумайте о повышении max_db_connections, а не о ставке на резерв.
  • exhaustions ненулевые: даже резерв был полным. Клиенты упёрлись в query_wait_timeout. Поднимите потолок.

Тонкости настройки см. в Pool Coordinator.

SHOW POOL_SCALING

user | database | inflight | creates | gate_waits | burst_gate_budget_ex | antic_notify | antic_timeout | create_fallback | replenish_def
app  | mydb     | 1        | 12345   | 87         | 3                    | 142          | 8             | 22              | 0
  • inflight — текущие создания соединений с бэкендом в процессе.
  • gate_waits растут: scaling_max_parallel_creates придушивает вас. Допустимо, если PostgreSQL под нагрузкой; поднимите, если PG может обработать больше параллельных вызовов connect().
  • Соотношение antic_notify и antic_timeout: высокий счётчик timeout означает, что упреждающее ожидание не успевает поймать возвращающееся соединение. Поднимите scaling_warm_pool_ratio, чтобы пул рос с опережением спроса.
  • create_fallback растёт — срабатывает предзамена: соединения истекают раньше, чем естественным образом возвращаются.

См. Пул под нагрузкой → Параметры тюнинга.

Аутентификация

Admin-база использует учётку из general.admin_username и general.admin_password:

general:
  admin_username: "admin"
  admin_password: "change_me"

Admin-соединения не проходят через правила pg_hba.conf — они идут напрямую в admin-обработчик. Ограничивайте admin-доступ на сетевом уровне (listen_addresses, фаервол) или используйте Unix-сокеты.

Куда дальше

Структурированное JSON-логирование

pg_doorman пишет структурированные JSON-логи при запуске с --log-format Structured. Каждая строка — самодостаточный JSON-объект с timestamp, уровнем, местом в исходниках и сообщением, готовый к приёму в Loki, Elasticsearch, Datadog или любой пайплайн логов, ожидающий JSON.

Включение

Три равноценных способа:

# Флаг командной строки
pg_doorman -F Structured /etc/pg_doorman/pg_doorman.yaml

# Длинная форма
pg_doorman --log-format Structured /etc/pg_doorman/pg_doorman.yaml

# Переменная окружения
LOG_FORMAT=Structured pg_doorman /etc/pg_doorman/pg_doorman.yaml

По умолчанию — Text (человекочитаемый). Флаг --log-format принимает значения Text, Structured или Debug; последнее на данный момент является алиасом для Text.

Формат вывода

{"timestamp":"2026-04-25T08:32:14.512Z","level":"INFO","file":"src/app/server.rs","line":357,"message":"Server is up at 0.0.0.0:6432"}
{"timestamp":"2026-04-25T08:32:14.514Z","level":"INFO","file":"src/pool/mod.rs","line":421,"message":"Pool 'mydb' initialized: 1 user, pool_size=40"}
{"timestamp":"2026-04-25T08:32:18.103Z","level":"WARN","file":"src/server/protocol_io.rs","line":189,"message":"Backend connection lost: connection reset by peer"}

Поля:

ПолеТипПримечания
timestampстрока RFC 3339UTC, точность до миллисекунд.
levelстрокаERROR, WARN, INFO, DEBUG, TRACE.
fileстрокаФайл исходника, который пишет лог.
lineцелоеНомер строки.
messageстрокаЧеловекочитаемое сообщение.

Вложенных полей и меток на событие нет — логгер pg_doorman сериализует обычные события макроса log в JSON. Для богатых метаданных (счётчики на пул, события на клиент) используйте Prometheus-метрики. См. Prometheus reference.

Уровень логирования

Задаётся через general.log_level в конфиге или переопределяется при старте:

general:
  log_level: "info"
pg_doorman -l debug -F Structured /etc/pg_doorman/pg_doorman.yaml

Изменение в рантайме через admin-базу:

SET log_level = 'debug';
SHOW LOG_LEVEL;

Это влияет только на текущий процесс. Чтобы изменения сохранялись, отредактируйте конфиг и выполните RELOAD или отправьте SIGHUP.

Рекомендуемый пайплайн

Для Kubernetes:

spec:
  containers:
    - name: pg_doorman
      image: ghcr.io/ozontech/pg_doorman:latest
      args:
        - "-F"
        - "Structured"
        - "/etc/pg_doorman/pg_doorman.yaml"
      env:
        - name: LOG_LEVEL
          value: "info"

Логи идут в stdout, рантайм контейнера их захватывает, ваш log shipper (Promtail, Fluent Bit, Vector) пересылает их как есть — JSON сохраняется на всём пути.

Для systemd:

[Service]
ExecStart=/usr/bin/pg_doorman -F Structured /etc/pg_doorman/pg_doorman.yaml
StandardOutput=journal
StandardError=journal

journalctl -u pg_doorman -o json возвращает JSON обратно.

Оговорки

  • Для production выбирайте Text (терминалы, syslog) или Structured (log shippers). Debug зарезервирован под будущее использование и сейчас равен Text.
  • file и line берутся из мест вызова макроса log. Они доступны в release-сборках, потому что pg_doorman поставляется с включённой отладочной информацией.
  • Логгер не включает trace-идентификаторы и корреляцию запросов. Для трассировки на запрос используйте SHOW CLIENTS и Prometheus-метрики.

Куда дальше

Перцентили задержек

pg_doorman измеряет задержки запросов и транзакций на пул, используя HDR Histogram. В Prometheus экспортируются четыре перцентиля: p50, p90, p95, p99.

Эта страница объясняет, откуда берутся числа и как их читать.

Что измеряется

Три серии задержек на пару user×database:

СерияЧто покрывает
query_histogramВремя от старта запроса до его завершения на бэкенде. Измеряет время выполнения PostgreSQL так, как его видит pg_doorman.
xact_histogramВремя от BEGIN (или первого оператора неявной транзакции) до COMMIT / ROLLBACK.
wait_histogramВремя, которое клиент провёл в ожидании, пока соединение с бэкендом не освободится.

wait_histogram — собственный вклад пула в задержку. Если p99 у wait_histogram высокий, а p99 у query_histogram низкий, узкое место — получение соединения, а не PostgreSQL.

Детали гистограммы

pg_doorman использует HDR Histogram с параметрами:

  • Максимальное значение: 10 минут (600 секунд).
  • Значащих цифр: 2 (около 0,1% относительной погрешности).

Расход памяти: около 10 KB на гистограмму. Три гистограммы на пару user×database — это ~30 KB на пул, что комфортно для сотен пулов.

По умолчанию горизонт отчёта — время жизни процесса. Гистограммы сбрасываются на SIGHUP (перезагрузка конфига) и при явном RECONNECT.

Odyssey использует TDigest, PgBouncer перцентили не экспортирует. HDR предпочтителен, когда вы знаете верхнюю границу (10 минут — щедрый запас для пула соединений); TDigest работает с неограниченными потоками.

Экспорт в Prometheus

# HELP pg_doorman_pools_queries_percentile Query latency percentiles in milliseconds
# TYPE pg_doorman_pools_queries_percentile gauge
pg_doorman_pools_queries_percentile{percentile="50",user="app",database="mydb"} 1.2
pg_doorman_pools_queries_percentile{percentile="90",user="app",database="mydb"} 4.7
pg_doorman_pools_queries_percentile{percentile="95",user="app",database="mydb"} 8.1
pg_doorman_pools_queries_percentile{percentile="99",user="app",database="mydb"} 24.5

# HELP pg_doorman_pools_transactions_percentile Transaction latency percentiles in milliseconds
# TYPE pg_doorman_pools_transactions_percentile gauge
pg_doorman_pools_transactions_percentile{percentile="50",user="app",database="mydb"} 3.8
# ... (90, 95, 99)

# HELP pg_doorman_pools_avg_wait_time Average client wait time in milliseconds
# TYPE pg_doorman_pools_avg_wait_time gauge
pg_doorman_pools_avg_wait_time{user="app",database="mydb"} 0.05

avg_wait_time — среднее, а не перцентиль (HDR для ожиданий тоже отслеживается, но сейчас экспортируется только среднее).

Чтение метрик

Здоровый пул

queries:    p50=1.2  p90=4.7   p95=8.1   p99=24.5
xacts:      p50=3.8  p90=11.2  p95=18.5  p99=42.7
wait avg:   0.05ms

p99 укладывается в 20× от p50 — типично для OLTP-нагрузок с редкими медленными запросами. Время ожидания — микросекунды, пул не является узким местом.

Пул под давлением

queries:    p50=1.5   p90=4.9   p95=8.5   p99=25.0
xacts:      p50=215   p90=1850  p95=2400  p99=4900
wait avg:   180ms

С задержкой запросов всё в порядке — PostgreSQL здоров. Но транзакции медленные, а время ожидания 180 мс. Клиенты выстраиваются в очередь за бэкендами. Проверьте SHOW POOLS на cl_waiting > 0 и SHOW POOL_COORDINATOR на вытеснения или исчерпания. Вероятное лечение: поднять pool_size или max_db_connections. См. Pool Coordinator.

Один медленный пользователь

user "fast_app":   queries p99=12   xacts p99=35
user "report_job": queries p99=4500 xacts p99=8000

report_job тянет общую базу вниз. С включённым Pool Coordinator медленные транзакции report_job приводят к тому, что под давлением он первым отдаёт свои соединения (вытеснение смещено по p95-времени транзакций). Без Coordinator выделите report_job его собственный min_guaranteed_pool_size, чтобы он не голодил fast_app.

Grafana

Пример запроса задержки запросов по перцентилю:

pg_doorman_pools_queries_percentile{database="mydb"}

Пример алерта: p99 запросов выше 100 мс в течение 5 минут:

pg_doorman_pools_queries_percentile{percentile="99"} > 100

Пример алерта на насыщение очереди:

pg_doorman_pools_avg_wait_time > 50

JSON дашборда лежит в директории grafana/ проекта.

Оговорки

  • Перцентили считаются на пул, а не на запрос. pg_doorman не скажет вам, какой именно запрос медленный, — для этого используйте pg_stat_statements в PostgreSQL.
  • HDR-гистограммы хранят значения, а не события. Один и тот же запрос, выполненный 100 тысяч раз, добавляет 100 тысяч сэмплов; частоту сэмплирования настроить нельзя.
  • Экспорт всех четырёх перцентилей на серию сделан намеренно — экспортировать сырые корзины гистограммы в Prometheus было бы значительно тяжелее и редко полезно.

Куда дальше

  • Admin Commands — читать перцентили напрямую через SHOW POOLS_EXTENDED.
  • Prometheus reference — полный список метрик с метками.
  • Pool Pressure — диагностические рецепты, когда перцентили выглядят неправильно.
  • Benchmarks — эталонные распределения перцентилей под нагрузкой.

Настройки

Формат конфигурационного файла

pg_doorman поддерживает два формата конфигурационного файла:

  • YAML (.yaml, .yml) — основной и рекомендуемый формат для новых конфигураций.
  • TOML (.toml) — поддерживается для обратной совместимости с уже существующими конфигурациями.

Формат определяется автоматически по расширению файла. Оба формата поддерживают одни и те же опции конфигурации и могут использоваться взаимозаменяемо.

Пример конфигурации YAML (рекомендуется)

general:
  host: "0.0.0.0"
  port: 6432
  admin_username: "admin"
  admin_password: "admin"

pools:
  mydb:
    server_host: "localhost"
    server_port: 5432
    pool_mode: "transaction"
    users:
      - username: "myuser"
        password: "mypassword"
        pool_size: 40

Пример конфигурации TOML (legacy)

[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "admin"

[pools.mydb]
server_host = "localhost"
server_port = 5432
pool_mode = "transaction"

[[pools.mydb.users]]
username = "myuser"
password = "mypassword"
pool_size = 40

Команда generate

Команда generate может выводить конфигурацию в любом из форматов. Формат определяется по расширению выходного файла. По умолчанию сгенерированная конфигурация содержит подробные inline-комментарии, поясняющие каждый параметр.

# Сгенерировать конфигурацию YAML (рекомендуется)
pg_doorman generate --output config.yaml

# Сгенерировать конфигурацию TOML (для обратной совместимости)
pg_doorman generate --output config.toml

# Сгенерировать полную справочную конфигурацию без подключения к PG
pg_doorman generate --reference --output config.yaml

# Сгенерировать справочную конфигурацию с комментариями на русском
pg_doorman generate --reference --ru --output config.yaml

# Сгенерировать конфигурацию без комментариев (только сериализация)
pg_doorman generate --no-comments --output config.yaml
ФлагОписание
--no-commentsОтключить inline-комментарии в сгенерированной конфигурации (по умолчанию комментарии включены)
--referenceСгенерировать полную справочную конфигурацию с примерами значений, без подключения к PostgreSQL
--russian-comments, --ruГенерировать комментарии на русском языке для быстрого старта
--format, -fФормат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение

Подключаемые файлы

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

include:
  files:
    - "pools.yaml"
    - "users.toml"

Человекочитаемые значения

pg_doorman поддерживает человекочитаемые форматы для значений продолжительности и размера в байтах, сохраняя обратную совместимость с числовыми значениями.

Формат продолжительности

Значения продолжительности можно задавать как:

  • Простые числа: интерпретируются как миллисекунды (например, 5000 = 5 секунд)
  • Строка с суффиксом:
    • ms — миллисекунды (например, "100ms")
    • s — секунды (например, "5s" = 5000 миллисекунд)
    • m — минуты (например, "5m" = 300000 миллисекунд)
    • h — часы (например, "1h" = 3600000 миллисекунд)
    • d — дни (например, "1d" = 86400000 миллисекунд)

Примеры:

general:
  # Все эти варианты эквивалентны (3 секунды):
  # connect_timeout: 3000      # обратная совместимость (миллисекунды)
  # connect_timeout: "3s"      # человекочитаемый формат
  # connect_timeout: "3000ms"  # явные миллисекунды
  connect_timeout: "3s"
  idle_timeout: "10m"        # 10 минут
  server_lifetime: "1h"      # 1 час

Формат размера в байтах

Значения размера в байтах можно задавать как:

  • Простые числа: интерпретируются как байты (например, 1048576 = 1 MB)
  • Строка с суффиксом (регистр не важен):
    • B — байты (например, "1024B")
    • K или KB — килобайты (например, "1K" или "1KB" = 1024 байта)
    • M или MB — мегабайты (например, "1M" или "1MB" = 1048576 байт)
    • G или GB — гигабайты (например, "1G" или "1GB" = 1073741824 байт)

Примечание: используются двоичные префиксы (1 KB = 1024 байта, не 1000 байт).

Примеры:

general:
  # Все эти варианты эквивалентны (256 MB):
  # max_memory_usage: 268435456  # обратная совместимость (байты)
  # max_memory_usage: "256MB"    # человекочитаемый формат
  # max_memory_usage: "256M"     # короткая форма
  max_memory_usage: "256MB"
  unix_socket_buffer_size: "1MB" # 1 MB
  worker_stack_size: "8MB"       # 8 MB

Общие настройки

host

Хост, на котором сервер будет принимать соединения (только TCP v4).

По умолчанию: "0.0.0.0".

port

Порт для входящих соединений.

По умолчанию: 5432.

backlog

TCP backlog для входящих соединений. При значении ноль в качестве TCP backlog используется значение max_connections.

По умолчанию: 0.

max_connections

Максимальное число клиентов, которые могут одновременно подключиться к пулеру. При достижении лимита:

  • Клиент, подключающийся без SSL, получит ожидаемую ошибку (код: 53300, сообщение: sorry, too many clients already).
  • Клиент, подключающийся через SSL, увидит сообщение о том, что сервер не поддерживает протокол SSL.

По умолчанию: 8192.

max_concurrent_creates

Максимальное число серверных соединений, которые могут создаваться параллельно в одном пуле. Параметр использует семафор для ограничения параллельного создания соединений, что заметно повышает производительность при холодном старте и пиковых сценариях.

Большие значения ускоряют прогрев пула, но могут увеличить нагрузку на сервер PostgreSQL во время штормов соединений. Меньшие значения дают более плавное создание соединений.

По умолчанию: 4.

tls_mode

Режим TLS для входящих соединений. Может принимать одно из следующих значений:

  • allow — TLS-соединения разрешены, но не обязательны. pg_doorman попытается установить TLS-соединение, если клиент его запросит.
  • disable — TLS-соединения запрещены. Все соединения устанавливаются без шифрования TLS.
  • require — TLS-соединения обязательны. pg_doorman принимает только соединения, использующие TLS-шифрование.
  • verify-full — TLS-соединения обязательны, и pg_doorman проверяет клиентский сертификат. Этот режим обеспечивает максимальный уровень безопасности.

По умолчанию: "allow".

tls_ca_cert

Файл с CA-сертификатом для проверки клиентского сертификата. Обязателен, когда tls_mode установлен в verify-full.

По умолчанию: None.

tls_private_key

Путь к файлу приватного ключа для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_certificate.

По умолчанию: None.

tls_certificate

Путь к файлу сертификата для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_private_key.

По умолчанию: None.

tls_rate_limit_per_second

Ограничение числа одновременных попыток создания TLS-сессии. Любое значение, отличное от нуля, означает, что клиенты должны проходить через очередь для установки TLS-соединения. В некоторых случаях это необходимо, чтобы запустить приложение, которое открывает много соединений при старте (так называемый «горячий старт»).

По умолчанию: 0.

daemon_pid_file

Включение этого параметра активирует режим демона. Закомментируйте, если хотите запускать pg_doorman в foreground с флагом -d.

По умолчанию: "/tmp/pg_doorman.pid".

syslog_prog_name

Если задан, pg_doorman начинает отправлять сообщения в syslog (через /dev/log или /var/run/syslog). Закомментируйте, если хотите логировать в stdout.

По умолчанию: None.

log_client_connections

Логировать подключения клиентов для мониторинга.

По умолчанию: true.

log_client_disconnections

Логировать отключения клиентов для мониторинга.

По умолчанию: true.

worker_threads

Число worker-потоков Tokio runtime (потоков ОС) для обслуживания клиентских соединений. Производительность масштабируется линейно до числа ядер CPU. Также определяет число шардов для внутренних concurrent hash maps (worker_threads * 4, округлённо до ближайшей степени двойки, минимум 4). В Kubernetes задавайте этот параметр явно — автоматическое определение CPU может вернуть число ядер хоста, а не лимит контейнера.

По умолчанию: 4.

worker_cpu_affinity_pinning

Привязывать каждый worker-поток к отдельному ядру CPU (sched_setaffinity). Отключается, если доступно меньше 3 ядер.

По умолчанию: false.

tokio_global_queue_interval

Настройки Tokio runtime. Управляет тем, как часто шедулер проверяет глобальную очередь задач. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.

По умолчанию: not set (uses tokio's default).

tokio_event_interval

Настройки Tokio runtime. Управляет тем, как часто шедулер проверяет внешние события (I/O, таймеры). Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.

По умолчанию: not set (uses tokio's default).

worker_stack_size

Настройки Tokio runtime. Задаёт размер стека для worker-потоков. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.

По умолчанию: not set (uses tokio's default).

max_blocking_threads

Настройки Tokio runtime. Задаёт максимальное число потоков для блокирующих операций. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.

По умолчанию: not set (uses tokio's default).

connect_timeout

Максимальное время ожидания при установке нового соединения с сервером PostgreSQL. Если соединение не удаётся установить за это время, попытка прерывается. Аналог server_connect_timeout из PgBouncer.

По умолчанию: 3000 (3 sec).

query_wait_timeout

Максимальное время ожидания клиентом серверного соединения, когда пул полностью занят. Если за это время серверное соединение не освобождается, клиент получает ошибку. Аналог query_wait_timeout из PgBouncer.

По умолчанию: 5000 (5 sec).

idle_timeout

Закрывать серверное соединение, которое простаивает (не выдано ни одному клиенту) дольше этого значения. Применяется только к соединениям, обслужившим хотя бы один клиентский запрос. Прогретые или дополненные соединения, никогда не выдававшиеся клиенту, под действие idle_timeout не попадают — они закрываются только по истечении server_lifetime. Каждое соединение получает джиттер ±20%, чтобы избежать синхронных массовых закрытий. Установите 0, чтобы отключить. Аналог server_idle_timeout из PgBouncer.

По умолчанию: 600000 (10 min).

server_lifetime

Максимальный возраст серверного соединения. Когда соединение превышает этот возраст и переходит в idle, оно закрывается на ближайшем цикле retain. Активные транзакции не прерываются. Применяется ко всем соединениям, включая прогретые, которые никогда не выдавались клиенту. Каждое соединение получает джиттер ±20%, чтобы избежать thundering herd. Установите 0, чтобы отключить. Аналог server_lifetime из PgBouncer.

По умолчанию: 1200000 (20 min).

retain_connections_time

Интервал проверки и закрытия idle-соединений, превысивших idle_timeout или server_lifetime. Задача retain выполняется периодически с этим интервалом и удаляет просроченные соединения.

По умолчанию: 30000 (30 sec).

retain_connections_max

Максимальное число idle-соединений, закрываемых за один цикл retain. При значении 0 все idle-соединения, превысившие idle_timeout или server_lifetime, закрываются немедленно. При положительном значении за цикл по всем пулам закрывается не более указанного числа соединений.

Этот параметр управляет агрессивностью закрытия idle-соединений в pg_doorman. При значении по умолчанию 3 за цикл retain закрывается до 3 соединений, что обеспечивает контролируемую очистку. Если нужна более быстрая очистка просроченных соединений, установите 0 (без ограничений), чтобы закрывать все просроченные соединения за каждый цикл retain.

По умолчанию: 3.

server_idle_check_timeout

Время, после которого idle-серверное соединение должно быть проверено перед выдачей клиенту. Это помогает обнаружить мёртвые соединения, возникшие из-за рестарта PostgreSQL, сетевых проблем или серверных idle-таймаутов.

Когда соединение простояло в пуле дольше этого таймаута, pg_doorman отправляет минимальный запрос (;), чтобы проверить, что соединение живо, прежде чем выдать его клиенту. Если проверка не проходит, соединение отбрасывается и берётся новое.

Установите 0, чтобы отключить проверку (не рекомендуется для production-окружений с возможной сетевой нестабильностью или рестартами PostgreSQL).

По умолчанию: 60s (60 seconds).

server_round_robin

Управляет тем, какое idle-серверное соединение выбирается для следующей транзакции. false (LRU): переиспользует самое недавно возвращённое соединение. Держит горячими меньше соединений, лучше для локальности shared buffer в PostgreSQL. true (Round Robin): равномерно ротирует все idle-соединения. Аналог server_round_robin из PgBouncer.

По умолчанию: false.

sync_server_parameters

В transaction mode каждая транзакция может попасть на разный бэкенд. При sync_server_parameters = true pg_doorman отслеживает изменения фиксированного набора параметров сессии и проигрывает их через SET на каждом новом бэкенде перед выполнением транзакции. Отслеживаемые параметры: client_encoding, DateStyle, TimeZone, standard_conforming_strings, application_name. Другие команды SET (например, statement_timeout, work_mem) НЕ отслеживаются. Каждый checkout стоит дополнительного roundtrip. Если вам нужна только видимость application_name в pg_stat_activity, используйте параметр application_name на уровне пула.

По умолчанию: false.

tcp_so_linger

По умолчанию pg_doorman отправляет RST вместо того, чтобы держать соединение открытым долгое время.

По умолчанию: 0.

tcp_no_delay

TCP_NODELAY для отключения алгоритма Нэйгла ради более низкой задержки.

По умолчанию: true.

tcp_keepalives_count

Число неподтверждённых TCP keepalive probes, после которого соединение считается мёртвым и закрывается.

По умолчанию: 5.

tcp_keepalives_idle

Keepalive включён по умолчанию и переопределяет дефолты ОС.

По умолчанию: 5.

tcp_keepalives_interval

Интервал в секундах между отдельными TCP keepalive probes после прохождения начального периода простоя (tcp_keepalives_idle).

По умолчанию: 5.

tcp_user_timeout

Задаёт опцию сокета TCP_USER_TIMEOUT для клиентских соединений (в секундах). Эта опция указывает максимальное время, в течение которого переданные данные могут оставаться неподтверждёнными, прежде чем TCP принудительно закроет соединение. Это помогает быстрее обнаруживать мёртвые клиентские соединения, чем keepalive probes, когда соединение активно отправляет данные, но удалённый конец стал недоступен (например, сбой сети, падение клиента).

При ненулевом значении, если данные остаются неподтверждёнными в течение указанного времени, соединение будет разорвано. Это особенно полезно, чтобы избежать задержек в 15–16 минут, вызванных TCP retransmission timeout, когда keepalive не помогает (например, при активной передаче данных).

Примечание: опция поддерживается только в Linux. На других ОС параметр игнорируется.

Установите 0, чтобы отключить (использовать значение по умолчанию ОС).

По умолчанию: 60.

unix_socket_buffer_size

Размер буфера для операций чтения и записи при подключении к PostgreSQL через unix-сокет.

По умолчанию: 1048576.

admin_username

Доступ к виртуальной admin-базе осуществляется по имени пользователя и паролю администратора.

По умолчанию: "admin".

admin_password

Доступ к виртуальной admin-базе осуществляется по имени пользователя и паролю администратора. Замените на ваш секрет.

По умолчанию: "admin".

prepared_statements

Переключатель для включения/отключения кеширования prepared statements.

По умолчанию: true.

prepared_statements_cache_size

Размер кеша prepared statements на уровне пула (общий для всех клиентов, подключающихся к одному пулу). Кеш хранит соответствие между хешем запроса и переписанным именем prepared statement.

По умолчанию: 8192.

client_prepared_statements_cache_size

Максимальное число prepared statements, кешируемых в одном клиентском соединении. Это механизм защиты от вредоносных или некорректно работающих клиентов, которые не вызывают DEALLOCATE и могут вызвать исчерпание памяти, создавая неограниченное число prepared statements в долгоживущих соединениях.

При достижении лимита самая старая запись вытесняется из клиентского кеша. Вытесненный statement по-прежнему может быть переиспользован позже, потому что кеш уровня пула (prepared_statements_cache_size) сохраняет соответствие запроса и имени на сервере.

Установите 0, чтобы отключить лимит (неограниченный размер кеша, надежда на вызов DEALLOCATE клиентом).

По умолчанию: 0 (unlimited).

message_size_to_be_stream

Когда сообщение DataRow PostgreSQL превышает этот порог, pg_doorman переключается в потоковый режим: данные пересылаются клиенту чанками по 4 KB вместо буферизации всего сообщения целиком. Это предотвращает OOM на запросах, возвращающих очень большие строки (например, таблицы с большими колонками bytea/text). Сам порог по умолчанию равен 1 MB.

По умолчанию: 1048576 (1 MB).

scaling_warm_pool_ratio

Доля прогретого пула в процентах (0–100). Когда размер пула ниже этого порога от max_size, новые соединения создаются немедленно. Выше порога пул сначала крутится через быстрые ретраи, затем переходит в event-driven цикл упреждающего ожидания, который ждёт возврата idle-соединения. Цикл ограничен оставшимся query_wait_timeout клиента минус резерв 500 ms на путь создания, поэтому он не может вытолкнуть вызывающего за его собственный дедлайн ожидания.

По умолчанию: 20.

scaling_fast_retries

Число быстрых ретраев через yield_now() для ожидания с низкой задержкой при выдаче соединений выше порога прогрева пула. Каждый ретрай занимает примерно 1–5 мкс. После исчерпания быстрых ретраев пул переходит в event-driven цикл упреждающего ожидания, ограниченный оставшимся query_wait_timeout клиента.

По умолчанию: 10.

scaling_max_parallel_creates

Ограничитель всплесков для создания соединений. Без этого лимита N параллельных вызывающих timeout_get, не нашедших соединения в idle-пуле, независимо инициируют backend-connect, создавая всплески thundering-herd под нагрузкой. С лимитом одновременно выполняется не больше указанного числа creates на пул; остальные кратко ждут на Notify и затем либо подхватывают только что возвращённое idle-соединение, либо занимают следующий слот для create. Значение по умолчанию 2 — компромисс между пропускной способностью и сглаживанием всплесков.

По умолчанию: 2.

max_memory_usage

Общий бюджет памяти для внутренних буферов, хранящих данные in-flight запросов по всем клиентским соединениям. При достижении лимита pg_doorman отклоняет новые запросы с ошибкой, пока существующие запросы не завершатся и не освободят свои буферы. Защищает процесс пулера от OOM при тяжёлой нагрузке или больших результирующих наборах.

По умолчанию: 268435456 (256 MB).

shutdown_timeout

При graceful shutdown (SIGTERM) pg_doorman ждёт до этого времени завершения in-flight транзакций перед принудительным закрытием соединений.

По умолчанию: 10000 (10 sec).

proxy_copy_data_timeout

Максимальное время ожидания операций копирования данных при проксировании, в миллисекундах.

По умолчанию: 15000 (15 sec).

server_tls_mode

Режим TLS для исходящих соединений к серверам PostgreSQL.

  • allow — Сначала пробовать без TLS; если сервер отказывает, повторить с TLS. Соответствует libpq sslmode=allow (по умолчанию).
  • disable — TLS не используется.
  • prefer — TLS используется, если сервер его поддерживает; иначе обычное соединение.
  • require — TLS обязателен, но сертификат сервера не проверяется.
  • verify-ca — TLS обязателен, и сертификат сервера проверяется по server_tls_ca_cert.
  • verify-full — TLS обязателен, сертификат проверяется, и hostname сервера должен совпадать с сертификатом.

По умолчанию: "allow".

server_tls_ca_cert

CA-сертификат для проверки сертификатов серверов PostgreSQL. Обязателен, когда server_tls_mode равен verify-ca или verify-full.

По умолчанию: None.

server_tls_certificate

Клиентский сертификат для mTLS с серверами PostgreSQL. Используйте в паре с server_tls_private_key.

По умолчанию: None.

server_tls_private_key

Приватный ключ для клиентского сертификата mTLS. Используйте в паре с server_tls_certificate.

По умолчанию: None.

hba

Список IP-адресов, с которых разрешено подключаться к pg-doorman.

По умолчанию: [].

pg_hba

Новый стиль контроля доступа клиентов в нативном формате pg_hba.conf PostgreSQL. Позволяет задавать тонкие правила доступа аналогично PostgreSQL: per-database, per-user, диапазоны адресов, требования TLS.

Указать general.pg_hba можно тремя способами:

  • Как многострочную строку с содержимым файла pg_hba.conf
  • Как объект с path, указывающий на файл на диске
  • Как объект с content, содержащий правила в виде строки

Примеры:

[general]
# Inline-содержимое (TOML-строка в тройных кавычках)
pg_hba = """
# type   database  user   address         method
host     all       all    10.0.0.0/8      md5
hostssl  all       all    0.0.0.0/0       scram-sha-256
hostnossl all      all    192.168.1.0/24  trust
"""

# Или загрузить из файла
# pg_hba = { path = "./pg_hba.conf" }

# Или встроить как однострочную строку
# pg_hba = { content = "host all all 127.0.0.1/32 trust" }

Поддерживаемые поля и методы:

  • Типы соединений: local, host, hostssl, hostnossl (TLS-зависимое сопоставление учитывается)
  • Сопоставитель базы: имя или all
  • Сопоставитель пользователя: имя или all
  • Адрес: CIDR-форма вроде 1.2.3.4/32 или ::1/128 (обязателен для правил, отличных от local)
  • Методы: trust, md5, scram-sha-256 (неизвестные методы парсятся, но трактуются проверяющим как «не разрешено»)

Приоритет и совместимость:

  • general.pg_hba имеет приоритет над устаревшим списком general.hba. Нельзя задавать оба одновременно; валидация конфигурации отклонит такую комбинацию.
  • Правила вычисляются по порядку; первое совпавшее правило определяет результат.

Поведение method = trust:

  • Когда совпавшее правило имеет trust, PgDoorman принимает соединение без запроса пароля. Это повторяет поведение PostgreSQL.
  • В частности, при срабатывании trust PgDoorman пропускает проверку пароля, даже если у пользователя сохранён пароль md5 или scram-sha-256. Это распространяется и на MD5, и на SCRAM-потоки.
  • TLS-ограничения правила соблюдаются: hostssl требует TLS, hostnossl запрещает TLS.

Доступ к admin-консоли:

  • Правила general.pg_hba применяются и к специальной admin-базе pgdoorman.
  • Это означает, что admin-доступ можно разрешить методом trust при наличии совпавшего правила, например:
    host  pgdoorman  admin  127.0.0.1/32  trust
    

Замечания и ограничения:

  • Поддерживается только минимальное подмножество pg_hba.conf, достаточное для большинства proxy-сценариев (type, database, user, address, method). Дополнительные опции (вроде clientcert) сейчас игнорируются.
  • Для методов аутентификации, отличных от trust, PgDoorman выполняет соответствующий challenge/response с клиентом.
  • Для потоков Talos/JWT/PAM, настроенных на уровне пула или пользователя, trust всё равно обходит запрос пароля у клиента; однако эти режимы могут использоваться, если trust не совпал.

pooler_check_query

Когда клиент отправляет ровно этот запрос как SimpleQuery, pg_doorman отвечает немедленно без перенаправления его в PostgreSQL. Полезно для health checks от балансировщиков нагрузки (HAProxy, в стиле pgbouncer SELECT 1) или keepalive-проб уровня приложения. Значение по умолчанию ; (пустой statement) — самая лёгкая возможная проверка.

По умолчанию: ";".

Настройки пула

Каждая запись в pool — это имя виртуальной базы данных, к которой может подключиться клиент pg-doorman.

[pools.exampledb] # Объявление базы данных 'exampledb'

server_host

Каталог с unix-сокетами или IPv4-адрес сервера PostgreSQL, обслуживающего этот пул.

Пример: "/var/run/postgresql" или "127.0.0.1".

По умолчанию: "127.0.0.1".

server_port

Порт, через который сервер PostgreSQL принимает входящие соединения.

По умолчанию: 5432.

server_database

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

application_name

Параметр application_name, отправляемый серверу при открытии соединения с PostgreSQL. Может быть полезен при настройке sync_server_parameters = false.

connect_timeout

Максимальное время на установку нового серверного соединения для этого пула, в миллисекундах. Если не задано, используется глобальная настройка connect_timeout.

По умолчанию: None (uses global setting).

idle_timeout

Закрывать idle-соединения в этом пуле, открытые дольше указанного значения, в миллисекундах. Если не задано, используется глобальная настройка idle_timeout.

По умолчанию: None (uses global setting).

server_lifetime

Закрывать серверные соединения в этом пуле, открытые дольше указанного значения, в миллисекундах. Применяется только к idle-соединениям. Если не задано, используется глобальная настройка server_lifetime.

По умолчанию: None (uses global setting).

pool_mode

Когда бэкенд-соединение возвращается в пул. transaction: освобождается после каждой транзакции. session: удерживается до отключения клиента. То же, что pool_mode в PgBouncer.

По умолчанию: "transaction".

log_client_parameter_status_changes

Логировать информацию о любой команде SET в лог.

По умолчанию: false.

cleanup_server_connections

Управляет тем, сбрасывает ли pg_doorman состояние сессии при возврате соединения в пул. Когда параметр включён и сессия была изменена, pg_doorman отправляет: RESET ROLE, плюс при необходимости RESET ALL (если использовался SET), DEALLOCATE ALL (если использовался PREPARE), CLOSE ALL (если открывались курсоры). Замечание: ROLLBACK для открытых транзакций выполняется всегда, независимо от этой настройки. Отключайте только если ваше приложение никогда не использует SET, prepared statements или курсоры и вы хотите сэкономить roundtrip на очистке.

По умолчанию: true.

scaling_warm_pool_ratio

Переопределяет глобальный scaling_warm_pool_ratio для этого пула. Если не задано, используется глобальная настройка.

scaling_fast_retries

Переопределяет глобальный scaling_fast_retries для этого пула. Если не задано, используется глобальная настройка.

max_db_connections

Жёсткий потолок суммарного числа серверных соединений к этой базе, разделяемый между всеми пользовательскими пулами. При достижении лимита и запросе нового соединения координатор сначала пытается вытеснить idle-соединения у других пользователей (учитывая их min_pool_size), затем ждёт возврата соединения и наконец откатывается к резервному пулу. Установите 0 (или опустите), чтобы отключить координацию — каждый пользовательский пул работает независимо, ограниченный только своим pool_size. Аналог max_db_connections из PgBouncer.

По умолчанию: 0 (disabled).

min_connection_lifetime

Минимальный возраст (в миллисекундах), которого должно достичь соединение, прежде чем оно сможет быть вытеснено координатором пула. Предотвращает циклическое переподключение между пользовательскими пулами, разделяющими одну базу: без этого порога idle-слот одного пользователя становится доступен для вытеснения сразу же, как только сосед запрашивает разрешение, и под устойчивой многопользовательской нагрузкой каждый пул отбирает слот у соседа каждые несколько секунд. Имеет значение только при max_db_connections > 0.

По умолчанию: 30000 (30 seconds).

reserve_pool_size

Число дополнительных соединений, разрешённых сверх max_db_connections в качестве крайней меры. Когда вытеснение не удаётся и соединения не возвращаются за reserve_pool_timeout, резервное соединение выдаётся запрашивающему с наивысшим приоритетом. Пользователи ниже своего min_pool_size получают абсолютный приоритет. Имеет значение только при max_db_connections > 0.

По умолчанию: 0.

reserve_pool_timeout

Сколько времени (в миллисекундах) ждать освобождения обычного соединения, прежде чем обратиться к резервному пулу. В этом окне координатор слушает возвраты соединений. Имеет значение только при max_db_connections > 0 и reserve_pool_size > 0.

По умолчанию: 3000 (3 seconds).

min_guaranteed_pool_size

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

Это отличается от min_pool_size (уровня пользователя): min_pool_size управляет прогревом и пополнением (проактивное создание соединений), тогда как min_guaranteed_pool_size влияет только на решения о вытеснении (никогда не создаёт соединения).

Эффективная защита для пользователя — max(user.min_pool_size, pool.min_guaranteed_pool_size). Установите 0 (или опустите), чтобы отключить защиту от вытеснения. Имеет значение только при max_db_connections > 0.

По умолчанию: 0 (no protection).

Настройки auth_query

Секция auth_query включает динамическую аутентификацию пользователей через запрос учётных данных у базы PostgreSQL во время подключения. Это позволяет pg_doorman аутентифицировать пользователей без статического перечисления их в конфигурационном файле.

pools:
  mydb:
    auth_query:
      query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
      user: "doorman_auth"
      password: "auth_password"

Существуют два режима работы:

  • Dedicated mode (задан server_user): все динамически аутентифицированные пользователи разделяют один пул соединений, который подключается к PostgreSQL как server_user. Это самая простая настройка, хорошо подходящая, когда всем пользователям нужен одинаковый бэкенд-доступ.
  • Passthrough mode (server_user не задан): каждый динамически аутентифицированный пользователь получает собственный пул соединений, подключающийся к PostgreSQL по его собственным учётным данным (MD5 pass-the-hash или SCRAM ClientKey passthrough). Это сохраняет идентичность каждого пользователя на бэкенде.

Статические пользователи (определённые в секции users) всегда проверяются первыми. auth_query используется только когда имя пользователя не найдено среди статических.

Рекомендация по безопасности

user, выполняющий auth-запросы, должен иметь доступ к хешам паролей (например, из pg_shadow). Не используйте суперпользователя для этой цели. Вместо этого создайте функцию SECURITY DEFINER, принадлежащую суперпользователю, и выделенную роль с минимальными привилегиями:

-- Создаём выделенную роль для auth-запросов
CREATE ROLE doorman_auth LOGIN PASSWORD 'strong_password';

-- Создаём функцию SECURITY DEFINER (выполняется с привилегиями владельца)
CREATE OR REPLACE FUNCTION pg_doorman_get_auth(p_usename TEXT)
RETURNS TABLE (usename name, passwd text)
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS
$$
  SELECT usename, passwd FROM pg_shadow WHERE usename = p_usename;
$$;

-- Выдаём execute только выделенной роли
REVOKE ALL ON FUNCTION pg_doorman_get_auth(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION pg_doorman_get_auth(TEXT) TO doorman_auth;

Затем используйте эту функцию в параметре query:

auth_query:
  query: "SELECT * FROM pg_doorman_get_auth($1)"
  user: "doorman_auth"
  password: "strong_password"

query

SQL-запрос для получения учётных данных. Должен возвращать колонку с именем passwd или password, содержащую MD5- или SCRAM-хеш. Если запрос возвращает ровно одну колонку, она используется независимо от имени. Любые лишние колонки игнорируются. В качестве плейсхолдера для имени пользователя используйте $1.

Пример: "SELECT passwd FROM pg_shadow WHERE usename = $1"

user

Имя пользователя PostgreSQL для executor-соединения, выполняющего auth-запросы.

password

Пароль executor-пользователя (открытым текстом). Может быть пустым, если сервер PostgreSQL использует аутентификацию trust для этого пользователя.

По умолчанию: "".

database

База для executor-соединений. Если не задана, используется имя пула.

По умолчанию: None (uses pool name).

workers

Число постоянных соединений с PostgreSQL, выделенных под выполнение SQL auth_query. Эти соединения открываются при старте и поддерживаются живыми. Они обрабатывают только поиск учётных данных — клиентский трафик данных идёт через отдельные соединения пула данных (pool_size). Увеличьте, если видите всплески задержки auth при высокой частоте подключений.

По умолчанию: 2.

server_user

Бэкенд-пользователь PostgreSQL для соединений с данными в dedicated mode. Когда задан, все динамически аутентифицированные пользователи разделяют один пул соединений, подключающийся под этим пользователем. Когда не задан, используется passthrough mode.

По умолчанию: None (passthrough mode).

server_password

Пароль server_user открытым текстом. Имеет смысл только когда задан server_user.

По умолчанию: None.

pool_size

Максимальное число бэкенд-соединений на каждый пул данных, создаваемый auth_query. Та же концепция, что и users[].pool_size для статически заданных пользователей. Сколько пулов будет создано, зависит от режима: server_user определяет, разделяют ли все динамические пользователи один пул или у каждого свой.

По умолчанию: 40.

min_pool_size

Минимальное число бэкенд-соединений, поддерживаемое в каждом пуле динамического пользователя в passthrough mode. Соединения прогреваются при первом создании пула и пополняются циклом retain. Установите 0, чтобы отключить (по умолчанию). Замечание: пулы с min_pool_size > 0 никогда не подвергаются garbage collection, и общее число бэкенд-соединений масштабируется как active_users × min_pool_size.

По умолчанию: 0.

cache_ttl

Максимальное время жизни кеша для успешно полученных учётных данных. Принимает строки продолжительности вроде "1h", "30m", "300s".

По умолчанию: "1h".

cache_failure_ttl

TTL кеша для записей «пользователь не найден» (negative cache). Предотвращает повторные запросы для несуществующих пользователей.

По умолчанию: "30s".

min_interval

Минимальный интервал между повторными запросами для одного и того же имени пользователя после неудачной аутентификации. Защищает бэкенд от избыточных запросов при попытках перебора.

По умолчанию: "1s".

Настройки пользователей пула

[pools.exampledb.users.0]
username = "exampledb-user-0" # Виртуальный пользователь, который может подключаться к этой виртуальной базе.

username

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

password

Верификатор пароля для аутентификации клиента. Поддерживает форматы MD5, SCRAM-SHA-256 и JWT. Хеши паролей можно скопировать напрямую из PostgreSQL: SELECT usename, passwd FROM pg_shadow.

auth_pam_service

PAM-сервис, отвечающий за авторизацию клиента. В этом случае pg_doorman игнорирует значение password.

server_username

Реальное имя пользователя PostgreSQL, используемое для подключения к серверу базы.

По умолчанию PgDoorman использует одно и то же username и для аутентификации клиента, и для серверных соединений, применяя passthrough authentication: криптографический материал из аутентификации клиента (MD5-хеш или SCRAM ClientKey) переиспользуется для аутентификации на бэкенде. Это устраняет потребность в server_password открытым текстом.

Passthrough mode (рекомендуется для пользователей с совпадающей идентичностью):

  • Опустите и server_username, и server_password
  • pg_doorman переиспользует доказательство аутентификации клиента для подключения к PostgreSQL
  • Для MD5: хеш из password используется напрямую
  • Для SCRAM: ClientKey извлекается из первой SCRAM-аутентификации клиента и кешируется
  • Требование: верификатор password должен совпадать с pg_authid на бэкенде (та же соль/итерации для SCRAM, тот же хеш для MD5)

Explicit credentials mode (когда идентичности различаются):

  • Установите server_username и server_password в реальные учётные данные PostgreSQL
  • server_password требует, чтобы server_username был задан
  • server_username без server_password допустим для аутентификации trust

server_password

Пароль открытым текстом для серверного пользователя PostgreSQL, указанного в server_username.

Когда server_password не задан и пользователь имеет право на passthrough (нет server_username или server_username равен username), PgDoorman использует passthrough authentication: криптографический материал из аутентификации клиента переиспользуется для бэкенд-соединения. Это убирает пароли открытым текстом из конфигурационных файлов.

server_password требует, чтобы server_username был задан.

pool_size

Максимальное число бэкенд-соединений с PostgreSQL для этого пользователя. В transaction mode соединения разделяются между клиентами, поэтому это значение обычно намного меньше числа клиентов. Аналог default_pool_size из PgBouncer, но настраивается per-user, а не глобально.

По умолчанию: 40.

min_pool_size

Минимальное число соединений, поддерживаемое в пуле для этого пользователя. Соединения прогреваются при старте (до первого цикла retain) и затем поддерживаются периодическим пополнением. Если задано, должно быть меньше или равно pool_size.

По умолчанию: None.

server_lifetime

Закрывать серверные соединения для этого пользователя, открытые дольше указанного значения, в миллисекундах. Применяется только к idle-соединениям. Если не задано, используется настройка server_lifetime пула.

По умолчанию: None (uses pool setting).

Passthrough Authentication

По умолчанию PgDoorman использует passthrough authentication: криптографическое доказательство клиента (MD5-хеш или SCRAM ClientKey) автоматически переиспользуется для аутентификации в PostgreSQL. Пароли открытым текстом в конфиге не нужны.

Задавайте server_username и server_password только когда бэкенд-пользователь PostgreSQL отличается от имени пользователя пула (например, переопределение имени пользователя или JWT-аутентификация):

users:
  - username: "app_user"              # имя для клиента
    password: "md5..."                # хеш для аутентификации клиента
    server_username: "pg_app_user"    # другой бэкенд-пользователь PostgreSQL
    server_password: "plaintext_pwd"  # пароль открытым текстом для этого пользователя

Настройки Prometheus

pg_doorman содержит экспортёр метрик Prometheus, который даёт детальное представление о производительности и поведении ваших пулов соединений. В этом документе описано, как включить и использовать экспортёр метрик Prometheus, а также какие метрики доступны.

Включение метрик Prometheus

Чтобы включить экспортёр метрик Prometheus, добавьте в конфигурационный файл следующее:

prometheus:
  enabled: true
  host: "0.0.0.0"  # Хост, на котором сервер метрик будет принимать соединения
  port: 9127       # Порт, на котором сервер метрик будет принимать соединения

Опции конфигурации

ОпцияОписаниеПо умолчанию
enabledВключить или отключить экспортёр метрик Prometheus.false
hostХост, на котором экспортёр метрик Prometheus будет принимать соединения."0.0.0.0"
portПорт, на котором экспортёр метрик Prometheus будет принимать соединения.9127

Настройка Prometheus

Добавьте следующий job в конфигурацию Prometheus, чтобы собирать метрики с pg_doorman:

scrape_configs:
  - job_name: 'pg_doorman'
    static_configs:
      - targets: ['<pg_doorman_host>:9127']

Замените <pg_doorman_host> на имя хоста или IP-адрес вашего инстанса pg_doorman.

Доступные метрики

pg_doorman экспортирует следующие метрики:

Системные метрики

МетрикаОписание
pg_doorman_total_memoryОбщий объём памяти, выделенный процессу pg_doorman, в байтах. Позволяет отслеживать потребление памяти приложением.

Метрики соединений

МетрикаОписание
pg_doorman_connection_countСчётчик новых соединений по типу, обработанных pg_doorman. Типы: 'plain' (нешифрованные соединения), 'tls' (шифрованные соединения), 'cancel' (запросы на отмену соединения), 'total' (сумма всех соединений).

Метрики сокетов (только Linux)

МетрикаОписание
pg_doorman_socketsСчётчик сокетов, используемых pg_doorman, по типу сокета. Типы: 'tcp' (IPv4 TCP-сокеты), 'tcp6' (IPv6 TCP-сокеты), 'unix' (Unix domain sockets), 'unknown' (сокеты нераспознанного типа). Доступно только в Linux.

Метрики пула

МетрикаОписание
pg_doorman_pools_clientsЧисло клиентов в пулах соединений по статусу, пользователю и базе. Значения статуса: 'idle' (подключён, но не выполняет запросы), 'waiting' (ждёт серверного соединения), 'active' (выполняет запросы). Помогает мониторить загрузку пулов и распределение клиентов.
pg_doorman_pools_serversЧисло серверов в пулах соединений по статусу, пользователю и базе. Значения статуса: 'active' (активно обслуживает клиентов) и 'idle' (доступен для новых соединений). Помогает мониторить доступность серверов и распределение нагрузки.
pg_doorman_pools_bytesОбщее число байт, переданных через пулы соединений, по направлению, пользователю и базе. Значения направления: 'received' (байты, полученные от клиентов) и 'sent' (байты, отправленные клиентам). Полезно для мониторинга сетевого трафика и выявления соединений с большим объёмом данных.

| pg_doorman_pool_size | Сконфигурированный максимальный размер пула на пользователя и базу. Полезен для расчёта оставшейся ёмкости пула вместе с pg_doorman_pools_servers. |

Метрики запросов и транзакций

МетрикаОписание
pg_doorman_pools_queries_percentileПерцентили времени выполнения запросов по пользователю и базе. Значения перцентилей: '99', '95', '90', '50' (медиана). Значения в миллисекундах. Помогает выявлять медленные запросы и тренды производительности по разным пользователям и базам.
pg_doorman_pools_transactions_percentileПерцентили времени выполнения транзакций по пользователю и базе. Значения перцентилей: '99', '95', '90', '50' (медиана). Значения в миллисекундах. Помогает мониторить производительность транзакций и выявлять долгие транзакции, которые могут влиять на производительность базы.
pg_doorman_pools_transactions_countСчётчик транзакций, выполненных в пулах соединений, по пользователю и базе. Помогает отслеживать объём транзакций и выявлять пользователей или базы с высоким темпом транзакций.
pg_doorman_pools_transactions_total_timeОбщее время, потраченное на выполнение транзакций в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить общую производительность транзакций и выявлять пользователей или базы с большим временем выполнения транзакций.
pg_doorman_pools_queries_countСчётчик запросов, выполненных в пулах соединений, по пользователю и базе. Помогает отслеживать объём запросов и выявлять пользователей или базы с высоким темпом запросов.
pg_doorman_pools_queries_total_timeОбщее время, потраченное на выполнение запросов в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить общую производительность запросов и выявлять пользователей или базы с большим временем выполнения запросов.
pg_doorman_pools_avg_wait_timeСреднее время ожидания клиентов в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить время ожидания клиентов и выявлять потенциальные узкие места.

Метрики auth_query

Эти метрики доступны только когда auth_query сконфигурирован для одного или нескольких пулов.

МетрикаОписание
pg_doorman_auth_query_cacheМетрики кеша auth_query по типу и базе. Типы: entries (текущее число закешированных учётных данных), hits (попадания в кеш с найденной валидной записью), misses (промахи в кеше, потребовавшие запроса к PostgreSQL), refetches (повторные запросы, вызванные ошибкой аутентификации с устаревшими учётными данными), rate_limited (попытки повторного запроса, ограниченные min_interval).
pg_doorman_auth_query_authРезультаты аутентификации auth_query по результату и базе. Результаты: success (успешная аутентификация) и failure (неверный пароль или несовпадение учётных данных).
pg_doorman_auth_query_executorМетрики executor auth_query по типу и базе. Типы: queries (всего запросов, выполненных к PostgreSQL для получения учётных данных) и errors (запросы, завершившиеся ошибкой подключения или выполнения).
pg_doorman_auth_query_dynamic_poolsМетрики жизненного цикла динамических пулов auth_query по типу и базе. Типы: current (сейчас активные динамические пулы), created (всего пулов создано с момента старта), destroyed (всего пулов, собранных garbage collection или удалённых на RELOAD). Имеет смысл только в passthrough mode.

Метрики серверов

МетрикаОписание
pg_doorman_servers_prepared_hitsСчётчик попаданий prepared statements в бэкендах баз по пользователю и базе. Помогает оценить эффективность prepared statements в снижении накладных расходов на парсинг запросов.
pg_doorman_servers_prepared_missesСчётчик промахов prepared statements в бэкендах баз по пользователю и базе. Помогает выявить запросы, которые могли бы выиграть от подготовки в prepared statements.

Метрики серверного TLS

Активны, если включён TLS к PostgreSQL (server_tls_mode != "disable").

МетрикаТипОписание
pg_doorman_server_tls_connectionsgauge per poolЧисло активных TLS-соединений к PostgreSQL.
pg_doorman_server_tls_handshake_duration_secondshistogram per poolРаспределение длительности TLS handshake.
pg_doorman_server_tls_handshake_errors_totalcounter per poolСчётчик неуспешных handshake. Алертить при ненулевой скорости.

Подробнее — см. Клиентский и серверный TLS.

Дашборд Grafana

Вы можете создать дашборд Grafana для визуализации этих метрик. Вот простой пример панелей, которые имеет смысл добавить:

  1. Число соединений по типу
  2. Использование памяти со временем
  3. Число клиентов и серверов по пулам
  4. Перцентили производительности запросов и транзакций
  5. Сетевой трафик по пулам

Примеры запросов

Несколько примеров запросов Prometheus, которые могут быть полезны:

Темп подключений

rate(pg_doorman_connection_count{type="total"}[5m])

Загрузка пула

sum by (database) (pg_doorman_pools_clients{status="active"}) / sum by (database) (pg_doorman_pools_servers{status="active"} + pg_doorman_pools_servers{status="idle"})

Медленные запросы

pg_doorman_pools_queries_percentile{percentile="99"}

Время ожидания клиентов

pg_doorman_pools_avg_wait_time

Hit rate кеша auth_query

pg_doorman_auth_query_cache{type="hits"} / (pg_doorman_auth_query_cache{type="hits"} + pg_doorman_auth_query_cache{type="misses"})

Темп ошибок auth_query

rate(pg_doorman_auth_query_auth{result="failure"}[5m])

title: Benchmarks

Benchmarks

pg_doorman vs pgbouncer vs odyssey. Each test runs pgbench for 30 seconds through a 40-connection pool.

Last updated: 2026-04-21 18:54 UTC

Environment

  • Instance: AWS Fargate (16 vCPU, 32 GB RAM)
  • Workers: pg_doorman: 12, odyssey: 12
  • pgbench jobs: 4 (global override)
  • Started: 2026-04-21 17:31:01 UTC
  • Finished: 2026-04-21 18:54:11 UTC
  • Total duration: 1h 23m 9s

Reading the tables

Throughput — pg_doorman TPS relative to each competitor:

ValueMeaning
+N%Faster by N%
-N%Slower by N%
≈0%Within 3%
xNN times faster or slower
Competitor got 0 TPS
N/AUnsupported
-Not tested

Latency — per-transaction latency in ms. Each cell: p50 / p95 / p99. Lower is better.


Simple protocol

Throughput

Testvs pgbouncervs odyssey
1 client-3%-9%
40 clients+82%-5%
120 clientsx2.6≈0%
500 clientsx2.5+6%
10,000 clientsx2.7+18%
1 client + Reconnect-6%x9.0
40 clients + Reconnect+21%x2.1
120 clients + Reconnect+19%+98%
500 clients + Reconnect+20%+97%
10,000 clients + Reconnect+63%x2.1
1 client + SSL-8%-8%
40 clients + SSLx2.1≈0%
120 clients + SSLx3.1+6%
500 clients + SSLx3.0+11%
10,000 clients + SSLx3.2+17%

Latency — p50 / p95 / p99 (ms)

Testpg_doorman (ms)pgbouncer (ms)odyssey (ms)
1 client0.07 / 0.07 / 0.080.07 / 0.08 / 0.080.06 / 0.07 / 0.07
40 clients0.26 / 0.36 / 0.440.46 / 0.67 / 0.710.22 / 0.42 / 0.57
120 clients0.49 / 1.86 / 3.031.81 / 2.12 / 2.240.54 / 1.33 / 1.84
500 clients3.54 / 5.64 / 6.467.59 / 8.71 / 9.330.99 / 13.19 / 22.41
10,000 clients69.42 / 71.72 / 75.83184.39 / 202.56 / 213.442.70 / 326.10 / 570.34
1 client + Reconnect0.06 / 0.08 / 0.090.06 / 0.06 / 0.070.07 / 0.09 / 0.10
40 clients + Reconnect1.07 / 2.19 / 2.521.02 / 2.34 / 11.931.00 / 2.74 / 3.35
120 clients + Reconnect3.36 / 6.33 / 7.623.37 / 7.10 / 31.614.67 / 9.45 / 11.54
500 clients + Reconnect13.77 / 25.15 / 28.8613.61 / 30.11 / 125.7922.99 / 41.95 / 48.02
10,000 clients + Reconnect295.83 / 515.96 / 559.54562.42 / 926.93 / 972.88597.47 / 1078.06 / 1365.21
1 client + SSL0.08 / 0.09 / 0.100.08 / 0.08 / 0.090.07 / 0.09 / 0.09
40 clients + SSL0.29 / 0.44 / 0.560.64 / 0.93 / 1.000.27 / 0.51 / 0.67
120 clients + SSL0.59 / 2.32 / 3.932.56 / 2.93 / 3.140.67 / 1.65 / 2.30
500 clients + SSL4.16 / 6.85 / 7.8410.89 / 12.64 / 13.551.23 / 16.20 / 27.94
10,000 clients + SSL82.15 / 86.57 / 91.75262.65 / 289.09 / 367.394.24 / 387.21 / 753.84

Extended protocol

Throughput

Testvs pgbouncervs odyssey
1 client+5%+40%
40 clients+98%+43%
120 clientsx2.8+60%
500 clientsx2.7+64%
10,000 clientsx2.8+74%
1 client + Reconnect-4%x3.0
40 clients + Reconnect+20%x2.2
120 clients + Reconnect+21%+88%
500 clients + Reconnect+21%+100%
10,000 clients + Reconnect+61%+27%
1 client + SSL+4%+36%
40 clients + SSLx2.3+48%
120 clients + SSLx3.2+65%
500 clients + SSLx3.4+69%
10,000 clients + SSLx3.4+73%
1 client + SSL + Reconnect+9%+13%
40 clients + SSL + Reconnect+96%+5%
120 clients + SSL + Reconnect+99%+5%
500 clients + SSL + Reconnectx2.0+5%
10,000 clients + SSL + Reconnect+93%+5%

Latency — p50 / p95 / p99 (ms)

Testpg_doorman (ms)pgbouncer (ms)odyssey (ms)
1 client0.07 / 0.07 / 0.080.07 / 0.08 / 0.080.09 / 0.10 / 0.11
40 clients0.25 / 0.35 / 0.430.48 / 0.72 / 0.760.32 / 0.62 / 0.84
120 clients0.47 / 1.82 / 2.991.87 / 2.27 / 2.390.83 / 2.33 / 3.48
500 clients3.45 / 5.48 / 6.267.77 / 9.35 / 9.691.31 / 18.35 / 31.99
10,000 clients67.94 / 70.05 / 72.43188.62 / 206.44 / 218.363.74 / 468.64 / 810.47
1 client + Reconnect0.07 / 0.08 / 0.080.06 / 0.08 / 0.090.10 / 0.12 / 0.13
40 clients + Reconnect1.08 / 2.21 / 2.551.03 / 2.40 / 11.130.92 / 2.77 / 3.66
120 clients + Reconnect3.27 / 6.20 / 7.443.23 / 7.11 / 33.544.61 / 9.52 / 11.37
500 clients + Reconnect13.83 / 25.40 / 29.1213.80 / 30.20 / 118.8823.89 / 43.77 / 67.05
10,000 clients + Reconnect298.47 / 519.98 / 573.71569.79 / 921.35 / 966.33549.71 / 1052.37 / 1402.38
1 client + SSL0.08 / 0.09 / 0.090.08 / 0.10 / 0.100.11 / 0.12 / 0.13
40 clients + SSL0.29 / 0.44 / 0.590.67 / 0.99 / 1.070.39 / 0.80 / 1.08
120 clients + SSL0.56 / 2.31 / 3.902.62 / 3.06 / 3.241.07 / 2.90 / 4.35
500 clients + SSL4.16 / 6.87 / 7.8812.30 / 14.21 / 15.641.71 / 22.66 / 38.83
10,000 clients + SSL81.93 / 86.01 / 89.18280.73 / 308.19 / 385.99139.94 / 557.71 / 844.51
1 client + SSL + Reconnect0.10 / 0.12 / 0.130.08 / 0.10 / 0.110.09 / 0.12 / 0.12
40 clients + SSL + Reconnect12.31 / 23.03 / 25.0224.07 / 44.29 / 46.7012.99 / 24.30 / 26.07
120 clients + SSL + Reconnect37.28 / 69.57 / 76.1873.98 / 137.78 / 147.3239.21 / 72.80 / 79.97
500 clients + SSL + Reconnect157.07 / 292.82 / 319.41311.77 / 593.21 / 673.84165.06 / 306.22 / 336.63
10,000 clients + SSL + Reconnect2954.51 / 5951.58 / 6556.465078.21 / 11531.36 / 12358.703096.16 / 6192.08 / 6844.73

Prepared protocol

Throughput

Testvs pgbouncervs odyssey
1 client-4%-8%
40 clientsx2.4-7%
120 clientsx3.5≈0%
500 clientsx3.3+8%
10,000 clientsx3.1+16%
1 client + Reconnect≈0%
40 clients + Reconnect≈0%
120 clients + Reconnect≈0%
500 clients + Reconnect+4%
10,000 clients + Reconnect+25%
1 client + SSL-4%-5%
40 clients + SSLx2.7≈0%
120 clients + SSLx3.8+6%
500 clients + SSLx3.7+11%
10,000 clients + SSLx3.9+15%

Latency — p50 / p95 / p99 (ms)

Testpg_doorman (ms)pgbouncer (ms)odyssey (ms)
1 client0.06 / 0.07 / 0.070.06 / 0.07 / 0.070.06 / 0.07 / 0.07
40 clients0.24 / 0.34 / 0.420.60 / 0.88 / 0.910.20 / 0.40 / 0.53
120 clients0.47 / 1.72 / 2.792.25 / 2.60 / 2.690.51 / 1.27 / 1.79
500 clients3.31 / 5.28 / 6.049.35 / 10.58 / 11.200.90 / 12.75 / 21.56
10,000 clients66.31 / 69.32 / 72.46205.26 / 221.39 / 243.492.76 / 306.09 / 534.07
1 client + Reconnect0.11 / 0.13 / 0.140.10 / 0.12 / 0.130.15 / 0.18 / 0.19
40 clients + Reconnect1.56 / 3.01 / 3.331.59 / 2.95 / 3.311.86 / 3.56 / 4.61
120 clients + Reconnect4.49 / 8.39 / 9.734.60 / 8.55 / 9.726.76 / 83.39 / 88.89
500 clients + Reconnect18.64 / 34.07 / 37.7219.43 / 35.46 / 39.8624.27 / 197.01 / 297.22
10,000 clients + Reconnect396.39 / 710.66 / 769.05483.59 / 927.03 / 1000.58483.21 / 1256.91 / 1563.08
1 client + SSL0.08 / 0.09 / 0.090.07 / 0.08 / 0.090.07 / 0.08 / 0.09
40 clients + SSL0.28 / 0.42 / 0.570.80 / 1.15 / 1.240.24 / 0.48 / 0.65
120 clients + SSL0.56 / 2.19 / 3.673.00 / 3.27 / 3.490.63 / 1.60 / 2.27
500 clients + SSL3.95 / 6.56 / 7.5112.70 / 13.91 / 15.231.10 / 15.54 / 26.49
10,000 clients + SSL79.24 / 84.27 / 88.85308.74 / 326.58 / 488.805.17 / 364.12 / 633.46

Notes

  • Odyssey performs poorly with extended query protocol in transaction pooling mode
  • Throughput values are relative ratios — comparable across runs on identical hardware
  • Latency values are absolute, measured per-transaction

Changelog

3.6.0 Apr 24, 2026

Patroni-assisted fallback

When pg_doorman runs next to PostgreSQL on the same machine and connects via unix socket, a Patroni switchover or PostgreSQL crash leaves the pooler without a backend. With patroni_api_urls configured, pg_doorman queries the Patroni REST API /cluster endpoint, picks a live cluster member, and routes new connections there.

Candidate selection: sync_standby first (most likely next leader), then replica, then any other member. Members with noloadbalance, nofailover, or archive tags are excluded. All candidates are TCP-probed in parallel; the first responding sync_standby wins immediately.

The local backend stays in cooldown for fallback_cooldown (default 30s). During the cooldown, subsequent connection requests reuse the cached fallback host without re-querying Patroni. Fallback connections use a short fallback_lifetime (defaults to fallback_cooldown) so the pool returns to the local backend once it recovers.

Configuration:

pools:
  mydb:
    patroni_api_urls:
      - "http://10.0.0.1:8008"
      - "http://10.0.0.2:8008"
    fallback_cooldown: "30s"
    patroni_api_timeout: "5s"
    fallback_connect_timeout: "5s"

Prometheus metrics: pg_doorman_patroni_api_requests_total, pg_doorman_fallback_connections_total, pg_doorman_patroni_api_errors_total, pg_doorman_fallback_active, pg_doorman_patroni_api_duration_seconds, pg_doorman_fallback_host, pg_doorman_fallback_cache_hits_total.

If you tracked this feature under its working name in 3.5.x dev builds, the config keys and metric names changed before the public release: patroni_discovery_urlspatroni_api_urls, failover_blacklist_durationfallback_cooldown, failover_discovery_timeoutpatroni_api_timeout, failover_connect_timeoutfallback_connect_timeout, failover_server_lifetimefallback_lifetime. Old pg_doorman_failover_* metrics are renamed to pg_doorman_patroni_api_* / pg_doorman_fallback_*.

Server-side TLS (pg_doorman → PostgreSQL)

Six SSL modes matching libpq semantics: disable, allow (default), prefer, require, verify-ca, verify-full. Mutual TLS supported via server_tls_certificate / server_tls_private_key.

Configuration is per-pool with global defaults in [general]. Cancel requests use TLS when the main connection used TLS.

Breaking change: server_tls (bool) and verify_server_certificate (bool) are removed. They were parsed but non-functional. Replace with:

Old configNew config
server_tls: falseserver_tls_mode: "disable"
server_tls: trueserver_tls_mode: "require"
server_tls: true + verify_server_certificate: trueserver_tls_mode: "verify-full"
(not set)server_tls_mode: "allow" (new default)

The new default allow tries plain TCP first. If the server rejects the connection (e.g. pg_hba.conf requires TLS), pg_doorman retries with TLS on a new TCP socket. This matches libpq sslmode=allow.

SHOW SERVERS now includes a tls column showing whether each backend connection uses TLS.

3.5.3 Apr 22, 2026

Prepared statement cache overflow under concurrent load

The pool-level prepared statement cache could grow well above its configured prepared_statements_cache_size under concurrent client traffic. Production showed 480 entries with a limit of 300. The check-then-insert sequence in the cache had a race: multiple clients passed the size check simultaneously, each inserted without evicting. Now insertion happens first, followed by eviction in a loop until the cache is within bounds.

3.5.2 Apr 21, 2026

Semaphore permit leak on direct handoff

Each return_object handoff (delivering a connection to a waiting client via oneshot channel) permanently consumed one semaphore permit. After max_size handoffs the pool semaphore was fully drained, blocking all new timeout_get callers. The pool could not create connections and stabilized at whatever size it reached during cold start (typically 4-8 out of 40).

Root cause: wrap_checkout calls permit.forget(), and the handoff path in return_object skipped add_permits(1). Now return_object restores the permit on both the handoff and idle-queue paths. Compensating add_permits(1) in pre_replace_one removed (no longer needed).

Burst gate select race

The tokio::select! in the burst gate loop randomly picked among ready branches. When sleep(5ms) or create_done won over an already-delivered oneshot, the connection was silently dropped, inflating slots.size without a live server. Fixed with biased; (oneshot checked first) and a try_recv drain that pushes orphaned connections to idle without double-counting the permit.

Migration fixes

  • Client ID collision after migration. The new process started its connection counter at 0, colliding with migrated client IDs. Now the counter advances past the highest migrated ID.

  • SCRAM passthrough state preserved. The ClientKey from the first client's SCRAM handshake is serialized in the migration payload (v2 format, backward compatible). The new process skips the ScramPending fallback to server_password.

Session mode statistics fix

xact_time percentiles in session mode showed the entire session duration instead of individual transaction time. Now recorded per-transaction at each ReadyForQuery(Idle), matching transaction mode semantics.

query_time had the same accumulation bug: the timer was set once before the inner loop and never reset, so each subsequent query reported the cumulative session duration. Now reset per-query in session mode.

Adaptive anticipation budget

Anticipation wait (formerly fixed 300-500ms) scales with real transaction latency: xact_p99 * 2 +/- 20% jitter, clamped to [5ms, 500ms]. Cold start default: 100ms.

Diagnostic logging

Slow checkout warnings (>500ms) now include pool state: size, avail, waiting, inflight, creates, gate_waits, antic_ok, antic_to, fallback. Phase-specific warnings added for semaphore timeout, burst gate timeout, coordinator exhaustion, and create failure.

3.5.1 Apr 20, 2026

systemd Type=notify support

pg_doorman now sends sd_notify(READY=1) on startup and sd_notify(MAINPID=<child_pid>) during binary upgrade. With Type=notify in the systemd unit, systemctl reload performs a zero-downtime binary upgrade without PID tracking issues — systemd follows the new process correctly and does not restart the service.

The shipped pg_doorman.service changes from Type=forking + --daemon to Type=notify (foreground). Existing installations using --daemon continue to work but do not benefit from client migration.

Docker STOPSIGNAL changed from SIGINT to SIGTERM to prevent binary upgrade in containers (where PID 1 exit kills the container).

3.5.0 Apr 15, 2026

Client migration during binary upgrade

Idle clients now transfer to the new process via Unix socket (SCM_RIGHTS) without reconnecting. Active-transaction clients finish their transaction on the old process, then migrate. Prepared statement caches are serialized and transparently re-parsed on the new backend. The old process exits once all clients have migrated or shutdown_timeout expires.

TLS connection migration (opt-in)

Build with --features tls-migration to migrate TLS sessions without re-handshake. A patched vendored OpenSSL 3.5.5 exports/imports symmetric cipher state (keys, IVs, sequence numbers). Linux-only. Offline builds supported via OPENSSL_SOURCE_TARBALL env var with SHA-256 verification.

3.4.0 Apr 11, 2026

Pool Coordinator — database-level connection limits

When multiple user pools share one PostgreSQL database, the sum of their pool_size values can exceed max_connections. A spike in one pool starves the others, or PostgreSQL rejects connections outright.

max_db_connections caps total backend connections per database across all user pools. When the cap is reached, the coordinator frees capacity through three mechanisms, tried in order:

  1. Reserve pool. If reserve_pool_size > 0 and the reserve has headroom, a permit is granted immediately — no eviction, no wait. The reserve is a burst buffer: idle reserve connections are upgraded to main permits by the retain cycle once pressure drops, and closed if they stay idle longer than min_connection_lifetime.

  2. Eviction. The coordinator closes one idle connection from a peer pool with the largest surplus above its min_guaranteed_pool_size floor. Candidates are ranked by p95 transaction time — slow pools donate first, because a 1 ms reconnect cost is negligible against a 15 ms p95 but doubles a 0.96 ms one. Only connections older than min_connection_lifetime (default 30 s) are eligible, which suppresses cyclic reconnect between pools that take turns stealing slots.

  3. Wait. If nothing is evictable, the caller parks for up to reserve_pool_timeout (default 3 s), waking on any peer connection return or permit drop. After the wait, the reserve is retried once more before the client receives an error.

Disabled by default (max_db_connections = 0) — zero overhead when not configured. The hot path (idle connection reuse) never touches the coordinator; only new connection creation does, at the cost of one atomic operation.

New pool-level config fields:

ParameterDefaultPurpose
max_db_connections0 (disabled)Hard cap on backend connections per database
min_connection_lifetime30000 msEviction age floor — connections younger than this are immune
reserve_pool_size0 (disabled)Extra permits above the cap, granted on burst
reserve_pool_timeout3000 msCoordinator wait budget before error
min_guaranteed_pool_size0Per-user eviction protection floor

New admin commands: SHOW POOL_COORDINATOR (per-database coordinator state), SHOW POOL_SCALING (per-pool checkout counters). Both are also exported as Prometheus metrics under pg_doorman_pool_coordinator{type, database} and pg_doorman_pool_scaling{type, user, database}.

See the pool pressure tutorial for acquisition phases, tuning recipes, and alert examples.

Connection checkout under pressure

Replaces scaling_cooldown_sleep (a fixed 10 ms delay before creating a backend connection) with a multi-phase checkout that reuses connections about to be returned before resorting to connect().

When the idle pool is empty and the pool is above its warm threshold (scaling_warm_pool_ratio, default 20%), a caller first spins briefly (scaling_fast_retries, default 10 yield iterations), then registers a direct-handoff waiter. Connections returned by other clients are delivered through the waiter channel — no idle-queue round-trip, no race with other checkout attempts. The waiter deadline is bounded by query_wait_timeout minus a 500 ms reserve for the create path. If no connection arrives, the caller proceeds to create.

Backend connect() calls are capped at scaling_max_parallel_creates (default 2) per pool. Callers above the cap wait for a peer create to finish or a connection to be returned. Background replenish (min_pool_size) respects the same cap and defers to the next retain cycle when the gate is full, so it does not compete with client-driven creates during spikes.

Connections nearing server_lifetime expiry (95% of age) trigger a pre-replacement: a background task creates a successor before the old connection fails recycle, so the next checkout hits the hot path.

The direct-handoff queue is FIFO. On a 500-client / 40-connection AWS Fargate benchmark, p99/p50 ratio is 1.08 (pg_doorman) vs 25.5 (Odyssey). Every client pays roughly the same queue cost.

Migration: remove scaling_cooldown_sleep from your config if present. Replace with scaling_max_parallel_creates (default 2) if you need to tune the concurrency cap.

Improvements:

  • Runtime log level control. SET log_level = 'debug' changes the log filter without restart; SET log_level = 'warn,pg_doorman::pool::pool_coordinator=debug' targets specific modules. SHOW LOG_LEVEL displays the current filter. Changes are ephemeral (lost on restart).

  • Log readability overhaul. Consistent [user@pool #cN] prefix. Durations as 4m30s instead of raw milliseconds. Stats line in logfmt. PG error newlines escaped. Expensive debug computations guarded by log_enabled!() to avoid allocations at production log levels.

  • Auth failure logs include client IP. SCRAM, MD5, JWT, and PAM failures show the source address.

  • Replenish failure noise suppression. Repeated min_pool_size failures log once at warn, then a periodic reminder every ~10 minutes with the failure count.

  • avg_xact_time column in SHOW POOLS. Average transaction time per pool, visible alongside existing connection counts.

  • Smart session cleanup in transaction mode. pg_doorman tracks which session state a client dirtied (SET, DECLARE CURSOR, prepared statements) and sends the matching reset on checkin. If the client cleaned up after itself — RESET ALL, CLOSE ALL, DEALLOCATE ALL, or DISCARD ALL — pg_doorman sees the confirmation and skips its own reset. Drivers like jackc/pgx that send a cleanup batch on disconnect no longer cause a redundant round-trip to PostgreSQL. A SET without a follow-up reset still triggers cleanup as before.

3.3.5 Mar 31, 2026

Bug Fixes:

  • Prepared statement eviction during batch breaks buffered Bind. When a client sent a batch like Parse(A), Bind(A), Parse(C), Sync and Parse(C) triggered server-side LRU eviction of statement A, the Close(A) was sent to PostgreSQL immediately (out-of-band), deleting A before the client buffer was flushed. Bind(A) then failed with prepared statement "DOORMAN_X" does not exist (error 26000). Two fixes: (1) has_prepared_statement() now promotes entries in the LRU on access (get() instead of contains()), so actively-used statements resist eviction. (2) Eviction Close is deferred until after the batch completes — the statement stays alive on PostgreSQL while Binds in the buffer are processed, then Close is sent as post-batch cleanup. If the client disconnects before Sync, checkin_cleanup detects the pending deferred closes and triggers DEALLOCATE ALL.

3.3.4 Mar 30, 2026

Bug Fixes:

  • Prepared statement cache desync after client disconnect. When a client sent Parse but disconnected before Sync/Flush, pg_doorman registered the statement in the server-side LRU cache but never sent the actual Parse to PostgreSQL (it was still in the client buffer, which was dropped on disconnect). The next client that got the same server connection and used the same query saw the stale cache entry, skipped sending Parse, and received prepared statement "DOORMAN_X" does not exist (error 26000) from PostgreSQL. Fixed by tracking a has_pending_cache_entries flag on the server connection: set when a statement is added to the cache without immediate Parse confirmation, cleared after successful buffer flush. If the client disconnects before flushing, checkin_cleanup detects the flag and triggers DEALLOCATE ALL to re-synchronize the cache. Zero overhead on the normal path (one boolean check per checkin).

3.3.3 Mar 26, 2026

Bug Fixes:

  • Log spam from missing /proc/net/tcp6 when IPv6 disabled. get_socket_states_count failed entirely if any of the three /proc files was absent, logging errors every 15 seconds and losing tcp/unix metrics that were available. Missing files are now skipped — counters stay at zero. Other I/O errors (permission denied) still propagate.

  • Protocol violation when streaming large DataRow with cached prepared statements. handle_large_data_row wrote accumulated protocol messages (BindComplete, RowDescription) directly to the client socket, bypassing reorder_parse_complete_responses. When Parse was skipped (prepared statement cache hit), the client received BindComplete without the synthetic ParseComplete — causing Received backend message BindComplete while expecting ParseCompleteMessage in Npgsql and similar drivers. Triggered when message_size_to_be_stream ≤ 64KB. Fixed by returning accumulated messages from recv() before entering the streaming path, so response reordering runs first. Same fix applied to handle_large_copy_data.

3.3.2 Mar 1, 2026

Breaking Changes:

  • auth_query config field renames: Two fields in the auth_query section have been renamed for clarity. auth_query.pool_size (number of connections for running auth queries) is now auth_query.workers. auth_query.default_pool_size (data pool size for dynamic users) is now auth_query.pool_size, matching the same parameter name used in static pools. Migration: rename pool_size to workers and default_pool_size to pool_size in your auth_query config. If you don't update, the old pool_size value (typically 1-2) will be interpreted as the data pool size, drastically reducing connection capacity. The old default_pool_size key is silently ignored and defaults to 40.

Bug Fixes:

  • Session mode: keep server connections alive after SQL errors. A query like SELECT 1/0 returns an ErrorResponse from PostgreSQL but leaves the connection fully usable. Previously, handle_error_response called mark_bad() unconditionally in async mode, so the connection was destroyed at session end. Now mark_bad is skipped when the pool runs in session mode. Transaction mode still calls mark_bad because the connection returns to a shared pool where protocol desync is dangerous.

  • Pool-level server_lifetime and idle_timeout overrides ignored: Pool-level overrides for server_lifetime and idle_timeout were silently ignored — the general (global) values were always used instead. Fixed in 6 places across 3 pool creation contexts (static pools, auth_query shared pools, dynamic pools). Now pool.server_lifetime and pool.idle_timeout correctly override the general settings when specified.

  • idle_timeout default was 83 hours instead of 10 minutes: The default idle_timeout was set to 300,000,000ms (83 hours), effectively disabling idle connection cleanup. Idle server connections could accumulate indefinitely. Changed default to 600,000ms (10 minutes).

  • retain_connections_max quota exhaustion causing unlimited closure: When retain_connections_max > 0 and the global counter reached the limit, the remaining quota became 0 via saturating_sub. Since 0 means "unlimited" in retain_oldest_first(), pools processed after quota exhaustion lost ALL idle connections in a single retain cycle instead of none. With non-deterministic HashMap iteration order, this bug manifested as random pools losing all connections. Fixed by adding an early return when the quota is exhausted.

  • retain_connections_max doc comment incorrectly stated default as 0 (unlimited): The actual default is 3.

  • server_lifetime default changed from 5 minutes to 20 minutes: The previous default of 5 minutes was shorter than idle_timeout (10 minutes), which meant idle_timeout could never trigger — connections were always killed by server_lifetime first. Changed to 20 minutes so that idle_timeout (10 min) handles idle cleanup while server_lifetime (20 min) rotates long-lived connections. Note: idle_timeout only applies to connections that have been used at least once — prewarmed/replenished connections that were never checked out by a client are not subject to idle_timeout and will only be closed when server_lifetime expires.

  • idle_timeout = 0 did not disable idle timeout: Setting idle_timeout to 0 was supposed to disable idle connection cleanup (consistent with PgBouncer's server_idle_timeout = 0 semantics and our own server_lifetime = 0 behavior). Instead, it closed connections after ~1ms of being idle. Fixed by adding an idle_timeout_ms > 0 guard before the elapsed time check.

  • idle_timeout had no jitter — synchronized mass closures: Unlike server_lifetime which applies ±20% per-connection jitter to prevent thundering herd, idle_timeout used a single pool-wide value. When many connections became idle simultaneously (e.g., after a traffic burst), they all expired at the exact same moment, causing mass closures in one retain cycle. Now idle_timeout applies the same ±20% per-connection jitter as server_lifetime.

  • retain_connections_max unfair quota distribution across pools: The retain cycle iterated pools via HashMap, whose order is deterministic within a process (fixed RandomState seed). The same pool always got iterated first and consumed the entire retain_connections_max quota, starving other pools. Expired connections in starved pools were never cleaned up by retain — clients had to discover them via failed recycle() checks, adding latency. Fixed by shuffling pool iteration order each cycle.

  • Retain and replenish used separate pool snapshots: The retain and replenish phases each called get_all_pools() separately. If POOLS was atomically updated between them (config reload, dynamic pool GC), retain operated on one set of pools and replenish on another, potentially missing pools that need replenishment. Fixed by using a single snapshot for both phases.

Testing:

  • PHP PDO_PGSQL driver added to test infrastructure. PHP 8.4 with pdo_pgsql extension is now included in the Nix-based Docker test image. Two BDD scenarios verify basic connectivity (SELECT 1) and session mode behavior (SQL error does not change backend PID). Run with make test-php or --tags @php.

New Features:

  • pool_size observability: New pg_doorman_pool_size Prometheus gauge exposes the configured maximum pool size per user/database. The pool_size column is also added to SHOW POOLS and SHOW POOLS_EXTENDED admin commands (after sv_login), allowing operators to compare current server connections against configured capacity directly from the admin console. Works for both static and dynamic (auth_query) pools.

  • PAUSE, RESUME, RECONNECT admin commands: New admin console commands for managing connection pools. PAUSE [db] blocks new backend connection acquisition (active transactions continue). RESUME [db] lifts the pause and unblocks waiting clients. RECONNECT [db] forces connection rotation by incrementing the pool epoch — idle connections are immediately closed and active connections are discarded when returned to the pool. Without arguments, all pools are affected; with a database name, only matching pools. Specifying a nonexistent database returns an error. Use SHOW POOLS to see the paused status column.

  • min_pool_size for dynamic auth_query passthrough pools: New auth_query.min_pool_size setting controls the minimum number of backend connections maintained per dynamic user pool in passthrough mode. Connections are prewarmed in the background when the pool is first created and replenished by the retain cycle after server_lifetime expiry. Pools with min_pool_size > 0 are never garbage-collected. Default is 0 (no prewarm — backward compatible). Note: total backend connections scale as active_users × min_pool_size.

3.3.1 Feb 26, 2026

Bug Fixes:

  • Fix Ctrl+C in foreground mode: Pressing Ctrl+C in foreground mode (with TTY attached) now performs a clean graceful shutdown instead of triggering a binary upgrade. Previously, each Ctrl+C would spawn a new pg_doorman process via --inherit-fd, leaving orphan processes accumulating. SIGINT in daemon mode (no TTY) retains its legacy binary upgrade behavior for backward compatibility with existing systemd units.

  • Minimum pool size enforcement (min_pool_size): The min_pool_size user setting is now enforced at runtime. After each connection retain cycle, pg_doorman checks pool sizes and creates new connections to maintain the configured minimum. Previously, min_pool_size was accepted in config but never applied — pools started empty and could drop to 0 connections even with min_pool_size set. Replenishment stops on the first connection failure to avoid hammering an unavailable server.

New Features:

  • SIGUSR2 for binary upgrade: New dedicated signal SIGUSR2 triggers binary upgrade + graceful shutdown in all modes (daemon and foreground). This is now the recommended signal for binary upgrades. The systemd service file has been updated to use SIGUSR2 for ExecReload.

  • UPGRADE admin command: New admin console command that triggers binary upgrade via SIGUSR2. Use it from psql connected to the admin database: UPGRADE;.

Improvements:

  • Pool prewarm at startup: When min_pool_size is configured, pg_doorman now creates the minimum number of connections immediately at startup, before the first retain cycle. Previously, pools started empty and connections were only created lazily on first client request or after the first retain interval (default 60s). This eliminates cold-start latency for the first clients connecting after pg_doorman restart.

  • Configurable connection scaling parameters: New general settings scaling_warm_pool_ratio, scaling_fast_retries, and scaling_cooldown_sleep allow tuning connection pool scaling behavior. All three can be overridden at the pool level. scaling_cooldown_sleep uses the human-readable Duration type (e.g. "10ms", "1s") consistent with other timeout fields.

  • max_concurrent_creates setting: Controls the maximum number of server connections that can be created concurrently per pool. Uses a semaphore instead of a mutex for parallel connection creation.

3.3.0 Feb 23, 2026

New Features:

  • Dynamic user authentication (auth_query): PgDoorman can now authenticate users dynamically by querying PostgreSQL at connection time — no need to list every user in the config. Supports pg_shadow, custom tables, and SECURITY DEFINER functions. The query must return a column named passwd or password (or any single column) containing an MD5 or SCRAM-SHA-256 hash.

  • Passthrough authentication: Default mode for both static and dynamic users — PgDoorman reuses the client's cryptographic proof (MD5 hash or SCRAM ClientKey) to authenticate to the backend automatically. No plaintext server_password in config needed when the pool user matches the backend PostgreSQL user.

  • Two auth_query modes:

    • Passthrough mode (default) — each dynamic user gets their own backend connection pool and authenticates as themselves, preserving per-user identity on the backend.
    • Dedicated mode (server_user set) — all dynamic users share a single backend pool under one PostgreSQL role.
  • Auth query caching: DashMap-based cache with configurable TTL, double-checked locking, rate-limited refetch, and request coalescing. Supports separate TTLs for successful and failed lookups.

  • SHOW AUTH_QUERY admin command: Displays per-pool metrics — cache entries/hits/misses, auth success/failure counters, executor stats, and dynamic pool count.

  • Prometheus metrics for auth_query: New metric families pg_doorman_auth_query_cache, pg_doorman_auth_query_auth, pg_doorman_auth_query_executor, pg_doorman_auth_query_dynamic_pools.

  • Idle dynamic pool garbage collection: Background task cleans up expired dynamic pools when all connections have been idle beyond server_lifetime. Zero overhead for static-only configs.

  • Smart password column lookup: Password column resolved by name (passwdpassword → single-column fallback), works with pg_shadow, custom tables, and arbitrary single-column queries.

Improvements:

  • server_username/server_password now optional: Previously documented as required for MD5/SCRAM hash configs. Now only needed when the backend user differs from the pool user (username mapping, JWT auth).

  • Data-driven config & docs generation: fields.yaml is the single source of truth for all config field descriptions (EN/RU). Reference docs, annotated configs, and inline comments are all generated from it.

Testing:

  • 39 new BDD scenarios (260+ steps) covering auth_query executor, end-to-end auth, HBA integration, passthrough mode, SCRAM-only auth, RELOAD/GC lifecycle, observability, and static user passthrough.

3.2.4 Feb 20, 2026

New Features:

  • Annotated config generation: The generate command now produces well-documented configuration files with inline comments for every parameter by default. Previously it only did plain serde serialization without any documentation.

  • --reference flag: Generates a complete reference config with example values without requiring a PostgreSQL connection. The root pg_doorman.toml and pg_doorman.yaml are now auto-generated from this flag, ensuring they always stay in sync with the codebase.

  • --format (-f) flag: Explicitly choose output format (yaml or toml). Default output format changed from TOML to YAML. When --output is specified, format is auto-detected from file extension; --format overrides auto-detection.

  • --russian-comments (--ru) flag: Generates comments in Russian for quick start guide. All ~100+ comment strings are translated to clear, simple Russian.

  • --no-comments flag: Disables inline comments for minimal config output (plain serde serialization, the old default behavior).

  • Passthrough authentication documentation: Documents passthrough auth as the default mode — server_username/server_password are no longer needed when the pool user matches the backend PostgreSQL user. PgDoorman reuses the client's MD5 hash or SCRAM ClientKey to authenticate to the backend automatically.

Testing:

  • Config field coverage guarantee: New test parses config struct source files (general.rs, pool.rs, user.rs, etc.) at compile time and verifies every pub field appears in annotated output. If someone adds a new config parameter but forgets to add it to annotated.rs, CI will fail with a clear message listing the missing fields.

  • BDD tests for generate command: End-to-end tests that generate TOML and YAML configs, start pg_doorman with them, and verify client connectivity.

Bug Fixes:

  • Fixed protocol desynchronization on prepared statement cache eviction in async mode: When asyncpg/SQLAlchemy uses Flush (instead of Sync) for pipelined Parse+Describe batches and the prepared statement LRU cache is full, eviction sends Close+Sync to the server. In async mode, recv() was exiting immediately when expected_responses==0, leaving CloseComplete and ReadyForQuery unread in the TCP buffer. The next recv() call would then read these stale messages instead of the expected response, causing protocol desynchronization. Fixed by temporarily disabling async mode during eviction so that recv() waits for ReadyForQuery as the natural loop terminator.

  • Fixed generated config startup failure: syslog_prog_name and daemon_pid_file are now commented out by default in generated configs. Previously they were uncommented, causing pg_doorman to fail when started in foreground mode or when syslog was unavailable.

  • Fixed Go test goroutine leak: TestLibPQPrepared now uses sync.WaitGroup to wait for all goroutines before test exit, fixing sporadic panics caused by logging after test completion.

  • Fixed protocol violation on flush timeout — client now receives ErrorResponse: When the 5-second flush timeout fires (server TCP write blocks because the backend is overloaded or unreachable), the FlushTimeout error was propagating via ? through handle_sync_flush → transaction loop → handle() without sending any PostgreSQL protocol message to the client. The TCP connection was simply dropped, causing drivers like Npgsql to report "protocol violation" due to unexpected EOF. Now pg_doorman sends a proper ErrorResponse with SQLSTATE 58006 and message containing "pooler is shut down now" before closing the connection, allowing client drivers to detect the error and reconnect gracefully.

3.2.3 Feb 10, 2026

Improvements:

  • Jitter for server_lifetime (±20%): Connection lifetimes now have a random ±20% jitter applied to prevent mass disconnections from PostgreSQL. When pg_doorman is under heavy load, it creates many connections simultaneously, which previously caused them all to expire at the same time, creating spikes of connection closures. Now each connection gets an individual lifetime calculated as base_lifetime ± random(20%). For example, with server_lifetime: 300000 (5 minutes), actual lifetimes range from 240s to 360s, spreading connection closures evenly over time.

3.2.2 Feb 9, 2026

New Features:

  • Configuration test mode (-t / --test-config): Added nginx-style configuration validation flag. Running pg_doorman -t or pg_doorman --test-config will parse and validate the configuration file, report success or errors, and exit without starting the server. Useful for CI/CD pipelines and pre-deployment configuration checks.

  • Configuration validation before binary upgrade: When receiving SIGINT for graceful shutdown/binary upgrade, the server now validates the new binary's configuration using -t flag before proceeding. If the configuration test fails, the shutdown is cancelled and critical error messages are logged to alert the operator. This prevents accidental downtime from deploying a binary with invalid configuration.

  • New retain_connections_max configuration parameter: Controls the maximum number of idle connections to close per retain cycle. When set to 0, all idle connections that exceed idle_timeout or server_lifetime are closed immediately. Default is 3, providing controlled cleanup while preventing connection buildup. Previously, only 1 connection was closed per cycle, which could lead to slow connection cleanup when many connections became idle simultaneously. Connection closures are now logged for better observability.

  • Oldest-first connection closure: When retain_connections_max > 0, connections are now closed in order of age (oldest first) rather than in queue order. This ensures that the oldest connections are always prioritized for closure, providing more predictable connection rotation behavior.

  • New server_idle_check_timeout configuration parameter: Time after which an idle server connection should be checked before being given to a client (default: 30s). This helps detect dead connections caused by PostgreSQL restart, network issues, or server-side idle timeouts. When a connection has been idle longer than this timeout, pg_doorman sends a minimal query (;) to verify the connection is alive before returning it to the client. Set to 0 to disable.

  • New tcp_user_timeout configuration parameter: Sets the TCP_USER_TIMEOUT socket option for client connections (in seconds). This helps detect dead client connections faster than keepalive probes when the connection is actively sending data but the remote end has become unreachable. Prevents 15-16 minute delays caused by TCP retransmission timeout. Only supported on Linux. Default is 60 seconds. Set to 0 to disable.

  • Removed wait_rollback mechanism: The pooler no longer attempts to automatically wait for ROLLBACK from clients when a transaction enters an aborted state. This complex mechanism was causing protocol desynchronization issues with async clients and extended query protocol. Server connections in aborted transactions are now simply returned to the pool and cleaned up normally via ROLLBACK during checkin.

  • Removed savepoint tracking: Removed the use_savepoint flag and related logic that was tracking SAVEPOINT usage. The pooler now treats savepoints as regular PostgreSQL commands without special handling.

Bug Fixes:

  • Fixed protocol desynchronization in async mode with simple prepared statements: When prepared_statements was disabled but clients used extended query protocol (Parse, Bind, Describe, Execute, Flush), the pooler wasn't tracking batch operations, causing expected_responses to be calculated as 0. This led to the pooler exiting the response loop immediately without waiting for server responses (ParseComplete, BindComplete, etc.). Now batch operations are tracked regardless of the prepared_statements setting.

Performance:

  • Removed timeout-based waiting in async protocol: The pooler now tracks expected responses based on batch operations (Parse, Bind, Execute, etc.) and exits immediately when all responses are received. This eliminates unnecessary latency in pipeline/async workloads.

3.1.8 Jan 31, 2026

Bug Fixes:

  • Fixed ParseComplete desynchronization in pipeline on errors: Fixed a protocol desynchronization issue (especially noticeable in .NET Npgsql driver) where synthetic ParseComplete messages were not being inserted if an error occurred during a pipelined batch. When the pooler caches a prepared statement and skips sending Parse to the server, it must still provide a ParseComplete to the client. If an error occurs before subsequent commands are processed, the server skips them, and the pooler now ensures all missing synthetic ParseComplete messages are inserted into the response stream upon receiving an ErrorResponse or ReadyForQuery.

  • Fixed incorrect use_savepoint state persistence: Fixed a bug where the use_savepoint flag (which disables automatic rollback on connection return if a savepoint was used) was not reset after a transaction ended.

3.1.7 Jan 28, 2026

Memory Optimization:

  • DEALLOCATE now clears client prepared statements cache: When a client sends DEALLOCATE <name> or DEALLOCATE ALL via simple query protocol, the pooler now properly clears the corresponding entries from the client's internal prepared statements cache. Previously, synthetic OK responses were sent but the client cache was not cleared, causing memory to grow indefinitely for long-running connections using many unique prepared statements. This fix allows memory to be reclaimed when clients properly deallocate their statements.

  • New client_prepared_statements_cache_size configuration parameter: Added protection against malicious or misbehaving clients that don't call DEALLOCATE and could exhaust server memory by creating unlimited prepared statements. When the per-client cache limit is reached, the oldest entry is evicted automatically. Set to 0 for unlimited (default, relies on client calling DEALLOCATE). Example: client_prepared_statements_cache_size: 1024 limits each client to 1024 cached prepared statements.

3.1.6 Jan 27, 2026

Bug Fixes:

  • Fixed incorrect timing statistics (xact_time, wait_time, percentiles): The statistics module was using recent() (cached clock) without proper clock cache updates, causing transaction time, wait time, and their percentiles to show extremely large incorrect values (e.g., 100+ seconds instead of actual milliseconds). The root cause was that the quanta::Upkeep handle was not being stored, causing the upkeep thread to stop immediately after starting. Now the handle is properly retained for the lifetime of the server, ensuring Clock::recent() returns accurate cached time values.

  • Fixed query time accumulation bug in transaction loop: Query times were incorrectly accumulated when multiple queries were executed within a single transaction. The query_start_at timestamp was only set once at the beginning of the transaction, causing each subsequent query's elapsed time to include all previous queries' durations (e.g., 10 queries of 100ms each would report the last query as ~1 second instead of 100ms). Now query_start_at is updated for each new message in the transaction loop, ensuring accurate per-query timing.

New Features:

  • New clock_resolution_statistics configuration parameter: Added general.clock_resolution_statistics parameter (default: 0.1ms = 100 microseconds) that controls how often the internal clock cache is updated. Lower values provide more accurate timing measurements for query/transaction percentiles, while higher values reduce CPU overhead. This parameter affects the accuracy of all timing statistics reported in the admin console and Prometheus metrics.

  • Sub-millisecond precision for Duration values: Duration configuration parameters now support sub-millisecond precision:

    • New us suffix for microseconds (e.g., "100us" = 100 microseconds)
    • Decimal milliseconds support (e.g., "0.1ms" = 100 microseconds)
    • Internal representation changed from milliseconds to microseconds for higher precision
    • Full backward compatibility maintained: plain numbers are still interpreted as milliseconds

3.1.5 Jan 25, 2026

Bug Fixes:

  • Fixed PROTOCOL VIOLATION with batch PrepareAsync
  • Rewritten ParseComplete insertion algorithm

Performance:

  • Deferred connection acquisition for standalone BEGIN: When a client sends a standalone BEGIN; or begin; query (simple query protocol), the pooler now defers acquiring a server connection until the next message arrives. Since BEGIN itself doesn't perform any actual database operations, this optimization reduces connection pool contention when clients are slow to send their next query after starting a transaction.
    • Micro-optimized detection: first checks message size (12 bytes), then content using case-insensitive comparison
    • If client sends Terminate (X) after BEGIN, no server connection is acquired at all
    • The deferred BEGIN is automatically sent to the server before the actual query

3.1.0 Jan 18, 2026

New Features:

  • YAML configuration support: Added support for YAML configuration files (.yaml, .yml) as the primary and recommended format. The format is automatically detected based on file extension. TOML format remains fully supported for backward compatibility.
    • The generate command now outputs YAML or TOML based on the output file extension.
    • Include files can mix YAML and TOML formats.
    • New array syntax for users in YAML: users: [{ username: "user1", ... }]
  • TOML backward compatibility: Full backward compatibility with legacy TOML format [pools.*.users.0] is maintained. Both the legacy map format and the new array format [[pools.*.users]] are supported.
  • Username uniqueness validation: Added validation to reject duplicate usernames within a pool, ensuring configuration correctness.
  • Human-readable configuration values: Duration and byte size parameters now support human-readable formats while maintaining backward compatibility with numeric values:
    • Duration: "3s", "5m", "1h", "1d" (or milliseconds: 3000)
    • Byte size: "1MB", "256M", "1GB" (or bytes: 1048576)
    • Example: connect_timeout: "3s" instead of connect_timeout: 3000
  • Foreground mode binary upgrade: Added support for binary upgrade in foreground mode by passing the listener socket to the new process via --inherit-fd argument. This enables zero-downtime upgrades without requiring daemon mode.
  • Optional tokio runtime parameters: The following tokio runtime parameters are now optional and default to None (using tokio's built-in defaults): tokio_global_queue_interval, tokio_event_interval, worker_stack_size, and the new max_blocking_threads. Modern tokio versions handle these parameters well by default, so explicit configuration is no longer required in most cases.
  • Improved graceful shutdown behavior:
    • During graceful shutdown, only clients with active transactions are now counted (instead of all connected clients), allowing faster shutdown when clients are idle.
    • After a client completes their transaction during shutdown, they receive a proper PostgreSQL protocol error (58006 - pooler is shut down now) instead of a connection reset.
    • Server connections are immediately released (marked as bad) after transaction completion during shutdown to conserve PostgreSQL connections.
    • All idle connections are immediately drained from pools when graceful shutdown starts, releasing PostgreSQL connections faster.

Performance:

  • Statistics module optimization: Major refactoring of the src/stats module for improved performance:
    • Replaced VecDeque with HDR histograms (hdrhistogram crate) for percentile calculations — O(1) percentile queries instead of O(n log n) sorting, ~95% memory reduction for latency tracking.
    • Histograms are now reset after each stats period (15 seconds) to provide accurate rolling window percentiles.

3.0.5 Jan 16, 2026

Bug Fixes:

  • Fixed panic (capacity overflow) in startup message handling when receiving malformed messages with invalid length (less than 8 bytes or exceeding 10MB). Now gracefully rejects such connections with ClientBadStartup error.

Testing:

  • Integration fuzz testing framework: Added comprehensive BDD-based fuzz tests (@fuzz tag) that verify pg_doorman's resilience to malformed PostgreSQL protocol messages.
  • All fuzz tests connect and authenticate first, then send malformed data to test post-authentication resilience.

CI/CD:

  • Added dedicated fuzz test job in GitHub Actions workflow (without retries, as fuzz tests should not be flaky).

3.0.4 Jan 16, 2026

New Features:

  • Enhanced DEBUG logging for PostgreSQL protocol messages: Added grouped debug logging that displays message types in a compact format (e.g., [P(stmt1),B,D,E,S] or [3xD,C,Z]). Messages are buffered and flushed every 100ms or 100 messages to reduce log noise.
  • Protocol violation detection: Added real-time protocol state tracking that detects and warns about protocol violations (e.g., receiving ParseComplete when no Parse was pending). Helps diagnose client-server synchronization issues.

Bug Fixes:

  • Fixed potential protocol violation when client disconnects during batch operations with cached prepared statements: disabled fast_release optimization when there are pending prepared statement operations.
  • Fixed ParseComplete insertion for Describe flow: now correctly inserts one ParseComplete before each ParameterDescription ('t') or NoData ('n') message instead of inserting all at once.

3.0.3 Jan 15, 2026

Bug Fixes:

  • Improved handling of Describe flow for cached prepared statements: added a separate counter (pending_parse_complete_for_describe) to correctly insert ParseComplete messages before ParameterDescription or NoData responses when Parse was skipped due to caching.

Testing:

  • Added comprehensive .NET client tests for Describe flow with cached prepared statements (describe_flow_cached.cs).
  • Added aggressive mixed tests combining batch operations, prepared statements, and extended protocol (aggressive_mixed.cs).

3.0.2 Jan 14, 2026

Bug Fixes:

  • Fixed protocol mismatch for .NET clients (Npgsql) using named prepared statements with Prepare(): ParseComplete messages are now correctly inserted before ParameterDescription and NoData messages in the Describe flow, not just before BindComplete.

3.0.1 Jan 14, 2026

Bug Fixes:

  • Fixed protocol mismatch for .NET clients (Npgsql): prevented insertion of ParseComplete messages between DataRow messages when server has more data available.

Testing:

  • Extended Node.js client test coverage with additional scenarios for prepared statements, error handling, transactions, and edge cases.

3.0.0 Jan 12, 2026

Major Release — Complete Architecture Refactoring

This release represents a significant milestone with a complete codebase refactoring that dramatically improves async protocol support, making PgDoorman the most efficient connection pooler for asynchronous PostgreSQL workloads.

New Features:

  • patroni_proxy — A new high-performance TCP proxy for Patroni-managed PostgreSQL clusters:
    • Zero-downtime connection management — existing connections are preserved during cluster topology changes
    • Hot upstream updates — automatic discovery of cluster members via Patroni REST API without connection drops
    • Role-based routing — route connections to leader, sync replicas, or async replicas based on configuration
    • Replication lag awareness with configurable max_lag_in_bytes per port
    • Least connections load balancing strategy

Improvements:

  • Complete codebase refactoring — modular architecture with better separation of concerns:
    • Client handling split into dedicated modules (core, entrypoint, protocol, startup, transaction)
    • Configuration system reorganized into focused modules (general, pool, user, tls, prometheus, talos)
    • Admin, auth, and prometheus subsystems extracted into separate modules
    • Improved code maintainability and testability
  • Enhanced async protocol support — significantly improved handling of asynchronous PostgreSQL protocol, providing better performance than other connection poolers for async workloads
  • Extended protocol improvements — better client buffering and message handling for extended query protocol
  • xxhash3 for prepared statement hashing — faster hash computation for prepared statement cache
  • Comprehensive BDD testing framework — multi-language integration tests (Go, Rust, Python, Node.js, .NET) with Docker-based reproducible environment

2.5.0 Nov 18, 2025

Improvements:

  • Reworked the statistics collection system, yielding up to 20% performance gain on fast queries.
  • Improved detection of SAVEPOINT usage, allowing the auto-rollback feature to be applied in more situations.

Bug Fixes / Behavior:

  • Less aggressive behavior on write errors when sending a response to the client: the server connection is no longer immediately marked as "bad" and evicted from the pool. We now read the remaining server response and clean up its state, returning the connection to the pool in a clean state. This improves performance during client reconnections.

2.4.3 Nov 15, 2025

Bug Fixes:

  • Fixed handling of nested transactions via SAVEPOINT: auto-rollback now correctly rolls back to the savepoint instead of breaking the outer transaction. This prevents clients from getting stuck in an inconsistent transactional state.

2.4.2 Nov 13, 2025

Improvements:

  • pg_hba rules now apply to the admin console as well; the trust method can be used for admin connections when a matching rule is present (use with caution; restrict by address/TLS).

Bug Fixes:

  • Fixed pg_hba evaluation: local records were mistakenly considered; PgDoorman only handles TCP connections, so local entries are now correctly ignored.

2.4.1 Nov 12, 2025

Improvements:

  • Performance optimizations in request handling and message processing paths to reduce latency and CPU usage.
  • pg_hba rules now apply to the admin console as well; the trust method can be used for admin connections when a matching rule is present (use with caution; restrict by address/TLS).

Bug Fixes:

  • Corrected logic where COMMIT could be mishandled similarly to ROLLBACK in certain error states; now transactional state handling is aligned with PostgreSQL semantics.

2.4.0 Nov 10, 2025

Features:

  • Added pg_hba support to control client access in PostgreSQL format. New general.pg_hba setting supports inline content or file path.
  • Clients that enter the aborted in transaction state are detached from their server backend; the proxy waits for the client to send ROLLBACK.

Improvements:

  • Refined admin and metrics counters: separated cancel connections and corrected calculation of error connections in admin output and Prometheus metrics descriptions.
  • Added configuration validation to prevent simultaneous use of legacy general.hba CIDR list with the new general.pg_hba rules.
  • Improved validation and error messages for Talos token authentication.

2.2.2 Aug 17, 2025

Features:

  • Added new generate feature functionality

Bug Fixes:

  • Fixed deallocate issues with PGX5 compatibility

2.2.1 Aug 6, 2025

Features:

  • Improve Prometheus exporter functionality

2.2.0 Aug 5, 2025

Features:

  • Added Prometheus exporter functionality that provides metrics about connections, memory usage, pools, queries, and transactions

2.1.2 Aug 4, 2025

Features:

  • Added docker image ghcr.io/ozontech/pg_doorman

2.1.0 Aug 1, 2025

Features:

  • The new command generate connects to your PostgreSQL server, automatically detects all databases and users, and creates a complete configuration file with appropriate settings. This is especially useful for quickly setting up PgDoorman in new environments or when you have many databases and users to configure.

2.0.1 July 24, 2025

Bug Fixes:

  • Fixed max_memory_usage counter leak when clients disconnect improperly.

2.0.0 July 22, 2025

Features:

  • Added tls_mode configuration option to enhance security with flexible TLS connection management and client certificate validation capabilities.

1.9.0 July 20, 2025

Features:

  • Added PAM authentication support.
  • Added talos JWT authentication support.

Improvements:

  • Implemented streaming for COPY protocol with large columns to prevent memory exhaustion.
  • Updated Rust and Tokio dependencies.

1.8.3 Jun 11, 2025

Bug Fixes:

  • Fixed critical bug where Client's buffer wasn't cleared when no free connections were available in the Server pool (query_wait_timeout), leading to incorrect response errors. #38
  • Fixed Npgsql-related issue. Npgsql#6115

1.8.2 May 24, 2025

Features:

  • Added application_name parameter in pool. #30
  • Added support for DISCARD ALL and DEALLOCATE ALL client queries.

Improvements:

  • Implemented link-time optimization. #29

Bug Fixes:

  • Fixed panics in admin console.
  • Fixed connection leakage on improperly handled errors in client's copy mode.

1.8.1 April 12, 2025

Bug Fixes:

  • Fixed config value of prepared_statements. #21
  • Fixed handling of declared cursors closure. #23
  • Fixed proxy server parameters. #25

1.8.0 Mar 20, 2025

Bug Fixes:

  • Fixed dependencies issue. #15

Improvements:

1.7.9 Mar 16, 2025

Improvements:

Bug Fixes:

  • Fixed issues with pqCancel messages over TLS protocol. Drivers should send pqCancel messages exclusively via TLS if the primary connection was established using TLS. Npgsql follows this rule, while PGX currently does not. Both behaviors are now supported.

1.7.8 Mar 8, 2025

Bug Fixes:

  • Fixed message ordering issue when using batch processing with the extended protocol.
  • Improved error message detail in logs for server-side login attempt failures.

1.7.7 Mar 8, 2025

Features:

  • Enhanced show clients command with new fields: state (waiting/idle/active) and wait (read/write/idle).
  • Enhanced show servers command with new fields: state (login/idle/active), wait (read/write/idle), and server_process_pid.
  • Added 15-second proxy timeout for streaming large message_size_to_be_stream responses.

Bug Fixes:

  • Fixed max_memory_usage counter leak when clients disconnect improperly.

Вклад в PgDoorman

Спасибо за интерес к развитию PgDoorman! Это руководство поможет настроить окружение для разработки и разобраться в процессе вклада в проект.

С чего начать

Зависимости

Чтобы запускать интеграционные тесты, нужно только:

  • Docker (обязательно)
  • Make (обязательно)

Установка Nix НЕ требуется -- воспроизводимость тестового окружения обеспечивается Docker-контейнерами, собранными через Nix.

Для локальной разработки (опционально):

  • Rust (последняя стабильная версия)
  • Git

Настройка окружения для разработки

  1. Сделайте fork репозитория на GitHub.
  2. Склонируйте свой fork:
    git clone https://github.com/YOUR-USERNAME/pg_doorman.git
    cd pg_doorman
    
  3. Добавьте upstream-репозиторий:
    git remote add upstream https://github.com/ozontech/pg_doorman.git
    

Локальная разработка

  1. Сборка проекта:

    cargo build
    
  2. Сборка для performance-тестов:

    cargo build --release
    
  3. Настройка PgDoorman:

    • Скопируйте пример конфигурации: cp pg_doorman.toml.example pg_doorman.toml
    • Подправьте настройки в pg_doorman.toml под ваше окружение.
  4. Запуск PgDoorman:

    cargo run --release
    
  5. Запуск unit-тестов:

    cargo test
    

Интеграционное тестирование

PgDoorman использует BDD-тесты (Behavior-Driven Development) с тестовым окружением на Docker. Воспроизводимость гарантирована -- все тесты выполняются внутри Docker-контейнеров с одинаковым окружением.

Тестовое окружение

Тестовый Docker-образ (собранный через Nix) включает:

  • PostgreSQL 16
  • Go 1.24
  • Python 3 с asyncpg, psycopg2, aiopg, pytest
  • Node.js 22
  • .NET SDK 8
  • Rust 1.87.0

Запуск тестов

Из корневой директории проекта:

# Скачать тестовый образ из registry
make pull

# Или собрать локально (10-15 минут на первом запуске)
make local-build

# Запустить все BDD-тесты
make test-bdd

# Запустить тесты с конкретным тегом
make test-bdd TAGS=@copy-protocol
make test-bdd TAGS=@cancel
make test-bdd TAGS=@admin-commands

# Открыть интерактивный shell в тестовом контейнере
make shell

Debug-режим

Включается переменной окружения DEBUG=1:

DEBUG=1 make test-bdd TAGS=@copy-protocol

Когда задан DEBUG=1:

  • Включается tracing с уровнем DEBUG.
  • В логах показываются ID потоков.
  • Включается номер строки.
  • Видны детали PostgreSQL-протокола.
  • Логируется детальное пошаговое выполнение.

Это полезно, когда:

  • Нужно отладить падающий тест.
  • Хочется разобраться в коммуникации на уровне протокола.
  • Расследуете проблемы с таймингами.
  • Разрабатываете новые тестовые сценарии.

Доступные теги тестов

ТегОписание
@goТесты Go-клиентов (lib/pq, pgx)
@pythonТесты Python-клиентов (asyncpg, psycopg2)
@nodejsТесты Node.js-клиентов (pg)
@dotnetТесты .NET-клиентов (Npgsql)
@javaТесты Java-клиентов (JDBC)
@phpТесты PHP-клиентов (PDO)
@rustТесты на уровне протокола, написанные на Rust
@auth-queryТесты auth query authentication
@copy-protocolТесты COPY-протокола
@cancelТесты отмены запросов
@admin-commandsКоманды admin-консоли
@admin-leakТесты на утечку admin-соединений
@buffer-cleanupТесты очистки буфера
@rollbackТесты функциональности rollback
@hbaТесты HBA-аутентификации
@prometheusТесты Prometheus-метрик
@fuzzFuzz-тесты на устойчивость
@benchЗамеры производительности
@binary-upgrade-grac-shutdownТесты binary upgrade и daemon-режима
@static-passthroughТесты static passthrough auth

Написание новых тестов

Тесты организованы как BDD-feature-файлы в tests/bdd/features/. Каждый feature-файл описывает тестовые сценарии в синтаксисе Gherkin.

Shell-тесты (рекомендуются для клиентских библиотек)

Shell-тесты запускают внешние команды (Go, Python, Node.js, .NET, Java, PHP) и проверяют их вывод. Это самый простой способ протестировать совместимость с клиентской библиотекой.

Пример (tests/bdd/features/my-feature.feature):

@go @mytag
Feature: My feature description

  Background:
    Given PostgreSQL started with pg_hba.conf:
      """
      local all all trust
      host all all 127.0.0.1/32 trust
      """
    And fixtures from "tests/fixture.sql" applied
    And pg_doorman started with config:
      """
      [general]
      host = "127.0.0.1"
      port = ${DOORMAN_PORT}
      admin_username = "admin"
      admin_password = "admin"

      [pools.example_db]
      server_host = "127.0.0.1"
      server_port = ${PG_PORT}

      [pools.example_db.users.0]
      username = "example_user_1"
      password = "md58a67a0c805a5ee0384ea28e0dea557b6"
      pool_size = 40
      """

  Scenario: Test my Go client
    When I run shell command:
      """
      export DATABASE_URL="postgresql://example_user_1:test@127.0.0.1:${DOORMAN_PORT}/example_db?sslmode=disable"
      cd tests/go && go test -v -run TestMyTest ./mypackage
      """
    Then the command should succeed
    And the command output should contain "PASS"

Реализация теста (на удобном вам языке):

  • Go: tests/go/mypackage/my_test.go
  • Python: tests/python/test_my.py
  • Node.js: tests/nodejs/my.test.js
  • .NET: tests/dotnet/MyTest.cs

Тесты на уровне протокола на Rust

Чтобы тестировать поведение PostgreSQL-протокола на уровне сообщений, используйте Rust-тесты. Они напрямую отправляют и получают сообщения PostgreSQL-протокола, что даёт точный контроль и возможность сравнения.

Пример (tests/bdd/features/protocol-test.feature):

@rust @my-protocol-test
Feature: Protocol behavior test
  Testing that pg_doorman handles protocol messages identically to PostgreSQL

  Background:
    Given PostgreSQL started with pg_hba.conf:
      """
      local all all trust
      host all all 127.0.0.1/32 trust
      """
    And fixtures from "tests/fixture.sql" applied
    And pg_doorman started with config:
      """
      [general]
      host = "127.0.0.1"
      port = ${DOORMAN_PORT}
      admin_username = "admin"
      admin_password = "admin"
      pg_hba.content = "host all all 127.0.0.1/32 trust"

      [pools.example_db]
      server_host = "127.0.0.1"
      server_port = ${PG_PORT}

      [pools.example_db.users.0]
      username = "example_user_1"
      password = ""
      pool_size = 10
      """

  @my-scenario
  Scenario: Query gives identical results from PostgreSQL and pg_doorman
    When we login to postgres and pg_doorman as "example_user_1" with password "" and database "example_db"
    And we send SimpleQuery "SELECT 1" to both
    Then we should receive identical messages from both

  @session-test
  Scenario: Session management test
    When we create session "one" to pg_doorman as "example_user_1" with password "" and database "example_db"
    And we send SimpleQuery "BEGIN" to session "one"
    And we send SimpleQuery "SELECT pg_backend_pid()" to session "one" and store backend_pid
    # ... ещё шаги

Доступные шаги Rust-тестов:

Сравнение протоколов (отправляет и в PostgreSQL, и в pg_doorman):

  • we login to postgres and pg_doorman as "user" with password "pass" and database "db"
  • we send SimpleQuery "SQL" to both
  • we send CopyFromStdin "COPY ..." with data "..." to both
  • we should receive identical messages from both

Управление сессиями (для сложных сценариев):

  • we create session "name" to pg_doorman as "user" with password "pass" and database "db"
  • we send SimpleQuery "SQL" to session "name"
  • we send SimpleQuery "SQL" to session "name" and store backend_pid
  • we abort TCP connection for session "name"
  • we sleep 100ms

Тестирование cancel-запросов:

  • we create session "name" ... and store backend key
  • we send SimpleQuery "SQL" to session "name" without waiting for response
  • we send cancel request for session "name"
  • session "name" should receive cancel error containing "text"

Добавление зависимостей

Если в тестовом окружении нужны дополнительные пакеты, отредактируйте tests/nix/flake.nix:

  • Python-пакеты добавляются в pythonEnv.
  • Системные пакеты -- в runtimePackages.

После изменения flake.nix пересоберите образ командой make local-build.

Правила вклада

Стиль кода

  • Следуйте Rust style guidelines.
  • Используйте осмысленные имена переменных и функций.
  • Добавляйте комментарии для нетривиальной логики.
  • Пишите тесты для новой функциональности.

Процесс Pull Request

  1. Создайте новую ветку для своей фичи или багфикса.
  2. Внесите изменения и закоммитьте их с понятными, описательными сообщениями.
  3. Напишите или обновите тесты, если требуется.
  4. Обновите документацию, отражая изменения.
  5. Откройте pull request в основной репозиторий.
  6. Реагируйте на замечания code review.

Issues

Если нашли баг или хотите предложить новую функциональность, создайте issue в репозитории на GitHub с:

  • Чётким, описательным заголовком.
  • Подробным описанием проблемы или фичи.
  • Шагами воспроизведения (для багов).
  • Ожидаемым и фактическим поведением (для багов).

Где получить помощь

Если нужна помощь с вкладом в проект:

  • Задавайте вопросы в GitHub issues.
  • Заходите в Telegram-канал: @pg_doorman.
  • Свяжитесь с maintainers.

Спасибо, что вносите вклад в PgDoorman!