PgDoorman

Многопоточный пулер соединений для PostgreSQL, написанный на Rust. Совместимая замена для PgBouncer и Odyssey, альтернатива PgCat. Три года в промышленной эксплуатации у Ozon под нагрузками Go (pgx), .NET (Npgsql), Python (asyncpg, SQLAlchemy) и Node.js.

Скачать PgDoorman 3.11.0 · Сравнение · Бенчмарки

Ключевые возможности

Кеш анонимного Parse

В extended protocol многие драйверы отправляют короткие параметризованные запросы как Parse без имени statement. PgDoorman переименовывает такой Parse на стороне PostgreSQL в служебный DOORMAN_<N> и хранит соответствие в пуле. Следующие Bind для той же формы запроса используют уже подготовленное состояние.

Это снижает повторную работу планировщика PostgreSQL на горячих OLTP-запросах без изменений в приложении. PgBouncer 1.21+ и Odyssey работают с именованными prepared statements, но анонимный Parse передают как есть; PgDoorman закрывает этот частый сценарий у драйверов по умолчанию.

Кеш не растёт бесконтрольно: анонимные записи истекают по таймауту бездействия, именованные освобождаются после последней ссылки. SHOW INTERNER показывает память текстов запросов, а метрики Prometheus — попадания, промахи и вытеснения.

Подробнее →

Координатор пулов

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

Доноры ранжируются по излишку свободных соединений. При равном излишке первым уступает пул с более высоким p95 времени транзакции: быстрые пулы сохраняют больше шансов на переиспользование, а у медленных вытеснение свободного соединения меньше мешает работе. Резервный пул сглаживает короткие всплески, а min_guaranteed_pool_size защищает важные нагрузки от вытеснения.

В PgBouncer max_db_connections задаёт общий лимит, но не перераспределяет уже открытые свободные соединения между пулами. В Odyssey прямого аналога нет.

Подробнее →

Fallback через Patroni

Если PgDoorman запущен рядом с PostgreSQL и локальный сервер пропадает во время Patroni switchover, новые серверные соединения временно уходят на другой живой узел кластера. PgDoorman выбирает узел через Patroni REST API (GET /cluster): сначала sync_standby, затем replica.

Локальный сервер уходит в период охлаждения, а fallback-соединения получают короткий lifetime. Когда локальный узел снова доступен, пул возвращается на него без отдельного HAProxy или consul-template перед пулером. Настройки patroni_api_urls и fallback_cooldown задаются в [general].

Подробнее →

Горячая замена процесса с переносом сессий

Перед SIGUSR2 или UPGRADE можно заменить бинарь и конфиг на диске. PgDoorman проверяет эту комбинацию через -t, запускает дочерний процесс уже с ней, передаёт ему слушающий сокет, а старый процесс продолжает обслуживать существующих клиентов и переносит мигрируемые сессии.

В новый процесс уходят connection_id, ключ отмены запроса, параметры PostgreSQL-сессии, состояние аутентификации к PostgreSQL и клиентский кеш prepared statements. Новый процесс восстанавливает клиента в том же пуле. Для приложения соединение не рвётся: без переподключения, повторного auth/SCRAM и потери prepared statements. Если на новом серверном соединении statement ещё не подготовлен, PgDoorman отправит нужный Parse при первом Bind.

В foreground-режиме TCP-сессии без TLS передаются через SCM_RIGHTS. TLS-сессии мигрируют только в Linux-сборке с фичей tls-migration и теми же tls_certificate/tls_private_key; в обычных пакетах и Docker-образе она выключена, поэтому TLS-клиенты дренируются. Клиенты внутри транзакции остаются на старом процессе и переезжают после COMMIT или ROLLBACK. У PgBouncer (-R, устарел с 1.20, или rolling restart через so_reuseport) и Odyssey (SIGUSR2 + bindwith_reuseport) старые сессии остаются в старом процессе до отключения клиентов.

Подробнее →

Встроенная диагностическая веб-консоль

PgDoorman отдаёт веб-консоль на том же адресе и порту, что и /metrics. Это локальная панель для разбора инцидента, а не замена долгосрочному мониторингу в Prometheus/Grafana.

На одном экране видны насыщение пулов, p95/p99 задержки запросов, транзакций и ожидания соединения, ошибки по SQLSTATE, долгие запросы, состояние prepared statements и query interner, хвост лога, CPU по потокам tokio-worker и память процесса (jemalloc, /proc/self/status, код/библиотеки, стеки, swap).

Из консоли можно выполнить Pause, Resume, Reconnect и Reload для одного пула или всего инстанса. Остальные экраны доступны только для чтения. Веб-интерфейс включается только при [web].ui = true и непустом general.admin_password, отличном от admin; иначе PgDoorman оставляет только /metrics и пишет WARN в лог.

Подробнее →

Почему PgDoorman

  • Кеш Parse на горячих запросах. PgDoorman переиспользует подготовленное состояние в пределах пула, включая безымянный Parse, который многие драйверы отправляют для коротких параметризованных запросов. Это снижает CPU планировщика PostgreSQL на повторяющихся OLTP-запросах; SHOW INTERNER показывает память текстов запросов, а метрики Prometheus — попадания, промахи и вытеснения.
  • Один пул для всех рабочих потоков. Рабочие потоки используют общий набор серверных соединений. При масштабировании PgBouncer несколькими процессами за so_reuseport каждый процесс держит свой пул, поэтому свободные соединения могут распределяться неравномерно.
  • Контроль всплесков при создании соединений. Если много клиентов одновременно ждут несколько свободных серверных соединений, PgDoorman ограничивает параллельное создание новых соединений (scaling_max_parallel_creates) и передаёт вернувшееся соединение самому старому ожидающему клиенту.
  • Предсказуемая задержка выдачи соединения. Ожидающие клиенты обслуживаются по FIFO. PgDoorman заранее заменяет серверные соединения перед истечением server_lifetime, чтобы ротация поколения не превращалась во всплеск задержки при выдаче соединения.
  • Быстрое обнаружение обрыва PostgreSQL. Если серверное соединение пропадает во время транзакции, PgDoorman отслеживает это параллельно чтению клиента и возвращает SQLSTATE 08006, не дожидаясь системного TCP keepalive.
  • Операционные инструменты в бинаре. Конфиг пишется в YAML или TOML, длительности задаются как 30s или 5m, pg_doorman generate --host ... собирает стартовый конфиг из существующего PostgreSQL, pg_doorman -t проверяет конфиг без запуска сервера, а /metrics доступен без отдельного экспортёра.

Сравнение

ФункцияPgDoormanPgBouncerOdyssey
Общий пул соединений для всех рабочих потоковДаНет, один рабочий потокРабочие процессы с отдельными пулами
Prepared statements в transaction poolingДаДа, с версии 1.21Да, через pool_reserve_prepared_statement
Кеш анонимного Parse для горячих параметризованных запросовДа, переиспользуется между клиентами пулаНет, только именованныеНет, только именованные
Общий лимит серверных соединений к базеДа, с вытеснением свободных соединенийНетНет
Переключение на резервный узел через PatroniДа, встроеноНетНет
Опережающая замена стареющих серверных соединенийДаНетНет
Обрыв серверного соединения во время транзакцииДа, возвращает 08006 без ожидания TCP keepaliveНет, ждёт TCP keepaliveНет, ждёт TCP keepalive
Горячая замена процесса с переносом свободных сессийДа, через SCM_RIGHTS; TLS-состояние при сборке tls-migrationНет, старые сессии остаются в старом процессеНет, старые сессии остаются в старом процессе
TLS-соединение от пулера к PostgreSQLДа, 5 режимов и reload по SIGHUPДа, server_tls_* и reload по RELOADНет
SCRAM passthrough без открытого пароля в конфигеДа, извлекает ClientKey из SCRAM proofДа, зашифрованный SCRAM secret через auth_query/userlist.txt, с 1.14Да
JWT-аутентификация (RSA-SHA256)ДаНетНет
PAM, pg_hba.conf и auth_queryДаДаДа
LDAP-аутентификацияНетДа, с версии 1.25Да
Формат конфигурацииYAML или TOMLINIСобственный формат
Структурированные JSON-логиДаНетДа, log_format "json"
Перцентили задержек p50/p90/p95/p99Да, через встроенный /metricsНет, только средние значенияДа, через отдельный экспортёр на Go
Диагностическая web-консольДа, встроеннаяНет, только admin-консоль через psqlНет, только admin-консоль через psql
Проверка конфига без запуска сервераДа, -tНетНет
Генерация начального конфига из PostgreSQLДа, generate --hostНетНет
HTTP-эндпоинт PrometheusВстроенный /metricsОтдельный экспортёрОтдельный экспортёр на Go

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

Бенчмарки

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%

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

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

Установите пакет через ваш дистрибутив:

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

# Fedora / RHEL family
sudo dnf copr enable @pg-doorman/pg-doorman
sudo dnf install pg_doorman

Дистрибутивные пакеты и Docker-образ собраны без фич tls-migration и pam. Матрица TLS-фич и инструкции по сборке — в Установке.

Либо запустите через Docker:

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

Минимальный конфиг (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

Сравнительная матрица фич для выбора пулера соединений PostgreSQL. Каждое утверждение про PgBouncer привязано к config reference и changelog; каждое утверждение про Odyssey — к docs проекта.

PgCat намеренно опущен: у него центр тяжести — шардинг и балансировка, а не drop-in замена PgBouncer, поэтому построчное сравнение вводит в заблуждение. Если нужен горизонтальный шардинг, см. репозиторий PgCat.

Цифры из бенчмарков — Бенчмарки.

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

ВозможностьPgDoormanPgBouncerOdyssey
MD5 passwordДаДаДа
SCRAM-SHA-256 (клиент → пулер)ДаДаДа
Сквозной SCRAM-SHA-256 (без открытого пароля в конфиге)Да (ClientKey извлекается из proof клиента)Да (с 1.14, encrypted SCRAM secret в auth_query / userlist.txt)Да
Сквозной MD5ДаДаДа
auth_query (динамические пользователи)ДаДаДа
Сквозной режим auth_query (своя идентичность PostgreSQL для каждого пользователя)ДаНет (один auth_user на все lookup-запросы)Да
Файл в формате pg_hba.confДа (файл или inline)Да (auth_hba_file)Да (с 1.4)
PAMДа (Linux)Да (auth_type=pam или через HBA)Да
JWT (RSA-SHA256)ДаНетНет
Talos (custom JWT с извлечением роли)Да (специфика Ozon)НетНет
LDAPНетДа (с 1.25)Да
SCRAM channel binding (scram-sha-256-plus)НетДаДа
User-name maps (cert/peer → DB user)НетДа (с 1.23)Да
Тонкая настройка scram_iterationsНетДа (с 1.25)Нет

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

TLS

ВозможностьPgDoormanPgBouncerOdyssey
Client-side TLS (режимы: disable, allow, require, verify-full)ДаДа (disable, allow, prefer, require, verify-ca, verify-full)Да
Server-side TLS к PostgreSQL (disable, allow, require, verify-ca, verify-full)Да (5 режимов)Да (server_tls_*, 6 режимов вкл. prefer)Нет
mTLS к PostgreSQL (отправка клиентского сертификата на backend)Да (server_tls_certificate + server_tls_private_key)Да (server_tls_key_file + server_tls_cert_file)Нет
Hot reload server-side TLS-сертификатовДа (SIGHUP)Да (через RELOAD / SIGHUP, "new file contents will be used for new connections")Нет
Hot reload client-facing TLS-сертификатовНет (требуется restart или горячая замена процесса)Да (через RELOAD / SIGHUP)Нет
Минимальная версия TLS настраиваетсяДа (по умолчанию TLS 1.2)Да (tls_protocols, default tlsv1.2,tlsv1.3)Настраивается, дефолты другие
Direct TLS handshake (PostgreSQL 17, без SSLRequest)НетДа (с 1.25)Нет
Контроль TLS 1.3 cipher suitesНетДа (с 1.25, client_tls13_ciphers/server_tls13_ciphers)Нет
Миграция TLS-сессии при горячей замене процессаДа (сборка tls-migration, Linux, по запросу)Нет (TLS-соединения отбрасываются при online restart)Нет

См. TLS.

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

ВозможностьPgDoormanPgBouncerOdyssey
Fallback через Patroni (встроенный lookup /cluster)ДаНетНет
Bundled TCP-прокси с маршрутизацией по ролям (patroni_proxy)ДаНетНет
Защита от лага репликДа (max_lag_in_bytes в patroni_proxy)НетДа (watchdog_lag_query + catchup_timeout)
Несколько хостов PostgreSQL с балансировкойДа (patroni_proxy)Да (с 1.24, load_balance_hosts)Да
target_session_attrs (read-write / read-only routing)Да (через роли patroni_proxy)НетДа
Sequential routing rules (правило-в-порядке-первое-совпадение)НетНетДа
Маршрутизация по типу соединения (TCP vs UNIX)НетНетДа
Выбор хоста с учётом availability zoneНетНетДа

См. Fallback через Patroni, patroni_proxy.

Пулинг

ВозможностьPgDoormanPgBouncerOdyssey
Режимы пулаsession, transactionsession, transaction, statementsession, transaction
Координатор пулов (лимит на базу с приоритетным вытеснением)Да (max_db_connections + вытеснение по p95)Нет (max_db_connections ставит клиентов в очередь, пока существующие соединения не закроются по idle timeout)Нет
Резервный пулДа (reserve_pool_size)Да (reserve_pool_size)Нет
Per-user min_guaranteed_pool_sizeДаНетНет
Опережающая замена при истечении server_lifetime (warm-up до экспирации старого)Да (порог 95%, до 3 параллельных)НетНет
Упреждающее ожидание и ограничение всплеска (scaling_warm_pool_ratio, быстрые повторы)ДаНетНет
Прямая передача (возвращающееся соединение уходит самому давно ждущему клиенту через in-process oneshot-канал)ДаНетНет
Строгий FIFO порядок ожидающихДаНет (LIFO через server_round_robin = 0)Нет
min_pool_size (warm connections)ДаНетДа
Prepared statements в transaction modeДа (именованные и анонимные, двухуровневый кеш, query interner)Да (именованные, с 1.21, max_prepared_statements)Да (именованные, pool_reserve_prepared_statement)
Кеш анонимного Parse для производительностиДа (DOORMAN_N, переиспользование между клиентами пула)Нет (анонимный Parse проходит без изменений)Нет (требуются именованные prepared statements)
Умная очистка при возврате соединения (пропустить DEALLOCATE ALL, если кеш не менялся)Да (RESET ALL / DEALLOCATE ALL по факту мутаций)Нет (всегда DISCARD ALL, если задан server_reset_query)Да (auto)
LISTEN / NOTIFY pinning в transaction modeНетНетЭкспериментально
Cross-rule connection cap (shared_pool)НетНетДа (с 1.5.1)
Команды администратора PAUSE / RESUME / RECONNECTДаДаДа (с 1.4.1)
GUC PostgreSQL на уровне пула в backend StartupMessageДа (startup_parameters: general → пул → passthrough auth_query; клиентские RESET ALL / DISCARD ALL возвращают эти значения; ошибки PG при запуске бэкенда доходят до клиента без переписывания)Нет эквивалентных операторских значений по умолчанию; отдельные клиентские startup-параметры можно отслеживать или игнорироватьНет (maintain_params сохраняет клиентские параметры при rebind; операторских GUC нет)

См. Координатор пулов, Пул под нагрузкой.

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

ВозможностьPgDoormanPgBouncerOdyssey
server_idle_check_timeout (probe перед checkout)ДаНетНет
idle_timeout (server-side)Да (idle_timeout)Да (server_idle_timeout)Да
server_lifetimeДаДаДа
query_wait_timeoutДаДаДа
client_idle_timeoutНетДа (с 1.24)Нет
transaction_timeout (enforced пулером)НетДа (с 1.25)Нет
max_user_client_connectionsНетДа (с 1.24)Нет
max_db_client_connectionsНетДа (с 1.24)Нет
Per-user query_timeoutНетДа (с 1.24)Нет
Per-user reserve_pool_sizeНетДа (с 1.24)Нет
Уведомление клиента, пока тот ждёт серверное соединениеНетДа (с 1.25, query_wait_notify)Да (pool_notice_after_waiting_ms)

См. Справочник по general-настройкам, Справочник по pool-настройкам.

Наблюдаемость

ВозможностьPgDoormanPgBouncerOdyssey
Встроенная веб-консоль администратораДа (HTML-консоль на том же порту, что и /metrics, включается через [web].ui)Нет (только psql admin-консоль)Нет (только psql admin-консоль)
Prometheus-эндпоинтВстроенный /metricsВнешний (pgbouncer_exporter)Внешний (Go-exporter sidecar, опрашивает admin-консоль)
Перцентили задержки на пул (p50, p90, p95, p99)Да (HDR Histogram)Нет (только средние в SHOW STATS)Да через exporter (TDigest, требует rule-опцию quantiles)
Счётчики prepared statements в SHOW STATSДаДа (с 1.24)Нет
Структурированные JSON-логиДа (--log-format structured)НетДа (log_format "json")
Управление уровнем логов в рантайме (SET log_level)ДаНетНет
SHOW POOL_COORDINATOR / SHOW POOL_SCALING / SHOW SOCKETSДаНетНет
SHOW PREPARED_STATEMENTSДаНетНет
SHOW INTERNER (записи / байты / предпросмотр по половинам)ДаНетНет
Ограниченный prepared-кеш (TTL у анонимных, клиентский LRU с разделением Named/Anonymous)ДаТолько named, с лимитом max_prepared_statements; анонимного кеша нетНет
SHOW HOSTS (CPU/память хоста)НетНетДа
SHOW RULES (дамп активной маршрутизации)НетНетДа
Метрики server-side TLS-соединений (длительность handshake, ошибки, активные)ДаНетНет
Метрики Patroni APIДаНетНет
Метрики fallback (active flag, текущий хост, hits)ДаНетНет

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

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

ВозможностьPgDoormanPgBouncerOdyssey
Горячая замена процесса с миграцией сессий (TCP-сокет, cancel keys, prepared cache)Да (SCM_RIGHTS, плюс TLS state со сборкой tls-migration)Нет: -R deprecated с 1.20; rolling restart через so_reuseport оставляет старые сессии на старом процессеНет: SIGUSR2 + bindwith_reuseport оставляет старые сессии на старом процессе
Формат конфигаYAML или TOMLINIСвой формат (lex/yacc)
Человекочитаемые длительности и размеры (30s, 1h, 256MB)ДаНет (целые микросекунды / байты)Нет
Режим проверки конфига (pg_doorman -t)ДаНетНет
Авто-конфиг из PostgreSQL (pg_doorman generate --host)ДаНетНет
Перезагрузка по SIGHUPДа (серверные TLS-сертификаты включены; клиентский TLS требует рестарта)Да (auth_file, auth_hba_file, server и client TLS certs)Да
systemd sd-notify (Type=notify)ДаНетНет
Лимит памяти (max_memory_usage)ДаНетНет
Лимит TCP-буферовДа (tcp_socket_buffer_size для клиентских TCP-сокетов и TCP-сокетов к PostgreSQL)Да (tcp_socket_buffer)Нет

См. Горячая замена процесса с переносом сессий, Сигналы.

Протокол

ВозможностьPgDoormanPgBouncerOdyssey
Simple queryДаДаДа
Extended queryДаДаЧастично
Pipelined batchesДаДаЧастично
Async FlushДаДаНет
Cancel requests поверх TLSДаДаДа
COPY IN / COPY OUTДаДаДа
Replication passthrough (replication=true startup)НетДа (с 1.23)Нет
Согласование версии протокола (3.2)НетДа (с 1.23)Нет
server_drop_on_cached_plan_errorНетНетДа (с 1.5.1)

Когда PgDoorman не подойдёт

  • Нужна LDAP-аутентификация. Используйте Odyssey или PgBouncer 1.25+.
  • Нужен replication passthrough для logical replication tools. Используйте PgBouncer 1.23+.
  • Нужен transaction_timeout, который применяет сам пулер. Используйте PgBouncer 1.25+.
  • Нужен горизонтальный шардинг внутри пулера. Используйте PgCat.

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

Обзор

Что делает PgDoorman

PgDoorman сидит между приложениями и PostgreSQL. Для приложения он выглядит как сервер PostgreSQL (тот же wire-протокол, та же строка подключения для psql); внутри он мультиплексирует много клиентских сессий на гораздо меньший набор реальных серверных соединений.

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

Изначально PgDoorman был форкнут из PgCat, но с тех пор переписан вокруг других целей: prepared statements в transaction mode, многопоточные общие пулы, интеграция с Patroni и горячая замена процесса с миграцией живых сессий. Сейчас это самостоятельная кодовая база.

Зачем вообще пулер

Каждое соединение к PostgreSQL стоит серверу около 10 МБ RAM, отдельный процесс и время на каждый handshake (auth, SCRAM, разрешение search_path). Без пулера приложение, открывающее N короткоживущих соединений в секунду, платит N×время-handshake. Пулер позволяет тем же N клиентам переиспользовать небольшой набор долгоживущих серверных соединений, и стоимость handshake оплачивается один раз на соединение с PostgreSQL, а не один раз на клиента.

Конкретные эффекты:

  • pool_size равный 40 обычно обслуживает несколько тысяч клиентских сессий для коротких OLTP-транзакций.
  • PostgreSQL не платит per-process memory overhead за соединения, которые иначе пришлось бы держать открытыми.
  • Failover, рестарт или rolling deploy не превращаются в лавину новых auth/SCRAM handshake-ов.

Режимы пула

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

Серверное соединение удерживается на время одной транзакции и возвращается в пул при COMMIT или ROLLBACK. Именно в этом режиме пулинг реально окупается.

Session

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

PgDoorman не реализует statement mode. См. режимы пула — точный контракт каждого режима и что работает в transaction mode у нас, чего нет у других пулеров.

Что есть для эксплуатации

  • Консоль администратора — PostgreSQL-совместимый эндпоинт для SHOW POOLS, SHOW CLIENTS, RELOAD, PAUSE, UPGRADE и т.д.
  • Prometheus /metrics — встроенный HTTP-эндпоинт с per-pool latency-перцентилями, счётчиками prepared statements, состоянием fallback и метриками TLS.
  • Видимость prepared-кешаSHOW INTERNER и SHOW POOLS_MEMORY показывают объём интернера и клиентскую разбивку Named / Anonymous, с парными метриками в Prometheus.
  • pg_doorman -t — валидация конфига без запуска сервера.
  • pg_doorman generate --host … — собрать starter-конфиг через интроспекцию существующего PostgreSQL.

См. команды администратора и Справочник Prometheus.

Куда дальше

Установка PgDoorman

PgDoorman работает на Linux и macOS. Для промышленной эксплуатации рекомендуем собирать самим — так вы контролируете версию 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

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

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

По умолчанию TLS-клиенты не могут перейти на новый процесс при горячей замене процесса — они получают ошибку 58006 и переподключаются. Чтобы TLS-соединения переходили на новый процесс без разрыва, соберите с фичей 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.

Сборка без доступа к интернету:

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. Полное описание процесса обновления, мониторинг и диагностика — в плавном обновлении бинаря.

Для упаковки в deb/rpm смотрите каталоги debian/ и pkg/ в репозитории.

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

Готовые 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"
curl -L -o pg_doorman.sha256 \
  "https://github.com/ozontech/pg_doorman/releases/download/VERSION/pg_doorman-TARGET.sha256"
sha256sum -c pg_doorman.sha256                    # должно вывести "OK"
chmod +x pg_doorman
sudo mv pg_doorman /usr/local/bin/

Пропуск checksum-шага означает доверие сетевому пути между вами и objects.githubusercontent.com. Не делайте так.

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

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

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

CMD образа по умолчанию запускает pg_doorman без аргументов. При WORKDIR /etc/pg_doorman это означает конфиг /etc/pg_doorman/pg_doorman.toml. Если монтируете YAML, передайте путь явно, как в примере выше.

Пробрасывайте 6432 для PostgreSQL-протокола. Если в конфиге включён web.enabled, дополнительно пробросьте 9127 для /metrics и веб-консоли; без этого флага listener не поднимается. Путь к конфигу можно задать позиционным аргументом или переменной CONFIG_FILE; также доступны LOG_LEVEL (по умолчанию info), LOG_FORMAT (text, structured или debug) и NO_COLOR.

В Dockerfile задан STOPSIGNAL SIGTERM, поэтому docker stop отправляет pg_doorman обычный сигнал остановки контейнера. Не используйте SIGINT для остановки контейнера: вне TTY этот сигнал запускает горячую замену процесса, что для контейнерного запуска обычно приводит к завершению PID 1.

Публичный образ собран без фич tls-migration и pam. Обычный TLS для клиентских и серверных соединений не зависит от tls-migration; эта фича нужна только для переноса TLS-сессий при горячей замене процесса. Для TLS migration или PAM соберите свой образ из публичного Dockerfile, добавив --features tls-migration и/или pam в шаг cargo build --release.

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

Сквозной разбор: ключи командной строки, минимальный конфиг, admin-консоль и эксплуатационные команды (PAUSE, RESUME, RECONNECT, RELOAD). Эта страница — вторая по порядку чтения после Обзора и Установки.

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

$ 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                 force colors off in the log output
  -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Принудительно отключить цвета в логах. Цвета также автоматически выключаются, если stderr не терминал (под systemd — это pipe в journald) и если переменная окружения 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"  # Поменяйте это в промышленной эксплуатации!

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

Приложение разговаривает по PostgreSQL wire-протоколу; уровень пулинга для него прозрачен.

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

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. В промышленной эксплуатации предпочитайте методы с паролями, если только не понимаете последствия полностью.

Мониторинг 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, отключая всех клиентов. В промышленной эксплуатации используйте её с осторожностью.

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 применяет большинство параметров конфигурации без разрыва существующих соединений — то, что нужно в промышленной эксплуатации, где простой недопустим.

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.
SIGUSR2Горячая замена процесса. Валидирует новый бинарь и конфиг флагом -t, запускает новый процесс, передаёт ему слушающий сокет и переносит сессии, где это возможно. Рекомендуется для обновлений. См. Горячая замена процесса с переносом сессий.
SIGINTForeground + TTY (Ctrl+C): только плавное завершение (без замены процесса). Режим демона / без TTY: горячая замена процесса для совместимости со старыми установками.
SIGTERMНемедленное завершение. Активные соединения обрываются.

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

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

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

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

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

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

МетодКогда использоватьХранит ли секрет в конфиге?
Сквозной режим (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: сквозная и явно заданная

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

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

Подробности смотрите в сквозном режиме, а про выделенный режим — в 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.

Куда дальше

  • Новая инсталляция? Прочтите сквозной режим и базовое использование.
  • Много пользователей с ротируемыми учётными данными? Используйте 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:

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

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

Сквозной режим

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. Дополнительные колонки игнорируются, кроме необязательной startup_parameters. В сквозном режиме pg_doorman читает эту колонку как text, json или jsonb; значение должно быть JSON-объектом с параметрами запуска PostgreSQL для конкретного пользователя. В выделенном режиме эта колонка игнорируется, а в лог пишется предупреждение.

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

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

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

Выделенный режим

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.

Мониторинг

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. Смотрите команды администратора.

Аутентификация 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 не реализована. Используйте 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

Заблокировать устаревший 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 нет.

Горячая замена процесса может загрузить новый сертификат и ключ для новых входящих TLS-подключений, но это не бесшовная ротация для уже открытых TLS-сессий. Для переноса TLS-сессий оба процесса должны использовать те же tls_certificate и tls_private_key, а сборка должна быть Linux + tls-migration; при смене этих файлов TLS-клиенты дренируются и переподключаются.

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

Минимум TLS 1.2 применяется на этапе handshake. PgDoorman не задаёт явный список шифров — эффективные шифры берутся из системной сборки OpenSSL. Если нужен жёсткий список, настройте его системно (/etc/ssl/openssl.cnf) или соберите OpenSSL с нужной политикой.

Direct TLS handshake (PostgreSQL 17, без SSLRequest) не поддерживается. Для управления шифрами TLS 1.3 или direct TLS из PostgreSQL 17 используйте 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-контекстом; новые соединения используют перезагруженные сертификаты. Перезагрузка не берёт блокировку на горячем пути (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).

Мониторинг

Серверный 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-запросы к PostgreSQL минуют серверный TLS — они идут по свежему обычному TCP-соединению. Это совпадает с дизайном протокола PostgreSQL (cancel отправляется по отдельному сокету).
  • Direct TLS handshake (быстрый handshake PostgreSQL 17 без SSLRequest) не поддерживается ни на одной из сторон.

Куда дальше

Режимы пула

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

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

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

pools:
  mydb:
    pool_mode: "transaction"

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

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

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

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

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

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

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

pools:
  legacy_app:
    pool_mode: "session"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Справочник

Координатор пулов

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

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

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

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

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

Координатор в pg_doorman ограничивает сумму и:

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

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

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

  • На одной базе работают разные приложения или пользователи, и нужен верхний предел числа серверных соединений (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 не нашлось свободного соединения.
    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. Найти кандидатов со свободными соединениями. Пользователь, у которого все соединения активны, не может стать донором: его работа ещё выполняется.
  2. Пропустить защищённых. Пользователь ниже min_guaranteed_pool_size исключается.
  3. Пропустить недавно созданные соединения. Соединения младше min_connection_lifetime не вытесняются; это снижает колебания при коротких простоях.
  4. Ранжировать по излишку. Пользователи с наибольшим числом свободных соединений сверх min_guaranteed_pool_size получают высший ранг.
  5. Разрешить ничью по p95 времени транзакции. Среди пулов с одинаковым излишком первым уступает тот, у кого выше p95. Высокий p95 означает, что каждая транзакция дольше держит соединение, а значит одно вытеснение ломает меньше повторных выдач.

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

Мониторинг

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.

Оговорки

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

Куда дальше

Кеширование анонимных prepared statements

PgDoorman кеширует анонимные сообщения Parse в transaction pooling. Многие драйверы отправляют короткие параметризованные запросы как Parse с пустым именем. Без подмены PostgreSQL заново строит план на каждом Bind, поэтому горячие OLTP-запросы каждый раз тратят CPU планировщика.

PgDoorman прозрачно переписывает каждый анонимный Parse в служебное имя DOORMAN_<N> на серверном соединении. План попадает в реестр именованных prepared statements и переиспользуется между Bind одного клиента и между разными клиентами одного пула. Главный эффект — меньше CPU на планирование и меньше повторных Parse для одинаковых форм запроса.

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

PgBouncer (1.21+) и Odyssey поддерживают prepared statements в transaction mode, но только для именованных statements. Анонимный Parse они передают без изменений. Отличие PgDoorman — подмена анонимного Parse. Ограничения кеша, LRU, TTL и метрики нужны, чтобы эта подмена не превращалась в рост памяти на динамическом SQL.

Что ускоряется

Кеш анонимного Parse убирает повторную работу на горячих параметризованных запросах:

  • бэкенд PostgreSQL не получает одинаковый Parse при каждом повторном обращении к уже известной форме запроса;
  • PostgreSQL может использовать prepared statement, который уже создан на этом серверном соединении;
  • разные клиенты одного пула используют одну пуловую запись, а не прогревают одинаковый запрос независимо друг от друга;
  • при попадании в кеш бэкенда PgDoorman синтезирует ParseComplete без обращения к PostgreSQL.

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

Базовая модель PostgreSQL

В сообщении Parse имя prepared statement задаёт его тип: пустое имя соответствует анонимному statement, любое непустое — именованному:

                          Время жизни в PG        Кеширование плана
  ─────────────────────   ─────────────────       ─────────────────────
  Анонимный (name="")     До следующего           Нет именованной
                          анонимного Parse        записи; каждый новый
                          или конца сессии        анонимный Parse
                                                  строит план заново
  Именованный             До Close /              Сначала custom-планы;
   (name="stmt_42")       DEALLOCATE /            после 5 custom-
                          конца сессии            выполнений возможен
                                                  generic-план

Большинство современных драйверов по умолчанию используют анонимные prepared для разовых параметризованных запросов: lib/pq (Go), libpq PQexecParams (C), часть режимов в pgjdbc и psycopg. Прикладной код выглядит как обычный параметризованный запрос, но в wire protocol уходит пустое имя.

Почему это проблема в transaction pooling

В transaction pooling одно серверное соединение по очереди обслуживает разных клиентов. Если пулер отправляет пустой Parse как есть, каждый Bind клиента приходит на соединение, у которого плана для этого запроса нет. Горячие OLTP-пути платят CPU планировщика на каждом обращении.

Именованные prepared решают проблему производительности планирования, но перекладывают учёт на пулер:

  • Пулер должен помнить именованные statements каждого клиента до его отключения, даже если общий кеш пула уже выселил запись.
  • На каждом Bind пулер должен проверить, знает ли текущее серверное соединение это имя, и при необходимости заново сделать Parse.
  • При отключении клиента пулер должен отправить Close или DEALLOCATE на правильное серверное соединение.
  • Драйверы, которые генерируют отдельное имя stmt_<seq> под каждый уникальный запрос, раздувают клиентский кеш: сотни записей на клиента, при тысячах подключений превращающиеся в миллионы записей в памяти.

Остаются два варианта: отказаться от кеширования плана для анонимных запросов или взять на себя полный учёт именованных. PgDoorman выбирает третий путь.

Что делает PgDoorman

На каждый анонимный Parse от клиента PgDoorman:

  1. Считает хеш по тексту запроса, OID типов параметров и короткому отпечатку параметров планировщика, которые клиент закрепил при подключении (search_path, default_transaction_isolation, default_transaction_read_only, default_text_search_config, role). Поэтому два клиента с одинаковым запросом и OID параметров, но с разными значениями search_path в StartupMessage, получат разные записи кэша и разные планы PostgreSQL. Другие параметры, которые тоже могут влиять на план (TimeZone, DateStyle, plan_cache_mode, enable_*, настройки стоимости JIT), в этот отпечаток не входят. Перед тем как использовать один и тот же prepared-запрос с разными значениями таких параметров, посмотрите ограничения в справке по sync_server_parameters.
  2. Ищет хеш в общем кеше пула. При промахе выделяет новое имя DOORMAN_<counter> и регистрирует запись Arc<Parse>.
  3. Записывает в клиентский кеш ключ Anonymous(hash), чтобы следующий Bind нашёл тот же DOORMAN_<N>.
  4. Отправляет Parse на бэкенд с переписанным именем.
  5. На соответствующем Bind (с пустым именем) переписывает имя statement в DOORMAN_<N> и проверяет, что текущий бэкенд уже держит запись; если нет, отправляет Parse повторно.

Клиент никогда не видит DOORMAN_<N>: имя живёт только на участке между PgDoorman и бэкендом. Когда нужный бэкенд уже держит запись, PgDoorman синтезирует ParseComplete сам и не делает round-trip.

Пример wire-protocol

Go-приложение, выполняющее

db.Query("SELECT * FROM t WHERE name = $1", "vasya")

через lib/pq, отправляет такой обмен:

  Клиент                  PgDoorman                  Backend
  ──────                  ─────────                  ───────
  Parse("", q)        ───►│ hash, miss → DOORMAN_42
                           │ pool_cache[hash] = Arc<Parse>
                           │ client_cache[Anon(hash)] = ...
                           │            Parse("DOORMAN_42") ─────►
                           │                   ◄── ParseComplete
                      ◄────│ ParseComplete
  Bind("", "vasya")   ───►│ rewrite "" → "DOORMAN_42"
                           │            Bind("DOORMAN_42") ──────►
                           │            Execute, Sync ───────────►
                           │               ◄── BindComplete, ...
                           │               ◄── ReadyForQuery
                      ◄────│ BindComplete, ...

Второй клиент с тем же запросом в том же пуле попадает в кеш пула и не отправляет Parse на бэкенд:

  Клиент B           PgDoorman                       Backend (тот же)
  ────────           ─────────                       ────────────────
  Parse("", q)  ───►│ hash hit → DOORMAN_42
                     │ server_cache содержит "DOORMAN_42"
                ◄────│ синтетический ParseComplete   (на бэкенд ничего)
  Bind("", v)   ───►│ rewrite "" → "DOORMAN_42"
                     │            Bind("DOORMAN_42") ────►
                     │            ...

Слои кеша

PgDoorman держит состояние prepared statements на трёх уровнях:

  Pool-level    DashMap<hash, CacheEntry>
                Один на пул. Хранит Arc<Parse> с именем DOORMAN_N.
                Размер:    prepared_statements_cache_size (по умолчанию 8192).
                Выселение: приближённый LRU.

  Client-level  Named:     AHashMap<String, CachedStatement>, без лимита.
                Anonymous: LruCache<u64, CachedStatement> ограничен
                           client_anonymous_prepared_cache_size (если не задан,
                           наследует prepared_statements_cache_size),
                           или AHashMap при размере 0.
                Выселение Anonymous локальное: Arc<Parse> отбрасывается,
                DOORMAN_<N> на бэкенде остаётся.

  Server-level  LruCache<String, ()>, на серверное соединение.
                Запоминает, какие DOORMAN_N этот бэкенд уже держит.
                Точный LRU; при выселении отправляет Close на бэкенд.

При вытеснении записи из Anonymous LRU PgDoorman отбрасывает локальную ссылку и не отправляет Close на бэкенд. Соответствующий DOORMAN_<N> будет переиспользован server-level LRU или закроется по server_lifetime (по умолчанию 20 минут) — что наступит раньше.

Текст запроса интернируется через Arc<str>: десять клиентов с одним и тем же анонимным запросом делят одну аллокацию в памяти.

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

  • API-нагрузки с малым набором горячих запросов. Десяток уникальных форм SELECT / INSERT на тысячи клиентов. Доля попаданий в кеш пула близка к 100 %, планировщик работает один раз на серверное соединение для каждой формы запроса, а следующие обращения идут через Bind к уже подготовленному statement.
  • Драйверы, привязанные к анонимным prepared. lib/pq, libpq PQexecParams, pgjdbc до достижения prepareThreshold. Без подмены они каждый раз перепланируют.
  • Смешанные пулы, где именованные и анонимные statements соседствуют. Анонимные получают тот же выигрыш от кеша планов, что и именованные, без роста клиентского кеша именованных statements.

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

  • Разовый / OLAP-трафик. Каждый запрос уникален. Когда кеш пула заполнен, каждая новая форма запроса ищет старую запись для вытеснения через O(N)-обход. Если инстанс обслуживает только такую нагрузку, отключите remap через prepared_statements: false.
  • Скрипты с одним statement. Цепочка connect → Parse → 1 Bind → disconnect не накапливает достаточно попаданий, чтобы окупить учёт. Накладные расходы на Parse ~700 нс — небольшие, но измеримые.
  • Асинхронные драйверы в режиме pipeline. Каждая сессия получает уникальное имя DOORMAN_async_<N>, чтобы избежать коллизий между одновременными незавершёнными операциями. Серверный кеш между сессиями не переиспользуется. Кеш пула по-прежнему делит текст запроса между сессиями; планировщик на бэкенде срабатывает один раз на сессию.

Эффективность этого ускорения измеряйте по rate(pg_doorman_servers_prepared_hits_total[5m]) и rate(pg_doorman_servers_prepared_misses_total[5m]). Устойчивая доля промахов выше 30 % означает, что подмена расходует CPU и память, но почти не даёт переиспользования планов. Тогда либо отключите её, либо увеличьте prepared_statements_cache_size.

Как это устроено у других пулеров

ПулерКеш Parse/плана для анонимного prepared statement
PgDoormanДа: прозрачная подмена на DOORMAN_<N>
PgBouncer 1.21+Нет: только named, анонимный проходит as-is
OdysseyНет: только named, pool_reserve_prepared_statement
PgCatНет: только named

В PgBouncer поддержка prepared statements появилась в 1.21, но ограничена именованными: анонимный Parse проходит без изменений, и каждый Bind запускает планировщик. Флаг pool_reserve_prepared_statement в Odyssey требует именованных statement; на анонимный трафик он не влияет. PgCat ведёт себя так же.

Кешировать план анонимных prepared сегодня умеет только PgDoorman.

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

ПараметрПо умолчаниюЭффект
prepared_statementstrueВключает подмену и кеширование prepared statements. false отключает функцию.
prepared_statements_cache_size8192Размер кеша пула в записях. Должен быть больше 0, пока prepared_statements = true.
server_prepared_statements_cache_sizeнаследует prepared_statements_cache_sizeРазмер LRU на серверное соединение для имён DOORMAN_<N>. 0 отключает хранение на бэкенде, но не подмену на уровне пула.
client_anonymous_prepared_cache_sizeнаследует prepared_statements_cache_sizeРазмер Anonymous LRU на клиента. 0 снимает ограничение. Named-часть всегда без лимита.

Named-часть клиентского кеша всегда без лимита и не зависит от client_anonymous_prepared_cache_size.

Полностью отключить подмену prepared statements (редко, для установок только с OLAP):

general:
  prepared_statements: false

Отдельного переключателя только для анонимных statements нет. Не используйте prepared_statements_cache_size: 0 как выключатель: pg_doorman отклоняет такое общее значение, пока prepared_statements включён.

Отличия от семантики PostgreSQL

Подмена меняет несколько протокольных деталей, на которые могут полагаться строгие приложения:

  • Один и тот же анонимный Parse, отправленный дважды, не стирает предыдущий. Каждая пара (query, param_types) живёт независимо в кеше пула под своим DOORMAN_<N>.
  • Close с пустым именем ничего не делает с кешами PgDoorman. Соответствующий DOORMAN_<N> живёт до выселения из LRU кеша пула или до закрытия пула.
  • Решение PostgreSQL между custom- и generic-планом становится общим для всех клиентов, использующих один DOORMAN_<N>. Именованный statement сначала получает custom-планы; после пяти custom-выполнений PostgreSQL может перейти на generic-план, если его оценочная стоимость достаточно близка. С PgDoorman эти выполнения могут прийти от разных клиентов, поэтому выбор generic-плана может отражать смешанное распределение параметров.

Приложения, которые опираются на PG-семантику "анонимный Parse стирает предыдущий", должны переключиться на именованные statement с явным Close.

Тюнинг

Размер кеша

Кеш prepared statements в PgDoorman состоит из трёх слоёв. Ими управляют три связанных параметра:

  • prepared_statements_cache_size (по умолчанию 8192) задаёт размер общего кеша пула — одна хеш-таблица на пул, ключом служит хеш запроса. Это верхняя граница на число различных форм запроса, которые пул помнит сразу для всех клиентов. Приближённый LRU: вытеснение проходит за O(N) по всему кешу и не отправляет Close на бэкенд (другие клиенты могут ещё держать Arc).
  • server_prepared_statements_cache_size (по умолчанию наследует prepared_statements_cache_size) задаёт размер серверного кеша — отдельный LRU на каждое серверное соединение, ключом служит имя DOORMAN_<N>. Это верхняя граница на число prepared statements, которое PgDoorman позволит держать одному бэкенду PostgreSQL. Точный LRU за O(1); при выселении в очередь бэкенда кладётся Close, который отправляется ближайшим Sync или Flush — представление pg_prepared_statements может временно показывать больше строк, чем потолок, пока не придёт следующий Sync.
  • client_anonymous_prepared_cache_size (по умолчанию наследует prepared_statements_cache_size) задаёт размер Anonymous LRU на клиента. 0 отключает LRU и снимает потолок: кеш растёт без ограничения. Любое положительное число задаёт лимит независимо от размера кеша пула.

Пуловый и серверный лимиты можно переопределить на уровне пула:

general:
  prepared_statements_cache_size: 8192
  server_prepared_statements_cache_size: 1024  # потолок на бэкенд жёстче

pools:
  oltp:
    # наследует оба значения из general
    pool_mode: "transaction"
  reporting:
    # у этого пула шире разнообразие запросов; пусть серверный кеш
    # вмещает больше
    server_prepared_statements_cache_size: 4096
    pool_mode: "transaction"

prepared_statements: false отключает подмену целиком и заодно обнуляет кеши на уровне пула и серверного соединения. Указать server_prepared_statements_cache_size: 0 при положительном размере кеша пула допустимо, но смысла мало: серверный кеш превратится в pass-through, и каждое попадание на другой бэкенд приведёт к повторному Parse.

Когда уменьшать server_prepared_statements_cache_size ниже размера кеша пула:

  • На бэкендах копится слишком много строк DOORMAN_<N> (pg_prepared_statements упирается в потолок, память планов растёт).
  • Хочется ускорить вытеснение через Close, не урезая попадания в кеш пула.

Когда оставить значения равными (поведение по умолчанию):

  • Нет измеренной проблемы с памятью на бэкендах. Достаточно наследования.

Размер client_anonymous_prepared_cache_size

Если параметр не задан, Anonymous LRU на клиента наследует вычисленный prepared_statements_cache_size пула (по умолчанию 8192). Явное значение перекрывает наследование: 0 отключает LRU, и кеш растёт без ограничения; любое положительное число задаёт потолок LRU.

Каждая запись хранит лёгкую структуру (hash, async_name?, Arc<Parse>) — сам Arc<Parse> делится с кешем пула, поэтому накладные расходы на клиента ≈ 80 байт на запись. На 10 000 подключённых клиентов × 256 записей × ~80 байт получаем около 200 МБ предсказуемого потолка на пулере.

Поднимайте лимит, когда:

  • ORM или генератор SQL выдаёт stmt_<seq> под каждый запрос и Anonymous LRU постоянно вытесняет записи (видно по устойчиво ненулевой скорости pg_doorman_clients_prepared_anonymous_evictions_total).
  • Приложение заведомо имеет широкое рабочее множество в одной сессии и скорость вытеснений соответствует этой нагрузке.

Уменьшайте лимит при очень больших числах подключений (50 000+ клиентов): на таком масштабе clients × cache_size × 80 байт учётной памяти на пулере может пересечь 1 ГБ, и урезание лимита уполовинивает её. max_memory_usage не ограничивает служебную память кеша prepared statements; этот параметр защищает буферы запросов в полёте.

Named всегда без лимита

Named-часть клиентского кеша не ограничена. PgDoorman держит Arc<Parse> для каждого именованного statement, который создал клиент, до его отключения или явного DEALLOCATE / DEALLOCATE ALL. Это согласуется с собственным контрактом PostgreSQL — именованные prepared живут до конца сессии — и исключает сценарий, при котором вытеснение Named-записи под нагрузкой ломает следующий Bind ошибкой prepared statement does not exist.

Обратная сторона: драйверы, генерирующие отдельное имя под каждый запрос (часть режимов pgjdbc и Hibernate, отдельные конфигурации .NET Npgsql), могут раздуть Named-часть без потолка. PgDoorman не может ограничить её безопасно; ответственность за переиспользование имён или явный DEALLOCATE лежит на приложении.

Сигнал давления есть только для Anonymous LRU — счётчик вытеснений pg_doorman_clients_prepared_anonymous_evictions_total. Для Named такого сигнала нет: следите за колонкой client_named_count в SHOW POOLS_MEMORY и метрикой pg_doorman_clients_prepared_named_entries на предмет неожиданного роста.

Окно роста памяти на бэкенде

При вытеснении записи из Anonymous LRU на стороне клиента PgDoorman отбрасывает только локальный Arc<Parse>. Соответствующий DOORMAN_<N> остаётся живым на каждом бэкенде, который когда-либо его обслуживал. Очищают его два механизма:

  • Server-level LRU. На каждом бэкенде ведётся свой LruCache<String, ()> имён DOORMAN_<N>, ограниченный server_prepared_statements_cache_size (или prepared_statements_cache_size, если отдельный серверный лимит не задан). При достижении лимита бэкенд отправляет Close на наименее давно использованное имя и освобождает план.
  • Ротация бэкенда. Бэкенд достигает server_lifetime (по умолчанию 20 мин), и pg_doorman закрывает его; новый бэкенд стартует с пустым кешем планов.

Худший случай по памяти на одном бэкенде — это server_prepared_statements_cache_size × средний размер плана (8192 × ~100 КБ — около 800 МБ) на стороне PostgreSQL. Чтобы сжать окно:

  • Снизьте server_prepared_statements_cache_size, чтобы серверный LRU быстрее вытесняла планы.
  • Снизьте server_lifetime, чтобы бэкенды ротировались чаще.

Системное представление pg_prepared_statements в PostgreSQL показывает имена, которые держит текущий бэкенд. Подсчёт строк там показывает, насколько близко бэкенд подошёл к лимиту.

Мониторинг

Команды администратора:

  • SHOW PREPARED_STATEMENTS — pool, hash, name, query, count_used, kind. Топ записей по count_used показывает горячие запросы, на которых кеш окупается. Колонка kind — последняя в наборе и принимает значения named, anonymous или mixed в зависимости от того, как клиенты использовали запись за её жизнь.

    Пример:

     pool         | hash               | name        | query             | count_used | kind
    --------------+--------------------+-------------+-------------------+------------+-----------
     sharded.user | 1234567890123456   | DOORMAN_1   | SELECT * FROM t1  |     150234 | anonymous
     sharded.user | 2345678901234567   | DOORMAN_2   | INSERT INTO t2 .. |      87654 | named
     sharded.user | 3456789012345678   | DOORMAN_3   | SELECT * FROM t3  |      45678 | mixed
    
  • SHOW POOLS_MEMORYpool_prepared_count, client_prepared_count, pool_prepared_bytes, client_prepared_bytes плюс разбивка по kind: client_named_count, client_anonymous_count, client_anonymous_evictions_alive. Последняя колонка суммирует счётчики вытеснений только по подключённым сейчас клиентам — отключившиеся клиенты выпадают из суммы, поэтому колонка не монотонна. Для накопительного счётчика читайте Prometheus-метрику pg_doorman_clients_prepared_anonymous_evictions_total.

Prometheus-метрики (полный список в Prometheus):

  • pg_doorman_pool_prepared_cache_entries{user, database}
  • pg_doorman_pool_prepared_cache_bytes
  • pg_doorman_clients_prepared_cache_entries
  • pg_doorman_clients_prepared_cache_bytes
  • pg_doorman_clients_prepared_named_entries{user, database}
  • pg_doorman_clients_prepared_anonymous_entries{user, database}
  • pg_doorman_clients_prepared_anonymous_evictions_total{user, database}
  • pg_doorman_servers_prepared_hits{user, database}
  • pg_doorman_servers_prepared_misses{user, database}
  • pg_doorman_servers_prepared_hits_total{user, database}
  • pg_doorman_servers_prepared_misses_total{user, database}
  • pg_doorman_async_clients_count

Для rate() и алертов используйте метрики с суффиксом _total. Метрики серверного кеша без _total — текущая сумма по живым бэкендам; она может уменьшаться при ротации бэкендов.

Алертинг

Скорость вытеснений в Anonymous LRU

Устойчиво ненулевая скорость на счётчике вытеснений означает, что LRU вытесняет записи быстрее, чем приложение переиспользует их. Шаблон алерта:

rate(pg_doorman_clients_prepared_anonymous_evictions_total[5m]) > 10
  for 10m

Порог в 10 вытеснений/с на пул — отправная точка, реальное значение зависит от формы трафика и числа подключений. Срабатывание алерта читайте как "лимит слишком мал или рабочее множество приложения шире, чем ожидалось"; решение — либо поднять client_anonymous_prepared_cache_size, либо разобраться, не генерирует ли приложение уникальные запросы на горячем пути.

Интерпретация kind = mixed

Каждая запись кеша пула помнит, использовали ли её клиенты как именованный statement, как анонимный, или обоими способами. kind = mixed означает, что одна и та же пара (query, param_types) была обработана хотя бы одним клиентом как named и хотя бы одним другим как anonymous за её текущую жизнь. У большинства нагрузок строк mixed нет; если в пуле их большинство, у клиентов разные драйверы или разные конфигурации драйверов против одной БД, и эту разнородность стоит проверить — иногда она задумана, иногда сигналит, что один из клиентов настроен неверно.

Число prepared statements на бэкенде

PostgreSQL отдаёт pg_prepared_statements для текущего бэкенда. Если память пулера в норме, но RSS бэкендов PostgreSQL растёт, посчитайте строки на каждом бэкенде:

SELECT count(*) FROM pg_prepared_statements;

Цифры около server_prepared_statements_cache_size означают, что серверный LRU работает на потолке, а ротация — второй способ освободить память планов. Если серверный лимит наследует prepared_statements_cache_size, используйте это значение как потолок. Снижение серверного лимита или server_lifetime уменьшает давление на память планов ценой более частых повторных Parse на бэкенде.

Ограниченный query interner

Глобальный интернер, в котором дедуплицируются тексты Parse, разделён на две независимые хеш-таблицы.

  • NAMED — тексты именованных prepared. Запись жива, пока пуловой или клиентский кеш держит ссылку через Arc<str>. Сборщик мусора убирает запись, когда её перестали удерживать снаружи интернера; двухтактовая отсрочка защищает запись, на которую сослались между двумя циклами обхода.
  • ANON — тексты анонимных prepared. Запись истекает по query_interner_anon_idle_ttl_seconds бездействия (60 секунд по умолчанию). 0 отключает TTL и возвращает поведение pg_doorman до 3.7 — оставлено для существующих установок.

Если anonymous Bind или Describe приходит после того, как pg_doorman потерял соответствующее состояние anonymous prepared, pg_doorman отвечает ERROR: unnamed prepared statement does not exist (SQLSTATE 26000). Типичные причины: вытеснение из клиентского Anonymous LRU, RESET INTERNER, TTL-вытеснение из interner или паттерн драйвера, который переиспользует unnamed prepared statements между пачками. Это та же ошибка, что PostgreSQL отдаёт напрямую в аналогичной ситуации; стандартные драйверы реагируют повторным Parse.

Бинарное обновление (SIGUSR2) переносит и NAMED, и ANON в новый процесс. Анонимные записи попадают в новый ANON-интернер со свежим last_used, и TTL отсчитывается заново с момента обновления.

Команды для оператора

SHOW INTERNER (через admin-сессию) выводит количество и объём текста для каждой половины:

kind      | entries | bytes
named     |     420 |    87654
anonymous |    1337 |   234567

SHOW INTERNER N показывает N самых тяжёлых записей: hash, kind, bytes, idle_ms (-1 для named — там нет последнего-использования, вместо него отслеживается состояние сборщика) и 120 первых символов текста запроса.

RESET INTERNER чистит обе половины. Активные клиенты заново сделают Parse при следующем использовании. Команда диагностическая.

Метрики Prometheus отражают SHOW INTERNER плюс гистограмму длительности обхода и счётчик синтетических 26000. Увеличивайте query_interner_anon_idle_ttl_seconds только когда synthetic misses совпадают с TTL-вытеснениями anonymous-записей или с подтверждённым паттерном cross-batch unnamed statements. Если промахи идут вместе с pg_doorman_clients_prepared_anonymous_evictions_total, увеличивайте client_anonymous_prepared_cache_size.

Справочник

Параметры запуска PostgreSQL

PgDoorman может задавать параметры PostgreSQL при открытии серверного соединения, не меняя postgresql.conf, ALTER ROLE или ALTER DATABASE. Это полезно, например, в таких случаях:

  • В горячем OLTP-пуле план переключается на generic после решения эвристики plan_cache_mode = auto и обратно уже не возвращается. ALTER ROLE SET plan_cache_mode = force_custom_plan затронет любую другую нагрузку под этой ролью, а изменить нужно только один пул.
  • Приложение не задаёт statement_timeout или idle_in_transaction_session_timeout, а быстро доработать его нельзя. Администратору БД нужно сессионное значение по умолчанию, которое сохранится после клиентского RESET ALL.
  • Одно приложение должно стабильно показывать конкретный application_name, независимо от значения, которое передаст драйвер, чтобы pg_stat_activity и аудит оставались читаемыми.

Для этого используется startup_parameters: набор GUC PostgreSQL, который pg_doorman добавляет в StartupMessage новых серверных соединений пула.

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

Значения применяются в три слоя. Более узкий слой переопределяет ключ из предыдущего.

[general.startup_parameters]
statement_timeout = "5s"

[pools.checkout.startup_parameters]
plan_cache_mode = "force_custom_plan"
work_mem        = "64MB"

После SIGHUP или RELOAD через консоль администратора каждое новое серверное соединение пула checkout открывается со значениями statement_timeout = 5s, plan_cache_mode = force_custom_plan и work_mem = 64MB. В других пулах остаётся только statement_timeout = 5s из general; остальные значения берутся из настроек PostgreSQL по умолчанию. Уже открытые бэкенды не меняются: новые значения вступают в силу по мере ротации соединений.

В сквозном режиме auth_query, когда server_user не задан, запрос аутентификации может вернуть необязательную колонку startup_parameters типа text, json или jsonb с JSON-объектом. Значения из этой колонки переопределяют general и настройки пула только для конкретного пользователя.

SELECT
  rolpassword AS passwd,
  CASE rolname
    WHEN 'vip' THEN '{"work_mem":"256MB"}'::text
    ELSE NULL::text
  END AS startup_parameters
FROM pg_authid
WHERE rolname = $1;

pg_doorman выбирает декодер по типу колонки, поэтому приведение ::text не требуется. Содержимое должно быть JSON-объектом, а значения — строками. Для других типов PostgreSQL, в том числе доменов поверх jsonb, pg_doorman пишет предупреждение и игнорирует пользовательские параметры из этой строки.

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

Изменения в пользовательской строке startup_parameters применяются только к новым серверным подключениям и только после того, как pg_doorman перечитает строку. Кеш auth_query хранит положительные записи в течение auth_query.cache_ttl (по умолчанию час); при обновлении кеша pg_doorman замечает изменение overlay и сбрасывает динамический пул, чтобы следующий логин пересоздал его с новыми значениями. Пока запись кеша не истекла, новые подключения клиентов получают прежний overlay. Чтобы выкатить изменения немедленно: уменьшить cache_ttl и сделать reload конфигурации, перезапустить pg_doorman или дождаться истечения TTL. Бэкенды, которые уже выданы клиентам, продолжают работать со значениями, сохранёнными при создании пула.

Что pg_doorman делает со значениями

pg_doorman добавляет итоговый набор параметров в StartupMessage каждого нового бэкенда. PostgreSQL сохраняет эти значения как сессионные значения по умолчанию (pg_settings.reset_val и pg_settings.source = 'client'). Поэтому клиентские RESET ALL и DISCARD ALL возвращают операторские значения, а не исходные значения PostgreSQL.

Значение видно со стороны клиента:

checkout=> SHOW plan_cache_mode;
 plan_cache_mode
-------------------
 force_custom_plan

checkout=> SET plan_cache_mode = 'auto'; RESET ALL; SHOW plan_cache_mode;
 plan_cache_mode
-------------------
 force_custom_plan

Валидация

При загрузке конфигурации pg_doorman проверяет:

  • Имена ключей должны соответствовать маске GUC PostgreSQL: ^[A-Za-z_][A-Za-z0-9_.]*$. Составные имена вроде auto_explain.log_min_duration допустимы; произвольная пунктуация нет.
  • Зарезервированные ключи (user, database, replication, options, role, session_authorization и всё, что начинается с _pq_.) отклоняются. pg_doorman управляет ими сам, либо PostgreSQL обрабатывает их в StartupMessage особым образом.
  • Значения не должны содержать нулевой байт.
  • Каждый уровень (general или pool) должен помещаться в лимит для операторских параметров: MAX_STARTUP_PACKET_LENGTH (10000 байт) минус 512 байт, зарезервированных под служебные ключи pg_doorman.

Перед запуском каждого бэкенда pg_doorman снова проверяет объединённый набор параметров по тому же лимиту. Слои, которые помещаются по отдельности, могут превысить лимит после объединения: general + pool может быть слишком большим сам по себе, а строка auth_query может переполнить уже допустимый базовый набор. Любое превышение теперь возвращается клиенту как PostgreSQL-ошибка с SQLSTATE 53400; пустой или урезанный StartupMessage не отправляется. В предупреждении записываются размеры в байтах, а pg_doorman_startup_parameters_dropped_total увеличивается на каждой отклонённой попытке запуска бэкенда.

Что происходит, если PG отвергает параметр

Если PostgreSQL отвергает заданный оператором параметр при запуске бэкенда, pg_doorman возвращает клиенту ErrorResponse PostgreSQL без изменений. Клиент видит тот же SQLSTATE (22023, 42704, 42501, 55P02 или любой другой код, который PostgreSQL вернул при отклонении StartupMessage) и то же сообщение, что увидел бы при прямом подключении к PostgreSQL.

pg_doorman не пробует повторить подключение без отклонённого параметра и не отключает этот ключ автоматически для пула. Следующее подключение клиента отправит тот же StartupMessage и получит ту же ошибку, пока оператор не исправит конфигурацию.

Наблюдаемость

Итоговые параметры по каждому пулу видны через административную SQL-консоль:

admin> SHOW STARTUP_PARAMETERS;
 user | database | parameter         | value             | source  | state
------+----------+-------------------+-------------------+---------+--------
 shop | checkout | plan_cache_mode   | force_custom_plan | pool    | applied
 shop | reports  | statement_timeout | 10s               | general | applied

Веб-интерфейс показывает эти же строки на странице пула в секции «Startup parameters (operator-injected)».

В Prometheus:

  • pg_doorman_backend_startup_parameter_errors_total{pool, sqlstate} считает попытки запуска бэкенда, которые PostgreSQL отклонил из-за параметра, заданного оператором. Имя параметра и пользователя пишутся в строку лога уровня warn; в лейблы они не включены, чтобы динамические auth_query-пулы не раздували количество серий.
  • pg_doorman_startup_parameters_dropped_total{pool, reason} считает случаи, когда pg_doorman отклонил startup_parameters до отправки StartupMessage: превышение лимита, неподдерживаемый тип или неверный JSON из auth_query, недопустимые ключи или значения, а также пользовательские значения, проигнорированные в выделенном режиме.

Практичное условие для алерта: если pg_doorman_backend_startup_parameter_errors_total растёт по одному и тому же пулу несколько минут подряд, новые подключения к этому пулу падают на одном и том же GUC. Конфигурацию нужно исправить до возврата трафика.

Когда это не нужно

  • Приложение само задаёт параметр на каждом подключении. Дублирование в startup_parameters добавляет ещё одну настройку без изменения поведения.
  • Тюнинг на одну транзакцию (SET LOCAL). startup_parameters задают сессионные значения по умолчанию; параметры уровня транзакции должно выставлять приложение.
  • Значения, которые зависят от текущего запроса. Параметры запуска действуют для всех транзакций бэкенда на протяжении его жизни; режима «на один statement» нет.

Справочник

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

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

Внутри каждого пула (database, user) работают два механизма: упреждающее ожидание (anticipation) ждёт возврата соединения от соседа перед новым connect(), а ограничитель всплесков задаёт жёсткий лимит на одновременные подключения к PostgreSQL внутри одного пула. Если включён max_db_connections, поверх них работает координатор (coordinator) — общий лимит серверных соединений к одной базе данных для всех её пулов.

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

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

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

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 одновременных подключений к PostgreSQL
   .             в один момент
Client_200 -[connect]-[auth]-[query]-[done]

PostgreSQL: 196 новых backend-процессов + 4 выполняющихся запроса

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

Режим независимых пулов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                  Получение соединения в режиме независимых пулов
                  -----------------------------------------------

   pool.get()
       |
       v
   +--------------+
   |  Фаза 1:     |  --- есть ----> вернуть свободное соединение
   |  recycle pop |
   +------+-------+
          | нет
          v
   +--------------+
   |  Фаза 2:     |  --- ниже порога ---> к фазе 5
   |  warm gate   |
   +------+-------+
          | выше порога
          v
   +--------------+
   |  Фаза 3:     |  --- есть ----> вернуть свободное соединение
   |  опрос       |
   +------+-------+
          | нет
          v
   +--------------+
   |  Фаза 4:     |  --- передача ----> вернуть соединение
   |  anticipate  |  --- таймаут -----> дальше
   |  передача    |
   +------+-------+
          |
          v
   +--------------+
   |  Фаза 5:     |  --- слот взят --> к фазе 6
   |  burst gate  |  --- нет слота --> ждать, повторить recycle
   +------+-------+
          |
          v
   +--------------+
   |  Фаза 6:     |
   |  connect()   | ----> вернуть новое соединение
   +--------------+

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

Тот же сценарий с 200 клиентами, но теперь в режиме независимых пулов и с 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, он просит либо сразу получить свободное соединение, либо запустить новый connect(), без ожидания. Фаза упреждающего ожидания и ожидание у ограничителя всплесков пропускаются. pg_doorman выполняет recycle на горячем пути, один раз пробует ограничитель всплесков, затем либо создаёт соединение, либо возвращает ошибку wait timeout.

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

Фоновый replenish

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

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

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

Прямая передача при возврате

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

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

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

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

  • Отсутствие голодания. При broadcast-notify клиент может проиграть гонку за пробуждение многократно. При прямой передаче соединение идёт ровно одному получателю, ожидающие с истёкшим дедлайном пропускаются. Нет лавины одновременных подключений, нет повторных проигрышей.

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

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

Упреждающая замена при истечении lifetime

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

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

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

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

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

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

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

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

N pools (users) × pool_size  =  верхняя граница серверных соединений

Пример с расчётом: три пула, у каждого pool_size = 40, без max_db_connections. В худшем случае получится 120 одновременных серверных соединений к этой базе, ограниченных только 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-соединения, репликацию и всплески.

Режим координатора

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

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

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

Три вещи:

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

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

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

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

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

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

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

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

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

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

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

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

На каждое пробуждение Фаза C сначала пытается захватить свободный слот неблокирующей проверкой, и только если дешёвый путь не сработал — пробует выселение. Пробуждение от освобождения permit-а оставляет свободный слот в семафоре — дешёвый путь берёт его, и ни один соседний бэкенд не закрывается. Пробуждение от idle-return не освобождает слот напрямую, но могло вырастить излишек соседа, поэтому повторная попытка выселения находит кандидата, которого мгновение назад не было, освобождает permit соседа, и следующая неблокирующая проверка срабатывает. Этот порядок (дешёвый путь сначала, выселение потом) закреплён регрессионным тестом: будущий рефакторинг не сможет случайно вернуть закрытия соседних бэкендов на пробуждениях от 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. Разбор симптома есть в разделе «Диагностика».

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

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

Перенос соединения из резерва в основной лимит

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

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

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

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

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

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

Получение слота координатора по требованию

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

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

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

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

        Координатор + получение соединения (JIT)
        ----------------------------------------

   pool.get()
       |
       v
   Фаза 1: recycle           --- есть ---> вернуть
       | нет
       v
   Фаза 2: порог прогрева    --- ниже ----+
       | выше порога                       |
       v                                    |
   Фаза 3: быстрый опрос     --- есть ---> вернуть
       | нет                               |
       v                                    |
   Фаза 4: прямая передача   --- есть ---> вернуть
       | дедлайн                           |
       v                                    |
       | <----------------------------------+
       v
   Фаза 5: ограничитель всплесков (scaling_max_parallel_creates)
              | слот получен
              v
   +-------------------------------+
   | JIT-захват координатора       |  только при max_db_connections > 0
   |  fast: неблокир. проверка     |  мгновенный ответ
   |  slow: отпустить слот         |  ожидание координатора (evict/return)
   |        → взять заново         |  затем продолжить create
   +------------+------------------+
                | permit получен
                v
   Фаза 6: server_pool.create()
                |
                v
                return new connection

Фазы пронумерованы так же, как в режиме независимых пулов. Захват координатора работает внутри слота ограничителя всплесков, когда max_db_connections > 0. В режиме независимых пулов он не работает.

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

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

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

Фоновое пополнение под координатором

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

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

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

ПараметрПо умолчаниюГдеЧто делает
scaling_warm_pool_ratio20 (процент)general, пулПорог, ниже которого соединения создаются без упреждающего ожидания. Ниже pool_size × ratio / 100 каждый запрос нового соединения идёт сразу к connect().
scaling_fast_retries10general, пулЧисло быстрых повторных проверок пригодности перед переходом к прямой передаче (ожиданию возврата от соседа).
scaling_max_parallel_creates2generalЖёсткий лимит одновременно идущих connect() к бэкенду на пул. Задачи сверх лимита ждут возврата свободного соединения или завершения чужого создания. Должен быть >= 1.
max_db_connectionsне задан (выключено)пулЛимит суммарного числа серверных соединений к базе по всем пользовательским пулам. Когда не задан, координатор не создаётся.
min_connection_lifetime30000 (ms)пулМинимальный возраст свободного соединения, после которого координатор может выселить его в пользу другого пула. 30-секундный порог подавляет циклический reconnect между соседними пулами.
reserve_pool_size0 (выключено)пулДополнительные permit-ы координатора поверх max_db_connections, выдаваемые по приоритету при исчерпании основного пула.
reserve_pool_timeout3000 (ms)пулМаксимальное время Фазы C перед повторной попыткой резерва или ошибкой.
min_guaranteed_pool_size0пулМинимум на пользователя, защищённый от выселения координатором. Соединения пользователя, у которого 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 с проверкой сертификата или медленный поиск в pg_authid);
  • в логах PostgreSQL появляется конкуренция за pg_authid;
  • бэкенд жалуется на переполнение очереди accept().

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

Размер при множестве пулов. Суммарный потолок одновременных подключений равен N pools × scaling_max_parallel_creates. Если за одним PostgreSQL стоит 10 пулов и в любой момент суммарно по ним всем вам нужно не более 8 одновременных подключений к PostgreSQL, поставьте 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 остаётся единственным ограничением;
  • вам не нужно выселение между пулами (некоторые нагрузки предпочитают жёсткую изоляцию между пользователями).

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 короткий: в этом случае лучше быстро дойти до повторной попытки резерва или ошибки, чем блокировать клиентов на ожидании координатора.

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

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

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

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

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

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

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

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

Мониторинг

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

Консоль администратора: SHOW POOL_SCALING

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

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

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

Консоль администратора: SHOW POOL_COORDINATOR

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

pgdoorman=> SHOW POOL_COORDINATOR;
КолонкаТипЗначение
databasetextИмя базы
max_db_conngaugeСконфигурированное max_db_connections
currentgaugeСколько всего серверных соединений сейчас удерживается под этим координатором (по всем пользовательским пулам)
reserve_sizegaugeСконфигурированное reserve_pool_size
reserve_usedgaugeСколько reserve-permit-ов используется прямо сейчас. Сходится обратно к 0, когда в основном лимите есть свободное место — retain-задача каждые retain_connections_time повышает свободные reserve-permit-ы до основных. Устойчивое ненулевое значение означает либо активный всплеск, либо базу, постоянно упёртую в max_db_connections.
evictionscounterСколько раз координатор выселил свободное соединение соседнего пула, чтобы освободить слот. С включённым reserve-first этот счётчик растёт только при реальном кросс-пуловом давлении — когда резерв заполнен и у соседа есть что выселить.
reserve_acqcounterСколько всего reserve-permit-ов выдал арбитр (Фаза 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 нет. Алерты здесь должны молчать.

После всплеска, повышение в процессе:

 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 означает, что в основном лимите есть место, поэтому retain-задача повысит эти три permit-а до основных на следующем цикле; reserve_used должен упасть до 0 в течение retain_connections_time (по умолчанию 30 секунд). Если не падает, смотрите раздел «Диагностика» ниже. evictions = 0 и reserve_acq > 0 вместе подтверждают, что reserve-first поглотил всплеск без закрытия соседних бэкендов.

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

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

Основной лимит полностью занят (main_used = current - reserve_used = 80, равно max_db_conn), резерв использован на 75%, много выселений, много выдач из резерва. База не просто иногда под давлением — она постоянно недоразмерена и выживает только за счёт того, что выселение ротирует соединения между пользователями, а 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 — сработал адаптивный таймаут, клиент перешёл к созданию
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).

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

Исчерпан лимит координатора (page). Клиент получил ошибку "database exhausted". Жёсткий отказ: не сработали ни резерв, ни выселение.

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

Проверка:

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, чтобы защитить соседей.

Ограничитель всплесков насыщен (warn). Ограничитель ждёт чужие создания чаще, чем проходит напрямую. Короткие всплески выше порога при 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])

Проверка:

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

Частый переход к созданию после упреждающего ожидания (warn). Фаза 4 упреждающего ожидания сдаётся, не поймав возврат, и проваливается в свежий 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

Проверка:

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 секунды) → дедлайн упреждающего ожидания (query_wait_timeout − 500 ms с верхней границей 500 ms) слишком короткий даже для нормальных возвратов. Поднимайте query_wait_timeout минимум до 2 × p99 длительности запроса.

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

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

Проверка:

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 стабильно стоит на лимите → ограничитель полон по другой причине (медленный connect()); сначала это.

Резервный пул постоянно используется (warn). Метрика reserve-permit не возвращался к нулю в течение 15 минут. retain-задача повышает свободные reserve-permit-ы до основных каждые retain_connections_time (по умолчанию 30 секунд), поэтому алерт означает, что путь повышения не в состоянии отработать или получить успех, а не что он забыл запуститься.

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

Проверка:

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

Координатор приближается к лимиту (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

Проверка:

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 соседям.

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

min_over_time(pg_doorman_pool_scaling{type="inflight_creates"}[5m])
  >= 2  # подставьте своё scaling_max_parallel_creates

Проверка:

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 → подключение к PostgreSQL медленный. Проверьте 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.

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

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

Проверка:

psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
  • evictions растёт и reserve_used == 0 → резерв выключен или исчерпан, выселение — единственный клапан сброса. Включите / поднимите reserve_pool_size, чтобы всплеск поглощался без закрытия соседних бэкендов.
  • 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):

# Сначала пул с максимальной долей ожиданий на ограничителе.
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, $10 }' \
  | sort -k3 -nr | head

# Сначала пул с максимальной долей fallback после дедлайна.
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, $9/$4, $4, $9 }' \
  | sort -k3 -nr | head

# Координатор: базы, ближайшие к исчерпанию.
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 (жёсткий лимит, без выселения; для изоляции есть переопределения pool_size на базу или на пользователя)max_db_connections (жёсткий лимит плюс выселение между пулами и резервный пул)
Резервный пулreserve_pool_size, reserve_pool_timeoutreserve_pool_size, reserve_pool_timeout (плюс приоритизация в арбитре по starving/queued)
Выселение между пользователямиНе поддерживается. Пользователь, удерживающий свободные соединения, морит голодом соседа, которому они нужны.Координатор выселяет свободные соединения у пользователя с наибольшим излишком над эффективным минимумом (max(user.min_pool_size, min_guaranteed_pool_size)).
Одновременные connect() к бэкенду в одном пулеОднопоточный, обрабатывает события последовательно в пределах пула, вызовы connect() выпускаются по одному.Ограничено scaling_max_parallel_creates (по умолчанию 2 на пул): не больше N одновременных подключений к PostgreSQL в пуле, лишние задачи ждут на ограничитель всплесков.
Упреждающее ожидание возвратовНет. Клиенты ждут следующего доступного соединения в порядке прихода, в пределах wait_timeout.Возвращающееся соединение будит ровно одного из ожидающих в очереди, часто ещё до того, как будет выпущен хоть один новый connect().
Прогрев min_pool_sizeПоддерживается на каждом такте цикла событий (без отдельной задачи replenish).Периодический фоновый replenish (retain_connections_time, по умолчанию 30 s), который отступает, когда ограничитель всплесков занят.
Повторный логин после ошибки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.Арбитр резерва оценивает запросы по (starving, queued_clients).
Мониторинг давления на новые соединенияSHOW POOLS, SHOW STATS. Текущие inflight-коннекты и результаты anticipation не видны вовсе.SHOW POOL_SCALING и SHOW POOL_COORDINATOR показывают каждый счётчик нового кодового пути.

В промышленной эксплуатации важнее всего три различия:

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

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

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

Диагностика

В логах одновременно несколько подключений к бэкенду

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

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

Исправление. Понизьте 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 откладывается, потому что ограничитель всплесков занят клиентским трафиком. Проверьте replenish_def в SHOW POOL_SCALING. Если он продолжает расти, replenish пропускает каждый retain-цикл.

Исправление. Так задумано: под нагрузкой ограничитель отдан клиентским запросам на создание. Пул дойдёт до 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 задержки растёт без видимой причины

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

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

Исправление. Сверьте 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 растёт пропорционально обороту пула: прямая передача работает, возвраты перехватываются, лишних 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"} растёт.

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

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

  1. Сравните current и max_db_conn в SHOW POOL_COORDINATOR. Если current стабильно стоит на лимите, ваша нагрузка его превышает. Либо поднимайте max_db_connections, либо ищите разогнавшийся пул.
  2. Посмотрите на частоту evictions. Если она нулевая или близка к нулю, выселение не помогает: либо свободные соединения каждого пула моложе 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-permit до основного permit держался за своим бэкендом, пока тот не доживёт до min_connection_lifetime и retain-цикл не поймает его свободным. При устойчивом клиентском трафике last_used() на бэкенде обновлялся быстрее, чем min_connection_lifetime, и permit никогда не отпускался.

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

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

Ограничитель всплесков как узкое место даже при низком трафике

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

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

Исправление. Разберитесь, почему connect() медленный со стороны PostgreSQL (слишком много SCRAM-итераций, конкуренция за блокировки pg_authid, медленный DNS, SSL handshake). Когда connect() станет быстрым, ограничитель перестанет быть узким местом. Поднятие scaling_max_parallel_creates лишь маскирует проблему и перенесёт шторм на 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, краш, плановое окно). Видна толпа клиентов, бьющих в ограничитель всплесков, inflight_creates стоит на лимите, частота creates_started резко растёт.

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

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

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

Глоссарий

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

Fallback через Patroni

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

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

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

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

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

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

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

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

Плановый switchover. DBA запускает patroni switchover --candidate node2. Patroni промотирует node2, затем останавливает PostgreSQL на node1. Между остановкой и тем, как Patroni перезапустит node1 как реплику node2, doorman на node1 не имеет локального сервера. С включённым fallback следующий клиентский запрос, не сумевший дойти до локального сокета, триггерит /cluster lookup, и новое соединение открывается к node2.

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

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

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

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

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

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

Fallback:
  клиент --unix--> doorman --TCP---> PostgreSQL (удалённый, из /cluster)
                      |
                      +-- GET /cluster --> Patroni API
  1. doorman пробует локальный Unix-сокет.
  2. Connection refused или ошибка сокета: doorman помечает локальный сервер как недоступный на fallback_cooldown (по умолчанию 30 секунд).
  3. doorman отправляет GET /cluster ко всем Patroni URL из конфига параллельно и берёт первый успешный ответ.
  4. Из списка участников doorman отбрасывает находящихся в периоде охлаждения и делит остальных на две волны по роли: волна 1 — все sync_standby; волна 2 — все остальные (replica + leader, в порядке discovery).
  5. Волна 1 (строгий приоритет sync_standby). doorman параллельно запускает Server::startup для каждого sync_standby, каждый под fallback_connect_timeout (по умолчанию 5 секунд). Первый sync_standby, успешно прошедший startup, выигрывает и его соединение отдаётся клиенту. Пока хоть один sync_standby ещё в процессе startup, replica/leader не учитываются — даже если replica уже готова. Цель — сохранить пишущий трафик: sync_standby это кандидат на promote с минимальной потерей данных.
  6. Волна 2 (без приоритета). Запускается, только если все sync_standby упали (или их нет в кластере). doorman параллельно стартует остальных кандидатов под тем же per-candidate timeout; выигрывает первый, кто успешно завершит startup — replica и leader идут на равных.
  7. Все кандидаты исчерпаны. Если обе волны кончились без победителя, в лог doorman пишется all fallback candidates rejected (3 startup_error, 1 timeout) с детерминированной разбивкой по причинам. Клиент получает очищенную FATAL-ошибку — Unable to retrieve server parameters … may be unavailable or misconfigured; разбор смотрите в логе doorman.
  8. Успешное соединение попадает в пул со сниженным lifetime (по умолчанию 30 секунд, совпадает с fallback_cooldown). На него действуют все обычные правила пула: лимиты координатора, idle timeout, ротация.
  9. Последующие соединения в рамках периода охлаждения идут к тому же fallback-хосту напрямую, без повторного запроса к Patroni API. Если кэшированный host позже отказывает на startup, doorman очищает кэш и выполняет один дополнительный раунд discovery.
  10. Когда период охлаждения истекает, doorman снова пробует локальный сокет. Если работает — штатный режим. Если нет — цикл повторяется.

Отказ отдельного кандидата (auth, database is starting up, таймаут) ставит кандидата в период охлаждения с экспоненциальным backoff; следующие раунды discovery пропускают эти хосты до истечения окна.

Ограничение времени ожидания клиента

Клиент никогда не ждёт fallback дольше query_wait_timeout (по умолчанию 5 секунд). Если дедлайн срабатывает, doorman прерывает fallback с записью fallback: outer deadline {ms}ms exceeded в лог, а клиент получает очищенную FATAL-ошибку — ту же, что и на любую другую startup-time ошибку. Дедлайн мягкий: жёсткую защиту от зависаний обеспечивает per-candidate fallback_connect_timeout, а внешний дедлайн — это верхняя граница того, сколько клиент сам готов ждать.

Период охлаждения для отдельных хостов

Кандидат, отказавший на startup, исключается из ближайшего discovery на fallback_connect_timeout (по умолчанию 5 секунд). Каждый последовательный отказ того же хоста удваивает период охлаждения, с верхним пределом 60 секунд. После окончания окна запись удаляется (lazy cleanup на следующем discovery-цикле), счётчик сбрасывается на следующем отказе. Это не даёт застрявшему кандидату (postgres в recovery, постоянная ошибка auth, медленная сеть) повторно тестироваться на каждый клиентский запрос и заваливать одновременно и кандидата, и Patroni API.

Запись на реплике

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

ERROR: cannot execute INSERT in a read-only transaction

Запросы на чтение работают нормально. При типичном switchover sync_standby промотируется раньше, чем doorman обнаруживает отказ, поэтому большинство запросов на запись проходит. В худшем случае ошибки записи длятся до истечения сниженного 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"Сколько локальный сервер остаётся помеченным как недоступный после ошибки соединения. В течение этого окна все новые соединения идут на fallback-хост.
patroni_api_timeout"5s"HTTP-таймаут запросов к Patroni API. Действует на каждый URL; так как все URL опрашиваются параллельно, реальный таймаут равен этому значению, а не умноженному на количество URL.
fallback_connect_timeout"5s"Дедлайн Server::startup для каждого кандидата (покрывает TCP-connect плюс StartupMessage round-trip) и база per-host cooldown после отказа startup. Один параметр на две роли — обе имеют одинаковую семантику «кандидат не отвечает».
fallback_lifetime= fallback_cooldownВремя жизни fallback-соединений. Короче штатного server_lifetime, чтобы doorman быстро вернулся к локальному серверу после восстановления.
connect_timeout ([general])"3s"Дедлайн Server::startup для локального сервера, в дополнение к существующим ролям для alive-check и TCP probe. Поднимите этот параметр, если ваш локальный PostgreSQL имеет медленный startup (большой WAL replay, прогрев shared_buffers).
query_wait_timeout ([general])"5s"Внешний дедлайн всего fallback-пути. Клиент никогда не ждёт серверное соединение дольше этого значения, независимо от количества перебираемых кандидатов.

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

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

Два и более 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, пока локальный сервер в периоде охлаждения и пул использует fallback
pg_doorman_fallback_hostgaugeТекущий активный fallback-хост (1 = активен). Лейблы: pool, host, port
pg_doorman_fallback_cache_hits_totalcounterПовторное использование кешированного fallback-хоста без запроса к Patroni API
pg_doorman_fallback_candidate_failures_totalcounterОтказ конкретного кандидата на startup. Labels: pool, reason (connect_error, startup_error, server_unavailable, timeout, other). По разбивке по reason при exhaustion видно, что произошло — auth-фейлы на всех нодах или сетевая проблема.
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, и в Patroni member.host, не hostname. Дедлайн startup покрывает DNS-резолв через TcpStream::connect, но 5-секундный DNS hang съест весь бюджет fallback_connect_timeout для конкретного кандидата прежде чем будет проверен следующий.

Объём логов при failure storm. Per-candidate <host>:<port> rejected (...) WARN ограничен одной строкой в 10 секунд на (pool, host, port). Подавленные строки уходят в DEBUG. Если вы видите один WARN там, где ожидали много — это rate-limit, не потерянные данные; проверяйте счётчик pg_doorman_fallback_candidate_failures_total для реального количества попыток.

Switchover whitelist и pg_doorman_fallback_host. Когда fallback-цель меняется (cooldown истёк, retry-раунд выбрал другой host), gauge для предыдущего (host, port) удаляется в той же операции, что устанавливает gauge для нового. Дашборды не видят два хоста одновременно помеченных как активные во время переключения.

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

Связь с patroni_proxy

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

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

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

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

Patroni Proxy

patroni_proxy — TCP-балансировщик для кластеров PostgreSQL под управлением Patroni. Слушает один или несколько портов, спрашивает у Patroni REST API кто сейчас leader / sync / async, и направляет новые соединения на нужную роль по стратегии least-connections. Не выполняет пулинг соединений, не парсит wire-протокол, не знает что за SQL внутри — это работа pg_doorman, развёрнутого ниже по цепочке.

Что он делает

  • Открытие членов кластера через опрос /cluster с интервалом cluster_update_interval (по умолчанию 3 с) и по запросу GET /update_clusters.
  • Маршрутизация по ролям. Каждый listen-порт привязан к одной или нескольким ролям (leader, sync, async, any). Соединения с этого порта попадают на члена, у которого совпадает одна из указанных ролей.
  • Least-connections. Для портов с несколькими допустимыми членами прокси держит счётчик соединений на каждого члена и отправляет новое соединение туда, где их меньше. Обновление кластера не сбрасывает эти счётчики.
  • Отбрасывает реплики со старыми данными. max_lag_in_bytes per-port исключает членов, у которых replication_lag (из /cluster) выше порога. Leader по лагу никогда не исключается.
  • Пропускает не-running. Допускаются только члены со state: "running"; starting, stopped, crashed и узлы с тегом noloadbalance фильтруются.

Операционно важно следующее: при изменении топологии patroni_proxy обновляет таблицу маршрутизации только для новых соединений. Существующие TCP-соединения к ещё живому backend не трогаются. По сравнению с HAProxy + confd, где reload рвёт все соединения через затронутую backend-секцию, это значит, что cluster_update_interval не воюет с долгоживущими транзакциями.

Роли

РольОписание
leaderPrimary / master
syncСинхронные standby-реплики
asyncАсинхронные реплики
anyЛюбой running-член кластера

Рекомендуемое развёртывание

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, владеет role-aware failover-решением и не лезет в дела пулера.

Если поток приложения небольшой и одного pg_doorman на кластер достаточно, схема сжимается до одного pg_doorman с включённым fallback через Patroni, а patroni_proxy можно вообще не разворачивать.

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

Пример 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-логирование, чтобы увидеть детальный жизненный цикл соединений.

Горячая замена процесса с переносом сессий

Перед SIGUSR2 или admin-командой UPGRADE можно заменить бинарь и конфиг на диске. Работающий процесс проверит эту комбинацию через -t, запустит дочерний процесс с теми же аргументами запуска, передаст ему слушающий сокет и переведёт на него новые подключения.

Старый процесс продолжает обслуживать уже подключённых клиентов и переносит свободные TCP-сессии без TLS в новый процесс через Unix-сокет. Клиент остаётся на том же TCP-соединении: вместе с файловым дескриптором передаются ключ отмены запроса, параметры PostgreSQL-сессии и кеш prepared statements.

Клиенты внутри транзакции продолжают работать на старом процессе и переезжают, когда транзакция завершится. Для приложений это убирает волну переподключений, а для PostgreSQL — всплеск auth/SCRAM.

TLS-сессии мигрируют только в Linux-сборке с фичей tls-migration. Дистрибутивные пакеты и Docker-образ собраны без неё, поэтому TLS-клиенты при такой замене дренируются и переподключаются к новому процессу.

Чем это отличается от PgBouncer / Odyssey

PgBouncer (-R, устарел с 1.20, или rolling restart через so_reuseport) и Odyssey (SIGUSR2 + bindwith_reuseport) переводят новые подключения на новый процесс, а старые сессии оставляют в старом до отключения клиентов. Сессии, prepared statements и TLS-состояние между процессами не переносятся.

pg_doorman передаёт живой клиентский сокет через SCM_RIGHTS, а в Linux-сборке с tls-migration может перенести и состояние OpenSSL.

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

Используйте дистрибутивный пакет, если он есть

Если pg_doorman поставлен через apt install pg-doorman или dnf install pg-doorman, заменяйте бинарник через пакетный менеджер. Обычный путь — apt-get install --only-upgrade pg-doorman или dnf upgrade pg-doorman. Ручной install ниже нужен только там, где бинарник раскладывают без пакетного менеджера.

# 1. Установите новый бинарник по пути, из которого запущен сервис,
#    и при необходимости обновите конфиг на диске.
install -m 0755 pg_doorman_new /usr/bin/pg_doorman

# 2. Проверьте, что новый процесс стартует с этим конфигом, до того
#    как запускать замену. SIGUSR2 тоже прогоняет `-t` и отменяет
#    замену при ошибке, но проверка здесь даёт шанс починить конфиг,
#    не трогая работающий сервер.
/usr/bin/pg_doorman -t /etc/pg_doorman/pg_doorman.toml

# 3. Запустите замену процесса. С `ExecReload=/bin/kill -SIGUSR2 $MAINPID`
#    в юните `systemctl reload` отправляет SIGUSR2 и запускает
#    горячую замену процесса. Дальше pg_doorman проверяет конфиг,
#    запускает дочерний процесс, мигрирует состояние где возможно и
#    дренирует старый процесс. systemd шлёт сигнал в MainPID, поэтому
#    выбирается ровно один процесс, даже если на хосте есть
#    другие инстансы pg_doorman. Прямой `kill -USR2 $(pgrep -f
#    /usr/bin/pg_doorman)` работает, но pgrep ищет по командной
#    строке и может попасть во все инстансы сразу — пакетные
#    установки используют systemctl.
sudo systemctl reload pg_doorman.service

# Успешный reload означает только, что systemd отправил SIGUSR2.
# Проверка конфига, запуск дочернего процесса, MAINPID handoff и
# миграция клиентов происходят внутри pg_doorman. Проверьте их следующим
# шагом и по логам.

# 4. Проверьте: systemd подхватил новый MainPID. При Type=notify
#    дочерний процесс отправляет `MAINPID=<new_pid>` во время передачи
#    управления. Состояние active и админ-консоль подтверждают, что
#    клиенты на месте.
systemctl show -p MainPID --value pg_doorman.service
psql -h pgdoorman -p 6432 -c 'SHOW POOLS;'  # отвечает новый процесс

Установки без systemd

Если процесс не управляется systemd, читайте PID-файл, который пишет режим демона (daemon_pid_file, по умолчанию /tmp/pg_doorman.pid), вместо парсинга pgrep: kill -USR2 "$(cat /var/run/pg_doorman/pg_doorman.pid)". Для запуска в foreground-режиме без systemd храните PID процесса-супервизора и шлите сигнал ему напрямую.

Ту же замену процесса можно запустить из консоли администратора:

UPGRADE;

UPGRADE шлёт SIGUSR2 запущенному процессу — тот же путь, что и kill -USR2. Успешный ответ команды означает, что сигнал отправлен, а не что проверка и миграция уже закончились.

Как работает замена

                        SIGUSR2
                           |
                           v
               +-----------------------+
               | 1. Проверка конфига   |
               |    (pg_doorman -t)    |   -- ошибка --> отмена
               +-----------+-----------+
                           |
                           v
               +-----------------------+
               | 2. Запуск нового      |
               |    socketpair()       |
               |    inherit-fd         |
               |    readiness pipe     |   — ожидание до 10 с
               +-----------+-----------+
                           |
             +-------------+-------------+
             |                           |
             v                           v
  +---------------------+    +---------------------+
  | СТАРЫЙ процесс      |    | НОВЫЙ процесс       |
  |                     |    |                     |
  | 3. Свободные        |    | migration_receiver  |
  |    сериализация     +--->+    восстановление   |
  |    dup() + SCM_RIGHTS    |    запуск клиента   |
  |                     |    |    handle()         |
  | 4. Клиенты в tx     |    |                     |
  |    дождаться COMMIT +--->+ Принимает новые     |
  |    мигрировать      |    | соединения          |
  |                     |    |                     |
  | 5. Таймер выхода    |    +---------------------+
  |    опрос 250 мс     |
  |    выход при 0      |
  +---------------------+

Фаза 1: Проверка конфига

Работающий процесс берёт путь, из которого стартовал сам, и запускает его с -t и текущим конфигом. После install и правки конфига из быстрого старта этот путь уже указывает на новый бинарник и новый файл конфига, поэтому проверяется именно процесс, который должен принять трафик. Если проверка проваливается, замена отменяется, а старый процесс продолжает обслуживать трафик. В логах появляется баннер:

!!!  BINARY UPGRADE ABORTED - SHUTDOWN CANCELLED  !!!
!!!  FIX THE CONFIGURATION BEFORE ATTEMPTING BINARY UPGRADE AGAIN  !!!
!!!  THE SERVER WILL CONTINUE RUNNING WITH THE CURRENT BINARY  !!!

Фаза 2: Запуск нового процесса

Foreground-режим:

  1. Создаётся Unix socketpair() для миграции клиентов.
  2. Файловый дескриптор слушающего сокета передаётся дочернему процессу через --inherit-fd.
  3. Канал готовности: родитель ждёт до 10 секунд байт от дочернего процесса. Дочерний процесс пишет в канал, когда начинает принимать соединения.
  4. Родитель закрывает свой слушающий сокет — новые соединения идут в дочерний процесс.

Режим демона:

Запускается новый фоновый процесс. Старый закрывает слушающий сокет. Миграция клиентов через socketpair() не используется: клиенты остаются на старом процессе. По истечении shutdown_timeout старый процесс выходит, а оставшиеся клиентские сокеты закрываются. Если клиенты должны мигрировать в новый процесс, используйте foreground-режим.

Фаза 3: Миграция свободных клиентов (foreground-режим)

Когда установлен флаг MIGRATION_IN_PROGRESS, каждый свободный клиент (нет активной транзакции, нет отложенного BEGIN, нет буферизованных данных на чтение) мигрирует:

  1. Сериализация: connection_id, secret_key, имя пула, username, параметры сервера, полный кеш prepared statements.
  2. dup() + SCM_RIGHTS: дескриптор клиентского TCP-сокета дублируется и передаётся новому процессу через Unix socketpair().
  3. Восстановление: новый процесс заново создаёт структуру клиента, подключает к нужному пулу и запускает handle().

Клиент не замечает миграции: нет повторного подключения, ошибки или повторной аутентификации. TCP-соединение остаётся тем же физическим сокетом.

Фаза 4: Доработка клиентов внутри транзакции

Клиент внутри BEGIN ... COMMIT продолжает работать на старом процессе. Его серверное соединение остаётся живым. После завершения транзакции (COMMIT или ROLLBACK) клиент становится свободным и мигрирует на следующей итерации цикла.

Отложенный BEGIN (сервер ещё не выделен) тоже блокирует миграцию. Клиент должен отправить запрос, тем самым материализовать BEGIN, затем выполнить COMMIT; после этого соединение можно перенести.

Фаза 5: Таймер завершения

Таймер завершения опрашивает CURRENT_CLIENT_COUNT каждые 250 мс. Когда все клиенты мигрировали или отключились — старый процесс вызывает process::exit(0).

Если shutdown_timeout истекает раньше, старый процесс выходит принудительно, а оставшиеся соединения закрываются.

Во время миграции drain_all_pools() откладывается: клиентам внутри транзакций нужны их серверные соединения. Дренирование пулов начинается только после завершения миграции или сброса MIGRATION_IN_PROGRESS.

Кеш prepared statements при миграции

Клиентский кеш prepared statements сериализуется при миграции:

  • Ключ записи: имя statement или хеш анонимного Parse
  • Хеш запроса
  • Полный текст запроса
  • OID типов параметров

В новом процессе:

  1. Каждая запись регистрируется в общем кеше пула (DashMap).
  2. На новых серверных соединениях ещё нет prepared statements.
  3. При первом Bind к мигрированной записи pg_doorman прозрачно отправляет Parse на новое серверное соединение. Клиент не видит дополнительного обмена.

Ограничения:

  • Если client_anonymous_prepared_cache_size нового конфига меньше, лишние анонимные записи вытесняются по LRU. Именованная часть не ограничена и переносится полностью. Оставшиеся записи работают нормально.
  • Анонимные prepared statements (Parse с пустым именем) переносятся в новый процесс, но требуют повторного Parse перед Bind.
  • DEALLOCATE ALL после миграции очищает переданный кеш. Повторный Parse с тем же именем использует новый текст запроса.

Миграция TLS-сессий

По умолчанию TLS-клиенты не мигрируют — зашифрованная сессия требует ключевой материал, который живёт внутри автомата OpenSSL. Такие клиенты дренируются при замене процесса: соединение закрывается при истечении shutdown_timeout, клиент переподключается к новому процессу.

Опциональная сборка с tls-migration решает эту проблему. Патченный OpenSSL экспортирует состояние симметричного шифрования, передаёт его вместе с файловым дескриптором через Unix-сокет, а новый процесс импортирует состояние и продолжает шифрование. Клиент не выполняет повторное TLS-рукопожатие.

Что экспортируется

Патч добавляет SSL_export_migration_state() и SSL_import_migration_state() в OpenSSL 3.5.5. Экспортируемые данные:

  • Версия TLS-протокола
  • ID набора шифров и длина тега
  • Симметричные ключи для чтения/записи (входные данные для AES key schedule, не развёрнутые ключи)
  • IV (nonce) для чтения/записи
  • Номера записей для чтения/записи (по 8 байт)
  • Для TLS 1.3: секреты клиентского и серверного трафика приложения

Этого достаточно для восстановления слоя TLS-записей в новом процессе и продолжения шифрования/дешифрования на том же TCP-соединении.

Сборка с миграцией TLS

cargo build --release --features tls-migration

Требует perl и patch в окружении сборки. Vendored OpenSSL 3.5.5 собирается из исходников с наложенным патчем.

Сборка без доступа к интернету

# Скачайте архив заранее
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 архива проверяется автоматически.

Ограничения

  • Только Linux. На macOS/Windows миграция TLS не поддерживается (native-tls использует Security.framework / SChannel, не OpenSSL).
  • Одинаковые сертификаты. Старый и новый процесс должны использовать одни и те же tls_private_key и tls_certificate. Состояние шифрования привязано к SSL_CTX, созданному из сертификата. Изменённые сертификаты приводят к ошибке импорта и отключению клиента.
  • FIPS несовместимо. Vendored OpenSSL не проходит FIPS-валидацию. Для FIPS используйте сборку без tls-migration (TLS-клиенты будут дренироваться вместо миграции).
  • Нет HSM/PKCS#11. Vendored OpenSSL собирается с no-engine.

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

  • TLS 1.3 KeyUpdate меняет ключи шифрования. Если любая сторона отправит KeyUpdate после экспорта состояния шифрования, импортированные ключи станут невалидными — соединение упадёт с ошибкой AEAD.

    Поведение драйверов (проверено апрель 2026):

    ДрайверАвтоматический 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Средний
    PostgreSQL serverНет — renegotiation отключён, KeyUpdate не вызываетсяНет

    Java-клиенты: JSSE автоматически отправляет KeyUpdate после ~128 GB зашифрованных данных. Баг JDK-8329548 может вызвать шторм сообщений KeyUpdate. Для Java с долгоживущими высоконагруженными соединениями миграция TLS может потерять соединения. Обходной путь: увеличить порог через jdk.tls.keyLimits в java.security, или отключить TLS между Java-клиентом и pg_doorman.

    Rust-клиенты с rustls: rustls ротирует ключи при лимитах AEAD (очень высокий порог, ~2^36 записей для AES-GCM). Для типичных PostgreSQL-нагрузок этот порог практически недостижим. Использование native-tls (OpenSSL) вместо rustls устраняет риск.

    Все OpenSSL-драйверы безопасны. OpenSSL явно не выполняет автоматический KeyUpdate (openssl#23566).

  • Данные SSL_pending не проверяются. Миграция происходит в точке простоя, где нет буферизованных данных приложения. Этот инвариант гарантируется состоянием клиента, но явная проверка SSL_pending() не выполняется.

  • Привязка к OpenSSL 3.5.5. Патч модифицирует внутренние структуры OpenSSL (ssl_local.h, rec_layer_s3.c, ssl_lib.c). При обновлении OpenSSL нужно проверить и переложить патч на новую версию.

Сигналы

СигналПоведение
SIGUSR2Горячая замена процесса + дренирование старого процесса. Рекомендуемый для всех режимов.
SIGINTВ foreground + TTY (Ctrl+C): только завершение, без замены процесса. В режиме демона или без TTY: горячая замена процесса для совместимости со старыми установками.
SIGTERMНемедленный выход. Транзакции обрываются. Все клиенты отключаются.
SIGHUPПеречитать конфигурацию без перезапуска. Без простоя.
UPGRADE (admin)Отправляет SIGUSR2 текущему процессу. Тот же эффект.

Поведение SIGINT для совместимости: SIGINT запускает горячую замену процесса в режиме демона или без TTY (например, под systemd). В интерактивном терминале Ctrl+C останавливает процесс без запуска нового. Используйте kill -USR2 или UPGRADE в консоли администратора для горячей замены в foreground-режиме.

Режим демона и foreground-режим

ForegroundРежим демона
Миграция клиентов через передачу fdДа (socketpair)Нет
Свободные клиенты сохраняютсяДаНет (закрываются при выходе старого процесса)
Клиенты внутри транзакцииЗавершают транзакцию, затем мигрируютРаботают до таймаута, затем закрываются
Запуск нового процессаНаследует файловый дескриптор слушающего сокетаЗапускается независимо
Рекомендуется дляsystemd, контейнеры, KubernetesСтарые установки

Для горячей замены процесса с миграцией клиентов запускайте pg_doorman в foreground-режиме. systemd управляет жизненным циклом процесса. Используйте Type=notify: юнит становится active только после сигнала готовности от pg_doorman, а новый процесс при SIGUSR2 обновляет MainPID на себя:

[Service]
Type=notify
# Дочерний процесс, который принимает трафик после SIGUSR2, должен
# отправить READY=1 и MAINPID=<new_pid> во время передачи управления.
NotifyAccess=exec
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.toml
# `systemctl reload` запускает горячую замену процесса: проверка конфига,
# новый процесс, миграция клиентов где возможно, затем дренирование
# старого процесса по shutdown_timeout из конфига pg_doorman.
ExecReload=/bin/kill -SIGUSR2 $MAINPID
# `systemctl stop` — немедленное завершение. Это не путь горячей замены,
# и он не ждёт миграции активных транзакций.
ExecStop=/bin/kill -SIGTERM $MAINPID
# При горячей замене новый дочерний процесс становится MainPID через sd_notify.
# KillMode=mixed при обычном stop шлёт SIGTERM только в MainPID, а
# оставшиеся процессы cgroup добивает SIGKILL только после TimeoutStopSec.
KillMode=mixed
TimeoutStopSec=60
# Не перезапускать сервис после чистой ручной остановки или после выхода
# старого процесса, который успешно передал трафик новому при замене.
Restart=on-failure
Nice=-15
# pg_doorman активно использует дескрипторы: каждый клиент и каждый
# бэкенд — отдельный fd, плюс служебные pipe. 65536 покрывает большинство
# OLTP-пулов; считается как `general.pool_size * num_pools` плюс
# несколько тысяч на клиентов.
LimitNOFILE=65536
# Сервисная учётка, которой принадлежит PID-файл. На многих установках
# postgres уже существует — переиспользование оставляет владение
# файлов согласованным с самим PostgreSQL.
User=postgres
Group=postgres
SyslogIdentifier=pg_doorman

systemctl reload pg_doorman отправляет SIGUSR2; нулевой код выхода означает только, что сигнал доставлен. Затем pg_doorman прогоняет -t на новом бинарнике, отменяет замену при битом конфиге, иначе порождает новый процесс и дренирует старый. UPGRADE; из админ-консоли идёт по тому же пути. Окно дренирования задаёт shutdown_timeout в pg_doorman.toml; TimeoutStopSec относится к обычному systemctl stop, а не к тому, сколько systemctl reload ждёт мигрировавшие сессии.

Продакшен-установки часто добавляют сверху ограничения ресурсов: MemoryMax=, CPUAffinity=2,3,4,5,6,7,8,9. Это зависит от нагрузки и не меняет контракт горячей замены.

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

shutdown_timeout

Максимальное время ожидания завершения транзакций перед принудительным закрытием соединений. Старый процесс завершается по истечении этого таймаута вне зависимости от оставшихся клиентов.

По умолчанию: 10 секунд.

Рекомендация для продакшена с длинными аналитическими запросами: 30–60 секунд.

[general]
shutdown_timeout = 60000  # миллисекунды

Слишком маленькое значение — риск оборвать активные транзакции. Слишком большое — задержка выхода старого процесса при зависшем клиенте (например, idle-in-transaction). Выбирайте значение, покрывающее самую длинную ожидаемую транзакцию, с запасом.

tls_private_key / tls_certificate

Чтобы миграция TLS отработала, оба процесса (старый и новый) должны загрузить один и тот же сертификат и приватный ключ для входящих клиентских TLS-соединений. Состояние шифрования привязано к SSL_CTX, созданному из этих файлов; при несовпадении импорт падает, и затронутые клиенты отключаются и переподключаются.

Клиентский TLS-материал не перезагружается по SIGHUP (это делает только серверный TLS, см. горячую перезагрузку TLS). Не совмещайте ротацию сертификата для входящих клиентских TLS-соединений с горячей заменой, где вы ожидаете миграцию TLS-сессий. Если файлы отличаются между старым и новым процессом, импорт TLS-состояния падает, и затронутые клиенты переподключаются даже с включённым tls-migration. Ротируйте этот сертификат в окно, где переподключения допустимы, либо оставьте те же файлы на время горячей замены процесса и меняйте сертификат позже через рестарт.

prepared_statements_cache_size

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

client_anonymous_prepared_cache_size

LRU-кеш анонимных записей на клиента. Клиентский кеш (и именованная, и анонимная части) сериализуется полностью при миграции. Если новый конфиг имеет меньшее значение, LRU вытесняет лишние анонимные записи. Именованные записи не ограничены этим параметром и мигрируют целиком.

Откат

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

Не используйте systemctl restart или SIGTERM для отката, если переподключения недопустимы: оба пути закрывают клиентские сессии, а не мигрируют их.

Мониторинг

Логи

Ключевые строки в логах при миграции:

INFO  Got SIGUSR2, starting binary upgrade and graceful shutdown
INFO  Validating configuration with: /usr/bin/pg_doorman -t pg_doorman.toml
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-метрики

МетрикаЗначение при замене процесса
pg_doorman_pools_clients{status="active"}Должна упасть до 0 на старом процессе
pg_doorman_pools_clients{status="idle"}Падает по мере миграции клиентов
pg_doorman_connections_total{type="total"}Новый процесс принимает свежие подключения; используйте rate() / increase()
pg_doorman_clients_prepared_cache_entriesПодтверждает перенос кэша

Консоль администратора

-- На новом процессе (старый отклоняет не-admin соединения)
SHOW POOLS;
SHOW CLIENTS;

Диагностика

Клиент получил 58006 или отключился вместо миграции

Ctrl+C в foreground-режиме. SIGINT в TTY означает завершение без горячей замены. Используйте kill -USR2 или UPGRADE в консоли администратора.

Режим демона. В режиме демона нет миграции через файловые дескрипторы. Существующие клиенты остаются на старом процессе и закрываются, когда он выходит. Переключитесь на foreground-режим.

PG_DOORMAN_CI_SHUTDOWN_ONLY=1 установлен. Эта переменная окружения принудительно включает режим только завершения (используется в CI-тестах). Уберите её.

Старый процесс не завершается

Длинная транзакция. Клиент застрял в BEGIN без COMMIT. Дождитесь shutdown_timeout или завершите транзакцию вручную.

Административные соединения. Они не мигрируют. Закройте административную сессию на старом процессе.

Принудительный выход: kill -TERM <old_pid> отправляет SIGTERM и завершает процесс без ожидания миграции.

TLS-соединение оборвалось после замены процесса

Бинарник собран без --features tls-migration. TLS-клиенты дренируются вместо миграции. Пересоберите с --features tls-migration.

Запуск не на Linux. Миграция TLS работает только на Linux.

Сертификат или ключ изменились. Старый процесс экспортировал состояние шифрования, привязанное к старому сертификату для входящих клиентских TLS-соединений. Если нужна миграция TLS-сессий, используйте те же файлы в обоих процессах. Ротация этого сертификата требует рестарта или окна, где переподключения допустимы.

"TLS migration not available" в логах

Новый процесс получил пакет миграции с TLS-данными, но собран без --features tls-migration или запущен не на Linux. Клиент отключается. Пересоберите новый бинарник с --features tls-migration.

"migration channel not ready" в логах

Канал MIGRATION_TX ещё не инициализирован. Новый процесс не завершил запуск, когда клиент попытался мигрировать. Клиент повторит попытку на следующей итерации простоя (через миллисекунды).

"migration channel send failed" в логах

Канал миграции переполнен (capacity: 4096). Возможно при одновременной миграции тысяч клиентов. Клиент повторит попытку на следующей итерации простоя.

"prepare_migration failed" в логах

Исходный fd клиента недоступен или dup() не удался. Возможные причины: исчерпание файловых дескрипторов, или клиент подключился через ветку кода, которая не сохраняет исходный fd. Проверьте ulimit -n.

Совместимость с клиентскими библиотеками: Библиотеки вроде github.com/lib/pq или Go database/sql могут потребовать настройки для обработки переподключения, если клиент не мигрировал и получил 58006 или закрытие соединения. См. issue.

Чек-лист перед продакшеном

Перед выкатом горячей замены процесса:

  • Запуск в foreground-режиме (не daemon) для миграции через файловые дескрипторы
  • shutdown_timeout покрывает самую длинную ожидаемую транзакцию (рекомендация: 30–60 секунд для OLTP, больше для аналитики)
  • Если используете TLS: сборка с --features tls-migration, оба процесса используют одинаковые файлы сертификата и ключа
  • Протестировать замену на staging: открыть сессию, отправить SIGUSR2, убедиться что сессия продолжает работать
  • В systemd-юните указано Type=notify с NotifyAccess=exec, ExecReload=/bin/kill -SIGUSR2 $MAINPID (тогда systemctl reload запускает горячую замену процесса с проверкой конфига), KillMode=mixed и Restart=on-failure
  • Мониторинг логов на ошибки миграции после первой замены в продакшене
  • Подтвердить что старый процесс завершился (PID-файл или pgrep)
  • Проверить Prometheus-метрики: клиенты на новом процессе

Prometheus во время дренирования

HTTP-слушатель, который отдаёт /metrics, использует SO_REUSEPORT. Пока старый процесс дренируется, а новый принимает клиентов, оба процесса делят один порт, и ядро распределяет scrape-запросы между ними. На отдельном scrape счётчики могут выглядеть так, будто откатились назад, пока старый процесс не завершится. Это окно длится не дольше shutdown_timeout.

Сигналы и перезагрузка

pg_doorman реагирует на четыре POSIX-сигнала: SIGHUP, SIGINT, SIGUSR2 и SIGTERM. Каждый делает одну конкретную вещь.

Краткая справка

СигналЭффектСуществующие соединенияКогда применять
SIGHUPПерезагрузить конфиг с диска.Сохраняются.Подкрутить пулы, ротировать серверные TLS-сертификаты, отредактировать pg_hba.conf.
SIGTERMНемедленное завершение.Закрываются.Остановка сервиса, когда переподключения допустимы.
SIGUSR2Горячая замена процесса: новый бинарь и конфиг, затем дренирование старого процесса.Мигрируют в новый процесс, где это возможно; TLS требует tls-migration.Раскатить изменения, которым нужен новый процесс.
SIGINTЗависит от TTY (см. ниже).По-разному.Ctrl+C при разработке; устарело для промышленной эксплуатации.

Перезагрузка (SIGHUP)

kill -HUP $(pidof pg_doorman)

Перечитывает файл конфига и применяет изменения. Что перезагружается:

  • Определения пулов (добавлены, удалены, изменён размер).
  • Списки пользователей, пароли, блоки auth_query.
  • Правила pg_hba.conf (файл или встроенное содержимое).
  • Серверные TLS-сертификаты и CA-бандлы (подмена без блокировок; существующие TLS-соединения сохраняют исходный контекст).
  • Публичные ключи Talos и JWT.
  • Уровень логирования и формат логов.

Что не перезагружается:

  • general.host, general.port — слушающий сокет фиксируется при старте.
  • general.tcp_socket_buffer_size для уже открытых сокетов — новое значение применяется только при приёме нового клиентского TCP-сокета или открытии нового TCP-сокета к PostgreSQL.
  • TLS-сертификаты для входящих клиентских подключений — нужен перезапуск процесса. Не ротируйте их во время горячей замены, где нужна миграция TLS-сессий.
  • Число рабочих потоков и параметры рантайма Tokio.

После SIGHUP SHOW CONFIG показывает новые значения. Уже открытые клиентские соединения не проверяются заново по pg_hba.conf; новые правила действуют только для новых подключений. Уже открытые TCP-сокеты сохраняют размер буфера, заданный при их создании.

Немедленное завершение (SIGTERM)

kill -TERM $(pidof pg_doorman)

pg_doorman логирует, сколько клиентов ещё в транзакциях, и выходит. Он не ждёт shutdown_timeout и не мигрирует активные транзакции. Все клиентские соединения закрываются при выходе процесса.

shutdown_timeout относится к дренированию при горячей замене процесса через SIGUSR2, а не к обычному завершению по SIGTERM.

Горячая замена процесса (SIGUSR2)

kill -USR2 $(pidof pg_doorman)

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

  1. Замените бинарник и/или конфиг на диске.
  2. Отправьте SIGUSR2 работающему процессу.
  3. Текущий процесс проверяет новый бинарник с конфигом через -t.
  4. Текущий процесс порождает дочерний процесс с теми же аргументами запуска, передаёт ему слушающий сокет и продолжает обслуживать существующих клиентов до миграции или завершения.
  5. Новые клиенты сразу подключаются к дочернему процессу.
  6. Старый процесс выходит, когда клиенты мигрировали или отключились (или по shutdown_timeout).

Дочерний процесс отправляет sd_notify MAINPID=<new_pid>, чтобы юниты systemd с Type=notify корректно отслеживали новый главный PID.

Если при горячей замене изменён general.tcp_socket_buffer_size, новый процесс применяет его к мигрировавшим клиентским TCP-сокетам при восстановлении клиента. Backend-сокеты через SCM_RIGHTS не передаются: новые backend-соединения получают настройку при открытии, а старые остаются в старом процессе до дренирования.

Полный протокол, миграцию TLS и откат смотрите в горячей замене процесса с переносом сессий.

SIGINT (Ctrl+C)

SIGINT зависит от контекста:

  • На переднем плане с TTY (разработка, cargo run): только завершение работы.
  • В режиме демона или без TTY: запускает горячую замену процесса и дренирование старого процесса, как SIGUSR2, для совместимости со старыми установками.

Путь горячей замены через SIGINT существует ради обратной совместимости с инсталляциями, которые отправляют SIGINT из init-скриптов. Новые инсталляции должны явно использовать SIGUSR2 для горячей замены и SIGTERM для остановки.

Интеграция с systemd

pg_doorman поддерживает Type=notify. Поставляемый юнит pg_doorman.service запускает бинарник в основном процессе и уведомляет systemd через sd_notify:

[Service]
Type=notify
NotifyAccess=exec
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.toml
ExecReload=/bin/kill -SIGUSR2 $MAINPID
ExecStop=/bin/kill -SIGTERM $MAINPID
SyslogIdentifier=pg_doorman
KillMode=mixed
TimeoutStopSec=60
Restart=on-failure
Nice=-15
User=postgres
Group=postgres
LimitNOFILE=65536

sd_notify READY=1 отправляется после привязки слушающего сокета и инициализации пулов. sd_notify MAINPID=<child> отправляется при горячей замене процесса, чтобы systemd корректно отслеживал новый процесс.

С таким юнитом systemctl reload pg_doorman означает горячую замену процесса (SIGUSR2), а не перечитывание конфига (SIGHUP). Если нужно только перечитать конфиг, используйте kill -HUP <pid>.

Если переходите с 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"

Куда дальше

Fastpath и large objects

Используйте эту страницу, если pgjdbc или Hibernate работают с PostgreSQL large objects через pg_doorman в режиме transaction pool.

pgjdbc LargeObjectManager вызывает функции PostgreSQL large object через Fastpath FunctionCall (F): lo_creat, lo_open, lo_read, lo_write, lo_close и другие. PostgreSQL отвечает FunctionCallResponse (V), а затем ReadyForQuery (Z). В V лежит результат функции. Состояние транзакции приходит в следующем ReadyForQuery.

До 3.10.7 pg_doorman не передавал FunctionCall в transaction pooling. Клиент мог отправить вызов функции large object и навсегда ждать ответа. Начиная с 3.10.7 pg_doorman передаёт вызов в PostgreSQL, возвращает клиенту FunctionCallResponse и освобождает backend только после ReadyForQuery, если PostgreSQL сообщил idle-состояние.

Transaction pooling

Дескрипторы large object живут внутри PostgreSQL-транзакции. Если после fastpath-вызова ReadyForQuery вернул статус T или E, pg_doorman оставляет за клиентом тот же backend. Backend освобождается только после idle-статуса I, обычно после COMMIT или ROLLBACK.

Fastpath-вызовы в autocommit освобождают backend сразу после ReadyForQuery со статусом idle.

Это соответствует поведению PgBouncer в transaction pooling для трафика FunctionCall.

Размер пула

Каждый активный вызов функции large object занимает один backend до ReadyForQuery. Считайте размер пула по числу одновременных чтений и записей large objects, а не только по обычному темпу SQL-запросов.

После включения такого трафика следите за:

  • SHOW POOLS: активные клиенты, активные серверы и ожидающие клиенты.
  • Ошибками query_wait_timeout.
  • Перцентилями задержек для пулов с large object трафиком.

Если всплески вызовов large object подводят клиентов к query_wait_timeout, увеличьте пул для нужной пары user/database или уменьшите параллелизм large object операций в приложении.

Большие чтения

pg_doorman передаёт большие DataRow, CopyData и FunctionCallResponse потоково, если они превышают general.message_size_to_be_stream. Большой fastpath-ответ lo_read уходит клиенту без предварительного буферизования всего ответа в памяти pg_doorman.

Потоковая передача ограничивает расход heap в pg_doorman, но не делает большие одиночные чтения бесплатными. Большой lo_read всё равно удерживает backend и сокетные буферы, пока PostgreSQL отправляет ответ. Лимиты сообщений протокола PostgreSQL тоже остаются. Читайте large objects порциями на стороне приложения.

Таймауты

server_lifetime применяется к idle backend в пуле. Он не прерывает backend, который выполняет чтение или запись large object.

Дескрипторы large object зависят от состояния PostgreSQL-транзакции. Если приложение оставляет large object транзакцию idle между fastpath-вызовами, PostgreSQL idle_in_transaction_session_timeout может закрыть backend. pg_doorman вернёт клиенту ошибку соединения. Держите large object транзакции короткими или настройте PostgreSQL-таймауты для сессий, которые работают с large objects.

Мониторинг query interner

Query interner дедуплицирует тексты Parse в памяти процесса pg_doorman. Хранилище разделено на две независимые хеш-таблицы — для именованных prepared (NAMED) и анонимных (ANON); каждая работает по своей политике. NAMED чистит пассивный сборщик по Arc::strong_count. ANON вытесняет по бездействию через query_interner_anon_idle_ttl_seconds. Обе половины публикуют метрики Prometheus: текущие значения, счётчики вытеснений и гистограмму длительности уборки. Плюс счётчик синтетических ошибок SQLSTATE 26000 — этот код pg_doorman возвращает клиентам, чей анонимный prepared statement выпал из всех кешей.

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

Дашборд

Главные панели (на первом экране)

  1. Сводка — общий объём интернера. sum(pg_doorman_query_interner_bytes) в разрезе инстансов. Красный порог 1.5 ГиБ, жёлтый — 500 МиБ. Главный сигнал по памяти.
  2. График — записи по типу. Две линии:
    • pg_doorman_query_interner_entries{kind="named"}
    • pg_doorman_query_interner_entries{kind="anonymous"} Окно шесть часов. Устойчивый рост любой из линий — повод открыть панели детализации.
  3. График — частота синтетических 26000. rate(pg_doorman_query_interner_synthetic_misses_total[5m]). Норма — плоский ноль. Любой всплеск означает: либо TTL вытеснил запись, на которую сослался клиент, либо драйвер рассчитывает на безымянный prepared statement, который остаётся доступным после Sync.

Детализация

  1. Скорость вытеснений с разбивкой по причине: sum by (kind, reason) (rate(pg_doorman_query_interner_evictions_total[5m])).
  2. Тепловая карта длительности уборки: histogram_quantile(0.5, rate(pg_doorman_query_interner_gc_duration_seconds_bucket[5m])), с P99 поверх.
  3. Среднее число байт на запись по типу: pg_doorman_query_interner_bytes / pg_doorman_query_interner_entries.

Корреляции

  1. Скорость вытеснений ANON в сравнении с общим темпом запросов. Линейная корреляция говорит о здоровом трафике; нелинейная — о взрыве динамического SQL от ORM.
  2. Частота синтетических 26000 в сравнении с P99 latency запросов. Корреляция означает, что TTL режет живой трафик; разбираться с медленной веткой.

Переменные дашборда

  • instance — сравнивать реплики.
  • kind — отфильтровать значения и счётчик до одной из половин.

Лейблы pool, user и database к интернеру неприменимы — он один на процесс. На панелях интернера они только введут читателя в заблуждение.

Правила алертов

Готовый блок groups: лежит в monitoring/prometheus-rules/query-interner.yaml. Пять алертов.

  • PgDoormanAnonInternerMemoryHigh (critical) — байты ANON выше 1.5 ГиБ. Уменьшить TTL или проверить ORM на динамический SQL.
  • PgDoormanAnonTTLTooShort (critical) — синтетические 26000 чаще 1/с в течение 10 минут. Сначала определить источник: клиентский LRU, RESET INTERNER, TTL-вытеснение anonymous-записей или поведение драйвера.
  • PgDoormanAnonInternerNotShrinking (warning) — ANON растёт, а вытеснение по TTL не идёт. TTL слишком велик, либо поток уникальных запросов превышает скорость их истечения.
  • PgDoormanInternerGCSlow (warning) — P99 уборки выше 50 мс на 15-минутном окне. Увеличить query_interner_gc_interval_seconds (этот параметр работает только при перезапуске: reload не изменит частоту проходов у работающего процесса) или сделать RESET INTERNER и уменьшить размеры кешей.
  • PgDoormanNamedInternerGrowsUnbounded (warning) — больше 100 000 записей в NAMED при почти нулевом вытеснении. Почти всегда баг: ссылка на Arc<str> удерживается навсегда.

Защита от холодного старта: у всех алертов for: > 5m. Пустой интернер сразу после запуска процесса их не зажигает.

Размеры

Стационарный объём ANON-интернера при условии, что 50% запросов идут через prepared, а средний SQL — 2 КиБ:

RPSTTL = 60 сTTL = 300 с
100~12k записей / ~24 МиБ~60k / ~120 МиБ
1 000~120k / ~240 МиБ~600k / ~1.2 ГиБ
10 000~1.2M / ~2.4 ГиБразмер не рекомендуется

Интернер глобален на процесс, поэтому объём кластера растёт линейно с числом реплик pg_doorman. Берите это как стартовую оценку для query_interner_anon_idle_ttl_seconds и бюджета RAM на хост; фактическое значение даёт pg_doorman_query_interner_bytes.

Реальный TTL

Сборщик ходит в два прохода. На первом помечает запись как кандидата, на втором удаляет — но только если за это время к записи никто не обратился. Проход выполняется каждые gc_interval / 4 секунд. По умолчанию (gc_interval = 60 с, anon_idle_ttl = 60 с) это раз в 15 секунд. Пометка ставится через 60–75 секунд после последнего использования, удаление — на следующем проходе. Итого запись простаивает 75–120 секунд, прежде чем сборщик её выкинет. Снижать anon_idle_ttl ниже 60 секунд смысла мало: чаще, чем раз в gc_interval / 4, сборщик не запускается.

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

Уменьшить TTL, когда давит память

Когда: горит PgDoormanAnonInternerNotShrinking, байты ANON подходят к лимиту памяти хоста.

Действие: уменьшить query_interner_anon_idle_ttl_seconds в general (например, с 60 до 30). Перечитать конфиг pg_doorman. Скорость вытеснений догонит новый порог.

Разобрать синтетические 26000 перед увеличением TTL

Когда: горит PgDoormanAnonTTLTooShort.

Действие: понять, какой клиент и какой запрос. У synthetic_misses нет меток, поэтому смотреть WARN-лог — он пишется на каждый промах и содержит client, pool, connection_id. Перед изменением конфига проверьте pg_doorman_clients_prepared_anonymous_evictions_total и pg_doorman_query_interner_evictions_total{kind="anonymous"}. Если промахи идут из клиентского Anonymous LRU, увеличьте client_anonymous_prepared_cache_size. Если они идут из TTL anonymous interner или от драйвера, который законно переиспользует безымянный Bind в следующей пачке, поднимите TTL (с 60 до 300, например). Если нет — переключите клиента на именованный prepared.

Сбросить интернер

Когда: разовая диагностика или жёсткое сжатие памяти под инцидентом.

Действие: psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman" -c "RESET INTERNER". Возвращает CommandComplete RESET. Активные клиенты заново разберут запрос при следующем использовании. Короткоживущие клиенты эффекта не заметят: их last_anonymous_hash всё ещё помнит хеш, который они зарегистрировали до сброса, и следующий Bind увидит отсутствие записи и один раз получит 26000. Драйвер на это отреагирует повторным Parse.

Правила предвычислений (recording rules)

Кластерные агрегаты, которые имеет смысл предвычислять, чтобы дашборды были дешевле:

groups:
  - name: pg_doorman_query_interner_recording
    interval: 30s
    rules:
      - record: pg_doorman:query_interner_total_bytes:5m
        expr: sum without (instance) (pg_doorman_query_interner_bytes)
      - record: pg_doorman:query_interner_eviction_rate:5m
        expr: |
          sum without (instance) (rate(pg_doorman_query_interner_evictions_total[5m]))

Первое правило позволяет общей сводной панели читать одну серию. Второе рисует темп вытеснений с разбивкой по причине без пересчёта rate() на каждой перерисовке дашборда.

Диагностика

Симптомы, на которые вы скорее всего наступите в первую неделю работы PgDoorman, и куда смотреть, когда наступили.

Ошибки аутентификации при подключении к PostgreSQL

Симптом: PgDoorman принимает клиентское соединение, но первый же запрос возвращает password authentication failed от PostgreSQL.

Когда username пула совпадает с ролью на backend

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

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

Для SCRAM оба процесса должны видеть одни и те же salt и iteration count — даже один отличающийся символ в сохранённом verifier ломает passthrough.

Когда username пула отличается от роли на backend

Когда username, под которым клиент подключается к pg_doorman, не совпадает с реальной ролью PostgreSQL, passthrough работать не может: у pg_doorman нет пароля для backend-роли. Укажите явные credentials:

users:
  - username: "app_user"              # имя для клиента
    password: "md5..."                # хеш для аутентификации client → pg_doorman
    server_username: "pg_app_user"    # реальная роль в PostgreSQL
    server_password: "plaintext_pwd"  # plaintext-пароль для этой роли
    pool_size: 40

Это же путь для JWT-аутентификации, где клиент не присылает пароль.

Где взять хеш пароля

pg_doorman generate --host … интроспектирует PostgreSQL и собирает конфиг с уже подставленными хешами. Быстрее, чем копировать руками из pg_shadow.

Файл конфигурации не найден

Симптом: PgDoorman при запуске завершается с configuration file not found.

По умолчанию бинарник ищет pg_doorman.toml в текущем рабочем каталоге. Либо назовите файл так и cd в его каталог, либо передайте путь явно:

pg_doorman /etc/pg_doorman/pg_doorman.yaml

Проверка перед запуском:

pg_doorman -t /etc/pg_doorman/pg_doorman.yaml

Клиенты получают 58006 (pooler is shut down now)

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

  • Got SIGUSR2, starting binary upgrade … — идёт горячая замена процесса. В foreground-режиме свободные TCP-клиенты без TLS должны мигрировать прозрачно; TLS-клиенты мигрируют только со сборкой tls-migration. 58006 получают клиенты, оставшиеся в транзакции после shutdown_timeout. В режиме демона миграции через файловые дескрипторы нет, и каждый клиент получает 58006 при закрытии соединения. См. Горячая замена процесса с переносом сессий → Диагностика.
  • Нет строки SIGUSR2 в логе — кто-то прислал SIGTERM или SIGINT, и пулер выключился без замены. Проверьте systemd-юнит, конкретный pid и operator runbook.

Если 58006 пришёл во время плановой замены — для этой подмножества клиентов это ожидаемое поведение. Настройте connection pool приложения на retry при transient-ошибках.

Размер пула слишком мал

Симптом: запросы ходят заметно дольше end-to-end, чем при прямом обращении к PostgreSQL.

Смотрите SHOW POOLS и SHOW POOLS_EXTENDED:

cl_waiting   — сколько клиентов сейчас в очереди за серверным соединением
maxwait      — самое долгое ожидание любого клиента, секунд
sv_idle      — свободные серверные соединения в пуле
sv_active    — серверные соединения, выданные клиентам

Если cl_waiting > 0 стабильно и sv_idle == 0, пул мал для нагрузки. Либо поднимайте pool_size для этого пользователя, либо разбирайтесь, почему sv_active не падает — длинные транзакции, idle-in-transaction сессии, медленный downstream-вызов, который держит серверное соединение.

Если у вас включён max_db_connections, смотрите ещё SHOW POOL_COORDINATOR на evictions (доноры под давлением отдают соединения) и exhaustions (лимит достигнут даже после вытеснений). См. Координатор пулов.

Куда подавать остальное

Проблема осталась?

Если вашей проблемы здесь нет — откройте issue на GitHub: версия pg_doorman, релевантный конфиг (с замазанными паролями), драйвер клиента и его версия, и совпадающие по времени строки логов из pg_doorman и PostgreSQL.

Команды администратора

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

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"

Команды администратора читаются через SHOW <subcommand> или выполняются голыми глаголами (PAUSE, RESUME, RECONNECT, RELOAD, SHUTDOWN, RESET INTERNER, 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Состояние координатора пулов на базу: текущие соединения, использование резерва, число вытеснений. См. Координатор пулов.
SHOW POOL_SCALINGМетрики anticipation/burst: in-flight create-операции, ожидания на воротах, anticipation notifies/timeouts.
SHOW PREPARED_STATEMENTSЗакэшированные prepared statements на пул: hash, имя, текст запроса, число попаданий.
SHOW INTERNERСводка query interner: число записей и байты для named- и anonymous-половины.
SHOW INTERNER <N>N самых крупных интернированных текстов запросов: hash, kind, idle age и предпросмотр SQL.
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 STARTUP_PARAMETERSИтоговые startup_parameters по каждому пулу: параметр, значение, источник и состояние применения.
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 (закрыть простаивающие, дренировать активные). Новые соединения берутся из PostgreSQL.
RELOADТо же, что и SIGHUP — перезагрузить конфиг с диска.
SHUTDOWNОтправляет SIGINT текущему процессу. Перед использованием в daemon mode см. Сигналы.
KILL <database>Сбросить всех клиентов, подключённых к конкретному пулу.
RESET INTERNERОчистить named- и anonymous-записи query interner. Диагностическая команда; активные клиенты заново делают Parse при следующем использовании.
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 STARTUP_PARAMETERS

user | database | parameter         | value             | source  | state
app  | mydb     | statement_timeout | 5s                | general | applied
app  | mydb     | plan_cache_mode   | force_custom_plan | pool    | applied
  • source показывает источник значения: general, pool или auth_query.
  • state показывает, будет ли значение отправлено в ближайший StartupMessage: applied, dropped_due_to_budget или stale.

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. Поднимите потолок.

Тонкости настройки см. в Координатор пулов.

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 растёт — срабатывает предзамена: соединения истекают раньше, чем естественным образом возвращаются.

См. Пул под нагрузкой → Параметры тюнинга.

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

Административная база использует учётку из general.admin_username и general.admin_password:

general:
  admin_username: "admin"
  admin_password: "change_me"

Административные соединения не проходят через правила pg_hba.conf — они идут напрямую в обработчик администратора. Ограничивайте административный доступ на сетевом уровне (listen_addresses, фаервол) или используйте Unix-сокеты.

Куда дальше

Веб-консоль

В pg_doorman встроена операторская веб-консоль. Она работает на том же HTTP-сервере, что отдаёт Prometheus-метрики; собранный фронтенд лежит внутри бинарника. Запуск консоли не добавляет внешних зависимостей: один процесс, один бинарь, один TCP-порт.

Включение

Консоль настраивается в секции [web]. Старое имя секции [prometheus] тоже принимается как алиас.

[web]
enabled = true
host = "0.0.0.0"
port = 9127

# Операторская консоль (по умолчанию выключена)
ui = true
ui_anonymous = false
log_tap_max_entries = 8192

При web.ui = true и general.admin_password, равном пустой строке или литералу "admin", консоль на старте переходит в режим «только метрики». HTTP-сервер продолжает отдавать /metrics, но административные эндпоинты иначе оказались бы открыты любому. Задайте настоящий пароль до того, как включать ui = true. Срабатывание этой проверки видно в логе по строке web.ui = true ignored: admin_password is default/empty.

ПараметрОписаниеПо умолчанию
enabledЗапускать ли HTTP-сервер. /metrics работает независимо от ui.false
hostАдрес, на котором слушает HTTP-сервер."0.0.0.0"
portПорт HTTP-сервера.9127
uiОтдавать SPA по / и публичные API-эндпоинты.false
ui_anonymousПри true публичные API-эндпоинты принимают запросы без авторизации. См. Роли доступа.false
log_tap_max_entriesРазмер кольцевого буфера в памяти для /api/logs. 0 отключает эндпоинт.8192

URL-карта

URLТребуемая рольНазначение
/, /pools, любой путь вне APIнетОболочка приложения. Отдаётся анонимно даже при ui_anonymous = false, чтобы прямая ссылка не открывала системный диалог Basic-авторизации браузера до того, как появится форма входа React.
/assets/*нетХэшированные JS, CSS, шрифты и SVG. Cache-Control: public, max-age=31536000, immutable.
/metricsнетPrometheus exposition format. От ui не зависит.
GET /api/auth/configнетСообщает SPA, подключён ли SSO и какая роль у текущего запроса.
GET /api/version, /api/overview, /api/pools, /api/clients, /api/servers, /api/connections, /api/stats, /api/databases, /api/users, /api/auth_query, /api/config, /api/log_level, /api/pool_coordinator, /api/pool_scaling, /api/sockets, /api/prepared, /api/interner, /api/top/clients, /api/top/prepared, /api/apps, /api/eventsAnonymous, когда ui_anonymous = true, иначе SsoJSON только для чтения, повторяет формат SHOW <admin-команда>.
GET /api/logs, /api/prepared/text/{hash}, /api/interner/top, /api/top/queriesSsoЭндпоинты только для чтения с персональными данными. /api/logs подключает буфер логов на первом запросе и отключает его через 2 минуты простоя. /api/top/queries возвращает первые ~120 символов SQL-текста из кеша. Эти данные не вынесены в публичную поверхность, потому что превью могут содержать литералы и идентификаторы клиентов.
POST /api/admin/{reload,pause,resume,reconnect}AdminУправляющие операции администратора. Семантика та же, что и у admin-протокола через psql.

Роли доступа

Сервер на каждом запросе вычисляет одну из трёх ролей. Проверка работает на стороне сервера; SPA дублирует её на клиенте только для того, чтобы не показывать действия, недоступные текущему оператору.

РольКак запрос её получаетЧто роль даёт
AnonymousУчётных данных нет, [web].ui_anonymous = true.Публичные /api/* только для чтения из таблицы выше плюс /metrics. На пути с персональными данными и /api/admin/* возвращается 401.
SsoВалидный JWT в Authorization: Bearer, в cookie sso_access_token= или в query ?token=, который не попадает в группу администраторов.Все эндпоинты чтения, включая пути с персональными данными. На POST /api/admin/* отдаётся 403.
AdminЛибо корректная пара Basic из [general].admin_username / admin_password, либо валидный JWT, у которого значение [web].sso_groups_claim пересекается с [web].sso_admin_groups.Полный доступ, включая POST /api/admin/{reload,pause,resume,reconnect}.

Когда в одном запросе есть и Basic, и SSO-токен, приоритет у Basic. Корректный admin-пароль даёт Admin независимо от состояния SSO. Неверный Basic-пароль не блокирует SSO-ветку: SSO-источники всё равно проверяются, и валидный JWT даёт роль Sso (или Admin, если совпала группа администраторов). Это покрывает типичный случай: в localStorage лежит просроченный JWT рядом с рабочим Basic-паролем.

Basic-пароль сравнивается за постоянное время, чтобы по длительности сравнения нельзя было угадывать символы. JWT проверяются по публичному ключу из [web].sso_public_key_file; разобранный ключ кэшируется на время жизни процесса и перечитывается на RELOAD.

fetch-обёртка SPA шлёт Accept: application/json, и сервер на ней отдаёт чистый 401 без WWW-Authenticate: Basic. Без этого браузер закешировал бы то, что оператор ввёл в системном диалоге Basic, и подставлял этот пароль поверх формы входа React. Инструменты с Accept: */* (curl, gh) получают challenge как обычно.

401 Unauthorized отдаётся, когда учётных данных не пришло или ни один вариант не прошёл парсинг и валидацию. 403 Forbidden — когда данные валидны, но роли не хватает для пути; тело — {"error":"forbidden","message":"admin role required"}. SPA на 401 повторно открывает форму входа, на 403 показывает неблокирующий баннер «admin role required», не уводя на форму входа.

Настройка SSO

SSO опциональный. По умолчанию ([web].sso_enabled = false) сервер обслуживает только роли Anonymous и Admin через Basic. Чтобы подключить внешний SSO-прокси:

  1. Получите от SSO-провайдера публичный RSA-ключ, которым он подписывает JWT, и сохраните его в PEM-файле (например, /etc/pg_doorman/sso-public.pem). Для oauth2-proxy ключ извлекается из приватного: openssl rsa -in private.pem -pubout -out public.pem. Для Keycloak — см. Keycloak ниже.

  2. Добавьте SSO-поля в [web]:

    [web]
    enabled = true
    ui = true
    host = "127.0.0.1"
    port = 9127
    ui_anonymous = false
    
    sso_enabled = true
    sso_proxy_url = "https://sso.example.com/oauth2/start"
    sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
    sso_audience = ["pg_doorman"]
    sso_allowed_users = ["*"]
    
  3. Перечитайте конфиг: kill -SIGHUP <pid> или psql -h <host> -p 6432 -U admin -d pgbouncer -c 'RELOAD'.

  4. Проверьте: curl http://<host>:9127/api/auth/config должен вернуть "sso_enabled":true и заданный sso_proxy_url.

ПолеНазначениеПо умолчанию
sso_enabledВключает SSO-ветку. Без неё JWT не валидируются.false
sso_proxy_urlURL, на который SPA уводит браузер по кнопке «Sign in via SSO». Бэкенд этот URL сам не вызывает.null
sso_public_key_fileПуть к PEM-файлу с публичным RSA-ключом. Читается на старте и при RELOAD.null
sso_audienceДопустимые значения JWT claim aud. Токен принимается, если совпадает хотя бы одно. Обязательное поле при sso_enabled = true.[]
sso_allowed_usersСписок разрешённых значений JWT claims preferred_username или sub. ["*"] принимает любой валидный токен; явный список пропускает только перечисленные имена.["*"]
sso_groups_claimИмя JWT claim, в котором лежат группы пользователя. Читается вместе с sso_admin_groups."groups"
sso_admin_groupsГруппы, которые поднимают SSO-пользователя до Admin. Пустой список оставляет каждый SSO-логин на роли Sso только для чтения.[]
sso_require_httpsОтклонять Bearer/cookie/query SSO-учётные данные, пришедшие по обычному HTTP. Запрос считается защищённым только если TCP-пир входит в trusted_proxies и прокси прислал X-Forwarded-Proto: https. По умолчанию выключено, чтобы SSO продолжал работать в схеме «TLS терминирует прокси → pg_doorman слушает HTTP во внутренней сети».false
trusted_proxiesCIDR доверенных обратных прокси для X-Forwarded-For, Forwarded и X-Forwarded-Proto. При пустом списке pg_doorman игнорирует эти заголовки и берёт адрес прямого TCP-пира. Если sso_require_https = true работает за прокси, который завершает TLS, добавьте CIDR этого прокси, чтобы доверять X-Forwarded-Proto: https. См. Журнал доступа.[]

Поднятие SSO-пользователя до Admin через claim с группами

По умолчанию SSO-логин получает роль Sso — доступ только для чтения к логам и SQL-текстам, но без POST /api/admin/*. Чтобы операторы могли запускать управляющие операции администратора через SSO без раздачи Basic-пароля, настройте sso_groups_claim и sso_admin_groups:

[web]
sso_enabled = true
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"]
sso_groups_claim = "groups"
sso_admin_groups = ["pg-doorman-admins"]

Когда в валидном JWT приходит "groups": [..., "pg-doorman-admins"], запрос получает роль Admin. В access-логе это выглядит как auth_role=admin auth_source=sso, и SSO-админы по-прежнему отличимы от Basic-админов. /api/auth/config отдаёт sso_admin_groups_configured = true, и SPA убирает из формы входа обещание «SSO grants read-only access».

Keycloak

Keycloak подписывает каждый JWT RSA-ключом realm'а. Публичную часть этого ключа нужно один раз выгрузить в PEM-файл, который читает pg_doorman.

Без UI — через JWKS-эндпоинт realm'а:

REALM=https://kc.example.com/realms/operators
curl -s "$REALM/protocol/openid-connect/certs" \
  | jq -r '.keys[] | select(.alg=="RS256") | "-----BEGIN CERTIFICATE-----\n" + .x5c[0] + "\n-----END CERTIFICATE-----"' \
  | openssl x509 -pubkey -noout \
  > /etc/pg_doorman/sso-public.pem

Через админ-консоль: Realm settingsKeys → строка с Algorithm = RS256 и Use = SIGPublic key → скопированное base64-тело завернуть в PEM-файл с заголовками -----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY-----.

Секция [web] под Keycloak выглядит так:

[web]
sso_enabled = true
sso_proxy_url = "https://kc.example.com/realms/operators/protocol/openid-connect/auth"
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"]    # client_id, заданный в Keycloak
sso_groups_claim = "groups"      # значение по умолчанию для маппера «groups»
sso_admin_groups = ["pg-doorman-admins"]

Чтобы Admin через group claim работал, добавьте клиенту маппер Group Membership (Clients → нужный client → Mappers). Без этого маппера Keycloak выдаёт токены без groups, и каждый оператор остаётся в роли Sso.

После ротации ключа realm'а заново выгрузите PEM и сделайте RELOAD — pg_doorman подхватит новый ключ без рестарта.

Когда SSO-конфигурация сломана

Опечатка в SSO-секции не должна выводить операторскую консоль из строя. При sso_enabled = true, но не загружаемом рантайме (нет PEM-файла, пустой audience, нечитаемый PEM) сервер пишет причину в лог на уровне error, оставляет SSO выключенным на этот запуск и обслуживает только Basic и Anonymous. Та же причина видна в двух точках, чтобы оператор заметил поломку, а не тихий откат на Basic:

  • /api/auth/config.sso_config_error содержит человекочитаемое сообщение. SPA показывает баннер с этим текстом в форме входа.
  • Метрика pg_doorman_web_sso_config_error равна 1, пока SSO запрошен, но не загружен. В паре с pg_doorman_web_sso_enabled подходит для alert-правила.

Логин из браузера

При первом заходе SPA получает /api/auth/config и показывает форму входа. Если в ответе пришёл sso_proxy_url, рядом с Basic-формой появляется кнопка Sign in via SSO; иначе — только Basic.

Клик по Sign in via SSO уводит браузер на ${sso_proxy_url}?redirect_to=<текущий URL>. Внешний proxy выполняет OAuth/OIDC-обмен и возвращает браузер обратно с ?token=<jwt>. SPA сохраняет токен в localStorage, чистит URL от параметра и шлёт Authorization: Bearer <jwt> на каждом следующем запросе.

В нижней части боковой панели отображается имя текущего пользователя: admin для Basic или sso: <preferred_username> для SSO. Кнопка Sign out очищает в localStorage оба ключа (pgdoorman.admin-auth и pgdoorman.sso-token) и заново открывает форму входа.

Тихое обновление токена запускается раз в 60 секунд. Когда до exp остаётся меньше 90 секунд, SPA открывает скрытый iframe с URL ${origin}/?sso_silent=1. Внутри iframe запускается минимальный SilentCallback без обычных polling-эффектов. Он через window.postMessage отдаёт новый токен parent-окну. Если тихое обновление не сработало:

  • при наличии Basic-данных SPA удаляет SSO-токен без редиректа, и дальнейшие запросы идут под Basic;
  • иначе SPA уходит на полный редирект через SSO-proxy.

Срок жизни JWT задавайте не меньше 5 минут — более короткие токены успевают истечь до тихого обновления.

SPA cookie не шлёт (credentials: "omit" на каждом fetch). Путь с cookie sso_access_token существует для сайдкаров, curl и oauth2-proxy вариантов, которые кладут токен в cookie на общем домене.

Basic-пароль по умолчанию живёт только в памяти React и пропадает после полной перезагрузки страницы. Галочка Remember me on this device в форме входа сохраняет его в localStorage, поэтому консоль открывается без повторного ввода. Очистка хранилища сайта в браузере удаляет и Basic, и SSO-запись.

Журнал доступа

После каждого ответа (200/401/403/404/5xx, включая запросы к /metrics) консоль пишет одну logfmt-строку в канал pg_doorman::web::access:

INFO pg_doorman::web::access method=GET path=/api/admin/reload query=false status=200 bytes=42 latency_ms=12 peer=10.0.1.5:42312 auth_role=admin auth_source=basic auth_user=admin

Поля:

  • method, path — HTTP-метод и URL-путь. Тела запросов и ответов в лог не пишутся.
  • query=true|false — была ли в запросе строка параметров. Сама строка не логируется, чтобы JWT в ?token= не попадал в журнал.
  • status, bytes, latency_ms — статус ответа, размер тела и полная задержка ответа.
  • peer — адрес инициатора запроса. По умолчанию это непосредственный TCP-peer. Если он попадает в [web].trusted_proxies, сервер разбирает X-Forwarded-For (или Forwarded, RFC 7239), идёт по цепочке справа налево, пропускает доверенные адреса и берёт первый недоверенный. Недоверенный клиент не может подделать поле — если TCP-peer не в списке доверенных, заголовки прокси игнорируются.
  • auth_roleadmin, sso, anonymous или rejected.
  • auth_sourcebasic, sso или -.
  • auth_user — имя пользователя из учётных данных или - для анонимов и отклонённых запросов.

Уровни:

  • info — все управляющие действия (POST /api/admin/*), все чтения персональных данных (/api/logs, /api/prepared/text/*, /api/interner/top, /api/top/queries), все эндпоинты аутентификации и SSO (/api/auth/*, /api/sso/*) и все ответы с кодом, отличным от 2xx.
  • debug — любые остальные успешные чтения (2xx), анонимные или под авторизованной ролью. SPA опрашивает /api/overview, /api/pools, /api/clients, /api/process раз в 1,5–3 секунды; при старом правиле «любой 2xx с авторизацией = info» оператор на странице Logs видел собственные polling-запросы. Рутинные чтения логируются на debug, а RUST_LOG=info остаётся для управляющих действий, auth-трафика и ошибок.

Отдельный канал pg_doorman::web::access позволяет фильтровать поток журнала доступа независимо от остальных логов. В выпадающем фильтре на странице Logs этот канал включается или исключается одним кликом.

Реальный IP клиента за обратным прокси

По умолчанию peer фиксирует TCP-адрес, который соединился с сервером. За обратным прокси это адрес самого прокси. Чтобы видеть реальный IP клиента, добавьте CIDR прокси в [web].trusted_proxies:

[web]
trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"]

Распознаются и X-Forwarded-For, и Forwarded. Несколько доверенных переходов в цепочке пропускаются. X-Forwarded-For, пришедший от недоверенного клиента, игнорируется, поэтому через эту настройку произвольный вызывающий не может управлять полем access-лога.

Метрики

МетрикаТипЛейблыНазначение
pg_doorman_web_sso_enabledgauge1, когда SSO загружен успешно, иначе 0.
pg_doorman_web_sso_config_errorgauge1, когда sso_enabled = true, но рантайм не поднялся.
pg_doorman_web_auth_attempts_totalcounterrole, sourceПопытки авторизации в разрезе итоговой роли (admin/sso/anonymous/rejected) и источника (basic/sso/none).
pg_doorman_web_requests_totalcounterstatus_class, roleЗапросы к веб-консоли в разрезе HTTP-статуса (1xx5xx) и роли.
pg_doorman_web_sso_validation_errors_totalcounterreasonОтказы валидации JWT по причине: signature, expired, audience, no_username, allowlist.

Устойчивый рост signature означает, что SSO-прокси ротировал ключ, а sso_public_key_file остался старый. Рост allowlist — кто-то снаружи sso_allowed_users упорно пытается войти. Рост 4xx для роли sso обычно указывает на сломанный прокси перед pg_doorman.

Диагностика

401 на JWT, который должен быть валиден. Проверьте, что aud совпадает хотя бы с одним значением из sso_audience и exp ещё не истёк. PEM проверяется через openssl rsa -pubin -in <pem> -text -noout. Счётчик pg_doorman_web_sso_validation_errors_total{reason} показывает, какая именно проверка не прошла.

403 на JWT, который должен быть валиден. Путь требует роли Admin (например, POST /api/admin/reload). Войдите по Basic admin-паролю или добавьте группу пользователя в [web].sso_admin_groups и перечитайте конфиг.

SPA не показывает Sign in via SSO. /api/auth/config не возвращает sso_proxy_url. Либо [web].sso_enabled = false, либо sso_proxy_url не задан, либо рантайм не поднялся (ищите sso_config_error в том же ответе).

Тихое обновление токена не срабатывает. SSO-прокси должен возвращать свежий токен без полного экрана логина, когда iframe приходит с активной сессией. У oauth2-proxy это включается флагом --silent-refresh=true.

JWT в cookie игнорируется. Cookie должна попасть на pg_doorman с того же домена, и aud обязан входить в sso_audience. SPA сама cookie не шлёт; cookie-аутентификация рассчитана на curl, сайдкары и oauth2-proxy-варианты, которые проставляют токен в cookie на общем домене.

Страницы

В боковой панели восемь пунктов. Экран инцидента открывается из Обзора. Страницы с SQL-текстом и логами требуют роли Sso или Admin; анонимные пользователи их не видят.

Обзор (/overview)

Страница по умолчанию. Показывает состояние всего процесса, основные метрики, очереди, насыщение пулов, частые SQLSTATE и свёрнутый блок ресурсов. Если пул ушёл на резервный бэкенд через Patroni, сверху появляется баннер со списком затронутых пулов.

Пулы (/pools)

Таблица всех пулов user@database: размер, активные соединения, ожидание, p95, ошибки, насыщение и флаг резервного бэкенда. Строка открывает Детали пула.

Детали пула (/pools/:poolId)

Подробности одного пула: режим, лимиты, текущие соединения, TLS, резервный бэкенд, SQLSTATE, параметры запуска PostgreSQL и причины сработавших порогов. Здесь же находятся действия PAUSE, RESUME, RECONNECT и общий RELOAD.

Клиенты (/clients)

Таблица клиентов с фильтрами в URL:

/clients?pool=shop_checkout&state=waiting&user=app

Фильтры: pool, database, user, state, application_name, peer-адрес. Сортировка: запросы, ошибки, возраст соединения и возраст текущего запроса. Вместе со страницей Серверы помогает связать клиента с PostgreSQL pid.

Серверы (/servers)

Таблица бэкенд-соединений из SHOW SERVERS: server_id, process_id, база, пользователь, приложение, состояние, возраст активного запроса, счётчики запросов и ошибок, трафик и TLS. Используйте server_id из строки клиента, чтобы найти pid в pg_stat_activity.

Приложения (/apps)

Одна строка на каждый application_name: активные клиенты, qps, tps, суммарные запросы, транзакции, ошибки и err / 1k q.

Кеши (/caches)

Две вкладки: кеш подготовленных запросов по пулам и кеш SQL-текста в процессе. Обе могут показывать SQL, поэтому доступны только Sso и Admin.

Логи (/logs)

Лента LogTap с фильтрами в URL:

/logs?level=ERROR&q=53300

Пауза останавливает только отображение; серверный буфер продолжает заполняться. При [web].log_tap_max_entries = 0 страница показывает, что поток логов выключен. Доступ: Sso / Admin.

Конфиг и состояние (/config)

Зеркало команд SHOW CONFIG, SHOW DATABASES, SHOW USERS, SHOW AUTH_QUERY, SHOW LOG_LEVEL, SHOW STARTUP_PARAMETERS, SHOW SOCKETS, SHOW POOL_SCALING, SHOW POOL_COORDINATOR. Страница помогает проверить текущую конфигурацию и увидеть, какие ключи применяются на RELOAD, а какие требуют рестарта. Кнопка Reload config доступна только Admin.

Экран инцидента (/wall)

Версия Обзора для большого экрана: карта насыщения пулов, крупные метрики и последние управляющие действия. По Esc возвращает на /overview.

Управляющие действия

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

ДействиеОбластьГде найтиПодтверждение
RELOADвсе пулыКонфиг и состояние · Детали пулаRELOAD
PAUSEодин user@databaseДетали пулаимя базы
RESUMEодин user@databaseДетали пула, когда пул паузированимя базы
RECONNECTодин user@databaseДетали пулаимя базы

Семантика та же, что в admin-протоколе через psql. PAUSE прекращает выдачу бэкендов в нужном пуле; уже идущие транзакции продолжают выполняться. RESUME снова разрешает выдачу. RECONNECT закрывает idle-бэкенды и отказывает активным при возврате. RELOAD перечитывает pg_doorman.toml; размер пула уменьшается по мере освобождения соединений.

Подтверждение вводом защищает от случайного RELOAD или PAUSE не того пула. Результат показывается уведомлением, а само действие пишется в access-лог и список последних событий администратора.

Горячие клавиши

Сочетания работают вне текстовых полей.

СочетаниеДействие
⌘ K / Ctrl KПоиск по страницам и пулам.
?Список горячих клавиш.
EscЗакрыть подсказку или окно. На /wall возвращает на Обзор.

Тема

Внизу боковой панели есть переключатель Light / System / Dark. По умолчанию используется Light. Выбор хранится в localStorage.

Встроенная справка

Рядом с заголовками метрик и секций есть значок (i). Подсказка объясняет, что означает число, откуда оно взято, как считается и какие пороги считаются нормой.

Сборка из исходников

Собранный веб-интерфейс лежит в git по пути frontend/dist/, чтобы RPM-, DEB- и Docker-сборки не зависели от Node.js. Разработчикам, правящим веб-интерфейс, нужно пересобирать его перед коммитом:

cd frontend
npm ci
npm run install-hooks   # одноразово: ставит pre-commit hook для синхронизации dist
npm run lint
npm run typecheck
npm run build

npm run install-hooks опционален. CI его не требует: проверка .github/workflows/frontend.yml запускает npm run check-dist и блокирует слияние, если исходники меняли без пересборки dist/. Она же запускает lint и typecheck на каждом PR, который трогает frontend/.

Развёртывание

/metrics доступен без авторизации на том же HTTP-сервере, что и консоль. Так задумано: иначе сломались бы существующие настройки Prometheus. Авторизация на /api/* не распространяется на /metrics — метрики раскрывают имена пулов, пользователей и БД, давление на пул, состояние auth_query и форму нагрузки. Либо держите секцию [web] на приватном host:port, доступном только системе сбора метрик, либо ставьте перед HTTP-сервером прокси, который добавляет авторизацию на /metrics отдельно.

Структурированное 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 обратно.

Оговорки

  • Для промышленной эксплуатации выбирайте text (терминалы, syslog) или structured (log shippers). debug зарезервирован под будущее использование и сейчас равен text.
  • file и line берутся из мест вызова макроса log. Они доступны в release-сборках, потому что pg_doorman поставляется с включённой отладочной информацией.
  • Логгер не включает trace-идентификаторы и корреляцию запросов. Для трассировки на запрос используйте SHOW CLIENTS и Prometheus-метрики.

Куда дальше

Перцентили задержек

Переход на гистограммы. Метрики pg_doorman_pools_queries_percentile, pg_doorman_pools_transactions_percentile и pg_doorman_pools_avg_wait_time устарели и будут удалены в 3.10. Для новых запросов PromQL используйте гистограммы:

  • pg_doorman_pools_query_duration_seconds
  • pg_doorman_pools_transaction_duration_seconds
  • pg_doorman_pools_wait_duration_seconds

Перцентили считайте через histogram_quantile(q, sum by (le, ...) (rate(_bucket[5m]))). Такой запрос можно агрегировать между репликами; усреднение заранее посчитанных перцентилей корректного результата не даёт.

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. См. Координатор пулов.

Один медленный пользователь

user "fast_app":   queries p99=12   xacts p99=35
user "report_job": queries p99=4500 xacts p99=8000

report_job тянет общую базу вниз. С включённым координатором пулов медленные транзакции report_job приводят к тому, что под давлением он первым отдаёт свои соединения (вытеснение смещено по p95-времени транзакций). Без координатора выделите 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 — диагностические рецепты, когда перцентили выглядят неправильно.
  • Бенчмарки — эталонные распределения перцентилей под нагрузкой.

Настройки

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

pg_doorman поддерживает два формата конфигурационного файла:

  • YAML (.yaml, .yml) — основной и рекомендуемый формат для новых конфигураций.
  • TOML (.toml) — поддерживается для обратной совместимости с уже существующими конфигурациями.

Формат определяется автоматически по расширению файла. Оба формата поддерживают одни и те же опции конфигурации и могут использоваться взаимозаменяемо.

Пример конфигурации YAML (рекомендуется)

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

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

Пример конфигурации TOML (для совместимости)

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

[pools.mydb]
server_host = "localhost"
server_port = 5432
pool_mode = "transaction"

[[pools.mydb.users]]
username = "myuser"
password = "md5..."  # хеш из pg_shadow / pg_authid
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_* и worker_stack_size, max_blocking_threads опциональны: на современных версиях tokio дефолты разумны, и их обычно не нужно трогать.

По умолчанию: not set (uses tokio's default).

tokio_event_interval

Настройки Tokio runtime. Задаёт, как часто шедулер проверяет внешние события (I/O, таймеры).

По умолчанию: not set (uses tokio's default).

worker_stack_size

Настройки Tokio runtime. Размер стека для worker-потоков.

По умолчанию: not set (uses tokio's default).

max_blocking_threads

Настройки Tokio runtime. Максимальное число потоков для блокирующих операций.

По умолчанию: 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%, чтобы избежать лавины одновременных закрытий. Установите 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, чтобы отключить проверку (не рекомендуется для промышленной эксплуатации с возможной сетевой нестабильностью или рестартами PostgreSQL).

По умолчанию: 60s (60 seconds).

server_round_robin

Задаёт, какое idle-серверное соединение выбирается для следующей транзакции. false (MRU, LIFO): берётся последнее возвращённое в пул соединение. Горячих соединений меньше, локальность shared buffer в PostgreSQL выше. true (Round Robin): равномерная ротация по всем idle-соединениям. Аналог server_round_robin из PgBouncer.

По умолчанию: false.

sync_server_parameters

В транзакционном режиме разные транзакции одного клиента могут выполняться на разных серверных соединениях. Если включить sync_server_parameters, pg_doorman перед началом транзакции применяет к выбранному серверному соединению параметры сессии, которые запросил клиент.

Есть два источника таких параметров:

  1. Сообщения PostgreSQL ParameterStatus: client_encoding, DateStyle, IntervalStyle, TimeZone, standard_conforming_strings, application_name. PostgreSQL сам сообщает об изменении этих параметров по протоколу.

  2. Параметры из клиентского StartupMessage (начиная с pg_doorman 3.10): любой безопасный параметр, который клиент прислал при подключении. pg_doorman отбрасывает служебные имена и имена только для чтения (is_superuser, server_version, lc_collate, transaction_isolation, ...) и префикс _pq_.. Так клиент может задать search_path, default_transaction_isolation, role и другие параметры, важные для планировщика, один раз при подключении. После этого pg_doorman применит их к любому серверному соединению, на котором окажется транзакция. Значения из операторской настройки startup_parameters всегда имеют приоритет над клиентским пакетом.

Ограничения, которые важно учитывать:

  • pg_doorman отслеживает только параметры, заданные при подключении, и параметры, о которых PostgreSQL сам присылает ParameterStatus. Если клиент после подключения выполнит SET search_path = ... или изменит другой параметр планировщика, о котором PostgreSQL не сообщает, pg_doorman не узнает об этом изменении. В таком случае повторное использование prepared statement может попасть на план, построенный при старом состоянии. Для таких клиентов лучше задавать параметры в StartupMessage, после изменения выполнять DISCARD ALL или переподключаться, либо отключить prepared_statements для пула.

  • В ключ кэша prepared statement входят текст запроса, OID параметров и отпечаток только этих параметров планировщика из StartupMessage: search_path, default_transaction_isolation, default_transaction_read_only, default_text_search_config, role. Другие параметры, которые тоже могут влиять на план (TimeZone, DateStyle, plan_cache_mode, enable_*, настройки стоимости JIT, параметры расширений), в ключ не входят. Если один и тот же запрос готовится под разными значениями таких параметров, отключите prepared_statements для пула или закрепите эти параметры на уровне роли или базы.

Когда состояние серверного соединения отличается от состояния клиента, pg_doorman делает дополнительный обмен с PostgreSQL для SET или RESET. Если вам нужна только видимость 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.

tcp_socket_buffer_size

Лимиты буферов ядра SO_RCVBUF и SO_SNDBUF для принятых клиентских TCP-сокетов и исходящих TCP-сокетов к PostgreSQL.

При значении по умолчанию 0 pg_doorman не вызывает setsockopt(SO_RCVBUF/SO_SNDBUF), поэтому буферами управляет автонастройка TCP в Linux. Буфер приёма каждого соединения может расти по требованию до net.ipv4.tcp_rmem[2] (обычно 6 MiB в Ubuntu/RHEL). Эта память не входит в RSS процесса; в зависимости от ядра и режима cgroup она может отображаться отдельно как socket memory, например как sock в cgroup v2 memory.stat, или быть заметна в основном как память ядра на хосте. Если MemFree заметно растёт после рестарта pg_doorman, проверьте источник через ss -m, /proc/net/sockstat, cgroup v2 memory.current и ключ sock в memory.stat.

Ненулевое значение вызывает setsockopt(SO_RCVBUF/SO_SNDBUF) один раз для каждого настраиваемого TCP-сокета. Это отключает автонастройку для этого сокета и задаёт фиксированные лимиты буферов отправки и приёма. Linux внутренне удваивает запрошенные значения — см. man 7 socket — и может ограничить их через net.core.rmem_max / net.core.wmem_max. Перед настройкой значений выше системного дефолта проверьте /proc/sys/net/core/rmem_max и /proc/sys/net/core/wmem_max. Применённые ядром значения видны через getsockopt, ss -m и DEBUG-логи pg_doorman.

Примерная верхняя граница в Linux: 4 * tcp_socket_buffer_size * число_TCP_сокетов. Pg_doorman задаёт буферы отправки и приёма, а Linux внутренне удваивает каждое значение. Например, tcp_socket_buffer_size = 65536 даёт около 256 KiB лимитов на один TCP-сокет, то есть примерно 15 MiB на 60 TCP-сокетов без учёта накладных расходов sk_buff. Считайте и клиентские TCP-сокеты, и TCP-сокеты к PostgreSQL. Фактически занятая память всё равно зависит от данных в очередях.

Этот параметр в первую очередь ограничивает память. Стартовый диапазон для OLTP внутри одного датацентра: 64 KiB – 256 KiB. Не ставьте меньше 64 KiB без замеров. Для WAN, межзонального трафика, больших результатов запроса и массовой передачи данных может понадобиться большее значение или поведение по умолчанию с автонастройкой TCP.

Значение применяется, когда pg_doorman настраивает TCP-сокет: к новым клиентским подключениям, новым TCP-соединениям к PostgreSQL и клиентским сокетам, восстановленным в новом процессе через SCM_RIGHTS. SIGHUP не пересматривает уже открытые сокеты. Чтобы применить новое значение к существующим сессиям, используйте горячую замену процесса для мигрирующих клиентов, RECONNECT/дренирование пулов для backend-сокетов или обычный рестарт, если переподключения допустимы.

Это аналог параметра tcp_socket_buffer в PgBouncer. В Odyssey и PgCat аналога нет, они наследуют поведение автонастройки TCP в Linux.

По умолчанию: 0.

unix_socket_buffer_size

Размер буфера для операций чтения и записи при подключении к PostgreSQL через unix-сокет.

По умолчанию: 1048576.

admin_username

Имя пользователя для виртуальной admin-базы.

По умолчанию: "admin".

admin_password

Пароль для виртуальной admin-базы. Замените на свой секрет.

По умолчанию: "admin".

prepared_statements

Включает подмену и кеширование prepared statements. Когда параметр выключен, pg_doorman передаёт Parse и Bind без переписывания через пуловый кеш prepared statements.

Если значение true, prepared_statements_cache_size должен быть больше 0.

По умолчанию: true.

prepared_statements_cache_size

Размер кеша prepared statements на уровне пула (общий для всех клиентов, подключающихся к одному пулу). Кеш хранит соответствие между хешем запроса и переписанным именем prepared statement.

Это не выключатель. Чтобы отключить подмену prepared statements, задайте prepared_statements: false; pg_doorman отклоняет общее значение prepared_statements_cache_size = 0, пока prepared_statements включён.

Полная картина того, как этот параметр взаимодействует с server_prepared_statements_cache_size, client_anonymous_prepared_cache_size и query interner — в туториале Кеширование анонимных prepared statements.

По умолчанию: 8192.

server_prepared_statements_cache_size

Размер LRU DOORMAN_<N> на каждое серверное соединение, независимо от кеша уровня пула. Если параметр не задан, наследуется итоговый prepared_statements_cache_size для пула: сначала проверяется per-pool override, затем общее значение. Per-pool override этого параметра имеет приоритет над общим.

Уменьшайте значение ниже размера пула, когда бэкенды накапливают слишком много DOORMAN_<N> (pg_prepared_statements упирается в лимит, плановая память растёт) или когда нужна более быстрая утилизация через Close без уменьшения hit rate на уровне пула. При prepared_statements: false принудительно равен 0.

По умолчанию: not set (наследует prepared_statements_cache_size).

client_anonymous_prepared_cache_size

Ограничивает Anonymous-часть per-client кеша prepared statements. Анонимные statements отправляются без явного имени и обычно короткоживущие; LRU задаёт верхнюю границу того, сколько таких записей один клиент может накопить, прежде чем будет выселена самая старая.

Если параметр не задан, наследуется итоговый prepared_statements_cache_size для пула. Установите 0, чтобы отключить LRU и хранить Anonymous в неограниченной хеш-таблице; задайте число, чтобы ограничить кеш на каждого клиента независимо от размера пула.

Named-часть per-client кеша (statements, созданные с явным именем через PREPARE или extended-query Parse) всегда без ограничения — этот параметр на неё не влияет. Named statements живут в кеше до закрытия клиентского соединения.

По умолчанию: not set (наследует prepared_statements_cache_size).

query_interner_gc_interval_seconds

Интернер запросов запускает двухцикловый mark-and-sweep сборщик. Named-записи вытесняются, когда вне интернера никто не держит Arc<str>; анонимные — когда простаивают дольше query_interner_anon_idle_ttl_seconds.

Этот параметр задаёт, как часто запускается коллектор. Реальный sweep тикает gc_interval / 4, поэтому помеченная на цикле N запись имеет примерно четверть интервала, чтобы её прочитали (и сняли пометку), пока цикл N+1 её не вытеснит.

Меньшие значения быстрее уменьшают интернер после волн отключений ценой большей нагрузки на CPU. Значение 0 отклоняется при старте.

Restart-only: изменения подхватываются только после рестарта; config reload не меняет частоту проходов работающего процесса.

По умолчанию: 60.

query_interner_anon_idle_ttl_seconds

Ограничивает верхнюю границу памяти, которую pg_doorman тратит на хранение SQL-текста анонимного prepared statement после последнего Bind или Parse, ссылающегося на тот же hash. Когда анонимная запись простаивает дольше указанных секунд, она помечается и вытесняется на следующем проходе, который всё ещё видит её бездействующей.

Значение 0 полностью отключает TTL: анонимные записи живут до перезапуска процесса. Это соответствует поведению до 3.7 и нужно для старых развёртываний, которые опираются на кросс-batch unnamed prepared statements; в остальных случаях оставляйте значение по умолчанию.

Live-reloadable: перечитывается на каждом проходе, поэтому config reload меняет эффективный TTL без рестарта.

По умолчанию: 60.

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-пуле, независимо инициируют подключение к PostgreSQL, создавая лавину подключений под нагрузкой. С лимитом одновременно выполняется не больше указанного числа 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-сетей в нотации CIDR, с которых разрешено подключение к pg_doorman. Для тонкого контроля доступа per-database и per-user используйте pg_hba (см. ниже).

По умолчанию: [].

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 не совпал.

startup_parameters

Базовые параметры PostgreSQL, которые pg_doorman добавляет в StartupMessage каждого нового бэкенда. startup_parameters уровня пула переопределяют эти значения по ключу, а passthrough auth_query может переопределить их для конкретного пользователя.

При загрузке конфигурации pg_doorman проверяет зарезервированные протокольные ключи (user, database, replication, options, _pq_.*), имена GUC, нулевые байты и размер этого уровня. Перед каждым запуском бэкенда объединённый набор параметров снова проверяется по лимиту MAX_STARTUP_PACKET_LENGTH PostgreSQL (10 000 байт). Любое переполнение отклоняет запуск бэкенда с SQLSTATE 53400 (configuration_limit_exceeded), вместо отправки частичного или пустого StartupMessage.

Если PostgreSQL отвергает параметр при запуске бэкенда, pg_doorman возвращает клиенту ErrorResponse PostgreSQL без изменений: повторной попытки без этого ключа не будет, сам ключ автоматически не отключается. Накопительный счётчик отказов экспортируется как pg_doorman_backend_startup_parameter_errors_total{pool, sqlstate}; имя параметра и пользователя остаются в строке лога уровня warn. Итоговые параметры по каждому пулу видны через SHOW STARTUP_PARAMETERS в административной SQL-консоли и через /api/pools в веб-интерфейсе.

По умолчанию: {}.

pooler_check_query

Когда клиент отправляет ровно этот запрос как SimpleQuery, pg_doorman обслуживает его через кеш ответа на уровне пула. Первая совпадающая проба за время жизни каждого пула отправляется в PostgreSQL, и полный ответ сохраняется. Последующие совпадающие пробы отвечаются из кеша без обращения к бэкенду.

Кеш индексируется по строке запроса. RELOAD с другим значением pooler_check_query инвалидирует кеш на следующей пробе; новое значение вызывает одну свежую пробу к бэкенду и затем обслуживается из кеша, пока значение снова не изменится. RELOAD с тем же значением не сбрасывает кеш. ErrorResponse от бэкенда передаётся клиенту без изменений и не кешируется, поэтому следующая проба снова идёт в PostgreSQL.

Поведение на холодном пуле изменилось: первая проба в каждом пуле теперь делает один round-trip к PostgreSQL даже для значения по умолчанию ;. Если PostgreSQL в этот момент недоступен, клиент-пробер увидит ошибку пробы вместо безусловного OK. Прежний хардкод локального ответа сообщал, что пулер здоров, даже когда PostgreSQL лежал, и для непустых значений вроде select 1 возвращал пустой ответ.

Контракт для оператора. Запрос должен быть детерминированным: один и тот же ввод обязан давать один и тот же набор байт, без побочных эффектов. Безопасные значения: ;, select 1, select 'pg_doorman', select version().

Небезопасные значения, которые кеш молча заморозит:

  • select now(), select clock_timestamp() — закешированный timestamp перестанет идти вперёд.
  • select pg_is_in_recovery() — failover поменяет роль на PostgreSQL, но закешированный ответ всё ещё будет показывать прежнюю роль.
  • select count(*) from <table> — закешированное число останется тем, что увидела первая проба.
  • UPDATE, INSERT, DELETE, CALL, DO — побочный эффект выполнится один раз, а ответ об успехе закешируется навсегда.

Доля попаданий в кеш экспортируется двумя счётчиками без меток: pg_doorman_pooler_check_query_backend_total (пробы, отправленные в PostgreSQL) и pg_doorman_pooler_check_query_cache_total (пробы, обслуженные из кеша). Отношение cache_total / (cache_total + backend_total) — это hit rate.

По умолчанию: ";".

Настройки пула

Каждая запись в 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 отправляет: 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).

startup_parameters

Параметры PostgreSQL уровня пула, которые pg_doorman добавляет в StartupMessage каждого нового бэкенда. Значения переопределяют general.startup_parameters по ключу. В passthrough-пулах auth_query колонка startup_parameters может переопределить и этот уровень для конкретного пользователя.

При загрузке конфигурации pg_doorman проверяет зарезервированные протокольные ключи, имена GUC, нулевые байты и размер этого уровня. Перед каждым запуском бэкенда объединённый набор параметров снова проверяется по лимиту MAX_STARTUP_PACKET_LENGTH PostgreSQL; если он не помещается, pg_doorman отклоняет запуск бэкенда с SQLSTATE 53400 (configuration_limit_exceeded) вместо отправки частичного или пустого StartupMessage.

По умолчанию: {}.

Настройки 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-хеш. Если запрос возвращает ровно одну колонку, pg_doorman использует её независимо от имени.

Дополнительные колонки игнорируются, кроме необязательной startup_parameters типа text. В passthrough-режиме pg_doorman читает её как JSON-объект с параметрами запуска PostgreSQL для этого пользователя. Dedicated-режим игнорирует эту колонку и пишет предупреждение. В качестве плейсхолдера для имени пользователя используйте $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-метрики на HTTP-сервере из секции [web]. Включите /metrics через [web]; таблицы ниже показывают, какие состояния пулера отражают метрики.

Включение HTTP-сервера

/metrics и встроенная веб-консоль обслуживаются одним и тем же HTTP-сервером [web]. Старые ключи prometheus.* пока принимаются как алиасы web.*, но новые конфиги должны использовать [web].

web:
  enabled: true     # включить HTTP-сервер для /metrics
  host: "0.0.0.0"
  port: 9127
  # Веб-консоль по умолчанию выключена; см. руководство по веб-консоли
  ui: false
  ui_anonymous: false

Опции конфигурации

Полное описание веб-консоли — в Веб-консоли. Минимум для /metrics:

ОпцияОписаниеПо умолчанию
enabledВключить или отключить HTTP-сервер с /metrics.false
hostАдрес, на котором HTTP-сервер принимает соединения."0.0.0.0"
portПорт HTTP-сервера.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_connections_totalНакопительный счётчик принятых клиентских соединений по типу: plain (без TLS), tls, cancel (запрос отмены), total (сумма). Для темпа подключений используйте rate(pg_doorman_connections_total[5m]).
pg_doorman_connection_countУстаревшая gauge-версия pg_doorman_connections_total; будет удалена в 3.10. Новые правила и панели должны использовать pg_doorman_connections_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_totalНакопительный счётчик байт, переданных через пулы соединений, по направлению (received/sent), пользователю и базе. Для пропускной способности используйте rate(pg_doorman_pools_bytes_total[5m]).
pg_doorman_pools_bytesУстаревшая gauge-версия pg_doorman_pools_bytes_total; будет удалена в 3.10.
pg_doorman_pool_sizeСконфигурированный максимальный размер пула на пользователя и базу. Полезен для расчёта оставшейся ёмкости пула вместе с pg_doorman_pools_servers.
pg_doorman_backend_startup_parameter_errors_totalНакопительный счётчик запусков бэкенда, которые PostgreSQL отклонил из-за startup_parameters. Лейблы: пул и SQLSTATE. Отклонённый параметр и имя пользователя пишутся в строку лога уровня warn, а не в лейблы метрики.
pg_doorman_startup_parameters_dropped_totalНакопительный счётчик событий, когда pg_doorman отбросил startup_parameters до отправки StartupMessage. Лейблы: пул и причина (cascade_budget_exceeded, packet_cap_exceeded, auth_query_oversize, auth_query_overlay_oversize, auth_query_bad_type, auth_query_invalid_json, auth_query_invalid_shape, auth_query_invalid_entry, dedicated_mode).

Метрики запросов и транзакций

МетрикаОписание
pg_doorman_pools_query_duration_secondsГистограмма времени выполнения запросов на стороне PostgreSQL по пулу, в секундах. Квантили считайте через histogram_quantile(q, sum by (le, user, database) (rate(pg_doorman_pools_query_duration_seconds_bucket[5m]))); QPS — через rate(..._count[5m]).
pg_doorman_pools_transaction_duration_secondsГистограмма полного времени транзакций по пулу, в секундах. Агрегируется так же, как pg_doorman_pools_query_duration_seconds.
pg_doorman_pools_wait_duration_secondsГистограмма времени ожидания выдачи backend-соединения клиенту, в секундах. Для p99 используйте histogram_quantile(0.99, ...).
pg_doorman_pools_transactions_totalНакопительный счётчик транзакций по пулу. Для TPS используйте rate(pg_doorman_pools_transactions_total[5m]).
pg_doorman_pools_queries_percentileУстаревшая метрика; будет удалена в 3.10. Это заранее посчитанные перцентили, которые нельзя корректно суммировать между репликами. Используйте pg_doorman_pools_query_duration_seconds_bucket и histogram_quantile().
pg_doorman_pools_transactions_percentileУстаревшая метрика; будет удалена в 3.10. Используйте pg_doorman_pools_transaction_duration_seconds.
pg_doorman_pools_transactions_countУстаревшая gauge-версия pg_doorman_pools_transactions_total; будет удалена в 3.10.
pg_doorman_pools_transactions_total_timeСумма времени выполнения транзакций в пулах соединений, по пользователю и базе. В миллисекундах.
pg_doorman_pools_queries_totalНакопительный счётчик запросов по пулу. Для QPS используйте rate(pg_doorman_pools_queries_total[5m]).
pg_doorman_pools_queries_countУстаревшая gauge-версия pg_doorman_pools_queries_total; будет удалена в 3.10.
pg_doorman_pools_queries_total_timeСумма времени выполнения запросов в пулах соединений, по пользователю и базе. В миллисекундах.
pg_doorman_pools_avg_wait_timeУстаревшая метрика; будет удалена в 3.10. Это среднее значение, которое сглаживает пики хвостовой задержки. Используйте pg_doorman_pools_wait_duration_seconds_bucket и histogram_quantile().

Метрики auth_query

Эти метрики доступны только когда auth_query сконфигурирован для одного или нескольких пулов.

МетрикаОписание
pg_doorman_auth_query_cache_totalНакопительные события кеша auth_query по типу (hits/misses/refetches/rate_limited) и базе. Текущее число записей остаётся в pg_doorman_auth_query_cache{type="entries"}.
pg_doorman_auth_query_auth_totalНакопительный счётчик результатов auth_query-аутентификации по result (success/failure) и базе.
pg_doorman_auth_query_executor_totalНакопительный счётчик событий исполнителя auth_query по типу (queries/errors) и базе.
pg_doorman_auth_query_dynamic_pools_totalНакопительный счётчик событий жизненного цикла динамических пулов auth_query по типу (created/destroyed) и базе. Текущее число пулов остаётся в pg_doorman_auth_query_dynamic_pools{type="current"}.
pg_doorman_auth_query_cacheТекущее число закешированных учётных данных (type="entries"). Накопительные значения в этой метрике устарели; используйте pg_doorman_auth_query_cache_total.
pg_doorman_auth_query_authУстаревшая gauge-версия pg_doorman_auth_query_auth_total; будет удалена в 3.10.
pg_doorman_auth_query_executorУстаревшая gauge-версия pg_doorman_auth_query_executor_total; будет удалена в 3.10.
pg_doorman_auth_query_dynamic_poolsМетрики жизненного цикла динамических пулов auth_query по типу и базе. Типы: current (сейчас активные динамические пулы), created (всего создано с момента старта), destroyed (всего удалено сборщиком или при RELOAD). Имеет смысл только в passthrough mode.

Метрики серверов

МетрикаОписание
pg_doorman_servers_prepared_hitsТекущая сумма попаданий в кеш prepared statements по активным бэкендам пула, с лейблами user и database. Gauge может уменьшаться при ротации бэкендов; для rate() используйте pg_doorman_servers_prepared_hits_total.
pg_doorman_servers_prepared_missesТекущая сумма промахов prepared statements по активным бэкендам пула, с лейблами user и database. Gauge может уменьшаться при ротации бэкендов; для rate() используйте pg_doorman_servers_prepared_misses_total.
pg_doorman_servers_prepared_hits_totalНакопительный счётчик попаданий в кеш prepared statements по всем бэкендам пула, с лейблами user и database. Используйте rate() для скорости попаданий.
pg_doorman_servers_prepared_misses_totalНакопительный счётчик промахов prepared statements по всем бэкендам пула, с лейблами user и database. Устойчивая ненулевая скорость означает, что запросы часто готовятся заново или кеш server_prepared_statements_cache_size слишком мал.

Метрики клиентского кеша prepared statements

Клиентский кеш prepared statements делится на неограниченную Named-таблицу и Anonymous LRU, ограниченный client_anonymous_prepared_cache_size. Если этот параметр не задан, используется итоговый prepared_statements_cache_size.

МетрикаОписание
pg_doorman_clients_prepared_named_entriesGauge с лейблами user и database. Сумма Named-записей по кешам всех подключённых клиентов. Named-записи не имеют верхнего лимита и живут до отключения клиента или DEALLOCATE. Устойчивый рост часто означает, что драйвер или ORM создаёт новые имена statement на каждый запрос.
pg_doorman_clients_prepared_anonymous_entriesGauge с лейблами user и database. Сумма Anonymous-записей по кешам всех подключённых клиентов. Anonymous-часть каждого клиента ограничена client_anonymous_prepared_cache_size, поэтому значение приближается максимум к connected_clients * cache_size.
pg_doorman_clients_prepared_anonymous_evictions_totalНакопительный счётчик вытеснений из Anonymous LRU, с лейблами user и database. Устойчивая ненулевая скорость означает, что client_anonymous_prepared_cache_size мал для нагрузки и LRU вытесняет записи быстрее, чем приложение успевает их повторно использовать.

Метрики query interner

Query interner общий для процесса. У этих метрик нет лейблов пула, пользователя или базы; для привязки к нагрузке смотрите метрики prepared statements выше и WARN-строки о synthetic miss.

МетрикаОписание
pg_doorman_query_interner_entriesGauge по kind (named или anonymous). Число интернированных текстов запросов. Обновляется один раз за проход GC.
pg_doorman_query_interner_bytesGauge по kind (named или anonymous). Суммарный объём интернированных текстов запросов в байтах. Обновляется один раз за проход GC.
pg_doorman_query_interner_evictions_totalCounter по kind и reason (gc_passive или ttl_expired). Named-записи удаляются, когда их больше не держит ни один кеш вне interner; anonymous-записи удаляются после idle TTL.
pg_doorman_query_interner_synthetic_misses_totalCounter синтетических ответов SQLSTATE 26000 для anonymous prepared statements, состояние которых уже недоступно при последующем Bind или Describe. Перед увеличением query_interner_anon_idle_ttl_seconds проверьте вытеснения из клиентского Anonymous LRU, WARN-логи, RESET INTERNER и TTL-вытеснения.
pg_doorman_query_interner_gc_duration_secondsГистограмма времени одного прохода GC interner (named и anonymous вместе), в секундах. Помогает увидеть, когда большой interner делает обход заметным.
pg_doorman_pooler_check_query_backend_totalCounter пробов pooler_check_query, отправленных в PostgreSQL (промах кеша или повторная проба после RELOAD). После прогрева значение должно быть стабильным; постоянно растущий rate означает, что популовый кеш не удерживает запись.
pg_doorman_pooler_check_query_cache_totalCounter пробов pooler_check_query, обслуженных из популового кеша ответа без обращения к бэкенду. Hit rate = cache_total / (cache_total + backend_total).

Метрики серверного TLS

Активны, если включён TLS к PostgreSQL (server_tls_mode != "disable").

МетрикаТипОписание
pg_doorman_server_tls_connectionsgauge по пулуЧисло активных TLS-соединений к PostgreSQL.
pg_doorman_server_tls_handshake_duration_secondshistogram по пулуРаспределение длительности TLS handshake.
pg_doorman_server_tls_handshake_errors_totalcounter по пулуСчётчик неуспешных TLS handshake. Алертить при ненулевой скорости.

Подробнее — см. Клиентский и серверный TLS.

Дашборд Grafana

Базовый набор панелей для дашборда:

  1. Число соединений по типу
  2. Использование памяти во времени
  3. Число клиентов и серверов по пулам
  4. Перцентили запросов и транзакций
  5. Сетевой трафик по пулам

Примеры запросов

Несколько примеров запросов Prometheus, которые могут быть полезны:

Темп подключений

rate(pg_doorman_connections_total{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"})

Медленные запросы (p99)

histogram_quantile(0.99, sum by (le, user, database) (rate(pg_doorman_pools_query_duration_seconds_bucket[5m])))

Время ожидания клиентов (p99)

histogram_quantile(0.99, sum by (le, user, database) (rate(pg_doorman_pools_wait_duration_seconds_bucket[5m])))

Hit rate кеша auth_query

rate(pg_doorman_auth_query_cache_total{type="hits"}[5m]) / clamp_min(rate(pg_doorman_auth_query_cache_total{type="hits"}[5m]) + rate(pg_doorman_auth_query_cache_total{type="misses"}[5m]), 0.001)

Темп ошибок auth_query

rate(pg_doorman_auth_query_auth_total{result="failure"}[5m])

Бенчмарки

Русская версия страницы с бенчмарками пока не ведётся. Чтобы не публиковать англоязычный текст под русской навигацией, здесь оставлена ссылка на оригинал.

Открыть бенчмарки на английском

История изменений

Русская версия changelog пока не ведётся. Чтобы не публиковать англоязычный текст под русской навигацией, здесь оставлена ссылка на оригинал.

Открыть changelog на английском

Вклад в 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!