PgDoorman
Многопоточный пулер соединений для PostgreSQL, написанный на Rust. Drop-in замена для PgBouncer, Odyssey и PgCat. В production в Ozon уже больше трёх лет под нагрузками Go (pgx), .NET (Npgsql), Python (asyncpg, SQLAlchemy) и Node.js.
Скачать PgDoorman 3.6.0 · Сравнение · Benchmarks
Что отличает PgDoorman
Три вещи, которых вы не найдёте в PgBouncer и Odyssey.
Ограничение числа соединений на уровне базы с приоритетным вытеснением. max_db_connections задаёт суммарное число backend-соединений к одной базе; когда лимит исчерпан, idle-соединения вытесняются у пользователей с наибольшим избытком, ранжируя их по p95 времени транзакции — самые медленные пулы отдают соединения первыми. Резервный пул поглощает короткие всплески. Per-user min_guaranteed_pool_size защищает критичные нагрузки.
В PgBouncer max_db_connections есть, но без вытеснения и без честности распределения. В Odyssey аналога нет.
Когда PgDoorman работает рядом с PostgreSQL на одной машине и switchover Patroni убивает локальный бэкенд, PgDoorman опрашивает Patroni REST API эндпоинт /cluster, выбирает живого члена кластера (предпочтение отдаётся sync_standby) и направляет новые соединения туда за 1–2 TCP round trips. Локальный бэкенд остаётся в cooldown; fallback-соединения используют короткий lifetime, чтобы пул вернулся к локальному узлу после восстановления.
Одна строка в [general] включает функцию для всех пулов. Никакого внешнего HAProxy, никакого consul-template.
Замените бинарник, не потеряв ни одного клиента. Новый процесс сразу принимает новые соединения, а существующие клиенты завершают свои транзакции на старом. TLS, состояние соединения и cancel keys переносятся корректно.
PgBouncer требует SO_REUSEPORT с отдельными процессами (что приводит к разбалансировке пулов). В Odyssey аналогичного механизма нет.
Почему PgDoorman
- Drop-in замена. Прозрачно кэширует и переименовывает prepared statements в транзакционном режиме — никаких
DISCARD ALL,DEALLOCATEили хаков в драйверах. - Многопоточность. Один общий пул на все рабочие потоки. PgBouncer однопоточен; запуск нескольких инстансов через
SO_REUSEPORTприводит к разбалансировке пулов. - Подавление thundering herd. Когда 200 клиентов одновременно борются за 4 idle-соединения, PgDoorman ограничивает число параллельных создаваемых backend-соединений и направляет ожидающих на возвращаемые соединения через direct handoff — большинство получают соединение за микросекунды.
- Ограниченная хвостовая задержка. Строгий FIFO через каналы direct-handoff удерживает p99 в пределах 10% от p50 независимо от числа клиентов. Опережающая замена при истечении
server_lifetime— никаких всплесков при ротации соединений. - Обнаружение разорванных бэкендов. Когда клиент держит открытую транзакцию, а бэкенд умирает (failover, OOM kill), PgDoorman сразу возвращает ошибку. Другие пулеры ждут TCP keepalive и оставляют клиентов висеть на минуты.
- Сделано для эксплуатации. Конфиг в YAML или TOML с человекочитаемыми длительностями (
"30s","5m").pg_doorman generate --host your-dbинтроспектирует PostgreSQL и собирает конфиг.pg_doorman -tвалидирует его перед деплоем. Prometheus-эндпоинт встроен.
Сравнение
| PgDoorman | PgBouncer | Odyssey | |
|---|---|---|---|
| Многопоточность | Да | Нет | Да |
| Prepared statements в транзакционном режиме | Да | Начиная с 1.21 | Начиная с 1.3 |
| Полная поддержка extended query protocol | Да | Да | Частично |
| Pool Coordinator с приоритетным вытеснением | Да | Нет | Нет |
| Patroni-assisted fallback (встроенный) | Да | Нет | Нет |
Опережающая замена при истечении server_lifetime | Да | Нет | Нет |
| Обнаружение застрявших бэкендов (idle-in-transaction) | Да | Нет | Нет |
| Graceful binary upgrade | Да | Ограниченно | Нет |
| Server-side TLS (mTLS, горячая перезагрузка) | Да | Нет | Нет |
| Auth: passthrough SCRAM (без пароля в открытом виде в конфиге) | Да | Нет | Да |
| Auth: JWT | Да | Нет | Нет |
Auth: PAM / pg_hba.conf / auth_query | Да | Да | Да |
| Auth: LDAP | Нет | Начиная с 1.25 | Да |
| Конфиг YAML / TOML | Да | Нет (INI) | Нет (свой формат) |
| JSON структурированное логирование | Да | Нет | Да |
| Перцентили задержки (p50/90/95/99) | Да | Нет | Да |
Режим проверки конфига (-t) | Да | Нет | Нет |
| Авто-конфиг из PostgreSQL | Да | Нет | Нет |
| Встроенный Prometheus-эндпоинт | Да | Внешний | Да |
Бенчмарки
AWS Fargate (16 vCPU), pool size 40, pgbench 30 с на тест:
| Сценарий | vs PgBouncer | vs Odyssey |
|---|---|---|
| Extended protocol, 500 клиентов + SSL | ×3.5 | +61% |
| Prepared statements, 500 клиентов + SSL | ×4.0 | +5% |
| Simple protocol, 10 000 клиентов | ×2.8 | +20% |
| Extended + SSL + Reconnect, 500 клиентов | +96% | ~0% |
Быстрый старт
Запуск через Docker:
docker run -p 6432:6432 \
-v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
ghcr.io/ozontech/pg_doorman
Минимальный конфиг (pg_doorman.yaml):
general:
host: "0.0.0.0"
port: 6432
admin_username: "admin"
admin_password: "change_me"
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "app"
password: "md5..." # хэш из pg_shadow / pg_authid
pool_size: 40
server_username и server_password опущены намеренно — PgDoorman переиспользует MD5-хэш клиента или SCRAM ClientKey для аутентификации в PostgreSQL. Никаких паролей в открытом виде в конфиге.
Руководство по установке → · Справочник по конфигурации →
Куда дальше
- Впервые с PgDoorman? Начните с Обзора, затем Установка и Базовое использование.
- Мигрируете с PgBouncer или Odyssey? Прочитайте Сравнение и Аутентификация.
- Используете Patroni? См. Patroni-assisted fallback и
patroni_proxy. - Готовитесь к production? Прочитайте Пул под нагрузкой и Pool Coordinator.
- Эксплуатация PgDoorman? См. Binary upgrade, Сигналы, Troubleshooting.
PgDoorman vs PgBouncer vs Odyssey vs PgCat
Практическая матрица фич для выбора пулера соединений PostgreSQL. PgDoorman нацелен на нагрузки, где важны prepared statements в транзакционном режиме, многопоточная производительность и удобство эксплуатации.
Числа бенчмарков — см. Benchmarks.
Аутентификация
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| MD5 пароль | Да | Да | Да |
| SCRAM-SHA-256 (клиент) | Да | Да | Да |
| Passthrough SCRAM-SHA-256 (без пароля в открытом виде в конфиге) | Да | Нет | Да |
| Passthrough MD5 | Да | Да | Да |
auth_query (динамические пользователи) | Да | Да | Да |
| Режим passthrough в auth_query (per-user идентичность бэкенда) | Да | Нет | Да |
Формат pg_hba.conf | Да (файл или inline) | Нет | Начиная с 1.4 |
| PAM | Да (Linux) | Да (HBA) | Да |
| JWT (RSA-SHA256) | Да | Нет | Нет |
| Talos (кастомный JWT с извлечением роли) | Да | Нет | Нет |
| LDAP | Нет | Начиная с 1.25 | Да |
SCRAM channel binding (scram-sha-256-plus) | Нет | Да | Да |
| Маппинг имён пользователей (cert/peer → DB user) | Нет | Начиная с 1.23 | Да |
Настраиваемый scram_iterations | Нет | Начиная с 1.25 | Нет |
См. Аутентификация.
TLS
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Client-side TLS (4 режима: disable, allow, require, verify-full) | Да | Да | Да |
| Server-side TLS к PostgreSQL (6 режимов, включая verify-ca, verify-full) | Да | Да | Нет |
| mTLS к PostgreSQL (клиентский сертификат) | Да | Да | Нет |
Горячая перезагрузка TLS-сертификатов по SIGHUP | Да (server-side) | Нет | Нет |
| Минимум TLS 1.2 + список шифров Mozilla | Да | Да | Нет (разрешает TLS 1.0) |
Direct TLS handshake (PG17, без SSLRequest) | Нет | Начиная с 1.25 | Нет |
| Управление шифрами TLS 1.3 | Нет | Начиная с 1.25 | Нет |
См. TLS.
Маршрутизация и высокая доступность
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
Patroni-assisted fallback (встроенный поиск через /cluster) | Да | Нет | Нет |
Встроенный TCP-прокси с маршрутизацией по ролям (patroni_proxy) | Да | Нет | Нет |
| Защита от отставания реплики | Да (max_lag_in_bytes в patroni_proxy) | Нет | Да (watchdog-запрос) |
| Round-robin / least-connections для нескольких хостов | Да (patroni_proxy) | Начиная с 1.24 | Да |
target_session_attrs (read-write / read-only) | Да (через роли в patroni_proxy) | Нет | Да |
| Последовательная маршрутизация (правила по порядку) | Нет | Нет | Да |
| Маршрутизация по типу соединения (TCP vs UNIX) | Нет | Нет | Да |
| Выбор хоста с учётом availability zone | Нет | Нет | Да |
См. Patroni-assisted fallback, patroni_proxy.
Пулинг
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Режимы пула (transaction, session) | Да | Да (+ statement) | Да |
Pool Coordinator (cross-user max_db_connections с приоритетным вытеснением) | Да | Нет (без вытеснения) | Нет |
Резервный пул с min_guaranteed_pool_size | Да | Только reserve | Нет |
Опережающая замена при истечении server_lifetime | Да | Нет | Нет |
Опережающее создание / burst scaling (scaling_warm_pool_ratio, быстрые retry) | Да | Нет | Нет |
| Direct-handoff (ожидающий получает возвращаемое соединение за микросекунды) | Да | Нет | Нет |
min_pool_size (прогретые соединения) | Да | Нет | Да |
| Кэш prepared statements (двухуровневый, query interner, statement remap) | Да | Начиная с 1.21 | Начиная с 1.3 |
Умный DISCARD при возврате | RESET ALL + сброс кэша | Нет | Да (авто) |
| Прикрепление LISTEN / NOTIFY в транзакционном режиме | Нет | Нет | Экспериментально |
Cross-rule ограничение соединений (shared_pool) | Нет | Нет | Начиная с 1.5.1 |
PAUSE / RESUME / RECONNECT | Да | Да | Да (1.4.1+) |
См. Pool Coordinator, Пул под нагрузкой.
Лимиты и таймауты
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
server_idle_check_timeout (проверка перед выдачей) | Да | Нет | Нет |
idle_timeout (серверное соединение) | Да | Да | Да |
server_lifetime | Да | Да | Да |
query_wait_timeout | Да | Да | Да |
client_idle_timeout | Нет | Начиная с 1.24 | Нет |
transaction_timeout | Нет | Начиная с 1.25 | Нет |
max_user_client_connections | Нет | Начиная с 1.24 | Нет |
Per-user query_timeout | Нет | Начиная с 1.24 | Нет |
Per-user reserve_pool_size | Нет | Начиная с 1.24 | Нет |
query_wait_notify (NOTICE при ожидании бэкенда) | Нет | Начиная с 1.25 | Да (pool_notice_after_waiting_ms) |
См. Справочник по general settings, Справочник по pool settings.
Observability
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Встроенный Prometheus-эндпоинт | Да | Внешний (pgbouncer_exporter) | Да |
| Перцентили задержки на пул (p50, p90, p95, p99) | Да (HDR Histogram) | Нет | Да (TDigest) |
| Счётчики prepared statements в статистике | Да | Начиная с 1.24 | Нет |
| JSON структурированное логирование | Да (--log-format Structured) | Нет | Да |
Управление уровнем логирования в рантайме (SET log_level) | Да | Нет | Нет |
Admin SHOW POOL_COORDINATOR / SHOW POOL_SCALING / SHOW SOCKETS | Да | Нет | Нет |
Admin SHOW PREPARED_STATEMENTS | Да | Нет | Нет |
Admin SHOW HOSTS (CPU/память хоста) | Нет | Нет | Да |
Admin SHOW RULES (дамп маршрутизации) | Нет | Нет | Да |
| Метрики TLS-соединений (длительность handshake, ошибки, активные) | Да (server-side) | Нет | Нет |
| Метрики Patroni API | Да | Нет | Нет |
| Метрики fallback (флаг активности, текущий хост, попадания) | Да | Нет | Нет |
См. Справочник по Prometheus-метрикам, Admin-команды.
Эксплуатация
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Graceful binary upgrade (zero-downtime, in-flight клиенты сохраняются) | Да | Ограниченно (SO_REUSEPORT) | Нет |
| YAML конфиг | Да | Нет (INI) | Нет (свой формат) |
| TOML конфиг | Да (legacy) | Нет | Нет |
Человекочитаемые длительности и размеры (30s, 1h, 256MB) | Да | Нет | Нет |
Режим проверки конфига (pg_doorman -t) | Да | Нет | Нет |
Авто-конфиг из PostgreSQL (pg_doorman generate --host) | Да | Нет | Нет |
Перезагрузка по SIGHUP | Да (включая server TLS-сертификаты) | Да | Да |
Интеграция с systemd sd-notify | Да (Type=notify) | Нет | Нет |
Лимит памяти (max_memory_usage) | Да | Нет | Нет |
См. Binary upgrade, Сигналы.
Протокол
| Фича | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Simple query | Да | Да | Да |
| Extended query | Да | Да | Частично |
| Pipelined batches | Да | Да | Частично |
| Async Flush | Да | Да | Нет |
| Cancel-запросы поверх TLS | Да | Да | Да |
COPY IN / COPY OUT | Да | Да | Да |
Проброс replication-соединений (replication=true в startup) | Нет | Начиная с 1.23 | Нет |
| Поддержка версии протокола 3.2 | Нет | Начиная с 1.23 | Нет |
server_drop_on_cached_plan_error | Нет | Нет | Начиная с 1.5.1 |
Когда PgDoorman не подходит
- Нужна аутентификация LDAP. Используйте Odyssey или PgBouncer 1.25+.
- Нужен SCRAM channel binding (
scram-sha-256-plus) end-to-end. Используйте PgBouncer или Odyssey. - Нужна сквозная replication для инструментов логической репликации. Используйте PgBouncer 1.23+.
- Нужна маршрутизация с учётом availability zone или последовательные правила в стиле
pg_hba. Используйте Odyssey. - Нужно, чтобы
transaction_timeoutпринудительно применялся пулером. Используйте PgBouncer 1.25+.
Если важны prepared statements в транзакционном режиме, Patroni HA без внешних прокси, многопоточный throughput и перезапуски без простоя — PgDoorman подходит ближе.
Обзор PgDoorman
Что такое PgDoorman?
PgDoorman — пулер соединений PostgreSQL на Rust. Изначально форк PgCat, теперь самостоятельный проект. Стоит между приложениями и PostgreSQL: переиспользует серверные соединения вместо того, чтобы открывать их под каждый клиентский запрос.
graph LR
App1[Приложение A] --> Pooler(PgDoorman)
App2[Приложение B] --> Pooler
App3[Приложение C] --> Pooler
Pooler --> DB[(PostgreSQL)]
Когда приложение подключается к PgDoorman, тот ведёт себя ровно как сервер PostgreSQL. Внутри PgDoorman либо создаёт новое соединение к настоящему PostgreSQL, либо переиспользует существующее из своего пула, что значительно сокращает накладные расходы на установку соединений.
Ключевые преимущества
- Меньше накладных расходов на соединения: снижается влияние установки новых соединений к базе на производительность.
- Оптимизация ресурсов: ограничивается число соединений к серверу PostgreSQL.
- Улучшенная масштабируемость: больше клиентских приложений могут подключаться к одной базе данных.
- Управление соединениями: инструменты для мониторинга и управления соединениями.
Режимы пулинга
Чтобы сохранить корректную семантику транзакций при эффективном пулинге соединений, PgDoorman поддерживает несколько режимов:
Транзакционный пулинг
В транзакционном режиме клиенту выдаётся серверное соединение только на время транзакции. Как только транзакция завершается, соединение возвращается в пул.
- Соединения переиспользуются между клиентами, поэтому пул из 40 backend-соединений обслуживает тысячи клиентов, пока транзакции короткие.
- Подходит большинству приложений, кроме тех, что полагаются на состояние сессии (
SET,WITH HOLDкурсоры, advisory-блокировки между транзакциями).
Сессионный пулинг
В сессионном режиме каждому клиенту выделяется собственное серверное соединение на всё время его клиентского соединения.
- Эксклюзивное выделение: соединение остаётся за клиентом до тех пор, пока тот не отключится.
- Поддержка возможностей сессии: подходит для приложений, использующих временные таблицы или переменные сессии.
Администрирование
PgDoorman предоставляет полный набор инструментов для мониторинга и управления:
- Admin-консоль: PostgreSQL-совместимый интерфейс для просмотра статистики и управления пулером.
- Параметры конфигурации: широкие возможности настройки поведения под конкретные требования.
- Мониторинг: подробные метрики использования соединений и производительности.
Подробнее об управлении PgDoorman — см. документацию admin-консоли.
Установка PgDoorman
PgDoorman работает на Linux и macOS. Для production рекомендуем собирать самим — так вы контролируете версию Rust, целевую платформу и зависимости. Также доступны готовые пакеты из репозиториев и статические бинарники. Docker — только для тестов.
Системные требования
- Linux (рекомендуется) или macOS
- PostgreSQL 10 или новее (любая поддерживаемая версия)
- Память пропорциональна размеру пулов (несколько МБ на пул + кэш prepared statements)
- Rust 1.87 или новее, если собираете из исходников
Сборка из исходников (рекомендуется)
Соберите со своим toolchain — это даёт контроль над версией компилятора, целевой платформой и зависимостями:
git clone https://github.com/ozontech/pg_doorman.git
cd pg_doorman
cargo build --release
sudo install -m 0755 target/release/pg_doorman /usr/local/bin/pg_doorman
cargo build --release собирает оптимизированный бинарник в target/release/pg_doorman. Требования к окружению и процесс разработки описаны в Участие в проекте.
Cargo features
| Feature | По умолчанию | Эффект |
|---|---|---|
tls-migration | выкл | Vendored OpenSSL 3.5.5 с патчем, позволяющим TLS-клиентам пережить обновление бинарника. Нужен для zero-downtime перезапуска TLS-клиентов. |
pam | выкл | Поддержка аутентификации PAM (Linux). |
Сборка с миграцией TLS-клиентов
По умолчанию TLS-клиенты не могут перейти на новый процесс при обновлении бинарника — они получают ошибку 58006 и переподключаются. Чтобы соединения переходили на новый процесс без разрыва, соберите с фичей tls-migration:
cargo build --release --features tls-migration
Сборка использует vendored OpenSSL 3.5.5 с патчем, который экспортирует и заново импортирует состояние TLS-шифров (ключи, IV, sequence numbers, TLS 1.3 traffic secrets) при передаче соединений между процессами. Зашифрованные клиенты остаются на том же TCP-соединении без повторного TLS handshake.
Требования:
- Только Linux (macOS и Windows используют системный TLS, не OpenSSL).
- Утилиты
perlиpatchвPATH. - Около 5 минут дополнительного времени сборки на компиляцию OpenSSL.
Офлайн-сборка (air-gapped среды):
curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz
OPENSSL_SOURCE_TARBALL=$(pwd)/openssl-3.5.5.tar.gz \
cargo build --release --features tls-migration
Старый и новый процесс должны использовать одни и те же tls_certificate и tls_private_key. Полное описание upgrade-процесса, мониторинг и диагностика — в Graceful Binary Upgrade → TLS migration.
Для упаковки в deb/rpm смотрите каталоги debian/ и pkg/ в репозитории. Пример Dockerfile.ubuntu22-tls собирает образ с поддержкой TLS migration на Ubuntu 22.04.
Пакеты из репозиториев
Готовые deb- и rpm-пакеты публикуются с теми же релизными тегами. Используйте их, когда сборка из исходников нежелательна.
Пакеты из Ubuntu PPA и Fedora COPR собираются без поддержки TLS. Если нужен TLS — для клиентских соединений, серверных соединений к PostgreSQL или для горячей миграции TLS при обновлении бинарника — собирайте из исходников с включённой TLS-фичей. См. Сборка из исходников выше.
Ubuntu / Debian (PPA)
sudo add-apt-repository ppa:vadv/pg-doorman
sudo apt update
sudo apt install pg-doorman
Поддерживаемые релизы: jammy (22.04 LTS), noble (24.04 LTS), questing (25.10), resolute (26.04 LTS).
Fedora / RHEL / CentOS / Rocky / AlmaLinux (COPR)
sudo dnf copr enable @pg-doorman/pg-doorman
sudo dnf install pg_doorman
Поддерживаемые цели: Fedora 39, 40, 41; EPEL 8 и 9 для семейства RHEL.
Пакет ставит systemd-юнит, конфиг по умолчанию и пользователя pg_doorman.
Готовые бинарники с GitHub Releases
Если ни сборка из исходников, ни пакеты из репозиториев не подходят, скачайте статический бинарник со страницы релизов:
# Замените VERSION и TARGET на нужные значения со страницы релизов.
curl -L -o pg_doorman \
"https://github.com/ozontech/pg_doorman/releases/download/VERSION/pg_doorman-TARGET"
chmod +x pg_doorman
sudo mv pg_doorman /usr/local/bin/
Перед деплоем сверьте SHA-256, опубликованный вместе с релизом.
Docker (только для тестов)
Docker поддерживается для разработки, CI и быстрых демо. Для production не рекомендуется — упаковка и управление жизненным циклом проще через пакеты из репозиториев выше.
docker run -p 6432:6432 \
-v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
ghcr.io/ozontech/pg_doorman
docker-compose.yaml с PostgreSQL в качестве sidecar лежит в example/ — для smoke-тестов.
Проверка установки
pg_doorman --version
pg_doorman -t /etc/pg_doorman/pg_doorman.yaml # проверяет конфиг
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c "SHOW VERSION;"
pg_doorman -t проверяет конфиг до деплоя — у PgBouncer и Odyssey такой возможности нет.
Куда дальше
- Базовое использование — первый конфиг, admin-консоль, мониторинг.
- Аутентификация — выбор подходящего метода.
- Сигналы и перезагрузка — сигналы, reload, интеграция с systemd.
- Graceful Binary Upgrade — замена бинарника без потери клиентов.
Базовое использование PgDoorman
PgDoorman -- пулер соединений PostgreSQL, основанный на PgCat. В этом руководстве описаны конфигурация, эксплуатация и администрирование.
Параметры командной строки
PgDoorman поддерживает несколько параметров командной строки для настройки поведения при запуске:
$ pg_doorman --help
PgDoorman: Nextgen PostgreSQL Pooler (based on PgCat)
Usage: pg_doorman [OPTIONS] [CONFIG_FILE] [COMMAND]
Commands:
generate Generate configuration for pg_doorman by connecting to PostgreSQL and auto-detecting databases and users
help Print this message or the help of the given subcommand(s)
Arguments:
[CONFIG_FILE] [env: CONFIG_FILE=] [default: pg_doorman.toml]
Options:
-l, --log-level <LOG_LEVEL> [env: LOG_LEVEL=] [default: INFO]
-F, --log-format <LOG_FORMAT> [env: LOG_FORMAT=] [default: text] [possible values: text, structured, debug]
-n, --no-color disable colors in the log output [env: NO_COLOR=]
-d, --daemon run as daemon [env: DAEMON=]
-h, --help Print help
-V, --version Print version
Доступные параметры
| Параметр | Описание |
|---|---|
-d, --daemon | Запуск в фоне. Без этого параметра процесс работает на переднем плане. В daemon-режиме обязательны daemon_pid_file и syslog_prog_name. После ухода в фон сообщения в stderr больше не пишутся. |
-l, --log-level | Уровень логирования: INFO, DEBUG или WARN. |
-F, --log-format | Формат логов. Возможные значения: text, structured, debug. |
-n, --no-color | Отключить цвета в выводе логов. |
-V, --version | Показать информацию о версии. |
-h, --help | Показать справку. |
Установка и настройка
Структура файла конфигурации
PgDoorman поддерживает форматы YAML и TOML. Для новых установок рекомендуется YAML. Конфигурация разбита на несколько секций:
general: # Глобальные настройки сервиса PgDoorman
pools:
<name>: # Настройки конкретного пула базы данных
users:
- ... # Настройки пользователя для этого пула
Некоторые параметры обязательно должны быть указаны в файле конфигурации, чтобы PgDoorman запустился, даже если у них есть значения по умолчанию. Например, для доступа к admin-консоли нужно указать admin username и password.
Минимальный пример конфигурации
Минимальный пример конфигурации для старта:
YAML (рекомендуется)
general:
host: "0.0.0.0" # Слушать на всех интерфейсах
port: 6432 # Порт для клиентских соединений
admin_username: "admin"
admin_password: "admin" # Поменяйте это в production!
pools:
exampledb:
server_host: "127.0.0.1" # Адрес сервера PostgreSQL
server_port: 5432 # Порт сервера PostgreSQL
pool_mode: "transaction" # Режим пулинга
users:
- pool_size: 40
username: "doorman"
password: "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="
TOML
[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "admin"
[pools.exampledb]
server_host = "127.0.0.1"
server_port = 5432
pool_mode = "transaction"
[pools.exampledb.users.0]
pool_size = 40
username = "doorman"
password = "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="
Полный список параметров конфигурации можно получить командой pg_doorman generate --reference --output ref.yaml -- она генерирует аннотированный конфиг со всеми параметрами и значениями по умолчанию.
Автоматическая генерация конфигурации
Команда generate создаёт файл конфигурации, подключаясь к серверу PostgreSQL и определяя базы данных и пользователей. По умолчанию сгенерированный конфиг содержит inline-комментарии, объясняющие каждый параметр.
# Посмотреть все доступные опции
pg_doorman generate --help
# Сгенерировать YAML-конфиг (рекомендуется)
pg_doorman generate --output pg_doorman.yaml
# Сгенерировать TOML-конфиг (для обратной совместимости)
pg_doorman generate --output pg_doorman.toml
# Сгенерировать reference-конфиг со всеми настройками (без подключения к PG)
pg_doorman generate --reference --output pg_doorman.yaml
# Сгенерировать reference-конфиг с русскими комментариями для быстрого старта
pg_doorman generate --reference --ru --output pg_doorman.yaml
# Сгенерировать конфиг без комментариев (plain serialization)
pg_doorman generate --no-comments --output pg_doorman.yaml
Команда generate поддерживает несколько опций:
| Опция | Описание |
|---|---|
--host | Хост PostgreSQL для подключения (если не указано, используется localhost) |
--port, -p | Порт PostgreSQL для подключения (по умолчанию: 5432) |
--user, -u | Пользователь PostgreSQL для подключения (нужны привилегии superuser, чтобы читать pg_shadow) |
--password | Пароль PostgreSQL для подключения |
--database, -d | База данных PostgreSQL для подключения (если не указано, используется имя пользователя) |
--ssl | Подключение к серверу PostgreSQL по SSL/TLS |
--pool-size | Размер пула в сгенерированной конфигурации (по умолчанию: 40) |
--session-pool-mode, -s | Сессионный режим пулинга в сгенерированной конфигурации |
--output, -o | Файл для сгенерированной конфигурации (если не указан, используется stdout) |
--server-host | Переопределить server_host в конфиге (если не указано, используется значение --host) |
--no-comments | Отключить inline-комментарии в сгенерированной конфигурации (по умолчанию они включены) |
--reference | Сгенерировать полный reference-конфиг с примерами значений, без подключения к PG |
--russian-comments, --ru | Сгенерировать комментарии на русском для быстрого старта |
--format, -f | Формат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение |
Команда подключается к PostgreSQL, обнаруживает базы данных и пользователей, затем создаёт документированный файл конфигурации.
Команда generate также учитывает стандартные переменные окружения PostgreSQL: PGHOST, PGPORT, PGUSER, PGPASSWORD и PGDATABASE.
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-конфиге.
Чтение информации о пользователях из PostgreSQL требует привилегий superuser, чтобы получить доступ к таблице pg_shadow.
Контроль доступа клиентов (pg_hba)
PgDoorman может применять правила доступа клиентов в стиле PostgreSQL pg_hba.conf через параметр general.pg_hba.
Правила можно встроить прямо в конфиг или сослаться на путь к файлу. Полные примеры -- в reference-разделе.
Trust-режим: когда правило использует trust, PgDoorman принимает соединения без запроса пароля у клиента -- зеркально поведению PostgreSQL. Учитываются TLS-зависимые типы правил: hostssl требует TLS, hostnossl его запрещает.
Запуск PgDoorman
После создания файла конфигурации запустите PgDoorman из командной строки:
$ pg_doorman pg_doorman.toml
Если файл конфигурации не указан, PgDoorman будет искать pg_doorman.toml в текущей директории.
Подключение к PostgreSQL через PgDoorman
После запуска PgDoorman подключайтесь к нему вместо прямого подключения к PostgreSQL:
$ psql -h localhost -p 6432 -U doorman exampledb
Строку подключения вашего приложения нужно изменить так, чтобы она указывала на PgDoorman, а не напрямую на PostgreSQL:
postgresql://doorman:password@localhost:6432/exampledb
PgDoorman прозрачно обработает пулинг соединений, поэтому приложению не нужно знать, что оно подключается через пулер.
Администрирование
Admin-консоль
PgDoorman предоставляет административный интерфейс через специальную базу данных pgdoorman (или pgbouncer для обратной совместимости):
$ psql -h localhost -p 6432 -U admin pgdoorman
После подключения можно посмотреть доступные команды:
pgdoorman=> SHOW HELP;
NOTICE: Console usage
DETAIL:
SHOW HELP|CONFIG|DATABASES|POOLS|POOLS_EXTENDED|POOLS_MEMORY|POOL_COORDINATOR|POOL_SCALING
SHOW CLIENTS|SERVERS|USERS|CONNECTIONS|STATS|PREPARED_STATEMENTS|AUTH_QUERY
SHOW LISTS|SOCKETS|LOG_LEVEL|VERSION
SET log_level = '<filter>'
RELOAD
SHUTDOWN
UPGRADE
PAUSE [db]
RESUME [db]
RECONNECT [db]
Admin-консоль на данный момент поддерживает только simple query protocol.
Некоторые драйверы баз данных используют extended query protocol для всех команд, что делает их непригодными для работы с admin-консолью. В таких случаях используйте клиент psql для администрирования.
Войти в admin-консоль может только пользователь, указанный в admin_username файла конфигурации.
Если это разрешено правилами general.pg_hba, в admin-консоль можно входить методом trust (без запроса пароля), например:
# Разрешить только локальному admin доступ к admin-БД без пароля
host pgdoorman admin 127.0.0.1/32 trust
Используйте trust с предельной осторожностью. Всегда ограничивайте по адресу и, по возможности, требуйте TLS через hostssl. В production предпочитайте методы с паролями, если только не понимаете последствия полностью.
Мониторинг PgDoorman
Admin-консоль предоставляет несколько команд для мониторинга текущего состояния PgDoorman:
SHOW STATS-- посмотреть статистику производительности.SHOW CLIENTS-- список текущих клиентских соединений.SHOW SERVERS-- список текущих серверных соединений.SHOW POOLS-- состояние пулов соединений.SHOW DATABASES-- список настроенных баз данных.SHOW USERS-- список настроенных пользователей.
Эти команды подробно описаны в разделе Команды admin-консоли ниже.
Перечитывание конфигурации
Если вы изменили pg_doorman.toml, изменения можно применить без перезапуска сервиса:
pgdoorman=# RELOAD;
При перечитывании конфигурации:
- PgDoorman читает обновлённый файл конфигурации.
- Обнаруживаются изменения параметров подключения к базам.
- Существующие серверные соединения закрываются при следующем возврате в пул (в соответствии с режимом пулинга).
- Новые серверные соединения сразу используют обновлённые параметры.
Это позволяет вносить изменения в конфигурацию с минимальным влиянием на приложения.
Команды 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_id | PID backend-процесса PostgreSQL (если доступен) |
database_name | Имя базы данных, к которой подключено соединение |
user | Username, под которым 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 | Имя базы данных (пула), к которой подключён клиент |
user | Username, с которым клиент подключился |
application_name | Имя приложения, заявленное клиентом |
addr | IP-адрес и порт клиента (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 | Имя базы данных |
user | Username, ассоциированный с пулом |
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;
| Колонка | Описание |
|---|---|
name | Username, как настроен в PgDoorman |
pool_mode | Режим пулинга, назначенный пользователю: session или transaction |
SHOW DATABASES
SHOW DATABASES показывает информацию обо всех настроенных пулах баз данных:
pgdoorman=> SHOW DATABASES;
| Колонка | Описание |
|---|---|
name | Имя настроенного пула |
host | Hostname сервера 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;
При выполнении:
- PgDoorman перестаёт принимать новые клиентские соединения.
- Существующим транзакциям даётся время завершиться (в пределах настроенного таймаута).
- Все соединения закрываются.
- Процесс завершается.
Команда SHUTDOWN останавливает сервис PgDoorman, отключая всех клиентов. В production используйте её с осторожностью.
SET log_level
Изменить уровень логирования на лету, не перезапуская пулер:
-- Глобальный уровень
pgdoorman=> SET log_level = 'debug';
-- На модуль (синтаксис RUST_LOG)
pgdoorman=> SET log_level = 'warn,pg_doorman::pool::pool_coordinator=debug';
-- Посмотреть текущий уровень
pgdoorman=> SHOW LOG_LEVEL;
-- Сбросить на стартовое значение
pgdoorman=> SET log_level = 'default';
Изменения временные -- теряются при перезапуске. Допустимые уровни: error, warn, info, debug, trace, off.
RELOAD
Команда RELOAD обновляет конфигурацию PgDoorman без перезапуска сервиса:
pgdoorman=> RELOAD;
Эта команда:
- Перечитывает файл конфигурации.
- Обновляет все изменяемые настройки.
- Применяет изменения параметров соединений для новых соединений.
- Сохраняет существующие соединения, пока они не вернутся в пул.
RELOAD применяет большинство параметров конфигурации без разрыва существующих соединений — то, что нужно в production, где простой недопустим.
PAUSE
Команда PAUSE [db] блокирует получение новых backend-соединений для указанной базы (или всех баз, если аргумент не задан). Активные транзакции продолжают работать -- блокируются только запросы на новые соединения.
-- Поставить на паузу все пулы
pgdoorman=> PAUSE;
-- Поставить на паузу только пулы конкретной базы
pgdoorman=> PAUSE mydb;
Клиенты, которые запросят новое backend-соединение во время паузы, будут ждать RESUME или истечения query_wait_timeout -- что наступит раньше. Если истечёт таймаут, клиент получит ошибку timeout.
Используйте SHOW POOLS для проверки состояния паузы -- колонка paused покажет 1 для приостановленных пулов.
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;
При выполнении:
- Внутренний epoch-счётчик пула увеличивается.
- Все idle-соединения сразу закрываются.
- Активные соединения (которые сейчас обслуживают транзакцию) продолжают работать, но утилизируются при возврате в пул -- они не будут переиспользованы.
То есть RECONNECT не прерывает активные транзакции. Новые соединения создаются по запросу с текущим epoch, поэтому они будут приняты recycle().
Постепенная ротация (минимум воздействия): Один RECONNECT -- idle-соединения сбрасываются сразу, активные -- по завершении текущей транзакции. Новые соединения создаются по мере необходимости.
Полная ротация (гарантированно все новые соединения): PAUSE → RECONNECT → RESUME -- сначала пауза не даёт стартовать новым транзакциям, потом RECONNECT помечает всё на утилизацию. После RESUME все последующие запросы получают свежие соединения.
После 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 во время PAUSE | RELOAD пересоздаёт пулы из конфигурации, поэтому состояние паузы теряется. Это ожидаемое поведение -- новая конфигурация означает новые пулы. |
| GC приостановленных динамических пулов | Приостановленные динамические пулы защищены от garbage collection даже при 0 соединений. |
| Replenish во время PAUSE | Пулы с min_pool_size не дозаполняются, пока на паузе -- новые соединения не создаются. Дозаполнение возобновляется после RESUME. |
| Время жизни соединений во время PAUSE | Retain-задача продолжает закрывать просроченные соединения (idle timeout, server lifetime). Соединения по-прежнему стареют. |
| Несколько вызовов RECONNECT | Каждый вызов увеличивает epoch ещё. Только соединения, созданные после последнего RECONNECT, считаются валидными. |
Обработка сигналов
PgDoorman реагирует на стандартные Unix-сигналы для управления и контроля. Сигналы посылаются через kill (например, kill -HUP <pid>).
| Сигнал | Эффект |
|---|---|
| SIGHUP | Перечитывание конфигурации -- эквивалент admin-команды RELOAD. |
| SIGUSR2 | Binary upgrade и graceful shutdown. Валидирует новый бинарник флагом -t, запускает новый процесс, затем завершается. Рекомендуется для обновлений. См. Binary upgrade. |
| SIGINT | Foreground + TTY (Ctrl+C): только graceful shutdown (без binary upgrade). Daemon / без TTY: binary upgrade и graceful shutdown (legacy-поведение). |
| SIGTERM | Немедленное завершение. Активные соединения обрываются. |
В окружениях на базе systemd unit-файл по умолчанию использует ExecReload=/bin/kill -SIGUSR2 $MAINPID, чтобы запускать binary upgrade при systemctl reload.
Аутентификация
pg_doorman аутентифицирует клиентов прежде чем перенаправить их к PostgreSQL. Поддерживается шесть методов; они выбираются в порядке приоритета на основании того, что присылает клиент и что задано в конфигурации пула.
Эта страница объясняет, как pg_doorman выбирает метод аутентификации. Подробности настройки смотрите по ссылкам каждого метода ниже.
Обзор методов
| Метод | Когда использовать | Хранит ли секрет в конфиге? |
|---|---|---|
| Passthrough (MD5 / SCRAM) | По умолчанию. Имя пользователя пула совпадает с пользователем PostgreSQL. | Хеш MD5 или SCRAM ClientKey, никогда — открытый пароль |
| auth_query | Много пользователей, динамическое заведение. Учётные данные ищутся в самом PostgreSQL. | Только секрет одного сервисного пользователя |
| PAM | Аутентификация на уровне OS (LDAP через pam_ldap, Kerberos, локальные учётки). Только Linux. | Нет |
| JWT | Доступ сервиса к базе по короткоживущим токенам, подписанным внешним IdP. | Только публичный ключ |
| Talos | JWT с встроенным извлечением роли. Используется в Ozon. | Только публичный ключ |
| pg_hba.conf | Ограничение того, кто откуда может подключаться (сетевой ACL), независимо от метода учётных данных. | Нет |
LDAP, Kerberos GSSAPI, аутентификация по сертификатам и SCRAM channel binding (scram-sha-256-plus) не поддерживаются. Смотрите Сравнение.
Порядок выбора метода
pg_hba.conf оценивается первым, до любой проверки учётных данных. Правило reject обрывает соединение; правило trust полностью пропускает проверку учётных данных.
После HBA pg_doorman выбирает метод проверки учётных данных в таком порядке:
- Talos. Активируется, когда клиент подключается с именем пользователя
talos. Пароль клиента разбирается как JWT, из него извлекается роль (owner/read_write/read_only), и соединение продолжается под этой производной идентичностью. - HBA Trust. Если
pg_hba.confсовпал с правиломtrust, проверки учётных данных не происходит. - PAM. Если у совпавшего пользователя задан
auth_pam_service, учётные данные уходят в PAM (только Linux). PAM приоритетнее статического пароля. - SCRAM static. Если
passwordпользователя в конфиге начинается сSCRAM-SHA-256$, pg_doorman запускает SCRAM-аутентификацию. - MD5 static. Если
passwordпользователя начинается сmd5, pg_doorman запускает MD5-аутентификацию. - JWT. Если
passwordпользователя начинается сjwt-pkey-fpath:, пароль клиента проверяется как JWT по публичному ключу с диска.
auth_query не входит в этот список выбора — он выполняется до диспетчеризации, чтобы наполнить список пользователей пула хешами, полученными из PostgreSQL. После того как auth_query вернёт значение passwd, выбор метода идёт по префиксу этого значения (SCRAM-SHA-256$ или md5).
Если ни один метод не подошёл к формату пароля, pg_doorman возвращает «Authentication method not supported» и закрывает соединение.
Аутентификация на стороне PostgreSQL: passthrough vs configured
pg_doorman аутентифицируется дважды: один раз как шлюз (клиент → pg_doorman) и один раз как бэкенд (pg_doorman → PostgreSQL). Возможны три варианта:
- Passthrough (по умолчанию). Хеш MD5 клиента или SCRAM
ClientKeyпереиспользуется для аутентификации на PostgreSQL. В конфиге нет открытого пароля. Требует, чтобыserver_usernameне был задан (или был равен имени клиента). - Заданный пользователь бэкенда. Установите
server_usernameиserver_passwordв блоке пользователя. pg_doorman будет аутентифицироваться на PostgreSQL под ними. Используйте, когда имя пользователя пула отвязано от пользователя базы (Talos, JWT, переименование). - auth_query в выделенном режиме. Установите
server_userвнутри блокаauth_query. Все динамически найденные пользователи разделят один пул бэкенда, аутентифицированный какserver_user. Это размен идентичности бэкенда на пользователя ради эффективности переиспользования пула.
Подробности смотрите в Passthrough, а про выделенный режим — в auth_query.
Ограничение подключений
pg_hba.conf применяется до проверки учётных данных. Распространённые шаблоны:
- Отвергнуть всё кроме localhost:
host all all 0.0.0.0/0 reject, а перед нимhost all all 127.0.0.1/32 trust. - Требовать TLS для нелокальных соединений:
hostssl all all 0.0.0.0/0 scram-sha-256иhostnossl all all 127.0.0.1/32 trust. - ACL по базам:
host mydb appuser 10.0.0.0/8 scram-sha-256.
Смотрите pg_hba.conf.
Куда дальше
- Новая инсталляция? Прочтите Passthrough и Basic usage.
- Много пользователей с ротируемыми учётными данными? Используйте auth_query.
- Идентичность сервиса по токенам? Используйте JWT.
- Аутентификация, интегрированная с OS? Используйте PAM.
- Ограничения на сетевом уровне? Настройте pg_hba.conf.
Passthrough-аутентификация (по умолчанию)
pg_doorman переиспользует криптографическое доказательство клиента — хеш MD5 или SCRAM ClientKey — чтобы аутентифицироваться на PostgreSQL. Открытый пароль никогда не покидает клиента и никогда не хранится в конфигурации пула.
Это рекомендуемая настройка, когда имя пользователя пула совпадает с пользователем PostgreSQL.
Как это работает
MD5
Протокол MD5-пароля PostgreSQL хранит на сервере md5(password + username). Клиент хеширует пароль тем же способом и присылает md5(stored_hash + salt). pg_doorman:
- Получает хешированный ответ клиента.
- Ищет сохранённый хеш MD5 в своём конфиге (или через
auth_query). - Проверяет, что ответ клиента совпадает.
- Передаёт сохранённый хеш в PostgreSQL как пароль во время аутентификации бэкенда. PostgreSQL принимает его, потому что именно этот хеш и хранится в
pg_authid.
Поле password в конфиге пула содержит сохранённый хеш в формате md5XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (32-символьный MD5 от password + username с буквальным префиксом md5).
SCRAM-SHA-256
SCRAM проверяет клиента, не пересылая ничего эквивалентного паролю. pg_doorman:
- Выполняет SCRAM handshake с клиентом, проверяя
ClientProof. - Извлекает
ClientKeyиз успешного обмена. - Выполняет SCRAM handshake с PostgreSQL, переиспользуя тот же
ClientKeyдля вычисления свежегоClientProofпод nonce бэкенда.
Поле password в конфиге пула содержит SCRAM-верификатор из pg_authid.rolpassword в формате SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey>.
pg_doorman не поддерживает SCRAM channel binding (scram-sha-256-plus).
Конфигурация
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "app"
password: "md5d41d8cd98f00b204e9800998ecf8427e"
pool_size: 40
Обратите внимание, чего здесь нет: ни server_username, ни server_password. pg_doorman распознаёт passthrough-режим по отсутствию этих полей.
Для SCRAM поле password выглядит так:
password: "SCRAM-SHA-256$4096:random_salt$stored_key:server_key"
Получение хеша
Подключитесь как суперпользователь к PostgreSQL и прочитайте pg_shadow (или pg_authid):
SELECT usename, passwd FROM pg_shadow WHERE usename = 'app';
Колонка passwd содержит либо хеш MD5 (md5...), либо SCRAM-верификатор (SCRAM-SHA-256$...) — в зависимости от значения password_encryption в момент установки пароля.
Чтобы принудительно сохранить MD5: SET password_encryption = 'md5'; ALTER ROLE app PASSWORD 'plaintext';
Чтобы принудительно SCRAM: SET password_encryption = 'scram-sha-256'; ALTER ROLE app PASSWORD 'plaintext';
Когда passthrough-режима недостаточно
Задавайте server_username и server_password явно, когда:
- Пользователь пула отличается от пользователя бэкенда (переименование).
- Клиент аутентифицируется через JWT — у него нет ни хеша MD5, ни ключа SCRAM, чтобы пробросить.
- Клиент аутентифицируется через Talos, и вы хотите фиксированную идентичность бэкенда на роль.
- Вы используете auth_query в выделенном режиме.
users:
- username: "external_app"
password: "jwt-pkey-fpath:/etc/pg_doorman/jwt.pub"
server_username: "app"
server_password: "md5..."
pool_size: 40
Автоматически сгенерированный конфиг
pg_doorman generate --host your-pg-host --user your-admin-user интроспектирует PostgreSQL и собирает конфиг с автоматически подставленными хешами из pg_shadow. Используйте это для новых инсталляций, чтобы избежать ошибок копирования.
pg_doorman generate --host db.example.com --user postgres --output pg_doorman.yaml
Подробнее про команду generate смотрите в Basic Usage.
auth_query
Получайте учётные данные пользователей из самого PostgreSQL вместо того, чтобы перечислять каждого пользователя в конфиге пула. Полезно, когда пользователи заводятся динамически или часто ротируются.
Два режима
pg_doorman поддерживает два режима; оба настраиваются в одном блоке auth_query. Выбор зависит от того, задан ли server_user:
- Режим passthrough (без
server_user): каждый аутентифицированный пользователь получает собственный пул бэкенда, аутентифицированный под ним же. Сохраняет идентичность бэкенда на пользователя дляcurrent_user, row-level security и audit logs. - Выделенный режим (с
server_user): все динамические пользователи разделяют один пул бэкенда, аутентифицированный какserver_user. Это размен идентичности бэкенда на более высокое переиспользование пула и меньшее число соединений.
auth_query в стиле PgBouncer — это выделенный режим. Odyssey поддерживает оба. В pg_doorman режим passthrough — по умолчанию.
Режим passthrough
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "postgres"
password: "md5..."
database: "postgres"
cache_ttl: "1h"
cache_failure_ttl: "30s"
Запрос должен возвращать колонку с именем passwd или password, содержащую хеш MD5 или SCRAM. Дополнительные колонки игнорируются.
user и password — это учётные данные, под которыми pg_doorman выполняет lookup-запрос. У них должно быть право читать колонку с учётными данными. Либо выдайте доступ к специально созданному представлению (рекомендуется), либо используйте пользователя из группы pg_read_server_files.
Когда клиент подключается как alice:
- pg_doorman выполняет запрос с
$1 = 'alice'и получает её хеш. - Кэширует хеш в памяти на
cache_ttlсекунд. - Выполняет passthrough-аутентификацию MD5 или SCRAM (смотрите Passthrough).
- Открывает соединение с бэкендом, аутентифицированное как
aliceс тем же хешем.
Выделенный режим (dedicated)
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "auth_lookup"
password: "md5..."
database: "postgres"
server_user: "app"
server_password: "md5..."
pool_size: 40
min_pool_size: 5
cache_ttl: "1h"
Установка server_user переключает режим. Теперь:
- Клиент аутентифицируется как
aliceпротив хеша, возвращённого запросом. - Пул бэкенда аутентифицирован как
app(значениеserver_user) и общий для всех динамических пользователей. current_userв PostgreSQL всегда будетapp, независимо от того, какой клиент подключился.
Используйте этот режим, когда у вас много пользователей (тысячи) и пулы бэкенда на каждого исчерпали бы слоты соединений PostgreSQL.
Рекомендуемая настройка PostgreSQL
Не используйте суперпользователя для lookup. Создайте отдельную функцию с SECURITY DEFINER:
CREATE OR REPLACE FUNCTION pg_doorman_lookup(uname text)
RETURNS TABLE(passwd text)
LANGUAGE sql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
SELECT passwd FROM pg_shadow WHERE usename = uname;
$$;
REVOKE ALL ON FUNCTION pg_doorman_lookup(text) FROM public;
GRANT EXECUTE ON FUNCTION pg_doorman_lookup(text) TO auth_lookup;
Затем в конфиге пула:
auth_query:
query: "SELECT passwd FROM pg_doorman_lookup($1)"
user: "auth_lookup"
password: "md5..."
Кэширование
| Параметр | По умолчанию | Назначение |
|---|---|---|
cache_ttl | "1h" | Сколько кэшируется успешный lookup. |
cache_failure_ttl | "30s" | Сколько кэшируется неуспешный lookup. Защищает от усиления brute-force атаки. |
min_interval | "1s" | Минимальный интервал между повторными lookup-запросами для одного пользователя. |
Длительности — строки в кавычках: "1h", "30m", "300s". Голое целое число интерпретируется как миллисекунды — cache_ttl: 3600 будет кэшировать на 3.6 секунды, не на час.
Кэш — пер-пуловый, в памяти, сбрасывается при RELOAD. После ротации пароля пользователя сделайте перезапуск или RELOAD.
Observability
SHOW AUTH_QUERY показывает статистику по базам:
database | cache_entries | cache_hits | cache_misses | cache_refetches | rate_limited | auth_success | auth_failure | executor_queries | executor_errors
Метрики Prometheus: pg_doorman_auth_query_cache, pg_doorman_auth_query_auth, pg_doorman_auth_query_executor, pg_doorman_auth_query_dynamic_pools. Смотрите Admin commands.
Аутентификация PAM
pg_doorman делегирует аутентификацию клиента сервису PAM на хосте. Используйте это для аутентификации, интегрированной с OS (LDAP через pam_ldap, Kerberos, локальные модули PAM), без хранения учётных данных на каждого пользователя в конфиге пула.
PAM работает только под Linux. Готовые бинарники собираются с поддержкой PAM.
Конфигурация
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "alice"
auth_pam_service: "pg_doorman"
server_username: "alice"
server_password: "md5..."
pool_size: 20
auth_pam_service — имя файла сервиса PAM в /etc/pam.d/. pg_doorman не проверяет имя сервиса при старте — убедитесь, что файл существует.
Поле password опускается, потому что проверкой занимается PAM. server_username и server_password обязательны: PAM аутентифицирует только клиента в pg_doorman; pg_doorman всё равно нужны учётные данные для соединения с бэкендом.
Пример сервиса PAM
/etc/pam.d/pg_doorman:
auth required pam_unix.so
account required pam_unix.so
Для аутентификации через LDAP:
auth required pam_ldap.so
account required pam_ldap.so
Настройте pam_ldap в /etc/ldap.conf (или /etc/nslcd.conf) под своё окружение.
Порядок выбора метода
PAM проверяется после Talos и HBA Trust, но до любого метода на основе пароля. Если у пользователя одновременно заданы auth_pam_service и статический password (с префиксом MD5, SCRAM или JWT), выигрывает PAM.
Смотрите Обзор.
Оговорки
- PAM блокирует поток-обработчик во время вызова аутентификации. Если ваш стек PAM делает сетевые вызовы (LDAP, Kerberos), ждите эпизодических всплесков задержки.
pam_unix.soтребует доступ на чтение к/etc/shadow— обычно только дляroot. Запускайте pg_doorman под пользователем с нужным членством в группе или используйте другой модуль PAM.- PAM не поддерживает passthrough SCRAM. Соединение с бэкендом всегда использует
server_usernameиserver_password. - Для LDAP без машинерии PAM в pg_doorman нет нативной поддержки LDAP. Используйте Odyssey или PgBouncer 1.25+.
Аутентификация JWT
Аутентифицируйте клиентов JSON Web Token, подписанным внешним поставщиком идентификации. pg_doorman проверяет подпись токена RSA-SHA256 по публичному ключу с диска, сверяет claim preferred_username и пробрасывает соединение в PostgreSQL под заданной идентичностью бэкенда.
Этот метод подходит для доступа сервиса к базе, когда короткоживущие токены выпускает OIDC-провайдер, Vault или внутренний токен-сервис.
Конфигурация
Сгенерируйте (или получите) публичный RSA-ключ и сошлитесь на него в поле password пользователя через префикс jwt-pkey-fpath::
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "billing-service"
password: "jwt-pkey-fpath:/etc/pg_doorman/jwt-public.pem"
server_username: "billing"
server_password: "md5..."
pool_size: 40
То, что клиент пришлёт как пароль, будет считаться JWT и проверяться по /etc/pg_doorman/jwt-public.pem. Токен должен:
- Быть подписан RS256 (RSA-SHA256). HS256 и EC-варианты не поддерживаются.
- Иметь claim
preferred_username, равный заданномуusername(billing-serviceв примере выше). - Проходить стандартную валидацию
expиnbf.
Соединение с бэкендом открывается как billing с хешем из server_password. Идентичность клиента (billing-service) отвязана от идентичности базы (billing).
Генерация пары ключей
openssl genrsa -out jwt-private.pem 2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem
Храните jwt-private.pem у издателя токенов. Раздавайте jwt-public.pem в pg_doorman.
Выпуск токена
Подойдёт любая RS256 JWT-библиотека. Пример на Python (PyJWT):
import jwt
import time
private_key = open("jwt-private.pem").read()
token = jwt.encode(
{
"preferred_username": "billing-service",
"iat": int(time.time()),
"exp": int(time.time()) + 300, # 5 минут
},
private_key,
algorithm="RS256",
)
Клиент подключается к pg_doorman с user=billing-service и password=<token>. Большинство драйверов PostgreSQL принимают любую строку в поле пароля.
Ротация токенов
pg_doorman читает файл публичного ключа один раз при старте и по SIGHUP. Чтобы ротировать ключ:
- Добавьте новый публичный ключ во вторую запись пользователя с параллельным именем.
- Сделайте reload (
kill -HUP). - Переключите издателя на новый ключ.
- Удалите старую запись пользователя после 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.
Как это работает
- Клиент подключается с именем пользователя
talosи JWT в качестве пароля. - pg_doorman читает поле
kidиз заголовка JWT и ищет соответствующий публичный ключ вgeneral.talos.keys. - Токен проверяется (RS256,
exp,nbf). - pg_doorman обходит ключи
resource_access, разбивает каждый по:и сверяет часть после двоеточия сgeneral.talos.databases. То есть ключ вида"postgres.stg:billing"совпадает с базойbilling. Роли из всех совпавших записей собираются вместе; побеждает наивысшая (owner>read_write>read_only). - Соединение аутентифицируется против пользователя пула, имя которого совпадает с ролью:
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 — один из:
| Тип | Совпадает с |
|---|---|
host | TCP, с TLS или без |
hostssl | TCP только с активным TLS |
hostnossl | TCP только когда TLS не активен |
local | Локальный Unix-сокет |
database — all, конкретное имя базы или список через запятую. replication не обрабатывается (pg_doorman не поддерживает проброс репликации).
user — all, конкретный пользователь или список через запятую. Префикс +groupname (членство в роли PostgreSQL) не поддерживается.
source_cidr — IPv4- или IPv6-CIDR. Обязателен для host, hostssl, hostnossl. Неприменим к local.
method — один из:
| Метод | Поведение |
|---|---|
trust | Полностью пропустить проверку учётных данных. Клиент допускается под тем именем, которое заявил. |
md5 | Принудительно требовать аутентификацию по паролю MD5. |
scram-sha-256 | Принудительно требовать аутентификацию SCRAM-SHA-256. |
reject | Отказать в соединении до любой проверки учётных данных. |
Правила оцениваются сверху вниз. Побеждает первое совпавшее.
Примеры
Требовать TLS из сети, разрешить открытое локально
hostssl all all 10.0.0.0/8 scram-sha-256
hostnossl all all 10.0.0.0/8 reject
host all all 127.0.0.1/32 trust
local all all trust
ACL по базам
host billing app_billing 10.0.0.0/8 scram-sha-256
host billing all 0.0.0.0/0 reject
host inventory app_inv 10.0.0.0/8 scram-sha-256
host all admin 10.1.1.0/24 scram-sha-256
host all all 0.0.0.0/0 reject
Заблокировать legacy MD5 из открытого интернета
hostssl all all 0.0.0.0/0 scram-sha-256
host all all 0.0.0.0/0 reject
Если в базе хранятся только хеши MD5, а клиент запрашивает SCRAM, аутентификация провалится с понятной ошибкой. Перед тем как ужесточать правила, переведите базу на SCRAM-SHA-256 (ALTER ROLE ... PASSWORD).
Отличия от pg_hba.conf PostgreSQL
- Нет ключевого слова
replication(pg_doorman не пробрасывает соединения репликации). - Нет методов
peer,ident,cert,gss,sspi,pam. PAM настраивается на пользователя черезauth_pam_service, не через HBA. - Нет префикса
+groupnameдля пользователя. - Нет регулярных выражений (синтаксис
/regex). - IPv6-CIDR поддерживается. IPv4-mapped IPv6 (
::ffff:1.2.3.4) сверяется с правилами IPv4.
Перезагрузка
kill -HUP $(pidof pg_doorman)
Существующие соединения заново не оцениваются. Новые соединения используют новые правила.
Оговорки
- Правила применяются к клиентам, подключающимся к pg_doorman, а не к PostgreSQL. Собственный
pg_hba.confPostgreSQL по-прежнему важен для соединения с бэкендом. trustдопускает клиента без какой-либо проверки учётных данных. Бэкенд всё равно должен аутентифицироваться как пользователь пула — но клиентская сторона не проверена. Используйтеtrustтолько в сетях, где адресу источника можно доверять (loopback, ограниченный Unix-сокет).- Поддержки LDAP, Kerberos и
peer-аутентификации нет — смотрите Сравнение.
TLS
pg_doorman терминирует TLS на клиентской стороне (клиенты → pg_doorman) и инициирует TLS на серверной стороне (pg_doorman → PostgreSQL). Стороны настраиваются независимо.
Клиентский TLS
Шифрование соединений между клиентскими приложениями и pg_doorman.
Режимы
| Режим | Поведение |
|---|---|
disable | Не анонсировать TLS. Клиенты, отправляющие SSLRequest, получают 'N' (отказ). |
allow | Анонсировать TLS, но принимать и обычный TCP. |
require | Требовать TLS. Обычные соединения разрываются после неудачного SSLRequest. |
verify-full | Требовать TLS и валидный клиентский сертификат. Используется для mTLS. |
verify-full — это mTLS: сервер проверяет сертификат клиента. Подготовьте набор клиентских CA через tls_ca_cert.
Конфигурация
general:
tls_mode: "require"
tls_certificate: "/etc/pg_doorman/tls/server.crt"
tls_private_key: "/etc/pg_doorman/tls/server.key"
tls_ca_cert: "/etc/pg_doorman/tls/client_ca.pem" # только для verify-full
tls_rate_limit_per_second: 100 # необязательное ограничение скорости handshake
Для разработки сертификат может быть самоподписанным; в продакшене обычно используют Let's Encrypt или внутренний CA.
Перезагрузка (клиентская сторона)
Клиентские сертификаты загружаются при старте. Их смена требует рестарта процесса. Для клиентского TLS перезагрузки по SIGHUP нет.
Для ротации сертификатов без простоя смотрите Binary Upgrade.
Политика шифров
Минимальный TLS 1.2. Список шифров — Mozilla «intermediate»; настройке не подлежит. Direct TLS handshake (PG17, без SSLRequest) не поддерживается.
Для управления шифрами TLS 1.3 или direct TLS из PG17 используйте PgBouncer 1.25+.
Серверный TLS
Шифрование соединений между pg_doorman и бэкендами PostgreSQL. Появилось в 3.6.0.
Режимы
| Режим | Поведение |
|---|---|
disable | Обычный TCP. |
allow (по умолчанию) | Сначала пробовать обычный TCP; если сервер отказывает, повторить попытку на новом сокете с TLS. Соответствует libpq sslmode=allow. |
prefer | Отправить SSLRequest; если сервер отвечает 'N', откатиться к обычному TCP. |
require | Требовать TLS. Падать, если сервер его не поддерживает. |
verify-ca | Требовать TLS и проверять серверный сертификат против настроенного CA. |
verify-full | Требовать TLS, проверять CA и проверять hostname сервера против сертификата. |
allow — режим по умолчанию ради обратной совместимости: существующие развёртывания, где у PostgreSQL настроен TLS, автоматически переходят на TLS без изменения конфигурации. Для новых развёртываний, которым нужны явные гарантии, выбирайте require или verify-full.
Конфигурация
general:
server_tls_mode: "verify-full"
server_tls_ca_cert: "/etc/pg_doorman/tls/pg_ca_bundle.pem"
# Необязательно: клиентский сертификат для mTLS к PostgreSQL
server_tls_certificate: "/etc/pg_doorman/tls/pg_client.crt"
server_tls_private_key: "/etc/pg_doorman/tls/pg_client.key"
server_tls_ca_cert принимает PEM-bundle (несколько CA-сертификатов, склеенных подряд). Загружаются все.
Горячая перезагрузка
По SIGHUP серверные сертификаты перечитываются с диска. Существующие соединения продолжают пользоваться исходным TLS-контекстом; новые соединения используют перезагруженные сертификаты. Перезагрузка lock-free через Arc<ArcSwap<...>> — без обрыва соединений, без задержек на handshake.
kill -HUP $(pidof pg_doorman)
Это единственный путь перезагрузки TLS. Клиентские сертификаты по SIGHUP не перезагружаются.
mTLS к PostgreSQL
Задайте server_tls_certificate и server_tls_private_key. PostgreSQL должен быть настроен с ssl_ca_file, соответствующим подписанту клиентского сертификата, а у роли в pg_hba.conf со стороны PostgreSQL должно быть clientcert=verify-ca (или verify-full).
Observability
Серверный TLS покрывают три серии Prometheus:
| Метрика | Тип | Назначение |
|---|---|---|
pg_doorman_server_tls_connections | gauge на пул | Число активных TLS-соединений к PostgreSQL. |
pg_doorman_server_tls_handshake_duration_seconds | histogram на пул | Бакеты продолжительности handshake. |
pg_doorman_server_tls_handshake_errors_total | counter на пул | Неудавшиеся handshake. Алерт при ненулевой скорости. |
Смотрите Справочник Prometheus.
Известные ограничения
- Протокол
COPYповерх серверного TLS не покрыт BDD-тестами. Поведение должно работать, но не верифицировано. - Cancel-запросы к бэкенду минуют серверный TLS — они идут по свежему обычному TCP-соединению. Это совпадает с дизайном протокола PostgreSQL (cancel отправляется по отдельному сокету).
- Direct TLS handshake (быстрый handshake PG17 без
SSLRequest) не поддерживается ни на одной из сторон.
Куда дальше
- Настройка нового кластера? Смотрите Установку.
- Ротация сертификатов? Смотрите Binary Upgrade и Сигналы.
- Hardening существующего развёртывания? Сочетайте с pg_hba.conf: принудительный
hostsslдля нелокальных соединений.
Режимы пула
pg_doorman поддерживает два режима пула: transaction и session. Режим задаётся для пула, при необходимости переопределяется для конкретного пользователя.
Режима statement нет. Покул на уровне отдельного оператора ломает больше драйверов, чем помогает, а pg_doorman агрессивно оптимизирован под транзакционный режим (кеш prepared statements, direct handoff, FIFO-планирование).
Транзакционный режим (рекомендуется)
pools:
mydb:
pool_mode: "transaction"
Backend-соединение удерживается в течение транзакции и возвращается в пул при COMMIT, ROLLBACK или неявном завершении.
Именно этот режим даёт ту самую эффективность соединений, ради которой существует pg_doorman: pool_size равный 40 обслуживает тысячи клиентов, пока транзакции остаются короткими.
Что работает в транзакционном режиме (там, где большинство пулеров проваливаются):
- Prepared statements. pg_doorman кеширует их в рамках пула, переименовывает имена statement между backend-соединениями и прозрачно повторно готовит запрос. Драйверы, привязанные к
unnamed-statement (Go pgx, .NET Npgsql, Python asyncpg), работают без настройки. - Pipelined-батчи и асинхронный поток
Flush. - Cancel-запросы поверх TLS.
LISTEN/NOTIFY— но только внутри транзакции; кросс-транзакционные уведомления теряются (так же, как в PgBouncer).
Что в транзакционном режиме не работает:
SETиRESETвне транзакции. Используйте сессионный режим для клиентов, опирающихся на изменение GUC уровня сессии (SET TIME ZONE,SET search_pathодин раз на соединение).- advisory-блокировки, удерживаемые между транзакциями. Используйте сессионный режим.
- курсоры, удерживаемые вне транзакций (
WITH HOLD). Используйте сессионный режим. SET LOCALработает как ожидается — он ограничен транзакцией.
Сессионный режим
pools:
legacy_app:
pool_mode: "session"
Backend-соединение удерживается в течение клиентской сессии. В пул возвращается только при отключении клиента.
Используйте, когда:
- Приложение использует состояние уровня сессии (
SET search_path,SET TIME ZONE). - Приложение использует курсоры WITH HOLD.
- Приложение использует advisory-блокировки между транзакциями.
- Вы переезжаете с немодифицированного PgBouncer-развёртывания, работавшего в сессионном режиме, и хотите подмену один-в-один.
В сессионном режиме pool_size фактически равен максимальному числу одновременных клиентов. Размер пула подбирается так, чтобы соответствовать max_connections PostgreSQL минус резервы.
Переопределение для конкретного пользователя
Режим пула можно переопределить для конкретного пользователя:
pools:
mydb:
pool_mode: "transaction"
users:
- username: "app"
password: "md5..."
pool_size: 40
- username: "admin_tools"
password: "md5..."
pool_size: 4
pool_mode: "session"
Полезно, когда одному пользователю (инструменты эксплуатации, миграции) нужна семантика сессии, а основное приложение остаётся в транзакционном режиме.
Очистка при возврате в пул
Транзакционный режим в подробностях: когда бэкенд уходит обратно в пул, pg_doorman выполняет RESET ALL и ROLLBACK (если cleanup_server_connections: true, как по умолчанию). Это сбрасывает:
- значения
SETуровня сессии; - курсоры;
- имена prepared statements, которые драйвер привязал к конкретным backend-именам (кеш prepared statements pg_doorman переживает — он индексируется текстом запроса, а не именем statement на бэкенде);
- advisory-блокировки (
pg_advisory_unlock_allнеявно входит вRESET ALL).
DEALLOCATE ALL и DISCARD ALL со стороны клиента также инициируют сброс всего, что закешировано pg_doorman для этого клиента в кеше prepared statements. Кеш уровня пула не затрагивается.
Чтобы отключить очистку (ради производительности в жёстко контролируемых развёртываниях):
pools:
mydb:
pool_mode: "transaction"
cleanup_server_connections: false
Делайте так только если вы уверены, что приложение никогда не оставляет состояние сессии.
Справочник
- Параметр
pool_mode: Настройки пула. cleanup_server_connections: Настройки пула.- Размер пула: Pool Coordinator, Пул под нагрузкой.
Pool Coordinator
Pool Coordinator ограничивает суммарное число backend-соединений к одной базе по всем пользователям пула, а при достижении лимита применяет приоритетное вытеснение. Это то, чем должен был стать max_db_connections в PgBouncer: справедливое применение лимита, резерв на короткие всплески и минимум на пользователя, защищающий критичные нагрузки.
Эта страница объясняет концепцию и сценарии применения. Рецепты тюнинга и разбор вывода SHOW POOL_COORDINATOR смотрите в Пул под нагрузкой.
Какую задачу решает
Без координатора каждый user-пул независим. pool_size равный 40 у пяти пользователей означает до 200 backend-соединений — и PostgreSQL приходится бороться за свои собственные лимиты.
max_db_connections в PgBouncer ограничивает общую сумму, но блокирует новые запросы, как только лимит достигнут. Кто первым занял слоты, тот и удерживает их независимо от интенсивности использования, а медленные нагрузки никогда не уступают быстрым.
Pool Coordinator в pg_doorman ограничивает сумму и:
- Вытесняет idle-соединения у пользователей с переизбытком, когда другому пользователю нужно вырасти.
- Ранжирует пользователей по p95 времени транзакции, чтобы медленные пулы отдавали слоты первыми. Пользователи с быстрыми запросами сохраняют преимущество переиспользования; пользователи с длинными транзакциями отдают первыми, потому что их соединения и так дольше простаивали.
- Резервирует небольшое переполнение для коротких всплесков. Настраивается отдельно от основного лимита.
- Гарантирует минимум на пользователя, который никогда не вытесняется. Критичные нагрузки сохраняют опору при конкуренции.
Когда использовать
Включайте координатор, когда:
- На одной базе сосуществуют разные нагрузки, и нужен верхний предел числа backend-соединений (
max_connectionsPostgreSQL, RAM, файловые дескрипторы). - Одна нагрузка имеет всплеск нагрузки, и вы хотите, чтобы она забирала простаивающие слоты у других, не вытесняя их навсегда.
- Вы работаете рядом с потолком соединений PostgreSQL и нуждаетесь в справедливой деградации, а не в обслуживании по принципу «кто первый встал, того и тапки».
Координатор не нужен, когда:
pool_sizeкаждого пользователя достаточно мал, чтобы их сумма комфортно укладывалась вmax_connectionsPostgreSQL.- Нагрузки предсказуемы и заранее размечены.
- Вы хотите простоту уровня PgBouncer.
max_db_connectionsбез вытеснения поддерживается, но не рекомендуется для общих баз.
Конфигурация
pools:
shared_db:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
# Общий лимит на всех пользователей этого пула.
max_db_connections: 80
# Резерв сверх max_db_connections для коротких всплесков.
# Захватывается, только если в течение reserve_pool_timeout не нашлось idle-соединения.
reserve_pool_size: 16
reserve_pool_timeout: "3s"
# Страховка для пользователя: соединения никогда не вытесняются у него, даже под давлением.
# Сумма по пользователям должна быть <= max_db_connections.
min_guaranteed_pool_size: 5
# Льготный период для вытеснения: соединения младше этого возраста не вытесняются.
# Защищает от колебания, когда нагрузка кратковременно простаивает.
min_connection_lifetime: "30s"
users:
- username: "fast_app"
password: "md5..."
pool_size: 40
- username: "batch_job"
password: "md5..."
pool_size: 60
Эффективный потолок: max_db_connections + reserve_pool_size = 96. Резерв поглощает доли секундные пики; если пик затягивается, включается вытеснение.
Как выбирается донор
Когда пользователь запрашивает новый бэкенд, а лимит уже достигнут:
- Найти кандидатов с idle-соединениями. Пользователь, у которого все соединения активны, не может стать донором — его работа в полёте.
- Пропустить защищённых. Пользователь ниже
min_guaranteed_pool_sizeисключается. - Пропустить недавно созданные соединения. Соединения младше
min_connection_lifetimeне вытесняются (это снижает колебание при коротких idle-промежутках). - Ранжировать по излишку. Пользователи с наибольшим числом idle-соединений сверх
min_guaranteed_pool_sizeполучают высший ранг. - Tiebreaker — p95 времени транзакции. Среди пулов с одинаковым излишком первым отдаёт медленный. Их соединения, скорее всего, простаивали потому, что следующий запрос ещё готовится на стороне приложения.
Выбранное idle-соединение закрывается; запрашивающий пользователь получает свежее соединение из PostgreSQL.
Observability
SHOW POOL_COORDINATOR показывает текущее состояние по каждой базе:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
shared_db | 80 | 78 | 16 | 2 | 142 | 18 | 0
evictionsбыстро растёт — один пользователь голодает раз за разом. Поднимитеmax_db_connectionsили задайте этому пользователюmin_guaranteed_pool_size.reserve_acqвысокий — всплески нормальны, но размер занижен; рассмотрите повышениеmax_db_connectionsвместо опоры на резерв.exhaustionsненулевой — резерв тоже был полным. Клиенты упёрлись вquery_wait_timeout, ожидая бэкенд. Поднимите лимит.
Prometheus: pg_doorman_pool_coordinator{type="..."} (gauge) и pg_doorman_pool_coordinator_total{type="evictions|reserve_acquisitions|exhaustions"} (counter). Смотрите Команды администратора и Справочник Prometheus.
Оговорки
- Координатор работает только внутри одного пула (одной базы). Кросс-пуловые / кросс-баз ограничения не поддерживаются.
- Вытеснение выбирает idle-соединения; пользователь, удерживающий все соединения в длинных транзакциях, не может стать донором, и другие пользователи могут голодать. Если ваша форма нагрузки именно такая — поднимите
max_db_connectionsили разделите нагрузки. min_guaranteed_pool_size— это нижняя граница для вытеснения, а неmin_pool_sizeдля прогрева. Эти соединения пул всё равно создаёт по требованию.- Задавать
max_db_connectionsбезmin_guaranteed_pool_size— это режим PgBouncer: работает, но мелкие пользователи голодают под давлением. Для общих баз всегда задавайте оба.
Куда дальше
- Как подобрать размер пула с разобранными примерами: Пул под нагрузкой → Согласование лимита с PostgreSQL.
- Тюнинг под нагрузкой: Пул под нагрузкой → Параметры тюнинга.
- Чтение вывода администратора: Команды администратора → SHOW POOL_COORDINATOR.
- Режимы пула (транзакционный против сессионного): Режимы пула.
Давление на пул
Pool pressure (давление на пул) описывает, как pg_doorman ведёт
себя, когда множество клиентов одновременно запрашивают
backend-соединение, а в idle-пуле (очереди свободных, готовых к
выдаче серверных соединений; «idle» означает «открыто, но никем не
используется прямо сейчас») пусто.
Решение о том, кто получит соединение, кто подождёт, кто инициирует
свежий connect() к PostgreSQL, а кому будет отказано, принимают
два механизма. Локально в каждом пуле (database, user) работают
упреждающее ожидание (anticipation) — ожидание возврата соединения
от соседа прежде чем тратить ресурсы на новый connect() — и
ограничитель всплесков (bounded burst gate) — жёсткий лимит на
одновременные backend-connect() внутри одного пула. Поверх них при
необходимости подключается координатор (coordinator),
общий для всех пулов ограничитель суммарного числа backend-соединений
к одной базе данных.
Аудитория: DBA или инженер эксплуатации, который уже знает PgBouncer и хочет понять, чем pg_doorman отличается и за чем нужно следить.
Зачем нужно давление на пул
Возьмём пул с pool_size = 40 и нагрузкой в 200 коротких транзакций,
приходящих в одну и ту же миллисекунду. В пуле 4 idle-соединения. В
наивном пулере первые 4 клиента забирают idle-соединения, а оставшиеся
196 независимо вызывают connect() к PostgreSQL. PostgreSQL получает
196 одновременных TCP connect-попыток, на каждую из которых нужно
выполнить SCRAM-аутентификацию и согласование параметров, только чтобы
обнаружить, что пул разрешает ещё 36 соединений. Backend-обращения к
pg_authid взлетают всплеском, потолок max_connections пробивается,
очередь accept() ядра насыщается, а tail latency (задержка в
хвосте распределения, p99/p99.9, то есть редкие, но самые медленные
запросы) уже подключённых клиентов растёт, потому что postmaster
PostgreSQL занят порождением backend'ов вместо выполнения запросов.
Это thundering herd (лавинообразный эффект): ситуация, когда множество
независимых задач одновременно реагируют на одно состояние и
устраивают шторм одинаковых запросов к общему ресурсу. В нашем
сценарии это 196 одновременных connect() к одному PostgreSQL listener'у
вместо размазанного по времени потока.
Time: ----------------------------------------->
Client_1 -[idle hit]--[query]-----[done]
Client_2 -[idle hit]--[query]-----[done]
Client_3 -[idle hit]--[query]-----[done]
Client_4 -[idle hit]--[query]-----[done]
Client_5 -[connect]-[auth]-[query]-[done]
Client_6 -[connect]-[auth]-[query]-[done]
. ^
. 196 backend connect()s
. fired in the same instant
Client_200 -[connect]-[auth]-[query]-[done]
PostgreSQL: 196 spawning backends + 4 running queries
Давление на пул подавляет это поведение. pg_doorman заставляет
большинство из этих 196 вызовов переиспользовать соединение, которое
другой клиент вот-вот вернёт, либо подождать несколько миллисекунд за
небольшим числом уже идущих backend-connect-ов. Частота connect() к
PostgreSQL остаётся ограниченной даже при всплесках клиентов.
Режим простого пула (plain pool mode)
Этот режим работает, когда max_db_connections не задан. Пулы
независимы, общей координации между ними нет, давление управляется
внутри каждого пула (database, user). Это режим по умолчанию, и
большинство инсталляций находятся именно в нём.
Прогрев пула с холодного старта
Пул с pool_size = 40 и min_pool_size = 0 стартует с нулём
соединений. Первый пришедший клиент не ждёт: pg_doorman сразу создаёт
backend-соединение. Второй делает то же самое, третий тоже,
пока пул не достигнет порога прогрева (warm threshold).
Порог прогрева равен pool_size × scaling_warm_pool_ratio / 100. При
дефолтном значении 20% и pool_size = 40 порог равен 8 соединениям.
Ниже этого порога pg_doorman создаёт соединения без раздумий: пул
холодный, цена ожидания выше цены коннекта, и клиенты не могут
конкурировать за idle-соединения, которых не существует.
Выше порога активируется зона упреждающего ожидания (anticipation zone):
pg_doorman считает, что пул уже разогрет и кто-то
из параллельных клиентов скоро вернёт занятое соединение, поэтому имеет
смысл подождать его возврата вместо того, чтобы тратить ресурсы на
новый connect(). Когда клиент не находит соединения в idle pool,
pg_doorman сначала пытается перехватить такой возврат.
Третья зона накладывается поверх обеих: при любом размере пула, если
число соединений, создаваемых прямо сейчас, достигает
scaling_max_parallel_creates (по умолчанию 2), пул упирается в
лимит ограничителя всплесков. Дополнительные вызовы ждут свободный слот независимо
от того, сколько idle-соединений существует.
Three pressure zones
--------------------
Pool size: 0 ----------- 8 ---------------------------- 40
^ ^ ^
| | |
| WARM ZONE | ANTICIPATION ZONE |
| | |
| size < | size >= warm_threshold |
| warm_thr | |
| | |
| Пропускает | Фаза 3: быстрый опрос |
| фазы 3 | Фаза 4: прямая передача |
| и 4. | (ожидание возврата от |
| Сразу к | соседа, дедлайн = |
| фазе 5 | wait_timeout − 500ms) |
| (огранич. | Затем фаза 5 |
| + connect) | |
Ограничение параллельных создаваемых соединений
(ортогонально размеру пула)
-----------------------------------------------
Создаваемых сейчас: 0 ---- 1 ---- 2 (= scaling_max_parallel_creates)
^
| На лимите: новый вызов встаёт
| в очередь на получение
| возвращённого соединения и ждёт
| завершения чужого создания.
Зоны warm и anticipation отслеживают текущий размер пула. Отдельно
работает bounded burst gate: жёсткий лимит на число одновременно
идущих connect() к PostgreSQL внутри одного пула. Этот лимит
срабатывает независимо от размера пула: пул может одновременно
находиться в anticipation zone и иметь все слоты burst gate занятыми
параллельными connect()-ами, под нагрузкой это типичная ситуация.
Пул ниже порога прогрева тоже может упереться в этот лимит, если во
время холодного заполнения одновременно приходит много клиентов.
Получение соединения
Когда клиент запрашивает соединение через pool.get(), pg_doorman
проходит по следующим фазам. Каждая фаза либо возвращает соединение,
либо передаёт управление следующей фазе.
Фаза 1 — горячий путь recycle. Берём первое соединение из
idle-очереди (двусторонняя очередь свободных серверных соединений
«голова очереди» — это то соединение, которое было возвращено раньше
всех). Если оно проходит проверку пригодности (recycle), отдаём его
клиенту.
Проверка пригодности откатывает любые открытые транзакции, запускает
проверку живости (liveness probe) если соединение простояло дольше
server_idle_check_timeout, и сверяет поколение соединения с текущим
поколением пула (reconnect-эпоха — счётчик, который увеличивается при
admin-команде RECONNECT и при обнаруженных сбоях backend-а).
Соединения, созданные до увеличения счётчика, эту проверку не проходят
и удаляются вместо возврата клиенту. Здоровый пул в установившемся
режиме идёт только этим путём.
Фаза 2 — warm zone gate. Если размер пула ниже порога прогрева, пропускаем anticipation и сразу переходим к созданию нового backend-соединения. Холодные пулы заполняются быстро.
Фаза 3 — anticipation spin. Выше порога прогрева повторяем
проверку пригодности до 10 раз в плотном цикле без пауз (контролируется
параметром scaling_fast_retries). Так перехватывается случай, когда
другой
клиент завершил свой запрос в том же микросекундном диапазоне и
вот-вот вернёт соединение. Полная стоимость порядка 10–50
микросекунд. Без sleep, без блокирующего I/O.
Фаза 4 — direct handoff (прямая передача). Если spin не поймал возврат, задача встаёт в очередь ожидающих. Когда любой клиент возвращает соединение, оно отправляется напрямую старейшему ожидающему, минуя idle-очередь целиком. Получатель забирает соединение без конкуренции с другими задачами — соединение никогда не попадает в общую idle-очередь и достаётся ровно одному адресату.
Если передача удалась, соединение проходит проверку пригодности. При успехе соединение возвращается вызывающему. При ошибке (устаревший backend) пул уменьшает текущий размер, и вызов проваливается в путь создания нового соединения.
Если соединение не пришло до дедлайна, ожидающий снимается с очереди. При попытке доставки пул обнаруживает снятого ожидающего, пропускает его и пробует следующего. Таким образом устаревшие ожидающие вычищаются по мере обработки очереди, без отдельного прохода.
Дедлайн адаптивный: min(query_wait_timeout - 500 ms, adaptive_cap),
где adaptive_cap вычисляется из реальной latency транзакций:
| Состояние пула | Бюджет | Пример |
|---|---|---|
| Холодный старт (нет данных) | 100ms ± 20% jitter | 80-120ms |
| Устойчивый режим (steady state) | xact_p99 × 2 ± 20% jitter | p99=0.7ms → 5ms (min); p99=50ms → 100ms |
| Высокая latency | Ограничено 500ms | p99=300ms → 500ms |
Фаза 1/2 ожидания семафора тратит из того же бюджета, поэтому
суммарное ожидание не может увести клиента за его query_wait_timeout.
Случайный разброс (jitter) ±20% предотвращает обрыв таймаутов (timeout cliff): без него N клиентов, вошедших в Phase 4 одновременно, выходят одновременно и лавиной входят в burst gate, создавая N новых backend-соединений для пула, которому нужно значительно меньше. С jitter'ом клиенты выходят порциями — первые создают соединения, и к моменту выхода последних эти соединения уже использованы и возвращены в idle queue для переиспользования.
Если дедлайн истёк без получения соединения, переходим к фазе 5.
Фаза 5 — ограничитель всплесков (bounded burst gate). Перед тем
как пойти на новый connect(), задача должна получить слот в
ограничителе — механизме, который параллельно пропускает не больше
scaling_max_parallel_creates (по умолчанию 2) задач на пул. Если
слот свободен, задача забирает его и идёт вызывать connect(). Если
все слоты заняты, задача встаёт в очередь на прямую передачу и
одновременно ожидает завершения чужого создания. Приоритет отдаётся
прямой передаче: если соединение вернулось, пока задача ждала
освобождения слота, оно доставляется напрямую — задача проверяет его
пригодность и возвращает клиенту. Если передачи не было, задача снова
пробует проверку пригодности и захват слота. Так лимитируется скорость
рождения новых backend-соединений в одном пуле, а не размер самого
пула.
Адаптивный таймаут ограничителя всплесков (adaptive timeout burst
gate). Цикл ограничителя лимитирован адаптивным бюджетом:
xact_p99 × 2 ± 20% jitter (min 20 ms, max 500 ms). Если задача
провела в цикле дольше бюджета, она прекращает ожидать прямую
передачу и переходит к захвату слота ограничителя напрямую. Без этого
механизма пул мог застрять на пороге прогрева навсегда: клиенты
бесконечно получали переиспользованные соединения через прямую
передачу, а до создания нового соединения дело не доходило. Счётчик
burst_gate_budget_exhausted отслеживает срабатывания.
Фаза 6 — backend connect. Запускаем connect(), аутентифицируемся,
отдаём соединение клиенту. Слот ограничителя освобождается автоматически по
завершении этой фазы независимо от исхода.
Plain mode acquisition flow
---------------------------
pool.get()
|
v
+--------------+
| Phase 1: | --- HIT ----> return idle connection
| recycle pop |
+------+-------+
| MISS
v
+--------------+
| Phase 2: | --- below warm ---> jump to phase 5
| warm gate |
+------+-------+
| above warm
v
+--------------+
| Phase 3: | --- HIT ----> return idle connection
| fast spin |
+------+-------+
| MISS
v
+--------------+
| Phase 4: | --- handoff ----> return connection
| anticipate | --- timeout ----> fall through
| direct h/o |
+------+-------+
|
v
+--------------+
| Phase 5: | --- slot taken --> proceed to phase 6
| burst gate | --- slot full --> wait, retry recycle
+------+-------+
|
v
+--------------+
| Phase 6: |
| connect() | ----> return new connection
+--------------+
Подавление всплеска в действии
Тот же сценарий с 200 клиентами в формате thundering herd, но теперь
в plain mode и с scaling_max_parallel_creates = 2:
Time: t=0ms t=5ms t=10ms t=15ms t=20ms t=25ms
C_1 [idle]--[query]-[done]
C_2 [idle]--[query]-[done]
C_3 [idle]--[query]-[done]
C_4 [idle]--[query]-[done]
C_5 [spin/wait]------[recycled C_1]--[query]-[done]
C_6 [spin/wait]------[recycled C_2]--[query]-[done]
C_7 [gate=1]-[connect]----[auth]--[query]-[done]
C_8 [gate=2]-[connect]----[auth]--[query]-[done]
C_9 [gate full, wait]---[recycled C_3]--[query]
C_10 [gate full, wait]---[recycled C_4]--[query]
.
. [...196 clients use a mix of recycle, anticipation, and at
. most 2 in-flight connects...]
.
C_200 [gate=2]-[connect]--[auth]--[query]--[done]
PostgreSQL: at most 2 spawning backends at any moment
+ the 4 connections that were already there
Тот же пул обслуживает все 200 клиентов, но PostgreSQL никогда не видит
больше scaling_max_parallel_creates (по умолчанию 2) одновременных
запусков backend-процессов из этого пула. Большинство клиентов попадают на
переиспользованное соединение от соседа, который завершил работу
мгновением раньше, а не на свежий connect().
Получение соединения без ожидания
Когда клиент устанавливает query_wait_timeout = 0, он просит либо
сразу попасть на idle-соединение, либо свежий connect, без ожидания.
Фаза anticipation и ожидание у burst gate пропускаются. pg_doorman
выполняет recycle на горячем пути, один раз пробует burst gate, затем
либо создаёт соединение, либо возвращает ошибку wait timeout.
Ограничение при включённом coordinator. Режим без ожидания
пропускает только anticipation и ожидание у burst gate внутри пути в
рамках одного пула. Если max_db_connections задан и фазы ожидания
координатора (B–D) занимают время, такой вызов всё равно блокируется
внутри coordinator.acquire() вплоть до reserve_pool_timeout (по
умолчанию 3000 ms). Чтобы на базах под координатором соблюсти строгий
дедлайн без ожидания, поставьте reserve_pool_timeout достаточно
низким, чтобы он помещался в ваш бюджет.
Фоновый replenish
Когда задан min_pool_size, фоновая задача периодически дополняет пул
до его минимума. Она использует тот же burst gate, что и клиентский
трафик. Эта задача не встаёт в очередь за занятым gate: если
gate занят, она немедленно сдаётся и повторяет попытку
на следующем retain-цикле (по умолчанию каждые 30 секунд, контролируется
параметром retain_connections_time).
Логика такова: во время всплеска нагрузки клиенты уже насыщают gate,
создавая соединения, которые им нужны прямо сейчас. Если фоновый
replenish будет конкурировать с ними за слоты, толку не будет: пул всё равно
поднимется выше min_pool_size за счёт клиентских запросов на создание.
При каждом таком отступлении фоновой задачи увеличивается счётчик
replenish_deferred.
Следствие: под нагрузкой min_pool_size поддерживается в режиме
best-effort (без гарантий: pg_doorman старается, но не обязуется
держать прогретыми ровно столько соединений, если клиенты съедают весь
бюджет). Если нужна жёсткая гарантия минимума, см. раздел
troubleshooting.
Прямая передача при возврате (direct handoff)
Когда соединение возвращается, пул первым делом проверяет очередь ожидающих (direct-handoff). Если хотя бы один ожидающий зарегистрирован, соединение отправляется напрямую старейшему из них, минуя idle-очередь. Ожидающие, чей вызывающий уже отвалился по таймауту, пропускаются: пул обнаруживает недоступного получателя и пробует следующего в очереди.
Если зарегистрированных ожидающих нет (типичный случай при высоком throughput, когда каждый checkout попадает в горячий путь), соединение кладётся в idle-очередь и будит ближайшего клиента, ожидающего на Фазе 1/2.
В обоих случаях координатор (если настроен) уведомляется о возврате, чтобы ожидающие Фазы C из соседних пулов могли просканировать кандидатов для eviction. Ожидающие внутри того же пула получают соединения напрямую, а не через общее уведомление.
FIFO-честность и распределение латенси
Очередь ожидающих — двусторонняя очередь (FIFO): новые ожидающие добавляются в конец, соединения доставляются из начала. Старейший ожидающий всегда получает следующее возвращённое соединение.
Это даёт измеримо другой профиль латенси по сравнению с пулерами, использующими широковещательное пробуждение (broadcast-notify) или LIFO-планирование. При 500 клиентах на пул из 40 соединений (AWS Fargate):
| Пулер | p50 (мс) | p95 (мс) | p99 (мс) | p99/p50 |
|---|---|---|---|---|
| pg_doorman | 9.93 | 10.50 | 10.69 | 1.08 |
| pgbouncer | 8.48 | 9.62 | 10.45 | 1.23 |
| odyssey | 0.88 | 12.93 | 22.46 | 25.5 |
p50 у Odyssey в 11 раз ниже, чем у pg_doorman — большинство транзакций попадают на горячее соединение мгновенно. Но p99 в 2 раза выше. Одни клиенты ждут больше 22 мс, а другие завершаются меньше чем за 1 мс. При FIFO каждый клиент платит примерно одинаковую цену за очередь.
Что это значит для эксплуатации:
-
SLO compliance. SLO «p99 < 15 мс» достижим с pg_doorman при этой нагрузке. С Odyssey те же настройки пула его нарушают. Единственный выход — overprovisioning: увеличивать число соединений, пока даже невезучие клиенты укладываются.
-
Отсутствие голодания. При broadcast-notify клиент может проиграть гонку за wake-up многократно. При direct handoff соединение идёт ровно одному получателю, протухшие waiter'ы пропускаются. Нет thundering herd, нет повторных проигрышей.
-
Предсказуемое capacity planning. Когда p50 ≈ p99, удвоение числа клиентов примерно удваивает латенси. При соотношении хвоста 25x изменение нагрузки вызывает непредсказуемые всплески p99.
Теория очередей подтверждает: среди non-preemptive дисциплин планирования FIFO минимизирует дисперсию времени ожидания при том же среднем, что и LIFO. Среднее одинаково — разница только в хвосте.
Упреждающая замена при истечении lifetime (pre-replacement)
Когда настроен server_lifetime, backend-соединения закрываются по
достижении индивидуального лимита (базовый ± 20% jitter). Закрытие
означает, что в пуле на одно idle-соединение меньше — последующие
checkout'ы могут попасть в зону упреждающего ожидания или путь
создания нового соединения, добавляя несколько миллисекунд к p99 во
время кластеров истечения lifetime.
Упреждающая замена (pre-replacement) убирает этот всплеск задержки. Когда checkout проверяет пригодность соединения и обнаруживает, что оно достигло 95% своего lifetime, фоновая задача создаёт замену и помещает её в idle-очередь. Когда старое соединение отклоняется при 100% lifetime, следующий checkout находит предсозданную замену через горячий путь — ноль ожидания.
Параллельно может работать до 3 упреждающих замен на пул. Во время
окна перекрытия пул временно держит max_size + 3 соединений. Когда
старые соединения умирают, текущий размер пула возвращается к
max_size.
Условия, предотвращающие неконтролируемый рост:
| Условие | Предотвращает |
|---|---|
| Пул не под давлением | Создание лишних при насыщении пула (старое соединение выживет, пропустив закрытие по lifetime) |
| Доля idle-соединений < 25% | Замену в переразмеренном пуле, который должен сжаться |
| Запас координатора >= 2 | Захват последнего слота координатора у соседнего пула |
| Lifetime >= 60 s | Срабатывание на коротких lifetime, где окно перекрытия слишком мало |
| Текущий размер <= max_size + cap | Накопление нескольких параллельных превышений |
| Лимит параллельных фоновых создаваемых (cap=3) | Неограниченное число фоновых создаваемых соединений |
Упреждающая замена срабатывает только на пути checkout (при проверке пригодности), не из фоновой задачи обслуживания. Idle-соединения, истекающие без checkout'а, закрываются фоновой задачей без замены — так пул естественно сжимается при падении нагрузки.
Согласование лимита с PostgreSQL
Перед чтением про координатор проверьте, что число backend-соединений
в худшем случае умещается в PostgreSQL. Без max_db_connections верхняя
граница для одной базы:
N pools (users) × pool_size = ceiling on backend connections
Пример с расчётом: три пула, у каждого pool_size = 40, без
max_db_connections. В худшем случае получится 120 одновременных
backend-соединений к этой базе, ограниченных только
scaling_max_parallel_creates на пул (по умолчанию 2 на каждый, то
есть до 6 одновременно идущих вызовов connect()). Если в PostgreSQL
выставлен max_connections = 100, база начнёт отказывать в новых
соединениях во время общего всплеска нагрузки, и клиенты получат
FATAL: too many connections.
Два решения:
- Понизить
pool_sizeтак, чтобыN × pool_sizeукладывалось нижеmax_connectionsс запасом наsuperuser_reserved_connections, слоты репликации и любые прямые коннекторы в обход pg_doorman. - Задать
max_db_connectionsкак жёсткий лимит (см. следующий раздел).
Эмпирическое правило: суммарная потребность pg_doorman не должна
превышать 80% от PostgreSQL max_connections - superuser_reserved_connections.
Оставшиеся 20% оставьте под admin-соединения, репликацию и всплески.
Режим координатора (coordinator mode)
Режим координатора активируется, когда у пула задан max_db_connections.
Он добавляет второй слой давления поверх того, что работает внутри
каждого пула: общий семафор, который ограничивает суммарное число
backend-соединений к базе по всем обслуживающим её user-пулам. Без
него единственным ограничением остаётся потолок N × pool_size из
предыдущего раздела. С max_db_connections = 80 к базе одновременно
может существовать только 80 соединений независимо от конфигурации
пулов, и координатор решает, какие пулы могут расти.
При max_db_connections = 0 (по умолчанию) координатор не создаётся.
Когда параметр задан, все механизмы plain mode, описанные выше,
продолжают работать; координатор добавляет один шаг получения
permit (разрешения на удержание слота в общем лимите на базу:
пока permit не получен, backend-соединение создавать нельзя) на
пути создания нового соединения. Переиспользование idle координатора
не касается.
Что добавляет координатор
Три вещи:
-
Жёсткий лимит общего числа соединений к базе. Если 80 уже занято, 81-й запрос ждёт или падает независимо от того, какой пул его подал.
-
Reserve pool (резервный пул). Когда общий лимит достигнут и у
reserve_pool_sizeесть свободное место, координатор сразу выдаёт permit из резерва — небольшого дополнительного пула поверхmax_db_connections, работающего как буфер под всплеск. Это Фаза R (reserve-first) в схеме ниже: ни одного соседнего backend не закрывается, ни одного ожидания не возникает. Резерв ограниченreserve_pool_size(по умолчанию 0, то есть выключен) и приоритизирован: голодающие пользователи (те, кто ниже своего эффективного минимума) и пользователи с большим числом ожидающих клиентов обслуживаются первыми через арбитра. -
Eviction (выселение). Fallback на случай, когда резерв выключен (
reserve_pool_size = 0) или уже полностью занят: координатор закрывает idle-соединение из пула другого пользователя, чтобы освободить main-слот. Кандидаты сортируются по p95 времени транзакции (по убыванию): медленные пулы отдают первыми, потому что лучше переносят стоимость пересоздания (1 ms pool wait добавляет 6.7% к 15 ms p95, но 104% к 0.96 ms p95). Излишек над эффективным минимумом — tiebreaker среди пулов с похожим p95. Только соединения старшеmin_connection_lifetime(по умолчанию 30 000 ms) попадают в список. 30-секундный порог подавляет циклический reconnect между соседними пулами, которые по очереди воруют слоты друг у друга.Эффективный минимум для user-пула равен
max(user.min_pool_size, pool.min_guaranteed_pool_size). Оба параметра защищают соединения от eviction; побеждает больший. Если снизить любой из них, эффективный минимум становится меньше и пользователь становится более доступен для eviction.
Фазы получения permit в координаторе
Когда путь внутри пула доходит до шага создания нового соединения, координатор проходит шесть фаз. Первая фаза, выдавшая permit, завершает последовательность.
Фаза A — Try-acquire (неблокирующая попытка: вернуть permit сразу, если есть свободный слот, иначе немедленно сообщить об отсутствии). Если лимит не достигнут, забираем слот и возвращаемся.
Фаза R — Reserve-first. Фаза A установила, что база заполнена. До
того как закрыть хоть одно соседское backend-соединение, координатор
проверяет, есть ли место в резервном пуле (reserve_in_use < reserve_pool_size). Если есть — сразу запрашивает permit у reserve
arbiter. При успехе вызывающий получает reserve-permit без eviction,
без закрытия соседнего backend и без ожидания на connection_returned.
В обычных условиях арбитр отвечает за доли миллисекунды.
Это путь, который держит p99 латенси низкой: reserve-permit стоит
одного round-trip с арбитром, тогда как старая последовательность
(Фаза B + Фаза C) могла заблокировать клиента на полный
reserve_pool_timeout, даже если в резерве было свободно. Фаза R не
работает при reserve_pool_size = 0 и проваливается в Фазу B, если
арбитр отказывает (все reserve permit-ы уже заняты, либо идёт гонка
с другим вызывающим).
Фаза B — Eviction. Выполняется, когда Фаза R не выдала permit:
reserve_pool_size = 0, либо резервный семафор был полностью занят
на момент проверки (reserve_in_use == reserve_pool_size), либо
арбитр отказал. Обходим все остальные user-пулы той же базы,
сортируем по p95 времени транзакции (по убыванию, медленные
первые) с излишком (spare) как tiebreaker, и закрываем одно
idle-соединение старше min_connection_lifetime у верхнего кандидата.
Permit выселенного соединения освобождается синхронно, слот становится
доступен сразу. Повторяем захват семафора. Если два вызова конкурируют,
проигравший переходит к следующей фазе. p95 кешируется каждые 15
секунд (stats cycle) — сканирование читает одно кешированное значение
на кандидата без блокировки гистограммы.
Фаза C — Wait. Выполняется, когда резерв отключён или полностью занят и Фаза B не нашла что выселить. Регистрируется подписка на уведомления, которая срабатывает на двух событиях:
- Был освобождён permit координатора соседнего пула — серверное
соединение физически закрыто (истёк
server_lifetime, ошибка проверки пригодности,RECONNECT), и слот семафора теперь свободен. - Соседний пул вернул соединение в свою idle-очередь — слот семафора НЕ освободился, но излишек над минимумом этого соседа только что вырос.
На каждое пробуждение Фаза C сначала пытается захватить свободный слот неблокирующей проверкой, и только если дешёвый путь не сработал — пробует выселение. Пробуждение от освобождения permit-а оставляет свободный слот в семафоре — дешёвый путь берёт его, и ни один соседний backend не закрывается. Пробуждение от idle-return не освобождает слот напрямую, но могло вырастить излишек соседа, поэтому повторная попытка eviction находит кандидата, которого мгновение назад не было, освобождает permit соседа, и следующая неблокирующая проверка срабатывает. Этот порядок (дешёвый путь сначала, выселение потом) закреплён регрессионным тестом: будущий рефакторинг не сможет случайно вернуть закрытия соседних backend-ов на пробуждениях от drop permit-а.
Ждём до reserve_pool_timeout (по умолчанию 3000 ms) либо пробуждения,
либо дедлайна. Этот таймаут применяется даже при
reserve_pool_size = 0: он задаёт бюджет всей фазы wait, а не только
окно входа в резерв. Если ваш query_wait_timeout короче, чем
reserve_pool_timeout, клиент сдаётся первым, и вы видите ошибки
wait timeout вместо более наглядной
all server connections to database 'X' are in use. Разбор симптома
есть в разделе troubleshooting.
Фаза D — Reserve retry. Фаза R уже пробовала этот путь один раз.
Фаза D повторяет попытку после того, как Фаза C исчерпала свой
бюджет ожидания — на случай, если за время ожидания соседний
reserve-держатель отпустил свой permit. Запросы ранжируются по паре
(starving, queued_clients), где starving означает, что пул сейчас
ниже своего эффективного минимума. Арбитр — это отдельный фоновый
процесс, который раздаёт permit-ы резервного пула из приоритетной
очереди.
Фаза E — Error. Если Фаза D тоже не выдала permit или резерв не
настроен, клиент получает ошибку:
all server connections to database 'X' are in use (max=N, ...).
Повышение из резерва в основной пул (reserve → main upgrade, retain task)
Reserve-permit — это буфер под всплеск, а не постоянное состояние.
После того как всплеск прошёл, backend, получивший reserve-permit,
продолжает жить как обычное idle-соединение, но его
permit координатора по-прежнему учитывается в reserve_in_use — даже
когда current < max_db_connections и в main-семафоре есть свободные
слоты. Без активного обслуживания SHOW POOL_COORDINATOR показывал
бы занятый резерв при том, что реальная ёмкость для всплеска пустая, и
следующему всплеску некуда расти.
Retain task запускается каждые retain_connections_time (по умолчанию
30 секунд) и делает бухгалтерскую перестановку: для каждого пула,
который не находится под давлением (см. определение ниже), он
обходит idle-очередь и для каждого backend-а, удерживающего
reserve-permit, пытается
забрать permit из main-семафора.
Пул считается под давлением, когда его per-pool семафор имеет
ноль свободных permit-ов. Одной колонки в SHOW POOLS, которая
напрямую показывала бы состояние семафора, нет, и наблюдаемые
колонки отстают от внутреннего состояния:
- Строгий прокси:
sv_active == pool_size. Каждое активное серверное соединение держит permit, поэтому когда все серверы в пуле активны, все permit-ы заняты. Это направление строгое. - Слабый прокси:
cl_waiting > 0означает, что как минимум один клиент находится внутриtimeout_get— это часто означает, что семафор пуст, но клиент, который уже взял permit и припарковался в Фазе 4 anticipation или Фазе C координатора, тоже числится ожидающим. Используйте как индикатор, не как доказательство.
Retain task пропускает пулы под давлением по двум причинам: upgrade
в такой момент просто отдаёт слот ожидающему клиенту (нет эффекта
на reserve_used), а закрытие reserve-соединения заставит этого
клиента делать свежий connect(). Очистка отработает на следующем
цикле. При успехе
reserve-permit возвращается в reserve-семафор, reserve_in_use
уменьшается на единицу, а тип permit у этого backend переключается с
reserve на main. Никакого переподключения, никакого дёргания соседа.
Обход прерывается на первом неудачном upgrade
в пуле: это доказывает, что main-семафор заполнен, и остальные
reserve-permit-ы этого пула проверять бессмысленно. Тот же retain-цикл
затем закрывает reserve-backend-ы, которые не удалось повысить до
основного и которые
простаивают дольше min_connection_lifetime.
При такой схеме reserve_in_use > 0 означает ровно одно: либо
всплеск сейчас идёт, либо он закончился не более чем
retain_connections_time назад. Исторический остаток reserve-ёмкости
сходится к нулю, как только в main появляется свободное место.
Получение permit координатора по требованию (JIT coordinator permits, burst gate первым)
Внутри пути получения соединения burst gate работает до
координатора. Это JIT (just-in-time) порядок: coordinator-permit
берётся только когда вызов уже занял burst gate слот и готов вызвать
connect().
Предыдущий порядок (координатор первый, потом gate) вызывал фантомные permit-ы (phantom permits): N вызовов захватывали по coordinator-permit и вставали в очередь за burst gate (cap=2). Реально создавали соединения только 2, но координатор видел N permit'ов в использовании и начинал выдавать reserve-permit'ы — хотя БД была далеко от предела.
С JIT-порядком в каждый момент coordinator-permit'ы держат не более
max_parallel_creates вызовов.
Остальные ждут gate слот без расходования бюджета координатора.
Блокировка головы очереди (head-of-line blocking) снимается разделением координатора на быстрый и медленный путь. Быстрый — неблокирующая проверка доступности слота координатора внутри слота ограничителя (мгновенно). Если не прошла — вызов освобождает слот ограничителя, ждёт координатора (eviction / возврат от соседа), и затем снова занимает слот ограничителя.
Coordinator + plain mode acquisition flow (JIT)
-----------------------------------------------
pool.get()
|
v
Phase 1: hot path recycle --- HIT ---> return
| MISS
v
Phase 2: warm gate --- below ---+
| above warm |
v |
Phase 3: fast spin --- HIT ---> return
| MISS |
v |
Phase 4: direct handoff --- HIT ---> return
| deadline |
v |
| <----------------------------------+
v
Phase 5: bounded burst gate (scaling_max_parallel_creates)
| slot acquired
v
+---------------------------+
| JIT coordinator acquire | only when max_db_connections > 0
| fast: неблокир. проверка | мгновенный ответ
| slow: release gate slot | ожидание координатора (evict/return)
| → re-acquire slot | затем продолжить create
+------------+--------------+
| permit granted
v
Phase 6: server_pool.create()
|
v
return new connection
Фазы пронумерованы так же, как в plain mode. Coordinator acquire
работает внутри burst gate слота, когда max_db_connections > 0.
В plain mode он не работает.
Когда координатор настроен, но лимит не достигнут
Если max_db_connections = 80, а текущее использование 30, фаза A
координатора всегда успешна. Фазы B–E никогда не запускаются. Поведение
идентично plain mode плюс одна атомарная инкрементация семафора на
каждое новое соединение. Горячий путь (повторное использование idle)
координатора вообще не касается, поэтому там у него нет измеримой
стоимости. Платят только новые создания соединений, и платят ровно
одной атомарной операцией.
По устройству координатор работает как лимит, а не очередь: он стоит ресурсов только когда вы упираетесь в потолок.
Фоновый replenish под координатором
replenish берёт permit координатора через неблокирующую проверку
доступности. Если база уже на лимите, replenish сдаётся и
повторяет попытку на следующем retain-цикле. Та же логика, что и у
burst gate: фоновая задача не должна бороться с клиентским трафиком
за скудные permit-ы.
Параметры тюнинга
Scaling-параметры по умолчанию глобальные. Для
scaling_warm_pool_ratio и scaling_fast_retries можно задать
переопределение в каждом пуле. scaling_max_parallel_creates
настраивается только глобально, переопределений на уровне пула для
него нет.
| Параметр | По умолчанию | Где | Что делает |
|---|---|---|---|
scaling_warm_pool_ratio | 20 (процент) | general, per-pool | Порог, ниже которого соединения создаются без anticipation. Ниже pool_size × ratio / 100 каждый запрос нового соединения идёт сразу к connect(). |
scaling_fast_retries | 10 | general, per-pool | Число быстрых повторных проверок пригодности в фазе anticipation перед переходом к прямой передаче (ожиданию возврата от соседа). |
scaling_max_parallel_creates | 2 | general | Жёсткий лимит одновременно идущих backend-connect() на пул. Задачи сверх лимита ждут возврата idle-соединения или завершения чужого создания. Должен быть >= 1. |
max_db_connections | не задан (выключено) | per-pool | Лимит суммарного числа backend-соединений к базе по всем user-пулам. Когда не задан, координатор не создаётся. |
min_connection_lifetime | 30000 (ms) | per-pool | Минимальный возраст idle-соединения, после которого координатор может выселить его в пользу другого пула. 30-секундный порог подавляет циклический reconnect между соседними пулами. |
reserve_pool_size | 0 (выключено) | per-pool | Дополнительные permit-ы координатора поверх max_db_connections, выдаваемые по приоритету при исчерпании основного пула. |
reserve_pool_timeout | 3000 (ms) | per-pool | Максимальное время ожидания координатора перед переходом к reserve pool. |
min_guaranteed_pool_size | 0 | per-pool | Минимум на пользователя, защищённый от eviction координатором. Соединения пользователя, у которого current_size <= min_guaranteed_pool_size, не могут быть выселены другими пользователями. |
Когда повышать scaling_max_parallel_creates
Повышайте, если:
burst_gate_waitsустойчиво растёт между скрейпами, иreplenish_deferredтоже ненулевой: клиентский трафик и фоновая задача оба борются за слоты, которых нет;connect()к бэкенду быстрый (< 50 ms), и у PostgreSQL есть запас поmax_connections;- скачки задержки соединения совпадают с ростом частоты
burst_gate_waits.
Жёсткий потолок. Не поднимайте scaling_max_parallel_creates выше
ни одного из этих лимитов:
pool_size / 4для самого маленького пула, в котором используется этот параметр. Выше этого значения лимит теряет смысл: одновременно создаётся половина пула, и сглаживание всплеска ломается.(PostgreSQL max_connections - superuser_reserved_connections) / (10 × N pools), гдеN poolsобозначает все пулы, делящие этот инстанс PostgreSQL. Выше этого значения суммарная частота одновременных коннектов превышает то, что бэкенд успевает обрабатывать, не переполняя очередьaccept().
Понижайте, если:
connect()к PostgreSQL дорогой (> 200 ms, например, SSL с проверкой сертификата или медленный lookuppg_authid);- в логах PostgreSQL появляется конкуренция за
pg_authid; - бэкенд жалуется на переполнение очереди
accept().
Симптом слишком низкого значения: частота burst_gate_waits растёт
быстрее, чем частота прихода клиентов. Симптом слишком высокого:
задержка connect() к PostgreSQL растёт, и connection storm
(шторм одновременных connect() к бэкенду, последствие thundering
herd) возвращается.
Размер при множестве пулов. Суммарный потолок одновременных
коннектов равен N pools × scaling_max_parallel_creates. Если за
одним PostgreSQL стоит 10 пулов и в любой момент суммарно по ним всем
вам нужно не более 8 одновременных backend-коннектов, поставьте
scaling_max_parallel_creates около desired_aggregate / N pools,
округляя вниз. Ниже 1 нельзя; если арифметика даёт меньше единицы,
уменьшайте N pools, объединяя пользователей.
Когда повышать scaling_warm_pool_ratio
Повышайте, если:
- пулы медленно прогреваются на старте, и
min_pool_sizeне используется; - клиенты ждут anticipation в момент, когда пул в основном пуст (anticipation активируется только выше порога прогрева, поэтому такого быть не должно, но более высокий ratio дополнительно сужает окно, в котором это может случиться).
Понижайте, если:
- пулы избыточны по размеру, и вы хотите, чтобы anticipation начинала подавлять создания на меньших значениях текущего размера.
Этот параметр редко требует вмешательства. Значение по умолчанию 20% подходит большинству нагрузок.
Когда задавать max_db_connections
Задавайте, если:
- один хост PostgreSQL обслуживает несколько пулов
(database, user), и суммаpool_sizeпо всем пулам превышаетmax_connectionsбазы; - нужен жёсткий потолок, который сохранится даже при неправильной конфигурации какого-то отдельного пула;
- нужна общая честность между пулами через eviction.
Оставляйте незаданным, если:
- один пул обслуживает одну базу, и
pool_sizeостаётся единственным ограничением; - вам не нужна никакая eviction между пулами (некоторые нагрузки предпочитают жёсткую изоляцию между пользователями).
reserve_pool_size и reserve_pool_timeout
Резерв работает как временный клапан переполнения, а не как
дополнительная постоянная ёмкость. Он предотвращает видимые клиенту
ошибки исчерпания во время коротких всплесков. В нормальном режиме
reserve_in_use большую часть времени должен быть равен нулю.
Эмпирическое правило размера: reserve_pool_size ≤ 0.25 × max_db_connections.
За этим порогом резерв перестаёт вести себя как буфер. Если половина
нагрузки постоянно лежит в резерве, поднимайте max_db_connections,
а не расширяйте клапан переполнения.
reserve_pool_timeout задаёт, сколько клиент ждёт в фазе C координатора
перед обращением к резерву. Значение 3000 ms по умолчанию консервативное.
Понижайте его, если ваш query_wait_timeout короткий и вам выгоднее
быстро переходить к резерву, чем блокировать клиентов на coordinator wait.
Рецепт тюнинга: снизить p99 checkout на базе под координатором
Профиль нагрузки: PostgreSQL отвечает за ~1 ms (низкий p99 длительности запроса), но клиенты видят p99 checkout latency 100–500 ms на базе под координатором. Задержка идёт от координатора, а не от PostgreSQL.
- Подтвердите фазу. Запустите
SHOW POOL_COORDINATORв момент всплеска latency. Вычислитеmain_used = current - reserve_used:currentвключает reserve-permit-ы, а рецепт зависит от того, заполнен ли именно main семафор.main_used == max_db_connиexhaustionsне растёт → доминирует wait-фаза. Клиент тратит свой бюджет в Фазе C перед тем, как провалиться в Фазу D. Переходите к шагу 2.main_used < max_db_connбез exhaustions → задержка идёт не от координатора. СмотритеSHOW POOL_SCALINGcreate_fallbackи troubleshooting plain-режима.
- Включите reserve-first, если он ещё не включён. Задайте
reserve_pool_sizeкак минимумmax(2, 0.1 × max_db_connections). Reserve-first выдаёт permit за sub-ms, когда в резерве есть место, так что клиент, раньше сидевший в Фазе C, платит только один round-trip к арбитру. - Уменьшите
reserve_pool_timeoutдо2 × p99 query latency, но не ниже. Для запроса в 1 ms нижняя граница обычно 20 ms; начните с 50 ms и неделю наблюдайтеreserve_acqиevictions. - Оставьте
min_connection_lifetimeна дефолте 30 000 ms, если у вас нет явной цели ускорить кросс-пуловую ребалансировку; понижение увеличивает частоту eviction и churn соединений.
За чем следить после каждого изменения (все в SHOW POOL_COORDINATOR):
| До | После | Вердикт |
|---|---|---|
reserve_acq не растёт | reserve_acq растёт | Reserve-first подхватил — checkout latency должен упасть; ожидаемо |
evictions стабилен | evictions падает | Фаза B перестала срабатывать, потому что Фаза R ловит вызывающего раньше; ожидаемо |
exhaustions 0 | exhaustions > 0 | Перетянули: reserve_pool_timeout ниже реального времени возврата от соседа |
reserve_used колеблется > 0 | reserve_used возвращается к 0 за 30 с | Retain upgrade-путь работает; делать ничего не надо |
Если p99 checkout не упал после шагов 2–3, путь не ограничен
координатором. Перечитайте SHOW POOL_SCALING по пострадавшему пулу:
create_fallback > 0 означает, что сам пул не может обслужить нагрузку
из возвратов, и лечить нужно pool_size, а не reserve_pool_size.
Нижняя граница. Не опускайте reserve_pool_timeout ниже
2 × ваш p99 query latency. Ниже этого порога фаза wait всегда
истекает раньше, чем соседний пул вернёт соединение, и резерв
превращается из клапана переполнения в обязательный permit для
каждого нового соединения. Reserve-permit-ов по замыслу мало, и
использовать их как постоянный источник означает сломать их
назначение.
Ловушка: query_wait_timeout < reserve_pool_timeout. Когда дедлайн
клиента короче фазы ожидания координатора, клиент сдаётся первым, и вы
видите ошибки wait timeout вместо более наглядной
all server connections to database 'X' are in use. Фазы wait и
reserve координатора отрабатывают полностью, но к этому моменту
клиента, которому нужен результат, уже нет. На старте валидатор
конфига pg_doorman выдаёт предупреждение; реагируйте на него.
Observability
pg_doorman экспортирует состояние давления на пул через admin-консоль и через Prometheus. Оба показывают одни и те же счётчики; выбирайте то, что подходит вашему стеку мониторинга.
Admin: SHOW POOL_SCALING
Счётчики пути anticipation + bounded burst в разрезе каждого пула.
Подключитесь к admin-базе pgdoorman и выполните:
pgdoorman=> SHOW POOL_SCALING;
| Колонка | Тип | Значение |
|---|---|---|
user | text | Пользователь пула |
database | text | База пула |
inflight | gauge | Вызовы connect() к бэкенду, выполняемые в этом пуле прямо сейчас. Ограничено scaling_max_parallel_creates. |
creates | counter | Сколько всего backend-соединений пул начинал создавать с момента старта. В паре с gate_waits используется для расчёта частоты попаданий на gate. |
gate_waits | counter | Сколько раз вызов наткнулся на заполненный burst gate и был вынужден ждать слот. Высокие значения говорят, что scaling_max_parallel_creates слишком низкий. |
antic_notify | counter | Попытки anticipation в Фазе 4, где прямая передача удалась. Инкрементируется один раз на успешное получение, до проверки пригодности. Высокий antic_notify при низком create_fallback — хороший признак: прямая передача ловит возвраты, клиенты не платят за connect(). |
antic_timeout | counter | Попытки anticipation в Фазе 4, где ожидание истекло без получения соединения, либо бюджет был нулевой. Инкрементируется ровно один раз при каждом провале Фазы 4 в путь создания. Высокий antic_timeout означает, что клиенты упираются в query_wait_timeout, не успев получить соединение через прямую передачу. |
create_fallback | counter | Фаза 4 не получила соединение через прямую передачу: дедлайн исчерпан или бюджет был нулевой. Именно эти ожидания превращаются в новый connect(). Стабильно ненулевой create_fallback значит, что клиентского бюджета не хватает на перехват возвратов: пул либо мал, либо запросы длиннее query_wait_timeout. |
replenish_def | counter | Запуски фонового replenish, упёршиеся в лимит burst gate и отложенные до следующего retain-цикла. Устойчиво ненулевые значения означают, что min_pool_size нельзя поддержать при текущей нагрузке. |
Все счётчики монотонные с момента старта. Считайте дельты между скрейпами; абсолютные значения полезны только для расчёта соотношений.
Admin: SHOW POOL_COORDINATOR
Состояние координатора в разрезе каждой базы. Присутствует только
для баз с max_db_connections > 0.
pgdoorman=> SHOW POOL_COORDINATOR;
| Колонка | Тип | Значение |
|---|---|---|
database | text | Имя базы |
max_db_conn | gauge | Сконфигурированное max_db_connections |
current | gauge | Сколько всего backend-соединений сейчас удерживается под этим координатором (по всем user-пулам) |
reserve_size | gauge | Сконфигурированное reserve_pool_size |
reserve_used | gauge | Сколько reserve-permit-ов используется прямо сейчас. Сходится обратно к 0, когда в main есть свободное место — retain task каждые retain_connections_time апгрейдит idle reserve-permit-ы в main. Устойчивое ненулевое значение означает либо активный всплеск, либо базу, постоянно упёртую в max_db_connections. |
evictions | counter | Сколько раз координатор выселил idle-соединение соседнего пула, чтобы освободить слот. С включённым reserve-first этот счётчик растёт только при реальном кросс-пуловом давлении — когда резерв заполнен и у соседа есть что выселить. |
reserve_acq | counter | Сколько всего reserve-permit-ов выдал arbiter (Фаза R быстрый путь плюс Фаза D fallback суммарно) |
exhaustions | counter | Сколько раз координатор вернул клиенту ошибку исчерпания. Это главный сигнал на пейджер. |
Чтение вывода SHOW POOL_COORDINATOR
Три снапшота и что каждый означает для оператора:
Здоровая спокойная база:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
mydb | 80 | 24 | 10 | 0 | 0 | 0 | 0
Нормальное устойчивое состояние. Запас большой, резерв спит, evictions нет, exhaustions нет. Алерты здесь должны молчать.
После всплеска, upgrade в процессе:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
mydb | 80 | 65 | 10 | 3 | 0 | 12 | 0
Всплеск занял большую часть max_db_connections и три соединения
перелились в резерв. current < max_db_conn означает, что в main
есть место, поэтому retain task повысит эти три permit-а до main
на следующем цикле; reserve_used должен упасть до 0 в течение
retain_connections_time (по умолчанию 30 секунд). Если не падает,
смотрите раздел troubleshooting ниже. evictions = 0 и
reserve_acq > 0 вместе подтверждают, что reserve-first поглотил
всплеск без закрытия соседских backend-ов.
Устойчивая перегрузка:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
mydb | 80 | 95 | 20 | 15 | 300 | 500 | 0
Main полностью занят (main_used = current - reserve_used = 80,
равно max_db_conn), резерв использован на 75%, много eviction-ов,
много reserve-грантов. База не просто иногда под давлением — она
постоянно недоразмерена и выживает только за счёт того, что
eviction ротирует соединения между пользователями, а reserve-first
поглощает каждого нового вызывающего. exhaustions = 0 означает,
что арбитр пока успевает, но любой переходный пик опрокинет
ситуацию. Действие: поднимите max_db_connections (сначала
проверьте запас PostgreSQL) или найдите жадный пул через
SHOW POOLS и уменьшите его pool_size.
Метрики Prometheus
Два семейства метрик в разрезе пула и два в разрезе координатора. Все
четыре живут в namespace pg_doorman_pool_scaling* и
pg_doorman_pool_coordinator*.
| Метрика | Тип | Лейблы | Источник |
|---|---|---|---|
pg_doorman_pool_scaling{type="inflight_creates"} | gauge | user, database | inflight из SHOW POOL_SCALING |
pg_doorman_pool_scaling_total{type="creates_started"} | counter | user, database | creates |
pg_doorman_pool_scaling_total{type="burst_gate_waits"} | counter | user, database | gate_waits |
pg_doorman_pool_scaling_total{type="burst_gate_budget_exhausted"} | counter | user, database | gate_budget_ex — adaptive timeout сработал, клиент перешёл к созданию |
pg_doorman_pool_scaling_total{type="anticipation_wakes_notify"} | counter | user, database | antic_notify |
pg_doorman_pool_scaling_total{type="anticipation_wakes_timeout"} | counter | user, database | antic_timeout |
pg_doorman_pool_scaling_total{type="create_fallback"} | counter | user, database | create_fallback |
pg_doorman_pool_scaling_total{type="replenish_deferred"} | counter | user, database | replenish_def |
pg_doorman_pool_coordinator{type="connections"} | gauge | database | current из SHOW POOL_COORDINATOR |
pg_doorman_pool_coordinator{type="reserve_in_use"} | gauge | database | reserve_used |
pg_doorman_pool_coordinator{type="max_connections"} | gauge | database | max_db_conn |
pg_doorman_pool_coordinator{type="reserve_pool_size"} | gauge | database | reserve_size |
pg_doorman_pool_coordinator_total{type="evictions"} | counter | database | evictions |
pg_doorman_pool_coordinator_total{type="reserve_acquisitions"} | counter | database | reserve_acq |
pg_doorman_pool_coordinator_total{type="exhaustions"} | counter | database | exhaustions |
Алерты для настройки
Алерты ниже покрывают режимы отказа, на которые стоит реагировать пейджером или варнингом. Они написаны на синтаксисе Prometheus; адаптируйте под свой стек. Все используют окна устойчивого условия, чтобы короткие всплески не будили дежурного.
Если вы часто перезагружаете pg_doorman, и пулы появляются и исчезают,
ограничьте алерты недавно активными пулами (например, добавьте фильтр
pg_doorman_pool_scaling_total{type="creates_started"} > 0).
Каждый алерт ниже содержит блок Runbook с одной диагностической командой и двумя-тремя ветвями, привязанными к конкретным значениям счётчиков.
Coordinator exhaustion (page). Клиент получил ошибку "database exhausted". Жёсткий отказ — резерв и eviction оба не сработали.
rate(pg_doorman_pool_coordinator_total{type="exhaustions"}[5m]) > 0
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
current — это суммарное число соединений main + reserve
(current == max_db_conn + reserve_size означает, что оба семафора
пусты).
current == max_db_conn + reserve_size→ оба семафора полностью пусты. Поднимитеmax_db_connections(сначала проверьте, что у PostgreSQL есть запасmax_connections) или увеличьте резерв.reserve_size == 0иcurrent == max_db_conn→ резерв выключен, main полностью занят. Задайтеreserve_pool_size, чтобы поглощать всплески, и, еслиexhaustionsпродолжит расти, поднимайтеmax_db_connections.current < max_db_conn + reserve_size, ноexhaustionsрастёт → гонка в Фазе R/D, такого не должно быть устойчиво; заведите баг со снапшотомSHOW POOL_COORDINATOR.- В
SHOW POOLSу одного пользователяsv_idleсильно больше, чем у остальных → один пул захватывает соединения. Уменьшите егоpool_sizeили задайтеmin_guaranteed_pool_size, чтобы защитить соседей.
Burst gate saturated (warn). Burst gate ждёт чужие create
чаще, чем проходит напрямую. Короткие всплески выше порога при
failover или рестарте нормальны. Устойчивые значения означают,
что scaling_max_parallel_creates слишком низкий.
rate(pg_doorman_pool_scaling_total{type="burst_gate_waits"}[5m])
> 0.5 * rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m])
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
inflight_createsсидит на лимите И вSHOW POOLSвидны клиенты вcl_waiting→connect()медленный со стороны бэкенда. Смотрите troubleshooting "Burst gate как узкое место даже при низком трафике" перед тем, как поднимать лимит.inflight_createsходит ниже лимита, ноgate_waitsрастёт → много коротких всплесков. Поднимайтеscaling_max_parallel_creates, оставаясь в пределах потолка из раздела тюнинга.- Горит только один пул → рассмотрите
min_guaranteed_pool_sizeдля соседей или уменьшитеpool_sizeу горячего.
Anticipation create fallback rate (warn). Фаза 4 anticipation
сдаётся, не поймав возврат, и проваливается в свежий connect().
В устойчивом состоянии должно быть близко к нулю.
rate(pg_doorman_pool_scaling_total{type="create_fallback"}[5m]) > 0.1
and
rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m]) > 0.1
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW STATS' | grep -E 'database|avg_xact_time|avg_query_time'
create_fallbackвысокий на одном пуле Иavg_xact_timeэтой базы растёт → долгие запросы держат соединения вне ротации. Сначала лечите долгий запрос; пул рассчитан на обычную длительность, а не на эту транзакцию.create_fallbackвысокий у всех пулов Иcreates_startedтоже растёт → нагрузка превышает то, что возвраты могут обслужить в пределах дедлайна. Поднимайтеpool_size.create_fallbackвысокий, ноquery_wait_timeoutкороткий (< 1 секунды) → дедлайн anticipation (query_wait_timeout − 500 msс верхней границей 500 ms) слишком короткий даже для нормальных возвратов. Поднимайтеquery_wait_timeoutминимум до2 × p99 длительности запроса.
Replenish deferred persistently (warn). Фоновая задача не
может поддерживать min_pool_size, потому что burst gate занят
клиентским трафиком.
increase(pg_doorman_pool_scaling_total{type="replenish_deferred"}[1h]) > 60
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
- Пострадавший пул показывает
sv_idle + sv_active < min_pool_sizeодновременно с растущимgate_waits→ replenish проигрывает клиентскому трафику. Поднимайтеscaling_max_parallel_creates, чтобы у фоновой задачи был запас, или принимайте defer как косметическую проблему (под нагрузкой клиенты сами поднимут пул вышеmin_pool_size). inflight_createsстабильно стоит на лимите → gate полон по другой причине (медленныйconnect()); сначала это.
Reserve pool continuously in use (warn). Gauge reserve-permit
не возвращался к нулю в течение 15 минут. Retain task апгрейдит
idle reserve-permit-ы обратно в main каждые retain_connections_time
(по умолчанию 30 секунд), поэтому алерт означает, что upgrade-путь
не в состоянии отработать или получить успех, а не что он забыл
запуститься.
min_over_time(pg_doorman_pool_coordinator{type="reserve_in_use"}[15m]) > 0
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
Вычислите main_used = current - reserve_used из строки — current
это суммарное число permit-ов main и reserve, а не только main.
main_used == max_db_conn→ main полностью занят, upgrade-у негде забрать слот. База недоразмерена; поднимайтеmax_db_connections.main_used < max_db_connИ каждый пул вSHOW POOLSимеетsv_active == pool_size(илиcl_waiting > 0как индикатор) → все пулы под давлением, retain task пропускает upgrade. Поднимайтеpool_sizeу того пула, у которого самый высокийcl_waitingили самое плотное соотношениеsv_active / pool_size.main_used < max_db_connИ ни у одного пула нет ни того, ни другого признака, а gauge всё равно ненулевой → заведите баг со снапшотамиSHOW POOL_COORDINATORиSHOW POOLS; этого не должно быть.
Coordinator approaching cap (warn). Ранний сигнал: координатор подходит к потолку.
pg_doorman_pool_coordinator{type="max_connections"} > 0
and
pg_doorman_pool_coordinator{type="connections"}
/ pg_doorman_pool_coordinator{type="max_connections"} > 0.85
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
currentмонотонно растёт часами → проблема планирования ёмкости. Поднимайтеmax_db_connections(сначала проверьте запас PostgreSQL) до следующего всплеска.currentколеблется рядом с лимитом → burst-driven. Поднимайтеreserve_pool_size, чтобы всплески поглощались без касанияmax_db_connections, потом следите заreserve_acq.- Один пул доминирует в
SHOW POOLS(sv_active + sv_idleсильно больше, чем у соседей) → один пул захватывает соединения; уменьшите егоpool_sizeили задайтеmin_guaranteed_pool_sizeсоседям.
Inflight stuck at cap (warn). inflight_creates, сидящий на
сконфигурированном лимите больше 5 минут, означает, что вызовы
connect() не завершаются.
min_over_time(pg_doorman_pool_scaling{type="inflight_creates"}[5m])
>= 2 # adjust to your scaling_max_parallel_creates value
Runbook:
time psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c 'SELECT 1'
psql -h $PG_HOST -p $PG_PORT -c \
"SELECT state, count(*) FROM pg_stat_activity GROUP BY state"
time psqlпоказываетconnect()> 500 ms → backend-connect медленный. Проверьтеpg_stat_sslна стоимость SSL-handshake,pg_authidна конкуренцию за поиск роли и время DNS-резолва с хоста pg_doorman.pg_stat_activityпоказывает много сессий вstartupилиauthenticating→ бэкенд спавнит процессы, но не разгружает очередь handshake. Вероятно, упёрлись вmax_connectionsна стороне базы — выполнитеSELECT setting FROM pg_settings WHERE name = 'max_connections'и сравните с активными сессиями.pg_stat_activityпо пользователю pg_doorman пустой → сетевая проблема или firewall между pg_doorman и PostgreSQL.
Coordinator thrashing (warn). Лимит исчерпан и идут eviction-ы: координатор постоянно закрывает соседние соединения, чтобы освободить место. Недоразмеренный пул, а не "иногда под давлением".
pg_doorman_pool_coordinator{type="connections"}
/ pg_doorman_pool_coordinator{type="max_connections"} > 0.95
and
rate(pg_doorman_pool_coordinator_total{type="evictions"}[5m]) > 0
Runbook:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
evictionsрастёт Иreserve_used == 0→ резерв выключен или исчерпан, eviction — единственный клапан сброса. Включите / поднимитеreserve_pool_size, чтобы всплеск поглощался без закрытия соседних backend-ов.evictionsИreserve_acqрастут одновременно → резерва тоже не хватает. Поднимайтеmax_db_connectionsилиreserve_pool_size; сначала проверьтеmax_connectionsу PostgreSQL.
Чтение admin-вывода во время инцидента
Admin-консоль принимает только SHOW <subcommand>, SET, RELOAD,
SHUTDOWN, UPGRADE, PAUSE, RESUME и RECONNECT. SHOW это не
виртуальная таблица, поэтому к admin-базе нельзя выполнить SELECT.
Чтобы запрашивать счётчики в shell-пайплайнах, запускайте SHOW из
psql и обрабатывайте вывод.
Шаблоны ниже используют psql к admin-листенеру (по умолчанию
учётные данные admin/admin):
# Highest burst-gate-wait ratio first (the hot pool).
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f inflight=%d defer=%d\n", $1, $2, $5/$4, $3, $9 }' \
| sort -k3 -nr | head
# Highest create fallback ratio (cycle gave up on deadline and paid connect()).
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f creates=%d fallback=%d\n", $1, $2, $8/$4, $4, $8 }' \
| sort -k3 -nr | head
# Coordinator: closest databases to exhaustion.
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_COORDINATOR' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $2>0 { printf "%-30s %.3f used=%d/%d reserve=%d exhaustions=%d\n", $1, $3/$2, $3, $2, $5, $8 }' \
| sort -k2 -nr
Позиции полей в awk соответствуют порядку колонок, описанному выше:
для POOL_SCALING это user|database|inflight|creates|gate_waits|gate_budget_ex|antic_notify|antic_timeout|create_fallback|replenish_def,
для POOL_COORDINATOR это database|max_db_conn|current|reserve_size|reserve_used|evictions|reserve_acq|exhaustions.
Сравнение с PgBouncer
PgBouncer и pg_doorman оба пулят соединения, но давление обрабатывают по-разному.
| Аспект | PgBouncer | pg_doorman |
|---|---|---|
| Лимит размера в каждом пуле | pool_size | pool_size |
| Лимит на уровне БД, общий для пулов | max_db_connections (жёсткий лимит, без eviction; для изоляции есть переопределения pool_size на базу или на пользователя) | max_db_connections (жёсткий лимит плюс eviction между пулами и reserve pool) |
| Reserve pool | reserve_pool_size, reserve_pool_timeout | reserve_pool_size, reserve_pool_timeout (плюс приоритизация в arbiter по starving/queued) |
| Eviction между пользователями | Не поддерживается. Пользователь, удерживающий idle-соединения, морит голодом соседа, которому они нужны. | Координатор выселяет idle-соединения у пользователя с наибольшим излишком над эффективным минимумом (max(user.min_pool_size, min_guaranteed_pool_size)). |
Одновременные connect() к бэкенду в одном пуле | Однопоточный, обрабатывает события последовательно в пределах пула, вызовы connect() выпускаются по одному. | Ограничено scaling_max_parallel_creates (по умолчанию 2 на пул): не больше N одновременных backend-коннектов в пуле, лишние задачи ждут на burst gate. |
| Anticipation возвратов | Нет. Клиенты ждут следующего доступного соединения в порядке прихода, в пределах wait_timeout. | Event-driven anticipation: возвращающееся соединение будит ровно одного из ожидающих в очереди, часто ещё до того, как будет выпущен хоть один новый connect(). |
Прогрев min_pool_size | Поддерживается на каждом такте event loop (без отдельной задачи replenish). | Периодический фоновый replenish (retain_connections_time, по умолчанию 30 s), который отступает, когда burst gate занят. |
| Повторный логин после ошибки | server_login_retry (по умолчанию 15 s) блокирует новые попытки логина после отказа бэкенда. | Аналога нет. Ошибки логина бэкенда пробрасываются клиенту на каждую попытку. |
| Jitter на lifetime | Нет. server_lifetime точный. | ±20% jitter на server_lifetime и idle_timeout, чтобы избежать одновременного массового закрытия. |
| Ключ поиска пула | (database, user, auth_type) | (database, user) |
| Честность между пользователями на общем лимите | First come first served на max_db_connections. | Reserve arbiter оценивает запросы по (starving, queued_clients). |
| Observability давления на новые соединения | SHOW POOLS, SHOW STATS. Текущие inflight-коннекты и результаты anticipation не видны вовсе. | SHOW POOL_SCALING и SHOW POOL_COORDINATOR показывают каждый счётчик нового кодового пути. |
В production важнее всего два различия:
-
Bounded burst gate. Размер пула в PgBouncer ограничивает, сколько соединений у вас есть, но не ограничивает, сколько вызовов
connect()выпускается одновременно, когда в один момент приходит много клиентов. pg_doorman ограничивает частоту одновременныхconnect()к бэкенду независимо от размера пула, поэтому внезапный всплеск трафика не превращается в connection storm к PostgreSQL. -
Cross-pool eviction.
max_db_connectionsв PgBouncer задаёт жёсткий потолок и не умеет перераспределять. Если пользователь A держит 80 idle-соединений, а пользователю B нужно одно, но лимит уже выбран, пользователь B ждёт или падает. Координатор pg_doorman может закрыть одно из соединений A (если оно старшеmin_connection_lifetime) и отдать слот B. -
FIFO direct handoff. PgBouncer ставит клиентов в порядке прихода и отдаёт следующее свободное соединение, но PgBouncer обрабатывает события на одном потоке — под нагрузкой порядок зависит от callback'ов libevent. pg_doorman отправляет возвращённые соединения напрямую каждому ожидающему в строгом FIFO-порядке. Результат — p99/p50 в пределах 1.1x при любом числе клиентов, тогда как пулеры без строгого FIFO показывают 10-25x раздувание хвоста при той же нагрузке.
Troubleshooting
В логах одновременно несколько backend connect
Симптом. В логах сервера (или в debug-логах pg_doorman) видно
5 или больше событий connect() к бэкенду в одну и ту же миллисекунду.
Кажется, что burst gate не работает.
Причина. Либо scaling_max_parallel_creates выставлен слишком
высоко (проверьте SHOW CONFIG или ваш pg_doorman.yaml), либо
существует 5 или больше пулов, каждый из которых независимо
открывает одновременные коннекты (gate работает в пределах одного
пула, а не глобально).
Исправление. Понизьте scaling_max_parallel_creates. Значение по
умолчанию 2 подходит большинству нагрузок. Когда пулов много,
суммарная частота одновременных коннектов равна
pools × scaling_max_parallel_creates, и это ожидаемо. Чтобы
ограничить и сумму, задайте max_db_connections на базу; тогда
координатор поставит в очередь создания сверх лимита.
min_pool_size не поддерживается
Симптом. Пул с min_pool_size = 10 показывает sv_idle = 4 в
SHOW POOLS и держится так минутами.
Причина. Фоновый replenish откладывается, потому что burst gate
занят клиентским трафиком. Проверьте replenish_def в
SHOW POOL_SCALING. Если он продолжает расти, replenish пропускает
каждый retain-цикл.
Исправление. Так задумано: под нагрузкой gate отдан клиентским
запросам на создание. Пул дойдёт до min_pool_size, когда клиентский
трафик ослабнет. Если нужна жёсткая гарантия минимума, поднимайте
scaling_max_parallel_creates, чтобы у replenish была свободная
ёмкость, либо сократите retain_connections_time, чтобы replenish
запускался чаще.
Для transaction pooling (pool_mode = transaction) значение
min_pool_size выше, чем pool_size / 2, обычно указывает на то,
что пул мал: большинство соединений должно быть доступно для выдачи
клиентам, а не пришпилено к минимуму. Для session pooling
эвристика не работает: min_pool_size = pool_size это нормальная
настройка, чтобы держать всё состояние сессий в горячем виде.
p99 latency растёт без видимой причины
Симптом. p99 клиентской задержки растёт, p50 остаётся ровным. Размер пула выглядит нормально, в логах нет ошибок.
Причина. Фаза 4 anticipation (direct handoff) держит клиентов
в ожидании возврата до query_wait_timeout - 500 ms, но возвраты
приходят медленнее, чем клиент готов ждать. Ожидающие либо получают
соединение через прямую передачу, либо проваливаются в путь создания
по истечении бюджета. Проверьте create_fallback в
SHOW POOL_SCALING: если он ненулевой и растёт, клиенты уходят
по таймауту.
Исправление. Сверьте query_wait_timeout с p95 длительности
запросов, обслуживаемых этим пулом. Два случая.
query_wait_timeoutсопоставим или короче p95: клиент не успевает дождаться освобождения соединения, каждый такой клиент платит полный бюджет ожидания. Либо поднимайтеquery_wait_timeout, либо увеличивайтеpool_size, чтобы освобождения приходили чаще.query_wait_timeoutзаметно длиннее p95, ноcreate_fallbackвсё равно растёт: пул слишком мал для своей нагрузки, все соединения заняты дольше p95 из-за конкуренции. Поднимайтеpool_size(иmax_db_connections, если задан координатор).create_fallbackнулевой, аantic_notifyрастёт пропорционально обороту пула: direct handoff работает, возвраты перехватываются, лишнихconnect()нет. Задержка приходит откуда-то ещё: PostgreSQL, сеть, клиентская сторона. ПроверьтеSHOW STATS avg_wait_time.
max_db_connections исчерпан, клиенты получают ошибки
Симптом. Клиенты видят ошибки вида all server connections to database 'X' are in use (max=80, ...).
pg_doorman_pool_coordinator_total{type="exhaustions"} растёт.
Причина. Все пять фаз координатора провалились: try-acquire не
взял слот, выселять нечего, ожидание истекло, а резерв либо исчерпан,
либо reserve_pool_size = 0.
Исправление. Пройдите по фазам по порядку.
- Сравните
currentиmax_db_connвSHOW POOL_COORDINATOR. Еслиcurrentстабильно стоит на лимите, ваша нагрузка его превышает. Либо поднимайтеmax_db_connections, либо ищите разогнавшийся пул. - Посмотрите на частоту
evictions. Если она нулевая или близка к нулю, eviction не помогает: либо idle-соединения каждого пула моложеmin_connection_lifetime(по умолчанию 30 000 ms), либо все остальные пулы стоят на своёмmin_guaranteed_pool_size. Понизьтеmin_connection_lifetime, если у вас очень короткие запросы и вы явно хотите более быстрый cross-pool rebalance, или увеличьтеmax_db_connections. - Сравните
reserve_usedиreserve_size. Если резерв занят полностью, поднимитеreserve_pool_size. Если резерв пустой, аexhaustionsвсё равно происходят, значит резерв не настроен (reserve_pool_size = 0). Задайте его, чтобы поглощать всплески. - Посмотрите
SHOW POOLSдля базы. Если у одного пользователяsv_idleнамного больше, чем у остальных, этот пользователь копит соединения; задайтеmin_guaranteed_pool_size, чтобы защитить мелких пользователей от вытеснения, либо понизьтеpool_sizeнакопителю.
Фаза ожидания координатора как узкое место
Симптом. Клиенты в среднем платят 3 секунды задержки, ровно
совпадающие с reserve_pool_timeout.
Причина. Фаза C wait стабильно истекает по таймауту. Попадание в
Фазу C при включённом reserve-first означает, что на момент прихода
вызывающего резерв уже был полностью занят, — единственный путь вперёд
это возврат от соседа. Либо база действительно стоит на лимите, и
соединения не возвращаются; либо reserve_pool_size = 0, и wait
отрабатывает до конца, прежде чем клиент получит хоть какой-то ответ.
Исправление. Понизьте reserve_pool_timeout, чтобы получать
быстрый отказ, либо задайте reserve_pool_size > 0, чтобы Фаза R /
Фаза D обрабатывала переполнение в рамках того же пути получения
соединения, вообще не заходя в Фазу C.
reserve_used остаётся ненулевым, а пул выглядит пустым
Симптом. SHOW POOL_COORDINATOR показывает reserve_used = 4
(или любое ненулевое значение) при том, что SHOW POOLS показывает
cl_waiting = 0, низкий cl_active и current < max_db_conn.
Резервный пул выглядит занятым «призраками».
Причина. В сборках до reserve→main upgrade reserve-permit
держался за своим backend, пока тот не доживёт до
min_connection_lifetime и retain-цикл не поймает его idle. При
устойчивом клиентском трафике last_used() на backend обновлялся
быстрее, чем min_connection_lifetime, и permit никогда не
отпускался.
Исправление. В текущих сборках это решается автоматически: retain
task каждые retain_connections_time (по умолчанию 30 секунд)
запускает повышение reserve-permit-ов до main. Для каждого
reserve-backend в пуле без давления permit меняется с reserve на main,
если в основном семафоре есть свободное место. Gauge reserve_used должен
упасть до нуля в течение одного retain-цикла.
Если reserve_used всё равно не уходит, значит пул либо под
устойчивым давлением (повышение пропускается, когда пул под давлением —
и это правильно, иначе ожидающий клиент тут же заберёт освободившийся
слот), либо current == max_db_connections (нет main-слота, который
можно забрать). Оба случая означают, что база честно исчерпана;
лечение — больше ёмкости, а не обход.
Burst gate как узкое место даже при низком трафике
Симптом. Частота gate_waits заметная, но частота creates
низкая, а inflight_creates всё время стоит на лимите.
Причина. connect() к бэкенду медленный. Каждый create держит
слот по нескольку секунд; даже с двумя слотами вы создадите всего
около 2 / connect_seconds соединений в секунду.
Исправление. Разберитесь, почему connect() медленный со стороны
PostgreSQL (слишком много SCRAM-итераций, конкуренция за блокировки
pg_authid, медленный DNS, SSL handshake). Когда connect() станет
быстрым, gate перестанет быть узким местом. Поднятие
scaling_max_parallel_creates лишь маскирует проблему и перенесёт
storm на PostgreSQL. Сначала разбирайтесь, потом поднимайте лимит.
Один пользователь забирает весь резерв
Симптом. reserve_acquisitions_total продолжает расти. Большую
часть резервов забирает один и тот же небольшой пользователь.
Причина. Пользователь ниже своего эффективного минимума
(max(user.min_pool_size, min_guaranteed_pool_size)), и координатор
не может удовлетворить этот минимум, не выселяя соединения у соседей.
Каждый клиентский запрос от этого пользователя попадает в Фазу R
(reserve-first), как только база заполнена, и захватывает
reserve-permit — арбитр даёт starving-пользователям наивысший балл,
поэтому они выигрывают грант. Более глубокий вопрос: почему
пользователю постоянно нужны свежие соединения? Либо его pool_size
слишком мал, чтобы поглотить собственную нагрузку, либо его трафик
всплесковый, и резерв делает ровно то, для чего нужен.
Исправление. Три варианта, выбор зависит от настоящей причины:
- Если
pool_sizeпользователя действительно мал для постоянной нагрузки, поднимитеpool_sizeи (если нужно)max_db_connections, чтобы увеличенный пул туда поместился. - Если у пользователя высокий эффективный минимум, который координатор
не может удовлетворить, понизьте тот параметр, который реально
задаёт границу (проверьте оба:
user.min_pool_sizeиmin_guaranteed_pool_size). - Если трафик действительно всплесковый, а резервы ловят всплески, оставьте как есть. Кратковременное использование резерва это и есть его назначение.
Клиенты получают wait timeout, а не database exhausted
Симптом. Под давлением координатора клиенты видят ошибку
wait timeout (во внутренних логах pg_doorman она записана как
PoolError::Timeout(Wait)), но
pg_doorman_pool_coordinator_total{type="exhaustions"} стоит на нуле.
Координатор так и не объявил исчерпание, при этом каждый клиент уходит
по таймауту.
Причина. query_wait_timeout короче, чем reserve_pool_timeout.
Клиент сдаётся раньше, чем фаза ожидания координатора успевает
завершиться. Счётчик exhaustions никогда не увеличивается, потому
что координатор в итоге получает permit для запроса, у которого
ожидающего клиента больше нет.
Исправление. Либо поднимите query_wait_timeout выше
reserve_pool_timeout плюс типичное время connect(), либо понизьте
reserve_pool_timeout (в пределах нижней границы из раздела тюнинга).
На старте валидатор конфига выдаёт предупреждение для такой
конфигурации; реагируйте на него.
Восстановление пула после рестарта PostgreSQL
Симптом. Мастер PostgreSQL перезапустился (failover, краш,
плановое окно). Видна толпа клиентов, бьющих в burst gate,
inflight_creates стоит на лимите, частота creates_started резко
растёт.
Причина. Когда pg_doorman замечает непригодный бэкенд (через
server_idle_check_timeout или упавший запрос), он повышает
reconnect-эпоху пула и сразу сливает все idle-соединения. Горячий
путь recycle становится пустым, и каждый пришедший после слива
клиент идёт по маршруту anticipation, burst gate, connect. При
scaling_max_parallel_creates = 2 каждый пул прирастает максимум
двумя соединениями за раз, и скорость ограничена задержкой
connect() к PostgreSQL.
Как выглядит здоровое восстановление. В первые несколько секунд
inflight_creates = 2 непрерывно, creates_started быстро растёт,
burst_gate_waits растёт вместе с ней. По мере того как новые
соединения начинают циркулировать, anticipation_wakes_notify
разгоняется, а create_fallback перестаёт расти: direct handoff
ловит возвраты внутри клиентского query_wait_timeout и новые
connect() уже не нужны. За время pool_size / 2 × connect()
секунд пул возвращается в норму.
Исправление. Обычно никакого. Bounded burst gate делает свою
работу: гасит connection storm к восстанавливающемуся primary, чтобы
тот не получил сотню одновременных коннектов на и без того перегруженный
postmaster. Если connect() действительно быстрый (< 50 ms), и у
вашего max_connections есть запас, поднимите
scaling_max_parallel_creates до 4 или 8, чтобы сократить
восстановление, но оставайтесь в пределах жёсткого потолка из раздела
тюнинга.
Глоссарий
- ограничитель всплесков (bounded burst gate) — ограничитель
на уровне пула, пропускающий не более
scaling_max_parallel_createsодновременных вызововconnect()к бэкенду. Задачи сверх лимита встают в очередь на прямую передачу и ожидают завершения чужого создания, пока слот не освободится. - permit координатора (coordinator permit) — разрешение на удержание одного слота в общем лимите координатора. Может быть основным (main) или резервным (reserve). Освобождается, когда backend-соединение физически уничтожается (а не когда оно возвращается в idle-очередь); при освобождении слот возвращается либо в основной, либо в резервный семафор.
- эффективный минимум — пол для eviction у user-пула,
равный
max(user.min_pool_size, pool.min_guaranteed_pool_size). Координатор защищает именно столько соединений на пользователя от выселения соседями. - прямая передача (direct handoff) — механизм доставки в Фазе 4. При возврате соединение отправляется напрямую старейшему зарегистрированному ожидающему, минуя idle-очередь. Соединение достаётся ровно одному адресату, конкуренции нет.
- Фаза R (reserve-first) — короткое замыкание координатора, вставленное между Фазой A и Фазой B. Когда база заполнена, а в резерве есть место, Фаза R выдаёт reserve-permit напрямую через арбитра, вместо того чтобы закрывать соседний backend или парковать клиента в Фазе C.
- жёсткий потолок Фазы 4 — каждый checkout выбирает случайный
потолок в диапазоне 300–500 ms. Верхняя граница времени ожидания
Фазы 4 anticipation, независимо от
query_wait_timeout. Не настраивается. Случайный разброс предотвращает синхронные таймауты, вызывающие лавину запросов в ограничитель всплесков. - арбитр резерва (reserve arbiter) — отдельный фоновый процесс,
владеющий reserve-permit-ами. Запросы на reserve ранжируются по
паре
(starving, queued_clients)и разгружаются из приоритетной очереди так, чтобы самые нуждающиеся пользователи получали permit первыми. - повышение из резерва в основной (reserve → main upgrade) — периодическая бухгалтерская перестановка. Когда idle-backend удерживает reserve-permit, а в основном семафоре есть запас, retain task забирает основной permit, возвращает резервный слот и переключает тип permit. Без переподключения.
- излишек над минимумом (spare_above_min) — текущий размер пула минус эффективный минимум, где текущий размер — это количество всех соединений пула (активные + idle вместе, а не только idle). Координатор использует это значение, чтобы выбрать жертву eviction: пул с самым большим излишком теряет соединение первым. Само соединение, чтобы его можно было выселить, всё равно должно быть свободным — излишек выбирает пул, а не конкретное соединение.
- голодающий пользователь (starving) — user-пул, у которого текущее число соединений ниже эффективного минимума. Арбитр резерва даёт starving-пользователям абсолютный приоритет перед обычными.
- пул под давлением (under pressure) — состояние, при котором все permit-ы пула заняты, то есть каждый слот сейчас используется. Retain task пропускает повышение/закрытие на таких пулах, потому что иначе слот просто перейдёт ожидающему клиенту.
- порог прогрева (warm threshold) —
pool_size × scaling_warm_pool_ratio / 100. Ниже этого размера пул пропускает упреждающее ожидание и идёт сразу вconnect(). Выше — упреждающее ожидание активно, и пул пытается ловить возвраты, прежде чем создавать новые backend-ы.
Patroni-assisted fallback
Когда pg_doorman работает на одной машине с PostgreSQL и подключён через unix socket, switchover в Patroni или аварийное падение PG оставляют doorman без бэкенда. Пока Patroni не закончит promote реплики или не перезапустит локальный PostgreSQL, все клиентские запросы падают.
Patroni-assisted fallback перекрывает этот промежуток. Когда локальный PostgreSQL перестаёт отвечать, pg_doorman запрашивает Patroni REST API, выбирает другого члена кластера и направляет новые соединения туда. Существующие соединения к мёртвому бэкенду закрываются при штатном recycle.
Это краткосрочная мера. Она перекрывает 10-30 секунд, пока Patroni завершает свой failover. Когда Patroni восстановит локальный PostgreSQL (как реплику нового primary или как восстановленный primary), pg_doorman сам вернётся к локальному socket.
Быстрый старт
Рекомендуемая схема — pg_doorman рядом с PostgreSQL на одной машине,
ходит к нему через unix socket. Patroni REST API тоже на localhost,
поэтому fallback включается одной строкой в [general]:
general:
patroni_api_urls: ["http://localhost:8008"]
Каждый пул подхватывает это автоматически. Когда unix socket перестаёт
отвечать, pg_doorman запрашивает /cluster, выбирает кандидата по
приоритету sync_standby > replica > leader и направляет новые
соединения на выбранный хост, пока локальный PostgreSQL не вернётся в
строй. Значения по умолчанию: cooldown 30s, HTTP-таймаут 5s, TCP-таймаут
5s, lifetime fallback-соединений 30s. Переопределить их можно через
параметры настройки.
Когда это помогает
Плановый switchover. DBA запускает patroni switchover --candidate node2.
Patroni промотирует node2, затем останавливает PostgreSQL на node1.
Между остановкой и тем, как Patroni перезапустит node1 как реплику node2,
doorman на node1 не имеет бэкенда. С включённым fallback doorman
подключается к node2 за 1-2 TCP round trip.
Аварийное падение. PostgreSQL на node1 убит OOM killer. Patroni ещё
не обнаружил сбой. doorman получает connection refused на unix socket,
запрашивает Patroni API и подключается к sync_standby (вероятному
следующему лидеру).
Когда это не помогает
Падение машины. Если вся машина недоступна, doorman мёртв вместе с ней. Для этого сценария нужна внешняя маршрутизация (HAProxy, patroni_proxy, DNS failover, VIP).
Ошибки аутентификации. Если PostgreSQL отклоняет credentials doorman, бэкенд жив. Fallback не активируется.
Как это работает
Штатный режим:
клиент --unix--> doorman --unix--> PostgreSQL (локальный)
Fallback:
клиент --unix--> doorman --TCP---> PostgreSQL (удалённый, из /cluster)
|
+-- GET /cluster --> Patroni API
- Doorman пробует локальный unix socket.
- Connection refused или socket error: doorman помечает локальный
backend как недоступный на
fallback_cooldown(по умолчанию 30 секунд). - doorman отправляет
GET /clusterко всем Patroni URL из конфига параллельно и берёт первый успешный ответ. - Из списка members doorman выбирает кандидата с наивысшим приоритетом:
сначала
sync_standby, потомreplica, потом любой другой. TCP connect ко всем кандидатам запускается параллельно; еслиsync_standbyотвечает, он выбирается немедленно, обходя replica. - Новое соединение попадает в пул со сниженным lifetime
(по умолчанию 30 секунд, совпадает с
fallback_cooldown). На него действуют все обычные правила пула: лимиты coordinator, idle timeout, recycle. - Последующие соединения в рамках cooldown идут к тому же fallback-хосту напрямую, без повторного запроса к Patroni API.
- Когда cooldown истекает, doorman снова пробует локальный socket. Если работает — штатный режим. Если нет — цикл повторяется.
Write-запросы на реплике
Если fallback-хост — реплика, которая ещё не промотирована, write-запросы получат ошибку от PostgreSQL:
ERROR: cannot execute INSERT in a read-only transaction
Read-запросы работают нормально. При типичном switchover sync_standby
промотируется раньше, чем doorman обнаруживает отказ, поэтому
большинство write-запросов проходит. В худшем случае write-ошибки
длятся до истечения сниженного lifetime (30 секунд), после чего
следующее соединение через свежий /cluster найдёт нового master.
Конфигурация
Добавьте patroni_api_urls к любому пулу, который должен
использовать fallback. Без этого параметра фича отключена,
doorman работает как раньше.
pools:
mydb:
pool_mode: transaction
server_host: "/var/run/postgresql"
server_port: 5432
# Адреса Patroni REST API. Укажите минимум 2 для отказоустойчивости.
# Первый ответивший URL выигрывает; порядок не важен.
patroni_api_urls:
- "http://10.0.0.1:8008"
- "http://10.0.0.2:8008"
- "http://10.0.0.3:8008"
TOML-эквивалент:
[pools.mydb]
pool_mode = "transaction"
server_host = "/var/run/postgresql"
server_port = 5432
patroni_api_urls = [
"http://10.0.0.1:8008",
"http://10.0.0.2:8008",
"http://10.0.0.3:8008",
]
Параметры настройки
Все параметры опциональны и имеют разумные значения по умолчанию.
| Параметр | По умолчанию | Описание |
|---|---|---|
fallback_cooldown | "30s" | Сколько локальный backend остаётся помеченным как недоступный после ошибки соединения. В течение этого окна все новые соединения идут на fallback-хост. |
patroni_api_timeout | "5s" | HTTP-таймаут запросов к Patroni API. Действует на каждый URL; так как все URL опрашиваются параллельно, реальный таймаут равен этому значению, а не умноженному на количество URL. |
fallback_connect_timeout | "5s" | Таймаут TCP connect к fallback-кандидатам. Действует на всю пачку параллельных connect, не на каждый member отдельно. |
fallback_lifetime | = fallback_cooldown | Lifetime fallback-соединений. Короче штатного server_lifetime, чтобы doorman быстро вернулся к локальному backend после восстановления. |
Что указывать в patroni_api_urls
Перечислите адреса Patroni REST API ваших узлов кластера. Endpoint
/cluster на любом узле Patroni возвращает полную топологию кластера,
поэтому даже одного URL достаточно для перечисления всех members.
Два и более URL рекомендуется: если первый URL указывает на ту же машину что и мёртвый PostgreSQL, он тоже не ответит. doorman опрашивает все URL параллельно и берёт первый ответ.
Prometheus-метрики
| Метрика | Тип | Описание |
|---|---|---|
pg_doorman_patroni_api_requests_total | counter | Количество запросов /cluster |
pg_doorman_fallback_connections_total | counter | Создано fallback-соединений |
pg_doorman_patroni_api_errors_total | counter | Неудачные запросы /cluster (все URL недоступны) |
pg_doorman_fallback_active | gauge | 1, пока локальный backend в cooldown и пул использует fallback |
pg_doorman_fallback_host | gauge | Текущий активный fallback-хост (1 = активен). Labels: pool, host, port |
pg_doorman_fallback_cache_hits_total | counter | Повторное использование кешированного fallback-хоста без запроса к Patroni API |
pg_doorman_patroni_api_duration_seconds | histogram | Время запроса /cluster |
Активные транзакции
Если PostgreSQL падает во время транзакции клиента, клиент получает ошибку соединения. doorman не переносит незавершённые транзакции на fallback-хост — клиент должен выполнить retry.
Новые запросы от этого и других клиентов автоматически идут через fallback.
Эксплуатационные заметки
Credentials. Все узлы кластера должны принимать те же username
и password, которые использует doorman. Patroni-кластеры обычно
разделяют pg_hba.conf через bootstrap-конфигурацию, но это не
гарантировано. Убедитесь, что fallback-узлы принимают настроенные
credentials.
TLS. Fallback-соединения используют тот же server_tls_mode,
что и локальный backend. Если локальный backend идёт через unix
socket (без TLS), fallback TCP-соединения тоже пойдут без TLS.
Настройте server_tls_mode явно, если fallback-соединения должны
быть зашифрованы.
DNS. Используйте IP-адреса в patroni_api_urls, а не hostname.
Неудача DNS-резолва во время failover добавляет задержку и может
привести к полному отказу запроса.
standby_leader. В standby-кластерах Patroni используется роль
standby_leader. doorman обрабатывает её как «other» (наименьший
приоритет, после sync_standby и replica). Для большинства
развёртываний это корректно.
Связь с patroni_proxy
patroni_proxy и Patroni-assisted fallback решают разные задачи.
patroni_proxy — TCP-балансировщик, разворачивается рядом с клиентскими приложениями. Маршрутизирует соединения к нужному узлу PostgreSQL по роли (leader, sync, async). Не пулит соединения.
Patroni-assisted fallback — встроен в pooler doorman, который разворачивается рядом с PostgreSQL. Обрабатывает ситуацию, когда локальный backend умер и doorman нуждается во временной альтернативе. Пулит соединения.
В рекомендуемой архитектуре (patroni_proxy → pg_doorman → PostgreSQL) fallback сохраняет read-трафик на уровне doorman при падении локального backend, не затрагивая маршрутизацию patroni_proxy.
Patroni Proxy
patroni_proxy — TCP-прокси для кластеров PostgreSQL под управлением Patroni. Делает одну вещь: балансирует TCP-соединения и обрабатывает failover в Patroni-кластере.
Обзор
В отличие от HAProxy, patroni_proxy не рвёт существующие соединения при изменении топологии кластера. Когда реплика добавляется или удаляется, переключаются только затронутые соединения — остальные продолжают работать.
Возможности
Управление соединениями без простоя
patroni_proxy не разрывает существующие соединения при изменении upstream-конфигурации. Это важно для долгих транзакций и приложений с активным использованием connection pool.
Hot-обновления upstream
- Автоматическое обнаружение членов кластера через Patroni REST API (endpoint
/cluster). - Периодический опрос с настраиваемым интервалом (
cluster_update_interval). - Немедленное обновление через HTTP API (endpoint
/update_clusters). - Перечитывание конфигурации сигналом SIGHUP без перезапуска.
Маршрутизация по ролям
Маршрутизация соединений на основе ролей узлов PostgreSQL:
| Роль | Описание |
|---|---|
leader | Узел primary/master |
sync | Синхронные standby-реплики |
async | Асинхронные реплики |
any | Любой доступный узел |
Умная балансировка нагрузки
- Стратегия Least Connections для распределения соединений между бэкендами.
- Счётчики соединений сохраняются при обновлениях кластера.
- Автоматическое исключение узлов с тегом
noloadbalance.
Учёт лага репликации
- Настраиваемый
max_lag_in_bytesдля каждого порта. - Автоматическое отключение клиентов, когда лаг реплики превышает порог.
- Влияет только на соединения к репликам (у leader лага нет).
Фильтрация по состоянию members
- В качестве бэкендов используются только члены со
state: "running". - Члены в состояниях
starting,stopped,crashedавтоматически исключаются. - Динамические изменения состояния обрабатываются при периодических обновлениях.
Рекомендуемая архитектура развёртывания
Двухслойная схема:
graph TD
App1[Приложение A] --> PP(patroni_proxy<br/>TCP-балансировка)
App2[Приложение B] --> PP
App3[Приложение C] --> PP
PP --> D1(pg_doorman<br/>пулинг)
PP --> D2(pg_doorman<br/>пулинг)
PP --> D3(pg_doorman<br/>пулинг)
D1 --> PG1[(PostgreSQL<br/>leader)]
D2 --> PG2[(PostgreSQL<br/>sync-реплика)]
D3 --> PG3[(PostgreSQL<br/>async-реплика)]
- pg_doorman разворачивается рядом с серверами PostgreSQL -- он обеспечивает пулинг соединений, кэширование prepared statements и оптимизации на уровне протокола, которым выгодна низкая задержка до базы.
- patroni_proxy разворачивается рядом с клиентскими приложениями -- он отвечает за TCP-маршрутизацию и failover, распределяя соединения по кластеру без накладных расходов пулинга.
Каждый компонент решает свою задачу: pg_doorman — пулинг и протокол, patroni_proxy — маршрутизация и failover.
Конфигурация
Пример patroni_proxy.yaml:
# Интервал обновления кластера в секундах (по умолчанию: 3)
cluster_update_interval: 3
# Адрес HTTP API для health checks и ручных обновлений (по умолчанию: 127.0.0.1:8009)
listen_address: "127.0.0.1:8009"
clusters:
my_cluster:
# Endpoint'ы Patroni API (несколько -- для отказоустойчивости)
hosts:
- "http://192.168.1.1:8008"
- "http://192.168.1.2:8008"
- "http://192.168.1.3:8008"
# Опционально: TLS-конфигурация для Patroni API
# tls:
# ca_cert: "/path/to/ca.crt"
# client_cert: "/path/to/client.crt"
# client_key: "/path/to/client.key"
# skip_verify: false
ports:
# Соединения к primary/master
master:
listen: "0.0.0.0:6432"
roles: ["leader"]
host_port: 5432
# Read-only соединения к репликам
replicas:
listen: "0.0.0.0:6433"
roles: ["sync", "async"]
host_port: 5432
max_lag_in_bytes: 16777216 # 16MB
Параметры конфигурации
| Параметр | По умолчанию | Описание |
|---|---|---|
cluster_update_interval | 3 | Интервал в секундах между опросами Patroni API |
listen_address | 127.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_clusters | GET | Запустить немедленное обновление всех членов кластера |
/ | GET | Health check (возвращает "OK") |
Сравнение с HAProxy + confd
| Возможность | patroni_proxy | HAProxy + 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, проверьте:
- Patroni API доступен с хоста patroni_proxy.
- У членов кластера
state: "running". - Роли в конфигурации совпадают с реальными ролями members.
- Если используется
max_lag_in_bytes-- проверьте текущий лаг реплик.
Соединения разрываются после обновления
С patroni_proxy этого происходить не должно. Если соединения всё-таки разрываются:
- Проверьте, действительно ли бэкенд-хост был удалён из кластера.
- Убедитесь, что порог
max_lag_in_bytesне превышается. - Включите debug-логирование, чтобы увидеть детальный жизненный цикл соединений.
Binary upgrade
Обновление pg_doorman без разрыва клиентских соединений. Старый процесс передаёт idle-клиентов новому через Unix socket -- клиенты продолжают работу на том же TCP-соединении без reconnect.
Быстрый старт
# 1. Заменить бинарник на диске
cp pg_doorman_new /usr/bin/pg_doorman
# 2. Запустить upgrade
kill -USR2 $(pgrep pg_doorman)
# 3. Проверить: старый PID исчез, клиенты на месте
pgrep pg_doorman # новый PID
Или через admin-консоль:
UPGRADE;
Как работает upgrade
SIGUSR2
|
v
+-----------------------+
| 1. Валидация конфига |
| (pg_doorman -t) | -- fail --> отмена, продолжаем
+-----------+-----------+
|
v
+-----------------------+
| 2. Запуск нового |
| socketpair() |
| inherit-fd |
| readiness pipe | -- ожидание до 10с
+-----------+-----------+
|
+-------------+-------------+
| |
v v
+---------------------+ +---------------------+
| СТАРЫЙ процесс | | НОВЫЙ процесс |
| | | |
| 3. Idle-клиенты | | migration_receiver |
| сериализация +--->+ восстановление |
| dup() + SCM_RIGHTS | запуск client |
| | | handle() |
| 4. Клиенты в tx | | |
| дождаться COMMIT +--->+ Принимает новые |
| мигрировать | | соединения |
| | | |
| 5. Shutdown timer | +---------------------+
| опрос 250мс |
| выход при 0 |
+---------------------+
Фаза 1: Валидация конфига
Текущий бинарник перезапускается с флагом -t и конфигом.
Если валидация проваливается -- upgrade отменяется, старый процесс
продолжает обслуживать трафик. В логах баннер:
!!! BINARY UPGRADE ABORTED - SHUTDOWN CANCELLED !!!
!!! FIX THE CONFIGURATION BEFORE ATTEMPTING BINARY UPGRADE AGAIN !!!
!!! THE SERVER WILL CONTINUE RUNNING WITH THE CURRENT BINARY !!!
Фаза 2: Запуск нового процесса
Foreground mode:
- Создаётся Unix
socketpair()для миграции клиентов. - Listener fd передаётся дочернему процессу через
--inherit-fd. - Readiness pipe: родитель ждёт до 10 секунд байт от дочернего процесса. Дочерний пишет в pipe, когда начинает принимать соединения.
- Родитель закрывает свой listener -- новые соединения идут в дочерний процесс.
Daemon mode:
Запускается новый daemon-процесс. Старый закрывает listener.
Миграция клиентов через socketpair не используется -- клиенты
дренируются (получают error 58006 при истечении shutdown_timeout).
Фаза 3: Миграция idle-клиентов (foreground)
Когда установлен флаг MIGRATION_IN_PROGRESS, каждый idle-клиент
(нет активной транзакции, нет pending deferred BEGIN, нет
буферизованных данных на чтение) мигрирует:
- Сериализация: connection_id, secret_key, имя пула, username, server parameters, полный кэш prepared statements.
- dup() + SCM_RIGHTS: TCP socket fd дублируется и передаётся новому процессу через Unix socketpair.
- Восстановление: новый процесс пересоздаёт Client struct,
подключает к нужному пулу и запускает
handle().
Клиент не замечает миграции. Никакого reconnect, никакого error, никакой повторной аутентификации. TCP-соединение -- тот же физический socket.
Фаза 4: Дренирование in-transaction клиентов
Клиент внутри BEGIN ... COMMIT продолжает работать на старом
процессе. Его серверное соединение остаётся живым. После завершения
транзакции (COMMIT или ROLLBACK) клиент становится idle и мигрирует
на следующей итерации цикла.
Deferred BEGIN (сервер ещё не выделен) тоже блокирует миграцию.
Клиент должен отправить запрос (сбросив deferred BEGIN), затем
COMMIT, и только потом мигрирует.
Фаза 5: Shutdown timer
Shutdown timer опрашивает CURRENT_CLIENT_COUNT каждые 250 мс.
Когда все клиенты мигрировали или отключились -- старый процесс
вызывает process::exit(0).
Если shutdown_timeout истекает раньше -- принудительный выход,
оставшиеся соединения закрываются.
Во время миграции drain_all_pools() откладывается: in-transaction
клиентам нужны их серверные соединения. Дренирование пулов начинается
только после завершения миграции или сброса MIGRATION_IN_PROGRESS.
Prepared statements
Кэш prepared statements каждого клиента сериализуется при миграции:
- Ключ statement (именованный или anonymous hash)
- Hash запроса
- Полный текст запроса
- OID типов параметров
В новом процессе:
- Каждая запись регистрируется в pool-level shared cache (DashMap).
- Серверные бэкенды свежие -- на них нет prepared statements.
- При первом
Bindк мигрированному statement pg_doorman прозрачно отправляетParseна новый бэкенд. Клиент не видит дополнительного round-trip.
Ограничения:
- Если
client_prepared_statements_cache_sizeнового конфига меньше, чем количество entries у клиента -- лишние вытесняются (LRU). Оставшиеся работают нормально. - Anonymous prepared statements (
Parseс пустым именем) переживают миграцию, но требуют повторногоParseпередBindв новом процессе. DEALLOCATE ALLпосле миграции очищает переданный кэш. ПовторныйParseс тем же именем использует новый текст запроса.
TLS migration
По умолчанию TLS-клиенты не мигрируют -- зашифрованная сессия
требует ключевой материал, который живёт внутри OpenSSL state machine.
Такие клиенты дренируются при upgrade: соединение закрывается при
истечении shutdown_timeout, клиент переподключается к новому процессу.
Opt-in фича tls-migration решает эту проблему. Патченный OpenSSL
экспортирует symmetric cipher state, передаёт его вместе с fd через
Unix socket, а новый процесс импортирует состояние и продолжает
шифрование. Клиент не делает повторный TLS handshake.
Что экспортируется
Патч добавляет SSL_export_migration_state() и
SSL_import_migration_state() в OpenSSL 3.5.5. Экспортируемые данные:
- Версия TLS-протокола
- ID cipher suite и tag length
- Symmetric keys для чтения/записи (входные данные для AES key schedule, не развёрнутые)
- IV (nonce) для чтения/записи
- Sequence numbers для чтения/записи (по 8 байт)
- Для TLS 1.3: server и client application traffic secrets
Этого достаточно для восстановления record layer в новом процессе и продолжения шифрования/дешифрования на том же TCP-соединении.
Сборка с TLS migration
cargo build --release --features tls-migration
Требует perl и patch в build-окружении. Vendored OpenSSL 3.5.5
собирается из исходников с наложенным патчем.
Offline-сборка (без доступа к интернету)
# Скачать tarball заранее
curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz
# Собрать с указанием пути
OPENSSL_SOURCE_TARBALL=./openssl-3.5.5.tar.gz \
cargo build --release --features tls-migration
SHA-256 tarball'а проверяется автоматически.
Ограничения
- Linux only. На macOS/Windows TLS migration не поддерживается (native-tls использует Security.framework / SChannel, не OpenSSL).
- Одинаковые сертификаты. Старый и новый процесс должны
использовать одни и те же
tls_private_keyиtls_certificate. Cipher state привязан к SSL_CTX, созданному из сертификата. Изменённые сертификаты приводят к ошибке импорта и отключению клиента. - FIPS несовместимо. Vendored OpenSSL не проходит FIPS-валидацию.
Для FIPS используйте сборку без
tls-migration(TLS-клиенты будут дренироваться вместо миграции). - Нет HSM/PKCS#11. Vendored OpenSSL собирается с
no-engine.
Известные ограничения
-
TLS 1.3 KeyUpdate меняет ключи шифрования. Если любая сторона отправит KeyUpdate после экспорта cipher state, импортированные ключи станут невалидными — соединение упадёт с ошибкой AEAD.
Поведение драйверов (проверено апрель 2026):
Драйвер Auto KeyUpdate? Риск libpq (psql, pgbench) Нет — OpenSSL не отправляет Нет asyncpg (Python) Нет — Python ssl = OpenSSL Нет node-postgres Нет — Node.js tls = OpenSSL Нет Npgsql (.NET) Нет — SslStream без KeyUpdate API Нет pgjdbc (Java) Да — JSSE после ~128 GB ( jdk.tls.keyLimits)Высокий tokio-postgres (rustls) Да — rustls при AEAD limit Средний PostgreSQL server Нет — renegotiation отключён, KeyUpdate не вызывается Нет Java-клиенты: JSSE автоматически отправляет KeyUpdate после ~128 GB зашифрованных данных. Баг JDK-8329548 может вызвать шторм KeyUpdate сообщений. Для Java с долгоживущими high-throughput соединениями TLS migration может потерять соединения. Обходной путь: увеличить порог через
jdk.tls.keyLimitsвjava.security, или отключить TLS между Java-клиентом и pg_doorman.Rust-клиенты с rustls: rustls ротирует ключи при лимитах AEAD (очень высокий порог, ~2^36 records для AES-GCM). Для PostgreSQL workloads практически недостижимо. Использование
native-tls(OpenSSL) вместо rustls устраняет риск.Все OpenSSL-драйверы безопасны. OpenSSL явно не выполняет автоматический key update (openssl#23566).
-
SSL_pending данные не проверяются. Миграция происходит в idle point, где нет буферизованных данных приложения. Инвариант idle point это гарантирует, но явная проверка
SSL_pending()не выполняется. -
Привязка к OpenSSL 3.5.5. Патч модифицирует внутренние структуры OpenSSL (
ssl_local.h,rec_layer_s3.c,ssl_lib.c). При обновлении OpenSSL нужно проверить и переложить патч на новую версию.
Сигналы
| Сигнал | Поведение |
|---|---|
SIGUSR2 | Binary upgrade + graceful shutdown. Рекомендуемый для всех режимов. |
SIGINT | В foreground + TTY (Ctrl+C): только shutdown, без upgrade. В daemon / non-TTY: binary upgrade (legacy-совместимость). |
SIGTERM | Немедленный выход. Транзакции обрываются. Все клиенты отключаются. |
SIGHUP | Перечитать конфигурацию без перезапуска. Без простоя. |
UPGRADE (admin) | Отправляет SIGUSR2 текущему процессу. Тот же эффект. |
Legacy-поведение SIGINT: SIGINT запускает binary upgrade в daemon mode или без TTY (например, под systemd). В интерактивном терминале Ctrl+C останавливает процесс без запуска нового. Используйте
kill -USR2илиUPGRADEв admin-консоли для binary upgrade в foreground mode.
Daemon vs foreground
| Foreground | Daemon | |
|---|---|---|
| Миграция клиентов через fd passing | Да (socketpair) | Нет |
| Idle-клиенты сохраняются | Да | Нет (drain с 58006) |
| In-tx клиенты | Завершают tx, затем мигрируют | Завершают tx, затем 58006 |
| Запуск нового процесса | Наследует listener fd | Запускается независимо |
| Рекомендуется для | systemd, контейнеры, k8s | Legacy-установки |
Для zero-downtime upgrade с миграцией клиентов запускайте в foreground mode. systemd управляет жизненным циклом процесса:
[Service]
Type=simple
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman.yaml
ExecReload=/bin/kill -SIGUSR2 $MAINPID
Конфигурация
shutdown_timeout
Максимальное время ожидания завершения транзакций перед принудительным закрытием соединений. Старый процесс завершается по истечении этого таймаута вне зависимости от оставшихся клиентов.
По умолчанию: 10 секунд.
Рекомендация для production с длинными аналитическими запросами: 30-60 секунд.
[general]
shutdown_timeout = 60000 # миллисекунды
Слишком маленькое значение -- риск убить активные транзакции. Слишком большое -- задержка выхода старого процесса при зависшем клиенте (например, idle-in-transaction). Выбирайте значение, покрывающее самую длинную ожидаемую транзакцию, с запасом.
tls_private_key / tls_certificate
Для TLS migration оба процесса (старый и новый) загружают одни и те же файлы. Если сертификат поменялся между версиями бинарника -- TLS-клиенты получат ошибку при импорте cipher state и будут отключены.
Ротацию сертификатов делайте через SIGHUP (reload конфига) до
binary upgrade.
prepared_statements_cache_size
Pool-level кэш prepared statements. Напрямую на миграцию не влияет, но pool cache в новом процессе должен быть достаточного размера для entries от мигрированных клиентов.
client_prepared_statements_cache_size
Per-client кэш prepared statements. Клиентский кэш сериализуется полностью при миграции. Если новый конфиг имеет меньшее значение -- LRU вытесняет лишние записи.
Мониторинг
Логи
Ключевые строки в логах при миграции:
INFO Got SIGUSR2, starting binary upgrade and graceful shutdown
INFO Validating configuration with: /usr/bin/pg_doorman -t pg_doorman.yaml
INFO Configuration validation successful
INFO Starting new process with inherited listener fd=5
INFO New process signaled readiness
INFO Client migration enabled
INFO [user@pool #c42] client 10.0.0.1:51234 migrated to new process
INFO waiting for 3 clients in transactions
INFO All clients disconnected, shutting down
INFO Migration sender finished
В новом процессе:
INFO migration receiver: listening for migrated clients
INFO [user@pool #c42] migrated client accepted from 10.0.0.1:51234
INFO migration receiver done: migration socket closed
INFO migration receiver: stopped
Prometheus-метрики
| Метрика | Значение при upgrade |
|---|---|
pg_doorman_pools_clients{status="active"} | Должна упасть до 0 на старом процессе |
pg_doorman_pools_clients{status="idle"} | Падает по мере миграции клиентов |
pg_doorman_connection_count{type="total"} | Старый: убывает, новый: растёт |
pg_doorman_clients_prepared_cache_entries | Подтверждает перенос кэша |
Admin-консоль
-- На новом процессе (старый отклоняет не-admin соединения)
SHOW POOLS;
SHOW CLIENTS;
Troubleshooting
Клиент получил "pooler is shut down now" (58006) вместо миграции
Ctrl+C в foreground mode. SIGINT в TTY = shutdown без upgrade.
Используйте kill -USR2 или UPGRADE в admin-консоли.
Daemon mode. Daemon mode не использует fd-based миграцию. Клиенты дренируются. Переключитесь на foreground mode.
PG_DOORMAN_CI_SHUTDOWN_ONLY=1 установлен. Эта переменная
окружения принудительно включает shutdown-only mode (используется
в CI-тестах). Уберите её.
Старый процесс не завершается
Длинная транзакция. Клиент застрял в BEGIN без COMMIT.
Дождитесь shutdown_timeout или завершите транзакцию вручную.
Admin-соединения. Admin-соединения не мигрируются. Закройте admin-сессию на старом процессе.
Принудительный выход: kill -TERM <old_pid> отправляет SIGTERM.
TLS-соединение оборвалось после upgrade
Бинарник собран без --features tls-migration. TLS-клиенты
дренируются вместо миграции. Пересоберите с feature flag.
Запуск не на Linux. TLS migration работает только на Linux.
Сертификат/ключ изменились. Старый процесс экспортировал cipher state, привязанный к старому сертификату. Используйте те же файлы для обоих процессов. Ротацию сертификатов делайте через SIGHUP до binary upgrade.
"TLS migration not available" в логах
Новый процесс получил миграционный payload с TLS-данными, но собран
без --features tls-migration или запущен не на Linux. Клиент
отключается. Пересоберите новый бинарник с feature flag.
"migration channel not ready" в логах
Канал MIGRATION_TX ещё не инициализирован. Новый процесс не
завершил запуск, когда клиент попытался мигрировать. Клиент
повторит попытку на следующей idle-итерации (через миллисекунды).
"migration channel send failed" в логах
Канал миграции переполнен (capacity: 4096). Возможно при одновременной миграции тысяч клиентов. Клиент повторит попытку на следующей idle-итерации.
"prepare_migration failed" в логах
Raw fd клиента недоступен или dup() не удался. Возможные причины:
исчерпание файловых дескрипторов, или клиент подключился через
code path, который не сохраняет raw fd. Проверьте ulimit -n.
Совместимость с клиентскими библиотеками: Библиотеки вроде
github.com/lib/pqили Godatabase/sqlмогут потребовать настройки для обработки reconnect при получении error 58006 (для клиентов в daemon mode или застрявших дольшеshutdown_timeout). См. issue.
Чек-лист перед production
Перед выкатом binary upgrade в production:
- Запуск в foreground mode (не daemon) для fd-based миграции
-
shutdown_timeoutпокрывает самую длинную ожидаемую транзакцию (рекомендация: 30-60 секунд для OLTP, больше для аналитики) -
Если используете TLS: сборка с
--features tls-migration, оба процесса используют одинаковые файлы сертификата и ключа - Протестировать upgrade в staging: открыть сессию, отправить SIGUSR2, убедиться что сессия продолжает работать
-
В systemd unit есть
ExecReload=/bin/kill -SIGUSR2 $MAINPID - Мониторинг логов на ошибки миграции после первого production upgrade
-
Подтвердить что старый процесс завершился (PID file или
pgrep) - Проверить Prometheus-метрики: клиенты на новом процессе
Сигналы и перезагрузка
pg_doorman реагирует на четыре POSIX-сигнала: SIGHUP, SIGINT, SIGUSR2 и SIGTERM. Каждый делает одну конкретную вещь.
Краткая справка
| Сигнал | Эффект | Существующие соединения | Когда применять |
|---|---|---|---|
SIGHUP | Перезагрузить конфиг с диска. | Сохраняются. | Подкрутить пулы, ротировать серверные TLS-сертификаты, отредактировать pg_hba.conf. |
SIGTERM | Плавное завершение работы. | Дренируются до простоя, затем закрываются. | Остановка сервиса. |
SIGUSR2 | Плавное обновление бинарника. | Мигрируют в новый процесс. | Замена бинарника без простоя. |
SIGINT | Зависит от TTY (см. ниже). | По-разному. | Ctrl+C при разработке; устарело для production. |
Перезагрузка (SIGHUP)
kill -HUP $(pidof pg_doorman)
Перечитывает файл конфига и применяет изменения. Что перезагружается:
- Определения пулов (добавлены, удалены, изменён размер).
- Списки пользователей, пароли, блоки
auth_query. - Правила
pg_hba.conf(файл или встроенное содержимое). - Серверные TLS-сертификаты и CA-бандлы (подмена без блокировок; существующие TLS-соединения сохраняют исходный контекст).
- Публичные ключи Talos и JWT.
- Уровень логирования и формат логов.
Что не перезагружается:
general.host,general.port— слушающий сокет фиксируется при старте.- Клиентские TLS-сертификаты — нужен перезапуск процесса или используйте обновление бинарника.
- Число рабочих потоков и параметры рантайма Tokio.
После перезагрузки SHOW CONFIG отображает новые значения. Существующие клиентские соединения не перепроверяются по новому pg_hba.conf — только новые соединения.
Плавное завершение работы (SIGTERM)
kill -TERM $(pidof pg_doorman)
pg_doorman:
- Прекращает принимать новые соединения.
- Закрывает простаивающие соединения с бэкендом.
- Логирует, сколько клиентов ещё в транзакциях.
- Ждёт до
shutdown_timeout(по умолчанию 10 с), пока клиенты завершат работу. - Выходит.
shutdown_timeout — жёсткий потолок. Клиенты, всё ещё находящиеся в транзакциях по его истечении, получают закрытие соединения.
general:
shutdown_timeout: "30s"
Для systemd установите TimeoutStopSec больше, чем shutdown_timeout, чтобы systemd не отправил SIGKILL, пока pg_doorman ещё дренирует.
Обновление бинарника (SIGUSR2)
kill -USR2 $(pidof pg_doorman)
Рекомендуемый способ заменить бинарник, не теряя клиентов:
- Замените бинарник на диске новой версией.
- Отправьте
SIGUSR2работающему процессу. - Текущий процесс порождает дочерний с новым бинарником, передаёт ему слушающий сокет и продолжает обслуживать существующих клиентов до их завершения.
- Новые клиенты сразу подключаются к дочернему процессу.
- Старый процесс выходит, когда последняя клиентская транзакция завершится (или по
shutdown_timeout).
Дочерний процесс отправляет sd_notify MAINPID=<new_pid>, чтобы юниты systemd с Type=notify бесшовно обновили свой главный PID.
Полный протокол, миграцию TLS и откат смотрите в Binary Upgrade.
SIGINT (Ctrl+C)
SIGINT зависит от контекста:
- На переднем плане с TTY (разработка,
cargo run): только плавное завершение работы. - В режиме демона или без TTY (legacy production): запускает обновление бинарника и плавное завершение работы, как
SIGUSR2.
Legacy-путь обновления через SIGINT существует ради обратной совместимости с инсталляциями, которые отправляют SIGINT из init-скриптов. Новые инсталляции должны явно использовать SIGUSR2 для обновления и SIGTERM для остановки.
Интеграция с systemd
pg_doorman поддерживает Type=notify. Поставляемый юнит pg_doorman.service запускает бинарник в основном процессе и уведомляет systemd через sd_notify:
[Service]
Type=notify
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.yaml
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=60
Restart=on-failure
sd_notify READY=1 отправляется после привязки слушающего сокета и инициализации пулов. sd_notify MAINPID=<child> отправляется при обновлении бинарника, чтобы systemd корректно отслеживал новый процесс.
Если переходите с Type=forking + --daemon, уберите --daemon и переключитесь на Type=notify — меньше движущихся частей и корректное отслеживание готовности. Старые инсталляции с --daemon продолжают работать, но не получают преимуществ sd_notify.
Режим демона
pg_doorman --daemon ответвляется в фон и пишет PID в daemon_pid_file (по умолчанию /tmp/pg_doorman.pid). Под systemd предпочитайте Type=notify вместо --daemon.
general:
daemon_pid_file: "/var/run/pg_doorman.pid"
Куда дальше
- Binary Upgrade — полный протокол обновления с миграцией TLS.
- Troubleshooting — что проверить, когда перезагрузка не подхватывает изменения.
- TLS — семантика перезагрузки серверных сертификатов по
SIGHUP.
Диагностика
Это руководство помогает решить типичные проблемы при использовании PgDoorman.
Ошибки аутентификации при подключении к PostgreSQL
Симптом: PgDoorman запустился успешно, но клиенты получают ошибки аутентификации вроде password authentication failed при попытке выполнить запрос.
Если username пула совпадает с пользователем backend-PostgreSQL
PgDoorman по умолчанию использует passthrough authentication -- криптографическое доказательство клиента (MD5-хэш или SCRAM ClientKey) повторно используется для аутентификации в PostgreSQL. Убедитесь, что поле password в конфиге содержит именно тот хэш, что хранится в pg_authid / pg_shadow:
SELECT usename, passwd FROM pg_shadow WHERE usename = 'your_user';
Скопируйте хэш (например, md5... или SCRAM-SHA-256$...) в поле password конфига. Хэш должен совпадать с тем, что хранится в PostgreSQL (та же соль и количество итераций для SCRAM).
Если username пула отличается от backend-пользователя
Когда обращённый к клиенту username в PgDoorman не совпадает с реальной ролью PostgreSQL, passthrough работать не может -- нужны явные credentials:
users:
- username: "app_user" # имя для клиента
password: "md5..." # хэш для аутентификации клиента
server_username: "pg_app_user" # реальная роль в PostgreSQL
server_password: "plaintext_pwd" # plaintext-пароль для этой роли
pool_size: 40
Это же касается JWT-аутентификации, где нет пароля для passthrough.
Хэши паролей пользователей можно получить из PostgreSQL запросом: SELECT usename, passwd FROM pg_shadow;
Или используйте команду pg_doorman generate, которая получает их автоматически.
Файл конфигурации не найден
Симптом: PgDoorman завершается с ошибкой "configuration file not found".
Решение: укажите путь к файлу конфигурации явно:
pg_doorman /path/to/pg_doorman.yaml
По умолчанию PgDoorman ищет pg_doorman.toml в текущей директории.
Pool size слишком мал
Симптом: клиенты долго ждут или получают ошибки про слишком большое количество соединений.
Решение: увеличьте pool_size для затронутого пользователя или проверьте значения cl_waiting и maxwait в admin-команде SHOW POOLS. Если maxwait стабильно высокий, значит пул мал для вашей нагрузки.
Если столкнулись с проблемой, которой здесь нет, откройте issue на GitHub.
Admin-команды
pg_doorman предоставляет admin-базу, совместимую с протоколом Postgres. Подключайтесь к тому же порту, что и обычные клиенты, но с dbname=pgdoorman и admin-учёткой из конфига:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman
Или через connection string psql:
psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman"
Admin-команды читаются через SHOW <subcommand> или выполняются голыми глаголами (PAUSE, RESUME, RECONNECT, RELOAD, SHUTDOWN, SET <param> = <value>).
Команды SHOW
| Команда | Назначение |
|---|---|
SHOW HELP | Список доступных команд. |
SHOW CONFIG | Текущая активная конфигурация. Только для чтения. |
SHOW DATABASES | По одной строке на пул: host, port, database, размер пула, режим. |
SHOW POOLS | Снимок утилизации пула на пару user×database: idle/active/waiting клиенты, idle/active серверы. |
SHOW POOLS_EXTENDED | SHOW POOLS плюс полученные/отправленные байты и среднее время ожидания. |
SHOW POOLS_MEMORY | Учёт памяти на пул для кэша prepared statements (клиентский и серверный). |
SHOW POOL_COORDINATOR | Состояние Pool Coordinator на базу: текущие соединения, использование резерва, число вытеснений. См. Pool Coordinator. |
SHOW POOL_SCALING | Метрики anticipation/burst: in-flight create-операции, ожидания на воротах, anticipation notifies/timeouts. |
SHOW PREPARED_STATEMENTS | Закэшированные prepared statements на пул: hash, имя, текст запроса, число попаданий. |
SHOW CLIENTS | Активные клиенты: ID, database, user, имя приложения, адрес, состояние TLS, счётчики transaction/query/error, возраст. |
SHOW SERVERS | Активные соединения с бэкендом: ID сервера, PID бэкенда, database, user, TLS, состояние, счётчики transaction/query, попадания/промахи кэша prepare, байты. |
SHOW CONNECTIONS | Число соединений по типу: total, errors, TLS, plain, cancel. |
SHOW STATS | Агрегированная статистика на пару user×database: всего транзакций, запросов, времени, байт, средние. |
SHOW LISTS | Счётчики по категориям (databases, users, pools, clients, servers). |
SHOW USERS | Список пользователей и их режимы пула. |
SHOW AUTH_QUERY | Кэш auth_query: попадания/промахи/перезапросы, успехи/отказы аутентификации, ошибки исполнителя, счётчики динамических пулов. |
SHOW SOCKETS | Счётчики TCP- и Unix-сокетов по состоянию (только Linux — читает /proc/net/). |
SHOW LOG_LEVEL | Текущий уровень логирования. |
SHOW VERSION | Версия pg_doorman. |
SHOW POOL_COORDINATOR и SHOW POOL_SCALING не имеют аналогов в PgBouncer или Odyssey — они показывают внутренние механизмы pg_doorman.
Управляющие команды
| Команда | Эффект |
|---|---|
PAUSE | Прекратить принимать новые клиентские запросы. Существующие клиенты завершают свои транзакции. |
PAUSE <database> | Поставить на паузу один пул. |
RESUME / RESUME <database> | Возобновить после PAUSE. |
RECONNECT / RECONNECT <database> | Принудительно пересоздать соединения с бэкендом (закрыть простаивающие, дренировать активные). Новые соединения берутся из PostgreSQL. |
RELOAD | То же, что и SIGHUP — перезагрузить конфиг с диска. |
SHUTDOWN | То же, что и SIGTERM — плавное завершение работы. |
KILL <database> | Сбросить всех клиентов, подключённых к конкретному пулу. |
SET log_level = '<level>' | Изменить уровень логирования в рантайме (error, warn, info, debug, trace). |
PAUSE/RESUME полезны при failover или окнах обслуживания. RECONNECT после ротации учётных данных в pg_authid гарантирует, что бэкенды используют новый пароль.
Чтение типового вывода
SHOW POOLS
database | user | cl_idle | cl_active | cl_waiting | sv_active | sv_idle | sv_used | maxwait
mydb | app | 12 | 4 | 0 | 4 | 36 | 0 | 0.0
cl_waiting > 0означает, что клиенты застряли в ожидании бэкенда. Либо поднимитеpool_size, либо проверьте медленные запросы.sv_idleсоответствует свободным бэкендам;sv_active— занятым;sv_used— зарезервированным координатором (см. ниже).maxwait— самое долгое текущее ожидание в секундах. Если оно вырастает заquery_wait_timeout, клиенты получают ошибки.
SHOW POOL_COORDINATOR
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
mydb | 80 | 78 | 16 | 2 | 142 | 18 | 0
evictionsбыстро растут: какой-то пользователь голодает раз за разом. Задайте или поднимитеmin_guaranteed_pool_sizeдля этого пользователя.reserve_acqвысокий: всплески — норма, но возможно вы недооценили размер. Подумайте о повышенииmax_db_connections, а не о ставке на резерв.exhaustionsненулевые: даже резерв был полным. Клиенты упёрлись вquery_wait_timeout. Поднимите потолок.
Тонкости настройки см. в Pool Coordinator.
SHOW POOL_SCALING
user | database | inflight | creates | gate_waits | burst_gate_budget_ex | antic_notify | antic_timeout | create_fallback | replenish_def
app | mydb | 1 | 12345 | 87 | 3 | 142 | 8 | 22 | 0
inflight— текущие создания соединений с бэкендом в процессе.gate_waitsрастут:scaling_max_parallel_createsпридушивает вас. Допустимо, если PostgreSQL под нагрузкой; поднимите, если PG может обработать больше параллельных вызововconnect().- Соотношение
antic_notifyиantic_timeout: высокий счётчик timeout означает, что упреждающее ожидание не успевает поймать возвращающееся соединение. Поднимитеscaling_warm_pool_ratio, чтобы пул рос с опережением спроса. create_fallbackрастёт — срабатывает предзамена: соединения истекают раньше, чем естественным образом возвращаются.
См. Пул под нагрузкой → Параметры тюнинга.
Аутентификация
Admin-база использует учётку из general.admin_username и general.admin_password:
general:
admin_username: "admin"
admin_password: "change_me"
Admin-соединения не проходят через правила pg_hba.conf — они идут напрямую в admin-обработчик. Ограничивайте admin-доступ на сетевом уровне (listen_addresses, фаервол) или используйте Unix-сокеты.
Куда дальше
- Prometheus reference — те же данные в машинно-читаемом виде.
- Pool Coordinator — что говорит вам
SHOW POOL_COORDINATOR. - Pool Pressure — что говорит вам
SHOW POOL_SCALING. - Troubleshooting — типичные сбои и их вывод в
SHOW.
Структурированное 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 3339 | UTC, точность до миллисекунд. |
level | строка | ERROR, WARN, INFO, DEBUG, TRACE. |
file | строка | Файл исходника, который пишет лог. |
line | целое | Номер строки. |
message | строка | Человекочитаемое сообщение. |
Вложенных полей и меток на событие нет — логгер pg_doorman сериализует обычные события макроса log в JSON. Для богатых метаданных (счётчики на пул, события на клиент) используйте Prometheus-метрики. См. Prometheus reference.
Уровень логирования
Задаётся через general.log_level в конфиге или переопределяется при старте:
general:
log_level: "info"
pg_doorman -l debug -F Structured /etc/pg_doorman/pg_doorman.yaml
Изменение в рантайме через admin-базу:
SET log_level = 'debug';
SHOW LOG_LEVEL;
Это влияет только на текущий процесс. Чтобы изменения сохранялись, отредактируйте конфиг и выполните RELOAD или отправьте SIGHUP.
Рекомендуемый пайплайн
Для Kubernetes:
spec:
containers:
- name: pg_doorman
image: ghcr.io/ozontech/pg_doorman:latest
args:
- "-F"
- "Structured"
- "/etc/pg_doorman/pg_doorman.yaml"
env:
- name: LOG_LEVEL
value: "info"
Логи идут в stdout, рантайм контейнера их захватывает, ваш log shipper (Promtail, Fluent Bit, Vector) пересылает их как есть — JSON сохраняется на всём пути.
Для systemd:
[Service]
ExecStart=/usr/bin/pg_doorman -F Structured /etc/pg_doorman/pg_doorman.yaml
StandardOutput=journal
StandardError=journal
journalctl -u pg_doorman -o json возвращает JSON обратно.
Оговорки
- Для production выбирайте
Text(терминалы, syslog) илиStructured(log shippers).Debugзарезервирован под будущее использование и сейчас равенText. fileиlineберутся из мест вызова макросаlog. Они доступны в release-сборках, потому что pg_doorman поставляется с включённой отладочной информацией.- Логгер не включает trace-идентификаторы и корреляцию запросов. Для трассировки на запрос используйте
SHOW CLIENTSи Prometheus-метрики.
Куда дальше
- Prometheus reference — для машинно-читаемых метрик.
- Latency Percentiles — для сигналов о производительности.
- Admin Commands — для интроспекции в рантайме.
Перцентили задержек
pg_doorman измеряет задержки запросов и транзакций на пул, используя HDR Histogram. В Prometheus экспортируются четыре перцентиля: p50, p90, p95, p99.
Эта страница объясняет, откуда берутся числа и как их читать.
Что измеряется
Три серии задержек на пару user×database:
| Серия | Что покрывает |
|---|---|
query_histogram | Время от старта запроса до его завершения на бэкенде. Измеряет время выполнения PostgreSQL так, как его видит pg_doorman. |
xact_histogram | Время от BEGIN (или первого оператора неявной транзакции) до COMMIT / ROLLBACK. |
wait_histogram | Время, которое клиент провёл в ожидании, пока соединение с бэкендом не освободится. |
wait_histogram — собственный вклад пула в задержку. Если p99 у wait_histogram высокий, а p99 у query_histogram низкий, узкое место — получение соединения, а не PostgreSQL.
Детали гистограммы
pg_doorman использует HDR Histogram с параметрами:
- Максимальное значение: 10 минут (600 секунд).
- Значащих цифр: 2 (около 0,1% относительной погрешности).
Расход памяти: около 10 KB на гистограмму. Три гистограммы на пару user×database — это ~30 KB на пул, что комфортно для сотен пулов.
По умолчанию горизонт отчёта — время жизни процесса. Гистограммы сбрасываются на SIGHUP (перезагрузка конфига) и при явном RECONNECT.
Odyssey использует TDigest, PgBouncer перцентили не экспортирует. HDR предпочтителен, когда вы знаете верхнюю границу (10 минут — щедрый запас для пула соединений); TDigest работает с неограниченными потоками.
Экспорт в Prometheus
# HELP pg_doorman_pools_queries_percentile Query latency percentiles in milliseconds
# TYPE pg_doorman_pools_queries_percentile gauge
pg_doorman_pools_queries_percentile{percentile="50",user="app",database="mydb"} 1.2
pg_doorman_pools_queries_percentile{percentile="90",user="app",database="mydb"} 4.7
pg_doorman_pools_queries_percentile{percentile="95",user="app",database="mydb"} 8.1
pg_doorman_pools_queries_percentile{percentile="99",user="app",database="mydb"} 24.5
# HELP pg_doorman_pools_transactions_percentile Transaction latency percentiles in milliseconds
# TYPE pg_doorman_pools_transactions_percentile gauge
pg_doorman_pools_transactions_percentile{percentile="50",user="app",database="mydb"} 3.8
# ... (90, 95, 99)
# HELP pg_doorman_pools_avg_wait_time Average client wait time in milliseconds
# TYPE pg_doorman_pools_avg_wait_time gauge
pg_doorman_pools_avg_wait_time{user="app",database="mydb"} 0.05
avg_wait_time — среднее, а не перцентиль (HDR для ожиданий тоже отслеживается, но сейчас экспортируется только среднее).
Чтение метрик
Здоровый пул
queries: p50=1.2 p90=4.7 p95=8.1 p99=24.5
xacts: p50=3.8 p90=11.2 p95=18.5 p99=42.7
wait avg: 0.05ms
p99 укладывается в 20× от p50 — типично для OLTP-нагрузок с редкими медленными запросами. Время ожидания — микросекунды, пул не является узким местом.
Пул под давлением
queries: p50=1.5 p90=4.9 p95=8.5 p99=25.0
xacts: p50=215 p90=1850 p95=2400 p99=4900
wait avg: 180ms
С задержкой запросов всё в порядке — PostgreSQL здоров. Но транзакции медленные, а время ожидания 180 мс. Клиенты выстраиваются в очередь за бэкендами. Проверьте SHOW POOLS на cl_waiting > 0 и SHOW POOL_COORDINATOR на вытеснения или исчерпания. Вероятное лечение: поднять pool_size или max_db_connections. См. Pool Coordinator.
Один медленный пользователь
user "fast_app": queries p99=12 xacts p99=35
user "report_job": queries p99=4500 xacts p99=8000
report_job тянет общую базу вниз. С включённым Pool Coordinator медленные транзакции report_job приводят к тому, что под давлением он первым отдаёт свои соединения (вытеснение смещено по p95-времени транзакций). Без Coordinator выделите report_job его собственный min_guaranteed_pool_size, чтобы он не голодил fast_app.
Grafana
Пример запроса задержки запросов по перцентилю:
pg_doorman_pools_queries_percentile{database="mydb"}
Пример алерта: p99 запросов выше 100 мс в течение 5 минут:
pg_doorman_pools_queries_percentile{percentile="99"} > 100
Пример алерта на насыщение очереди:
pg_doorman_pools_avg_wait_time > 50
JSON дашборда лежит в директории grafana/ проекта.
Оговорки
- Перцентили считаются на пул, а не на запрос. pg_doorman не скажет вам, какой именно запрос медленный, — для этого используйте
pg_stat_statementsв PostgreSQL. - HDR-гистограммы хранят значения, а не события. Один и тот же запрос, выполненный 100 тысяч раз, добавляет 100 тысяч сэмплов; частоту сэмплирования настроить нельзя.
- Экспорт всех четырёх перцентилей на серию сделан намеренно — экспортировать сырые корзины гистограммы в Prometheus было бы значительно тяжелее и редко полезно.
Куда дальше
- Admin Commands — читать перцентили напрямую через
SHOW POOLS_EXTENDED. - Prometheus reference — полный список метрик с метками.
- Pool Pressure — диагностические рецепты, когда перцентили выглядят неправильно.
- Benchmarks — эталонные распределения перцентилей под нагрузкой.
Настройки
Формат конфигурационного файла
pg_doorman поддерживает два формата конфигурационного файла:
- YAML (
.yaml,.yml) — основной и рекомендуемый формат для новых конфигураций. - TOML (
.toml) — поддерживается для обратной совместимости с уже существующими конфигурациями.
Формат определяется автоматически по расширению файла. Оба формата поддерживают одни и те же опции конфигурации и могут использоваться взаимозаменяемо.
Пример конфигурации YAML (рекомендуется)
general:
host: "0.0.0.0"
port: 6432
admin_username: "admin"
admin_password: "admin"
pools:
mydb:
server_host: "localhost"
server_port: 5432
pool_mode: "transaction"
users:
- username: "myuser"
password: "mypassword"
pool_size: 40
Пример конфигурации TOML (legacy)
[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "admin"
[pools.mydb]
server_host = "localhost"
server_port = 5432
pool_mode = "transaction"
[[pools.mydb.users]]
username = "myuser"
password = "mypassword"
pool_size = 40
Команда generate
Команда generate может выводить конфигурацию в любом из форматов. Формат определяется по расширению выходного файла. По умолчанию сгенерированная конфигурация содержит подробные inline-комментарии, поясняющие каждый параметр.
# Сгенерировать конфигурацию YAML (рекомендуется)
pg_doorman generate --output config.yaml
# Сгенерировать конфигурацию TOML (для обратной совместимости)
pg_doorman generate --output config.toml
# Сгенерировать полную справочную конфигурацию без подключения к PG
pg_doorman generate --reference --output config.yaml
# Сгенерировать справочную конфигурацию с комментариями на русском
pg_doorman generate --reference --ru --output config.yaml
# Сгенерировать конфигурацию без комментариев (только сериализация)
pg_doorman generate --no-comments --output config.yaml
| Флаг | Описание |
|---|---|
--no-comments | Отключить inline-комментарии в сгенерированной конфигурации (по умолчанию комментарии включены) |
--reference | Сгенерировать полную справочную конфигурацию с примерами значений, без подключения к PostgreSQL |
--russian-comments, --ru | Генерировать комментарии на русском языке для быстрого старта |
--format, -f | Формат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение |
Подключаемые файлы
Подключаемые файлы могут быть в любом из форматов, форматы можно смешивать. Например, основная конфигурация в YAML может подключать TOML-файлы и наоборот:
include:
files:
- "pools.yaml"
- "users.toml"
Человекочитаемые значения
pg_doorman поддерживает человекочитаемые форматы для значений продолжительности и размера в байтах, сохраняя обратную совместимость с числовыми значениями.
Формат продолжительности
Значения продолжительности можно задавать как:
- Простые числа: интерпретируются как миллисекунды (например,
5000= 5 секунд) - Строка с суффиксом:
ms— миллисекунды (например,"100ms")s— секунды (например,"5s"= 5000 миллисекунд)m— минуты (например,"5m"= 300000 миллисекунд)h— часы (например,"1h"= 3600000 миллисекунд)d— дни (например,"1d"= 86400000 миллисекунд)
Примеры:
general:
# Все эти варианты эквивалентны (3 секунды):
# connect_timeout: 3000 # обратная совместимость (миллисекунды)
# connect_timeout: "3s" # человекочитаемый формат
# connect_timeout: "3000ms" # явные миллисекунды
connect_timeout: "3s"
idle_timeout: "10m" # 10 минут
server_lifetime: "1h" # 1 час
Формат размера в байтах
Значения размера в байтах можно задавать как:
- Простые числа: интерпретируются как байты (например,
1048576= 1 MB) - Строка с суффиксом (регистр не важен):
B— байты (например,"1024B")KилиKB— килобайты (например,"1K"или"1KB"= 1024 байта)MилиMB— мегабайты (например,"1M"или"1MB"= 1048576 байт)GилиGB— гигабайты (например,"1G"или"1GB"= 1073741824 байт)
Примечание: используются двоичные префиксы (1 KB = 1024 байта, не 1000 байт).
Примеры:
general:
# Все эти варианты эквивалентны (256 MB):
# max_memory_usage: 268435456 # обратная совместимость (байты)
# max_memory_usage: "256MB" # человекочитаемый формат
# max_memory_usage: "256M" # короткая форма
max_memory_usage: "256MB"
unix_socket_buffer_size: "1MB" # 1 MB
worker_stack_size: "8MB" # 8 MB
Общие настройки
host
Хост, на котором сервер будет принимать соединения (только TCP v4).
По умолчанию: "0.0.0.0".
port
Порт для входящих соединений.
По умолчанию: 5432.
backlog
TCP backlog для входящих соединений. При значении ноль в качестве TCP backlog используется значение max_connections.
По умолчанию: 0.
max_connections
Максимальное число клиентов, которые могут одновременно подключиться к пулеру. При достижении лимита:
- Клиент, подключающийся без SSL, получит ожидаемую ошибку (код:
53300, сообщение:sorry, too many clients already). - Клиент, подключающийся через SSL, увидит сообщение о том, что сервер не поддерживает протокол SSL.
По умолчанию: 8192.
max_concurrent_creates
Максимальное число серверных соединений, которые могут создаваться параллельно в одном пуле. Параметр использует семафор для ограничения параллельного создания соединений, что заметно повышает производительность при холодном старте и пиковых сценариях.
Большие значения ускоряют прогрев пула, но могут увеличить нагрузку на сервер PostgreSQL во время штормов соединений. Меньшие значения дают более плавное создание соединений.
По умолчанию: 4.
tls_mode
Режим TLS для входящих соединений. Может принимать одно из следующих значений:
allow— TLS-соединения разрешены, но не обязательны. pg_doorman попытается установить TLS-соединение, если клиент его запросит.disable— TLS-соединения запрещены. Все соединения устанавливаются без шифрования TLS.require— TLS-соединения обязательны. pg_doorman принимает только соединения, использующие TLS-шифрование.verify-full— TLS-соединения обязательны, и pg_doorman проверяет клиентский сертификат. Этот режим обеспечивает максимальный уровень безопасности.
По умолчанию: "allow".
tls_ca_cert
Файл с CA-сертификатом для проверки клиентского сертификата. Обязателен, когда tls_mode установлен в verify-full.
По умолчанию: None.
tls_private_key
Путь к файлу приватного ключа для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_certificate.
По умолчанию: None.
tls_certificate
Путь к файлу сертификата для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_private_key.
По умолчанию: None.
tls_rate_limit_per_second
Ограничение числа одновременных попыток создания TLS-сессии. Любое значение, отличное от нуля, означает, что клиенты должны проходить через очередь для установки TLS-соединения. В некоторых случаях это необходимо, чтобы запустить приложение, которое открывает много соединений при старте (так называемый «горячий старт»).
По умолчанию: 0.
daemon_pid_file
Включение этого параметра активирует режим демона. Закомментируйте, если хотите запускать pg_doorman в foreground с флагом -d.
По умолчанию: "/tmp/pg_doorman.pid".
syslog_prog_name
Если задан, pg_doorman начинает отправлять сообщения в syslog (через /dev/log или /var/run/syslog). Закомментируйте, если хотите логировать в stdout.
По умолчанию: None.
log_client_connections
Логировать подключения клиентов для мониторинга.
По умолчанию: true.
log_client_disconnections
Логировать отключения клиентов для мониторинга.
По умолчанию: true.
worker_threads
Число worker-потоков Tokio runtime (потоков ОС) для обслуживания клиентских соединений.
Производительность масштабируется линейно до числа ядер CPU.
Также определяет число шардов для внутренних concurrent hash maps (worker_threads * 4, округлённо до ближайшей степени двойки, минимум 4).
В Kubernetes задавайте этот параметр явно — автоматическое определение CPU может вернуть число ядер хоста, а не лимит контейнера.
По умолчанию: 4.
worker_cpu_affinity_pinning
Привязывать каждый worker-поток к отдельному ядру CPU (sched_setaffinity). Отключается, если доступно меньше 3 ядер.
По умолчанию: false.
tokio_global_queue_interval
Настройки Tokio runtime. Управляет тем, как часто шедулер проверяет глобальную очередь задач. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.
По умолчанию: not set (uses tokio's default).
tokio_event_interval
Настройки Tokio runtime. Управляет тем, как часто шедулер проверяет внешние события (I/O, таймеры). Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.
По умолчанию: not set (uses tokio's default).
worker_stack_size
Настройки Tokio runtime. Задаёт размер стека для worker-потоков. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.
По умолчанию: not set (uses tokio's default).
max_blocking_threads
Настройки Tokio runtime. Задаёт максимальное число потоков для блокирующих операций. Современные версии tokio хорошо справляются с этим по умолчанию, поэтому параметр опционален.
По умолчанию: not set (uses tokio's default).
connect_timeout
Максимальное время ожидания при установке нового соединения с сервером PostgreSQL. Если соединение не удаётся установить за это время, попытка прерывается. Аналог server_connect_timeout из PgBouncer.
По умолчанию: 3000 (3 sec).
query_wait_timeout
Максимальное время ожидания клиентом серверного соединения, когда пул полностью занят. Если за это время серверное соединение не освобождается, клиент получает ошибку. Аналог query_wait_timeout из PgBouncer.
По умолчанию: 5000 (5 sec).
idle_timeout
Закрывать серверное соединение, которое простаивает (не выдано ни одному клиенту) дольше этого значения.
Применяется только к соединениям, обслужившим хотя бы один клиентский запрос. Прогретые или дополненные
соединения, никогда не выдававшиеся клиенту, под действие idle_timeout не попадают — они закрываются
только по истечении server_lifetime. Каждое соединение получает джиттер ±20%, чтобы избежать
синхронных массовых закрытий. Установите 0, чтобы отключить. Аналог server_idle_timeout из PgBouncer.
По умолчанию: 600000 (10 min).
server_lifetime
Максимальный возраст серверного соединения. Когда соединение превышает этот возраст и переходит
в idle, оно закрывается на ближайшем цикле retain. Активные транзакции не прерываются.
Применяется ко всем соединениям, включая прогретые, которые никогда не выдавались клиенту.
Каждое соединение получает джиттер ±20%, чтобы избежать thundering herd. Установите 0, чтобы отключить.
Аналог server_lifetime из PgBouncer.
По умолчанию: 1200000 (20 min).
retain_connections_time
Интервал проверки и закрытия idle-соединений, превысивших idle_timeout или server_lifetime.
Задача retain выполняется периодически с этим интервалом и удаляет просроченные соединения.
По умолчанию: 30000 (30 sec).
retain_connections_max
Максимальное число idle-соединений, закрываемых за один цикл retain.
При значении 0 все idle-соединения, превысившие idle_timeout или server_lifetime, закрываются немедленно.
При положительном значении за цикл по всем пулам закрывается не более указанного числа соединений.
Этот параметр управляет агрессивностью закрытия idle-соединений в pg_doorman. При значении по умолчанию 3
за цикл retain закрывается до 3 соединений, что обеспечивает контролируемую очистку. Если нужна
более быстрая очистка просроченных соединений, установите 0 (без ограничений), чтобы закрывать все
просроченные соединения за каждый цикл retain.
По умолчанию: 3.
server_idle_check_timeout
Время, после которого idle-серверное соединение должно быть проверено перед выдачей клиенту. Это помогает обнаружить мёртвые соединения, возникшие из-за рестарта PostgreSQL, сетевых проблем или серверных idle-таймаутов.
Когда соединение простояло в пуле дольше этого таймаута, pg_doorman отправляет минимальный запрос (;),
чтобы проверить, что соединение живо, прежде чем выдать его клиенту. Если проверка не проходит,
соединение отбрасывается и берётся новое.
Установите 0, чтобы отключить проверку (не рекомендуется для production-окружений с возможной
сетевой нестабильностью или рестартами PostgreSQL).
По умолчанию: 60s (60 seconds).
server_round_robin
Управляет тем, какое idle-серверное соединение выбирается для следующей транзакции.
false (LRU): переиспользует самое недавно возвращённое соединение. Держит горячими меньше соединений, лучше для локальности shared buffer в PostgreSQL.
true (Round Robin): равномерно ротирует все idle-соединения.
Аналог server_round_robin из PgBouncer.
По умолчанию: false.
sync_server_parameters
В transaction mode каждая транзакция может попасть на разный бэкенд. При sync_server_parameters = true
pg_doorman отслеживает изменения фиксированного набора параметров сессии и проигрывает их через SET
на каждом новом бэкенде перед выполнением транзакции. Отслеживаемые параметры: client_encoding, DateStyle, TimeZone,
standard_conforming_strings, application_name. Другие команды SET (например, statement_timeout, work_mem)
НЕ отслеживаются. Каждый checkout стоит дополнительного roundtrip. Если вам нужна только видимость
application_name в pg_stat_activity, используйте параметр application_name на уровне пула.
По умолчанию: false.
tcp_so_linger
По умолчанию pg_doorman отправляет RST вместо того, чтобы держать соединение открытым долгое время.
По умолчанию: 0.
tcp_no_delay
TCP_NODELAY для отключения алгоритма Нэйгла ради более низкой задержки.
По умолчанию: true.
tcp_keepalives_count
Число неподтверждённых TCP keepalive probes, после которого соединение считается мёртвым и закрывается.
По умолчанию: 5.
tcp_keepalives_idle
Keepalive включён по умолчанию и переопределяет дефолты ОС.
По умолчанию: 5.
tcp_keepalives_interval
Интервал в секундах между отдельными TCP keepalive probes после прохождения начального периода простоя (tcp_keepalives_idle).
По умолчанию: 5.
tcp_user_timeout
Задаёт опцию сокета TCP_USER_TIMEOUT для клиентских соединений (в секундах). Эта опция указывает
максимальное время, в течение которого переданные данные могут оставаться неподтверждёнными,
прежде чем TCP принудительно закроет соединение. Это помогает быстрее обнаруживать мёртвые
клиентские соединения, чем keepalive probes, когда соединение активно отправляет данные, но удалённый
конец стал недоступен (например, сбой сети, падение клиента).
При ненулевом значении, если данные остаются неподтверждёнными в течение указанного времени, соединение будет разорвано. Это особенно полезно, чтобы избежать задержек в 15–16 минут, вызванных TCP retransmission timeout, когда keepalive не помогает (например, при активной передаче данных).
Примечание: опция поддерживается только в Linux. На других ОС параметр игнорируется.
Установите 0, чтобы отключить (использовать значение по умолчанию ОС).
По умолчанию: 60.
unix_socket_buffer_size
Размер буфера для операций чтения и записи при подключении к PostgreSQL через unix-сокет.
По умолчанию: 1048576.
admin_username
Доступ к виртуальной admin-базе осуществляется по имени пользователя и паролю администратора.
По умолчанию: "admin".
admin_password
Доступ к виртуальной admin-базе осуществляется по имени пользователя и паролю администратора. Замените на ваш секрет.
По умолчанию: "admin".
prepared_statements
Переключатель для включения/отключения кеширования prepared statements.
По умолчанию: true.
prepared_statements_cache_size
Размер кеша prepared statements на уровне пула (общий для всех клиентов, подключающихся к одному пулу). Кеш хранит соответствие между хешем запроса и переписанным именем prepared statement.
По умолчанию: 8192.
client_prepared_statements_cache_size
Максимальное число prepared statements, кешируемых в одном клиентском соединении. Это механизм защиты
от вредоносных или некорректно работающих клиентов, которые не вызывают DEALLOCATE и могут вызвать
исчерпание памяти, создавая неограниченное число prepared statements в долгоживущих соединениях.
При достижении лимита самая старая запись вытесняется из клиентского кеша. Вытесненный statement
по-прежнему может быть переиспользован позже, потому что кеш уровня пула (prepared_statements_cache_size)
сохраняет соответствие запроса и имени на сервере.
Установите 0, чтобы отключить лимит (неограниченный размер кеша, надежда на вызов DEALLOCATE клиентом).
По умолчанию: 0 (unlimited).
message_size_to_be_stream
Когда сообщение DataRow PostgreSQL превышает этот порог, pg_doorman переключается в потоковый режим:
данные пересылаются клиенту чанками по 4 KB вместо буферизации всего сообщения целиком.
Это предотвращает OOM на запросах, возвращающих очень большие строки (например, таблицы с большими
колонками bytea/text). Сам порог по умолчанию равен 1 MB.
По умолчанию: 1048576 (1 MB).
scaling_warm_pool_ratio
Доля прогретого пула в процентах (0–100). Когда размер пула ниже этого порога
от max_size, новые соединения создаются немедленно. Выше порога пул сначала
крутится через быстрые ретраи, затем переходит в event-driven цикл упреждающего ожидания,
который ждёт возврата idle-соединения. Цикл ограничен оставшимся query_wait_timeout
клиента минус резерв 500 ms на путь создания, поэтому он не может вытолкнуть
вызывающего за его собственный дедлайн ожидания.
По умолчанию: 20.
scaling_fast_retries
Число быстрых ретраев через yield_now() для ожидания с низкой задержкой при выдаче
соединений выше порога прогрева пула. Каждый ретрай занимает примерно 1–5 мкс.
После исчерпания быстрых ретраев пул переходит в event-driven цикл упреждающего ожидания,
ограниченный оставшимся query_wait_timeout клиента.
По умолчанию: 10.
scaling_max_parallel_creates
Ограничитель всплесков для создания соединений. Без этого лимита N параллельных
вызывающих timeout_get, не нашедших соединения в idle-пуле, независимо инициируют
backend-connect, создавая всплески thundering-herd под нагрузкой. С лимитом одновременно
выполняется не больше указанного числа creates на пул; остальные кратко ждут на Notify
и затем либо подхватывают только что возвращённое idle-соединение, либо занимают следующий
слот для create. Значение по умолчанию 2 — компромисс между пропускной способностью
и сглаживанием всплесков.
По умолчанию: 2.
max_memory_usage
Общий бюджет памяти для внутренних буферов, хранящих данные in-flight запросов по всем клиентским соединениям. При достижении лимита pg_doorman отклоняет новые запросы с ошибкой, пока существующие запросы не завершатся и не освободят свои буферы. Защищает процесс пулера от OOM при тяжёлой нагрузке или больших результирующих наборах.
По умолчанию: 268435456 (256 MB).
shutdown_timeout
При graceful shutdown (SIGTERM) pg_doorman ждёт до этого времени завершения in-flight транзакций перед принудительным закрытием соединений.
По умолчанию: 10000 (10 sec).
proxy_copy_data_timeout
Максимальное время ожидания операций копирования данных при проксировании, в миллисекундах.
По умолчанию: 15000 (15 sec).
server_tls_mode
Режим TLS для исходящих соединений к серверам PostgreSQL.
allow— Сначала пробовать без TLS; если сервер отказывает, повторить с TLS. Соответствует libpq sslmode=allow (по умолчанию).disable— TLS не используется.prefer— TLS используется, если сервер его поддерживает; иначе обычное соединение.require— TLS обязателен, но сертификат сервера не проверяется.verify-ca— TLS обязателен, и сертификат сервера проверяется поserver_tls_ca_cert.verify-full— TLS обязателен, сертификат проверяется, и hostname сервера должен совпадать с сертификатом.
По умолчанию: "allow".
server_tls_ca_cert
CA-сертификат для проверки сертификатов серверов PostgreSQL. Обязателен, когда server_tls_mode равен verify-ca или verify-full.
По умолчанию: None.
server_tls_certificate
Клиентский сертификат для mTLS с серверами PostgreSQL. Используйте в паре с server_tls_private_key.
По умолчанию: None.
server_tls_private_key
Приватный ключ для клиентского сертификата mTLS. Используйте в паре с server_tls_certificate.
По умолчанию: None.
hba
Список IP-адресов, с которых разрешено подключаться к pg-doorman.
По умолчанию: [].
pg_hba
Новый стиль контроля доступа клиентов в нативном формате pg_hba.conf PostgreSQL. Позволяет задавать тонкие правила доступа аналогично PostgreSQL: per-database, per-user, диапазоны адресов, требования TLS.
Указать general.pg_hba можно тремя способами:
- Как многострочную строку с содержимым файла
pg_hba.conf - Как объект с
path, указывающий на файл на диске - Как объект с
content, содержащий правила в виде строки
Примеры:
[general]
# Inline-содержимое (TOML-строка в тройных кавычках)
pg_hba = """
# type database user address method
host all all 10.0.0.0/8 md5
hostssl all all 0.0.0.0/0 scram-sha-256
hostnossl all all 192.168.1.0/24 trust
"""
# Или загрузить из файла
# pg_hba = { path = "./pg_hba.conf" }
# Или встроить как однострочную строку
# pg_hba = { content = "host all all 127.0.0.1/32 trust" }
Поддерживаемые поля и методы:
- Типы соединений:
local,host,hostssl,hostnossl(TLS-зависимое сопоставление учитывается) - Сопоставитель базы: имя или
all - Сопоставитель пользователя: имя или
all - Адрес: CIDR-форма вроде
1.2.3.4/32или::1/128(обязателен для правил, отличных отlocal) - Методы:
trust,md5,scram-sha-256(неизвестные методы парсятся, но трактуются проверяющим как «не разрешено»)
Приоритет и совместимость:
general.pg_hbaимеет приоритет над устаревшим спискомgeneral.hba. Нельзя задавать оба одновременно; валидация конфигурации отклонит такую комбинацию.- Правила вычисляются по порядку; первое совпавшее правило определяет результат.
Поведение method = trust:
- Когда совпавшее правило имеет
trust, PgDoorman принимает соединение без запроса пароля. Это повторяет поведение PostgreSQL. - В частности, при срабатывании
trustPgDoorman пропускает проверку пароля, даже если у пользователя сохранён парольmd5илиscram-sha-256. Это распространяется и на MD5, и на SCRAM-потоки. - TLS-ограничения правила соблюдаются:
hostsslтребует TLS,hostnosslзапрещает TLS.
Доступ к admin-консоли:
- Правила
general.pg_hbaприменяются и к специальной admin-базеpgdoorman. - Это означает, что admin-доступ можно разрешить методом
trustпри наличии совпавшего правила, например:host pgdoorman admin 127.0.0.1/32 trust
Замечания и ограничения:
- Поддерживается только минимальное подмножество
pg_hba.conf, достаточное для большинства proxy-сценариев (type, database, user, address, method). Дополнительные опции (вродеclientcert) сейчас игнорируются. - Для методов аутентификации, отличных от
trust, PgDoorman выполняет соответствующий challenge/response с клиентом. - Для потоков Talos/JWT/PAM, настроенных на уровне пула или пользователя,
trustвсё равно обходит запрос пароля у клиента; однако эти режимы могут использоваться, еслиtrustне совпал.
pooler_check_query
Когда клиент отправляет ровно этот запрос как SimpleQuery, pg_doorman отвечает немедленно
без перенаправления его в PostgreSQL. Полезно для health checks от балансировщиков нагрузки (HAProxy, в стиле pgbouncer SELECT 1)
или keepalive-проб уровня приложения. Значение по умолчанию ; (пустой statement) — самая лёгкая возможная проверка.
По умолчанию: ";".
Настройки пула
Каждая запись в pool — это имя виртуальной базы данных, к которой может подключиться клиент pg-doorman.
[pools.exampledb] # Объявление базы данных 'exampledb'
server_host
Каталог с unix-сокетами или IPv4-адрес сервера PostgreSQL, обслуживающего этот пул.
Пример: "/var/run/postgresql" или "127.0.0.1".
По умолчанию: "127.0.0.1".
server_port
Порт, через который сервер PostgreSQL принимает входящие соединения.
По умолчанию: 5432.
server_database
Опциональный параметр, определяющий, к какой базе нужно подключаться на сервере PostgreSQL.
application_name
Параметр application_name, отправляемый серверу при открытии соединения с PostgreSQL. Может быть полезен при настройке sync_server_parameters = false.
connect_timeout
Максимальное время на установку нового серверного соединения для этого пула, в миллисекундах. Если не задано, используется глобальная настройка connect_timeout.
По умолчанию: None (uses global setting).
idle_timeout
Закрывать idle-соединения в этом пуле, открытые дольше указанного значения, в миллисекундах. Если не задано, используется глобальная настройка idle_timeout.
По умолчанию: None (uses global setting).
server_lifetime
Закрывать серверные соединения в этом пуле, открытые дольше указанного значения, в миллисекундах. Применяется только к idle-соединениям. Если не задано, используется глобальная настройка server_lifetime.
По умолчанию: None (uses global setting).
pool_mode
Когда бэкенд-соединение возвращается в пул.
transaction: освобождается после каждой транзакции. session: удерживается до отключения клиента.
То же, что pool_mode в PgBouncer.
По умолчанию: "transaction".
log_client_parameter_status_changes
Логировать информацию о любой команде SET в лог.
По умолчанию: false.
cleanup_server_connections
Управляет тем, сбрасывает ли pg_doorman состояние сессии при возврате соединения в пул.
Когда параметр включён и сессия была изменена, pg_doorman отправляет: RESET ROLE, плюс при необходимости
RESET ALL (если использовался SET), DEALLOCATE ALL (если использовался PREPARE), CLOSE ALL
(если открывались курсоры). Замечание: ROLLBACK для открытых транзакций выполняется всегда, независимо
от этой настройки. Отключайте только если ваше приложение никогда не использует SET, prepared statements
или курсоры и вы хотите сэкономить roundtrip на очистке.
По умолчанию: true.
scaling_warm_pool_ratio
Переопределяет глобальный scaling_warm_pool_ratio для этого пула. Если не задано, используется глобальная настройка.
scaling_fast_retries
Переопределяет глобальный scaling_fast_retries для этого пула. Если не задано, используется глобальная настройка.
max_db_connections
Жёсткий потолок суммарного числа серверных соединений к этой базе, разделяемый между всеми
пользовательскими пулами. При достижении лимита и запросе нового соединения координатор сначала
пытается вытеснить idle-соединения у других пользователей (учитывая их min_pool_size), затем ждёт
возврата соединения и наконец откатывается к резервному пулу. Установите 0 (или опустите), чтобы
отключить координацию — каждый пользовательский пул работает независимо, ограниченный только своим
pool_size. Аналог max_db_connections из PgBouncer.
По умолчанию: 0 (disabled).
min_connection_lifetime
Минимальный возраст (в миллисекундах), которого должно достичь соединение, прежде чем оно сможет быть
вытеснено координатором пула. Предотвращает циклическое переподключение между пользовательскими пулами,
разделяющими одну базу: без этого порога idle-слот одного пользователя становится доступен для вытеснения
сразу же, как только сосед запрашивает разрешение, и под устойчивой многопользовательской нагрузкой
каждый пул отбирает слот у соседа каждые несколько секунд. Имеет значение только при max_db_connections > 0.
По умолчанию: 30000 (30 seconds).
reserve_pool_size
Число дополнительных соединений, разрешённых сверх max_db_connections в качестве крайней меры.
Когда вытеснение не удаётся и соединения не возвращаются за reserve_pool_timeout, резервное
соединение выдаётся запрашивающему с наивысшим приоритетом. Пользователи ниже своего min_pool_size
получают абсолютный приоритет. Имеет значение только при max_db_connections > 0.
По умолчанию: 0.
reserve_pool_timeout
Сколько времени (в миллисекундах) ждать освобождения обычного соединения, прежде чем
обратиться к резервному пулу. В этом окне координатор слушает возвраты соединений.
Имеет значение только при max_db_connections > 0 и reserve_pool_size > 0.
По умолчанию: 3000 (3 seconds).
min_guaranteed_pool_size
Дефолт уровня пула для минимального числа соединений на пользователя, защищённых от вытеснения координатором. Когда координатору нужно освободить слот соединения для другого пользователя, он не будет вытеснять соединения у пользователя, у которого их количество меньше или равно этому значению.
Это отличается от min_pool_size (уровня пользователя): min_pool_size управляет прогревом
и пополнением (проактивное создание соединений), тогда как min_guaranteed_pool_size влияет
только на решения о вытеснении (никогда не создаёт соединения).
Эффективная защита для пользователя — max(user.min_pool_size, pool.min_guaranteed_pool_size).
Установите 0 (или опустите), чтобы отключить защиту от вытеснения. Имеет значение только при max_db_connections > 0.
По умолчанию: 0 (no protection).
Настройки auth_query
Секция auth_query включает динамическую аутентификацию пользователей через запрос учётных данных
у базы PostgreSQL во время подключения. Это позволяет pg_doorman аутентифицировать пользователей
без статического перечисления их в конфигурационном файле.
pools:
mydb:
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "doorman_auth"
password: "auth_password"
Существуют два режима работы:
- Dedicated mode (задан
server_user): все динамически аутентифицированные пользователи разделяют один пул соединений, который подключается к PostgreSQL какserver_user. Это самая простая настройка, хорошо подходящая, когда всем пользователям нужен одинаковый бэкенд-доступ. - Passthrough mode (
server_userне задан): каждый динамически аутентифицированный пользователь получает собственный пул соединений, подключающийся к PostgreSQL по его собственным учётным данным (MD5 pass-the-hash или SCRAM ClientKey passthrough). Это сохраняет идентичность каждого пользователя на бэкенде.
Статические пользователи (определённые в секции users) всегда проверяются первыми. auth_query используется только когда имя пользователя не найдено среди статических.
user, выполняющий auth-запросы, должен иметь доступ к хешам паролей (например, из pg_shadow). Не используйте суперпользователя для этой цели. Вместо этого создайте функцию SECURITY DEFINER, принадлежащую суперпользователю, и выделенную роль с минимальными привилегиями:
-- Создаём выделенную роль для auth-запросов
CREATE ROLE doorman_auth LOGIN PASSWORD 'strong_password';
-- Создаём функцию SECURITY DEFINER (выполняется с привилегиями владельца)
CREATE OR REPLACE FUNCTION pg_doorman_get_auth(p_usename TEXT)
RETURNS TABLE (usename name, passwd text)
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS
$$
SELECT usename, passwd FROM pg_shadow WHERE usename = p_usename;
$$;
-- Выдаём execute только выделенной роли
REVOKE ALL ON FUNCTION pg_doorman_get_auth(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION pg_doorman_get_auth(TEXT) TO doorman_auth;
Затем используйте эту функцию в параметре query:
auth_query:
query: "SELECT * FROM pg_doorman_get_auth($1)"
user: "doorman_auth"
password: "strong_password"
query
SQL-запрос для получения учётных данных. Должен возвращать колонку с именем passwd или password, содержащую MD5- или SCRAM-хеш. Если запрос возвращает ровно одну колонку, она используется независимо от имени. Любые лишние колонки игнорируются. В качестве плейсхолдера для имени пользователя используйте $1.
Пример: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user
Имя пользователя PostgreSQL для executor-соединения, выполняющего auth-запросы.
password
Пароль executor-пользователя (открытым текстом). Может быть пустым, если сервер PostgreSQL использует аутентификацию trust для этого пользователя.
По умолчанию: "".
database
База для executor-соединений. Если не задана, используется имя пула.
По умолчанию: None (uses pool name).
workers
Число постоянных соединений с PostgreSQL, выделенных под выполнение SQL auth_query.
Эти соединения открываются при старте и поддерживаются живыми. Они обрабатывают только поиск
учётных данных — клиентский трафик данных идёт через отдельные соединения пула данных (pool_size).
Увеличьте, если видите всплески задержки auth при высокой частоте подключений.
По умолчанию: 2.
server_user
Бэкенд-пользователь PostgreSQL для соединений с данными в dedicated mode. Когда задан, все динамически аутентифицированные пользователи разделяют один пул соединений, подключающийся под этим пользователем. Когда не задан, используется passthrough mode.
По умолчанию: None (passthrough mode).
server_password
Пароль server_user открытым текстом. Имеет смысл только когда задан server_user.
По умолчанию: None.
pool_size
Максимальное число бэкенд-соединений на каждый пул данных, создаваемый auth_query.
Та же концепция, что и users[].pool_size для статически заданных пользователей.
Сколько пулов будет создано, зависит от режима: server_user определяет, разделяют ли
все динамические пользователи один пул или у каждого свой.
По умолчанию: 40.
min_pool_size
Минимальное число бэкенд-соединений, поддерживаемое в каждом пуле динамического пользователя в passthrough mode. Соединения прогреваются при первом создании пула и пополняются циклом retain. Установите 0, чтобы отключить (по умолчанию). Замечание: пулы с min_pool_size > 0 никогда не подвергаются garbage collection, и общее число бэкенд-соединений масштабируется как active_users × min_pool_size.
По умолчанию: 0.
cache_ttl
Максимальное время жизни кеша для успешно полученных учётных данных. Принимает строки продолжительности вроде "1h", "30m", "300s".
По умолчанию: "1h".
cache_failure_ttl
TTL кеша для записей «пользователь не найден» (negative cache). Предотвращает повторные запросы для несуществующих пользователей.
По умолчанию: "30s".
min_interval
Минимальный интервал между повторными запросами для одного и того же имени пользователя после неудачной аутентификации. Защищает бэкенд от избыточных запросов при попытках перебора.
По умолчанию: "1s".
Настройки пользователей пула
[pools.exampledb.users.0]
username = "exampledb-user-0" # Виртуальный пользователь, который может подключаться к этой виртуальной базе.
username
Имя пользователя, под которым клиенты подключаются к этому пулу. Должно быть уникальным в рамках пула.
password
Верификатор пароля для аутентификации клиента. Поддерживает форматы MD5, SCRAM-SHA-256 и JWT.
Хеши паролей можно скопировать напрямую из PostgreSQL: SELECT usename, passwd FROM pg_shadow.
auth_pam_service
PAM-сервис, отвечающий за авторизацию клиента. В этом случае pg_doorman игнорирует значение password.
server_username
Реальное имя пользователя PostgreSQL, используемое для подключения к серверу базы.
По умолчанию PgDoorman использует одно и то же username и для аутентификации клиента, и для серверных соединений, применяя passthrough authentication: криптографический материал из аутентификации клиента (MD5-хеш или SCRAM ClientKey) переиспользуется для аутентификации на бэкенде. Это устраняет потребность в server_password открытым текстом.
Passthrough mode (рекомендуется для пользователей с совпадающей идентичностью):
- Опустите и
server_username, иserver_password - pg_doorman переиспользует доказательство аутентификации клиента для подключения к PostgreSQL
- Для MD5: хеш из
passwordиспользуется напрямую - Для SCRAM: ClientKey извлекается из первой SCRAM-аутентификации клиента и кешируется
- Требование: верификатор
passwordдолжен совпадать сpg_authidна бэкенде (та же соль/итерации для SCRAM, тот же хеш для MD5)
Explicit credentials mode (когда идентичности различаются):
- Установите
server_usernameиserver_passwordв реальные учётные данные PostgreSQL server_passwordтребует, чтобыserver_usernameбыл заданserver_usernameбезserver_passwordдопустим для аутентификации trust
server_password
Пароль открытым текстом для серверного пользователя PostgreSQL, указанного в server_username.
Когда server_password не задан и пользователь имеет право на passthrough (нет server_username или server_username равен username), PgDoorman использует passthrough authentication: криптографический материал из аутентификации клиента переиспользуется для бэкенд-соединения. Это убирает пароли открытым текстом из конфигурационных файлов.
server_password требует, чтобы server_username был задан.
pool_size
Максимальное число бэкенд-соединений с PostgreSQL для этого пользователя. В transaction mode соединения разделяются между клиентами, поэтому это значение обычно намного меньше числа клиентов. Аналог default_pool_size из PgBouncer, но настраивается per-user, а не глобально.
По умолчанию: 40.
min_pool_size
Минимальное число соединений, поддерживаемое в пуле для этого пользователя. Соединения прогреваются при старте (до первого цикла retain) и затем поддерживаются периодическим пополнением. Если задано, должно быть меньше или равно pool_size.
По умолчанию: None.
server_lifetime
Закрывать серверные соединения для этого пользователя, открытые дольше указанного значения, в миллисекундах. Применяется только к idle-соединениям. Если не задано, используется настройка server_lifetime пула.
По умолчанию: None (uses pool setting).
По умолчанию PgDoorman использует passthrough authentication: криптографическое доказательство клиента (MD5-хеш или SCRAM ClientKey) автоматически переиспользуется для аутентификации в PostgreSQL. Пароли открытым текстом в конфиге не нужны.
Задавайте server_username и server_password только когда бэкенд-пользователь PostgreSQL отличается от имени пользователя пула (например, переопределение имени пользователя или JWT-аутентификация):
users:
- username: "app_user" # имя для клиента
password: "md5..." # хеш для аутентификации клиента
server_username: "pg_app_user" # другой бэкенд-пользователь PostgreSQL
server_password: "plaintext_pwd" # пароль открытым текстом для этого пользователя
Настройки Prometheus
pg_doorman содержит экспортёр метрик Prometheus, который даёт детальное представление о производительности и поведении ваших пулов соединений. В этом документе описано, как включить и использовать экспортёр метрик Prometheus, а также какие метрики доступны.
Включение метрик Prometheus
Чтобы включить экспортёр метрик Prometheus, добавьте в конфигурационный файл следующее:
prometheus:
enabled: true
host: "0.0.0.0" # Хост, на котором сервер метрик будет принимать соединения
port: 9127 # Порт, на котором сервер метрик будет принимать соединения
Опции конфигурации
| Опция | Описание | По умолчанию |
|---|---|---|
enabled | Включить или отключить экспортёр метрик Prometheus. | false |
host | Хост, на котором экспортёр метрик Prometheus будет принимать соединения. | "0.0.0.0" |
port | Порт, на котором экспортёр метрик Prometheus будет принимать соединения. | 9127 |
Настройка Prometheus
Добавьте следующий job в конфигурацию Prometheus, чтобы собирать метрики с pg_doorman:
scrape_configs:
- job_name: 'pg_doorman'
static_configs:
- targets: ['<pg_doorman_host>:9127']
Замените <pg_doorman_host> на имя хоста или IP-адрес вашего инстанса pg_doorman.
Доступные метрики
pg_doorman экспортирует следующие метрики:
Системные метрики
| Метрика | Описание |
|---|---|
pg_doorman_total_memory | Общий объём памяти, выделенный процессу pg_doorman, в байтах. Позволяет отслеживать потребление памяти приложением. |
Метрики соединений
| Метрика | Описание |
|---|---|
pg_doorman_connection_count | Счётчик новых соединений по типу, обработанных pg_doorman. Типы: 'plain' (нешифрованные соединения), 'tls' (шифрованные соединения), 'cancel' (запросы на отмену соединения), 'total' (сумма всех соединений). |
Метрики сокетов (только Linux)
| Метрика | Описание |
|---|---|
pg_doorman_sockets | Счётчик сокетов, используемых pg_doorman, по типу сокета. Типы: 'tcp' (IPv4 TCP-сокеты), 'tcp6' (IPv6 TCP-сокеты), 'unix' (Unix domain sockets), 'unknown' (сокеты нераспознанного типа). Доступно только в Linux. |
Метрики пула
| Метрика | Описание |
|---|---|
pg_doorman_pools_clients | Число клиентов в пулах соединений по статусу, пользователю и базе. Значения статуса: 'idle' (подключён, но не выполняет запросы), 'waiting' (ждёт серверного соединения), 'active' (выполняет запросы). Помогает мониторить загрузку пулов и распределение клиентов. |
pg_doorman_pools_servers | Число серверов в пулах соединений по статусу, пользователю и базе. Значения статуса: 'active' (активно обслуживает клиентов) и 'idle' (доступен для новых соединений). Помогает мониторить доступность серверов и распределение нагрузки. |
pg_doorman_pools_bytes | Общее число байт, переданных через пулы соединений, по направлению, пользователю и базе. Значения направления: 'received' (байты, полученные от клиентов) и 'sent' (байты, отправленные клиентам). Полезно для мониторинга сетевого трафика и выявления соединений с большим объёмом данных. |
| pg_doorman_pool_size | Сконфигурированный максимальный размер пула на пользователя и базу. Полезен для расчёта оставшейся ёмкости пула вместе с pg_doorman_pools_servers. |
Метрики запросов и транзакций
| Метрика | Описание |
|---|---|
pg_doorman_pools_queries_percentile | Перцентили времени выполнения запросов по пользователю и базе. Значения перцентилей: '99', '95', '90', '50' (медиана). Значения в миллисекундах. Помогает выявлять медленные запросы и тренды производительности по разным пользователям и базам. |
pg_doorman_pools_transactions_percentile | Перцентили времени выполнения транзакций по пользователю и базе. Значения перцентилей: '99', '95', '90', '50' (медиана). Значения в миллисекундах. Помогает мониторить производительность транзакций и выявлять долгие транзакции, которые могут влиять на производительность базы. |
pg_doorman_pools_transactions_count | Счётчик транзакций, выполненных в пулах соединений, по пользователю и базе. Помогает отслеживать объём транзакций и выявлять пользователей или базы с высоким темпом транзакций. |
pg_doorman_pools_transactions_total_time | Общее время, потраченное на выполнение транзакций в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить общую производительность транзакций и выявлять пользователей или базы с большим временем выполнения транзакций. |
pg_doorman_pools_queries_count | Счётчик запросов, выполненных в пулах соединений, по пользователю и базе. Помогает отслеживать объём запросов и выявлять пользователей или базы с высоким темпом запросов. |
pg_doorman_pools_queries_total_time | Общее время, потраченное на выполнение запросов в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить общую производительность запросов и выявлять пользователей или базы с большим временем выполнения запросов. |
pg_doorman_pools_avg_wait_time | Среднее время ожидания клиентов в пулах соединений, по пользователю и базе. Значения в миллисекундах. Помогает мониторить время ожидания клиентов и выявлять потенциальные узкие места. |
Метрики auth_query
Эти метрики доступны только когда auth_query сконфигурирован для одного или нескольких пулов.
| Метрика | Описание |
|---|---|
pg_doorman_auth_query_cache | Метрики кеша auth_query по типу и базе. Типы: entries (текущее число закешированных учётных данных), hits (попадания в кеш с найденной валидной записью), misses (промахи в кеше, потребовавшие запроса к PostgreSQL), refetches (повторные запросы, вызванные ошибкой аутентификации с устаревшими учётными данными), rate_limited (попытки повторного запроса, ограниченные min_interval). |
pg_doorman_auth_query_auth | Результаты аутентификации auth_query по результату и базе. Результаты: success (успешная аутентификация) и failure (неверный пароль или несовпадение учётных данных). |
pg_doorman_auth_query_executor | Метрики executor auth_query по типу и базе. Типы: queries (всего запросов, выполненных к PostgreSQL для получения учётных данных) и errors (запросы, завершившиеся ошибкой подключения или выполнения). |
pg_doorman_auth_query_dynamic_pools | Метрики жизненного цикла динамических пулов auth_query по типу и базе. Типы: current (сейчас активные динамические пулы), created (всего пулов создано с момента старта), destroyed (всего пулов, собранных garbage collection или удалённых на RELOAD). Имеет смысл только в passthrough mode. |
Метрики серверов
| Метрика | Описание |
|---|---|
pg_doorman_servers_prepared_hits | Счётчик попаданий prepared statements в бэкендах баз по пользователю и базе. Помогает оценить эффективность prepared statements в снижении накладных расходов на парсинг запросов. |
pg_doorman_servers_prepared_misses | Счётчик промахов prepared statements в бэкендах баз по пользователю и базе. Помогает выявить запросы, которые могли бы выиграть от подготовки в prepared statements. |
Метрики серверного TLS
Активны, если включён TLS к PostgreSQL (server_tls_mode != "disable").
| Метрика | Тип | Описание |
|---|---|---|
pg_doorman_server_tls_connections | gauge per pool | Число активных TLS-соединений к PostgreSQL. |
pg_doorman_server_tls_handshake_duration_seconds | histogram per pool | Распределение длительности TLS handshake. |
pg_doorman_server_tls_handshake_errors_total | counter per pool | Счётчик неуспешных handshake. Алертить при ненулевой скорости. |
Подробнее — см. Клиентский и серверный TLS.
Дашборд Grafana
Вы можете создать дашборд Grafana для визуализации этих метрик. Вот простой пример панелей, которые имеет смысл добавить:
- Число соединений по типу
- Использование памяти со временем
- Число клиентов и серверов по пулам
- Перцентили производительности запросов и транзакций
- Сетевой трафик по пулам
Примеры запросов
Несколько примеров запросов Prometheus, которые могут быть полезны:
Темп подключений
rate(pg_doorman_connection_count{type="total"}[5m])
Загрузка пула
sum by (database) (pg_doorman_pools_clients{status="active"}) / sum by (database) (pg_doorman_pools_servers{status="active"} + pg_doorman_pools_servers{status="idle"})
Медленные запросы
pg_doorman_pools_queries_percentile{percentile="99"}
Время ожидания клиентов
pg_doorman_pools_avg_wait_time
Hit rate кеша auth_query
pg_doorman_auth_query_cache{type="hits"} / (pg_doorman_auth_query_cache{type="hits"} + pg_doorman_auth_query_cache{type="misses"})
Темп ошибок auth_query
rate(pg_doorman_auth_query_auth{result="failure"}[5m])
title: Benchmarks
Benchmarks
pg_doorman vs pgbouncer vs odyssey. Each test runs pgbench for 30 seconds through a 40-connection pool.
Last updated: 2026-04-21 18:54 UTC
Environment
- Instance: AWS Fargate (16 vCPU, 32 GB RAM)
- Workers: pg_doorman: 12, odyssey: 12
- pgbench jobs: 4 (global override)
- Started: 2026-04-21 17:31:01 UTC
- Finished: 2026-04-21 18:54:11 UTC
- Total duration: 1h 23m 9s
Reading the tables
Throughput — pg_doorman TPS relative to each competitor:
| Value | Meaning |
|---|---|
| +N% | Faster by N% |
| -N% | Slower by N% |
| ≈0% | Within 3% |
| xN | N times faster or slower |
| ∞ | Competitor got 0 TPS |
| N/A | Unsupported |
| - | Not tested |
Latency — per-transaction latency in ms. Each cell: p50 / p95 / p99. Lower is better.
Simple protocol
Throughput
| Test | vs pgbouncer | vs odyssey |
|---|---|---|
| 1 client | -3% | -9% |
| 40 clients | +82% | -5% |
| 120 clients | x2.6 | ≈0% |
| 500 clients | x2.5 | +6% |
| 10,000 clients | x2.7 | +18% |
| 1 client + Reconnect | -6% | x9.0 |
| 40 clients + Reconnect | +21% | x2.1 |
| 120 clients + Reconnect | +19% | +98% |
| 500 clients + Reconnect | +20% | +97% |
| 10,000 clients + Reconnect | +63% | x2.1 |
| 1 client + SSL | -8% | -8% |
| 40 clients + SSL | x2.1 | ≈0% |
| 120 clients + SSL | x3.1 | +6% |
| 500 clients + SSL | x3.0 | +11% |
| 10,000 clients + SSL | x3.2 | +17% |
Latency — p50 / p95 / p99 (ms)
| Test | pg_doorman (ms) | pgbouncer (ms) | odyssey (ms) |
|---|---|---|---|
| 1 client | 0.07 / 0.07 / 0.08 | 0.07 / 0.08 / 0.08 | 0.06 / 0.07 / 0.07 |
| 40 clients | 0.26 / 0.36 / 0.44 | 0.46 / 0.67 / 0.71 | 0.22 / 0.42 / 0.57 |
| 120 clients | 0.49 / 1.86 / 3.03 | 1.81 / 2.12 / 2.24 | 0.54 / 1.33 / 1.84 |
| 500 clients | 3.54 / 5.64 / 6.46 | 7.59 / 8.71 / 9.33 | 0.99 / 13.19 / 22.41 |
| 10,000 clients | 69.42 / 71.72 / 75.83 | 184.39 / 202.56 / 213.44 | 2.70 / 326.10 / 570.34 |
| 1 client + Reconnect | 0.06 / 0.08 / 0.09 | 0.06 / 0.06 / 0.07 | 0.07 / 0.09 / 0.10 |
| 40 clients + Reconnect | 1.07 / 2.19 / 2.52 | 1.02 / 2.34 / 11.93 | 1.00 / 2.74 / 3.35 |
| 120 clients + Reconnect | 3.36 / 6.33 / 7.62 | 3.37 / 7.10 / 31.61 | 4.67 / 9.45 / 11.54 |
| 500 clients + Reconnect | 13.77 / 25.15 / 28.86 | 13.61 / 30.11 / 125.79 | 22.99 / 41.95 / 48.02 |
| 10,000 clients + Reconnect | 295.83 / 515.96 / 559.54 | 562.42 / 926.93 / 972.88 | 597.47 / 1078.06 / 1365.21 |
| 1 client + SSL | 0.08 / 0.09 / 0.10 | 0.08 / 0.08 / 0.09 | 0.07 / 0.09 / 0.09 |
| 40 clients + SSL | 0.29 / 0.44 / 0.56 | 0.64 / 0.93 / 1.00 | 0.27 / 0.51 / 0.67 |
| 120 clients + SSL | 0.59 / 2.32 / 3.93 | 2.56 / 2.93 / 3.14 | 0.67 / 1.65 / 2.30 |
| 500 clients + SSL | 4.16 / 6.85 / 7.84 | 10.89 / 12.64 / 13.55 | 1.23 / 16.20 / 27.94 |
| 10,000 clients + SSL | 82.15 / 86.57 / 91.75 | 262.65 / 289.09 / 367.39 | 4.24 / 387.21 / 753.84 |
Extended protocol
Throughput
| Test | vs pgbouncer | vs odyssey |
|---|---|---|
| 1 client | +5% | +40% |
| 40 clients | +98% | +43% |
| 120 clients | x2.8 | +60% |
| 500 clients | x2.7 | +64% |
| 10,000 clients | x2.8 | +74% |
| 1 client + Reconnect | -4% | x3.0 |
| 40 clients + Reconnect | +20% | x2.2 |
| 120 clients + Reconnect | +21% | +88% |
| 500 clients + Reconnect | +21% | +100% |
| 10,000 clients + Reconnect | +61% | +27% |
| 1 client + SSL | +4% | +36% |
| 40 clients + SSL | x2.3 | +48% |
| 120 clients + SSL | x3.2 | +65% |
| 500 clients + SSL | x3.4 | +69% |
| 10,000 clients + SSL | x3.4 | +73% |
| 1 client + SSL + Reconnect | +9% | +13% |
| 40 clients + SSL + Reconnect | +96% | +5% |
| 120 clients + SSL + Reconnect | +99% | +5% |
| 500 clients + SSL + Reconnect | x2.0 | +5% |
| 10,000 clients + SSL + Reconnect | +93% | +5% |
Latency — p50 / p95 / p99 (ms)
| Test | pg_doorman (ms) | pgbouncer (ms) | odyssey (ms) |
|---|---|---|---|
| 1 client | 0.07 / 0.07 / 0.08 | 0.07 / 0.08 / 0.08 | 0.09 / 0.10 / 0.11 |
| 40 clients | 0.25 / 0.35 / 0.43 | 0.48 / 0.72 / 0.76 | 0.32 / 0.62 / 0.84 |
| 120 clients | 0.47 / 1.82 / 2.99 | 1.87 / 2.27 / 2.39 | 0.83 / 2.33 / 3.48 |
| 500 clients | 3.45 / 5.48 / 6.26 | 7.77 / 9.35 / 9.69 | 1.31 / 18.35 / 31.99 |
| 10,000 clients | 67.94 / 70.05 / 72.43 | 188.62 / 206.44 / 218.36 | 3.74 / 468.64 / 810.47 |
| 1 client + Reconnect | 0.07 / 0.08 / 0.08 | 0.06 / 0.08 / 0.09 | 0.10 / 0.12 / 0.13 |
| 40 clients + Reconnect | 1.08 / 2.21 / 2.55 | 1.03 / 2.40 / 11.13 | 0.92 / 2.77 / 3.66 |
| 120 clients + Reconnect | 3.27 / 6.20 / 7.44 | 3.23 / 7.11 / 33.54 | 4.61 / 9.52 / 11.37 |
| 500 clients + Reconnect | 13.83 / 25.40 / 29.12 | 13.80 / 30.20 / 118.88 | 23.89 / 43.77 / 67.05 |
| 10,000 clients + Reconnect | 298.47 / 519.98 / 573.71 | 569.79 / 921.35 / 966.33 | 549.71 / 1052.37 / 1402.38 |
| 1 client + SSL | 0.08 / 0.09 / 0.09 | 0.08 / 0.10 / 0.10 | 0.11 / 0.12 / 0.13 |
| 40 clients + SSL | 0.29 / 0.44 / 0.59 | 0.67 / 0.99 / 1.07 | 0.39 / 0.80 / 1.08 |
| 120 clients + SSL | 0.56 / 2.31 / 3.90 | 2.62 / 3.06 / 3.24 | 1.07 / 2.90 / 4.35 |
| 500 clients + SSL | 4.16 / 6.87 / 7.88 | 12.30 / 14.21 / 15.64 | 1.71 / 22.66 / 38.83 |
| 10,000 clients + SSL | 81.93 / 86.01 / 89.18 | 280.73 / 308.19 / 385.99 | 139.94 / 557.71 / 844.51 |
| 1 client + SSL + Reconnect | 0.10 / 0.12 / 0.13 | 0.08 / 0.10 / 0.11 | 0.09 / 0.12 / 0.12 |
| 40 clients + SSL + Reconnect | 12.31 / 23.03 / 25.02 | 24.07 / 44.29 / 46.70 | 12.99 / 24.30 / 26.07 |
| 120 clients + SSL + Reconnect | 37.28 / 69.57 / 76.18 | 73.98 / 137.78 / 147.32 | 39.21 / 72.80 / 79.97 |
| 500 clients + SSL + Reconnect | 157.07 / 292.82 / 319.41 | 311.77 / 593.21 / 673.84 | 165.06 / 306.22 / 336.63 |
| 10,000 clients + SSL + Reconnect | 2954.51 / 5951.58 / 6556.46 | 5078.21 / 11531.36 / 12358.70 | 3096.16 / 6192.08 / 6844.73 |
Prepared protocol
Throughput
| Test | vs pgbouncer | vs odyssey |
|---|---|---|
| 1 client | -4% | -8% |
| 40 clients | x2.4 | -7% |
| 120 clients | x3.5 | ≈0% |
| 500 clients | x3.3 | +8% |
| 10,000 clients | x3.1 | +16% |
| 1 client + Reconnect | ≈0% | ∞ |
| 40 clients + Reconnect | ≈0% | ∞ |
| 120 clients + Reconnect | ≈0% | ∞ |
| 500 clients + Reconnect | +4% | ∞ |
| 10,000 clients + Reconnect | +25% | ∞ |
| 1 client + SSL | -4% | -5% |
| 40 clients + SSL | x2.7 | ≈0% |
| 120 clients + SSL | x3.8 | +6% |
| 500 clients + SSL | x3.7 | +11% |
| 10,000 clients + SSL | x3.9 | +15% |
Latency — p50 / p95 / p99 (ms)
| Test | pg_doorman (ms) | pgbouncer (ms) | odyssey (ms) |
|---|---|---|---|
| 1 client | 0.06 / 0.07 / 0.07 | 0.06 / 0.07 / 0.07 | 0.06 / 0.07 / 0.07 |
| 40 clients | 0.24 / 0.34 / 0.42 | 0.60 / 0.88 / 0.91 | 0.20 / 0.40 / 0.53 |
| 120 clients | 0.47 / 1.72 / 2.79 | 2.25 / 2.60 / 2.69 | 0.51 / 1.27 / 1.79 |
| 500 clients | 3.31 / 5.28 / 6.04 | 9.35 / 10.58 / 11.20 | 0.90 / 12.75 / 21.56 |
| 10,000 clients | 66.31 / 69.32 / 72.46 | 205.26 / 221.39 / 243.49 | 2.76 / 306.09 / 534.07 |
| 1 client + Reconnect | 0.11 / 0.13 / 0.14 | 0.10 / 0.12 / 0.13 | 0.15 / 0.18 / 0.19 |
| 40 clients + Reconnect | 1.56 / 3.01 / 3.33 | 1.59 / 2.95 / 3.31 | 1.86 / 3.56 / 4.61 |
| 120 clients + Reconnect | 4.49 / 8.39 / 9.73 | 4.60 / 8.55 / 9.72 | 6.76 / 83.39 / 88.89 |
| 500 clients + Reconnect | 18.64 / 34.07 / 37.72 | 19.43 / 35.46 / 39.86 | 24.27 / 197.01 / 297.22 |
| 10,000 clients + Reconnect | 396.39 / 710.66 / 769.05 | 483.59 / 927.03 / 1000.58 | 483.21 / 1256.91 / 1563.08 |
| 1 client + SSL | 0.08 / 0.09 / 0.09 | 0.07 / 0.08 / 0.09 | 0.07 / 0.08 / 0.09 |
| 40 clients + SSL | 0.28 / 0.42 / 0.57 | 0.80 / 1.15 / 1.24 | 0.24 / 0.48 / 0.65 |
| 120 clients + SSL | 0.56 / 2.19 / 3.67 | 3.00 / 3.27 / 3.49 | 0.63 / 1.60 / 2.27 |
| 500 clients + SSL | 3.95 / 6.56 / 7.51 | 12.70 / 13.91 / 15.23 | 1.10 / 15.54 / 26.49 |
| 10,000 clients + SSL | 79.24 / 84.27 / 88.85 | 308.74 / 326.58 / 488.80 | 5.17 / 364.12 / 633.46 |
Notes
- Odyssey performs poorly with extended query protocol in transaction pooling mode
- Throughput values are relative ratios — comparable across runs on identical hardware
- Latency values are absolute, measured per-transaction
Changelog
3.6.0 Apr 24, 2026
Patroni-assisted fallback
When pg_doorman runs next to PostgreSQL on the same machine and connects via unix socket, a Patroni switchover or PostgreSQL crash leaves the pooler without a backend. With patroni_api_urls configured, pg_doorman queries the Patroni REST API /cluster endpoint, picks a live cluster member, and routes new connections there.
Candidate selection: sync_standby first (most likely next leader), then replica, then any other member. Members with noloadbalance, nofailover, or archive tags are excluded. All candidates are TCP-probed in parallel; the first responding sync_standby wins immediately.
The local backend stays in cooldown for fallback_cooldown (default 30s). During the cooldown, subsequent connection requests reuse the cached fallback host without re-querying Patroni. Fallback connections use a short fallback_lifetime (defaults to fallback_cooldown) so the pool returns to the local backend once it recovers.
Configuration:
pools:
mydb:
patroni_api_urls:
- "http://10.0.0.1:8008"
- "http://10.0.0.2:8008"
fallback_cooldown: "30s"
patroni_api_timeout: "5s"
fallback_connect_timeout: "5s"
Prometheus metrics: pg_doorman_patroni_api_requests_total, pg_doorman_fallback_connections_total, pg_doorman_patroni_api_errors_total, pg_doorman_fallback_active, pg_doorman_patroni_api_duration_seconds, pg_doorman_fallback_host, pg_doorman_fallback_cache_hits_total.
If you tracked this feature under its working name in 3.5.x dev builds, the config keys and metric names changed before the public release: patroni_discovery_urls → patroni_api_urls, failover_blacklist_duration → fallback_cooldown, failover_discovery_timeout → patroni_api_timeout, failover_connect_timeout → fallback_connect_timeout, failover_server_lifetime → fallback_lifetime. Old pg_doorman_failover_* metrics are renamed to pg_doorman_patroni_api_* / pg_doorman_fallback_*.
Server-side TLS (pg_doorman → PostgreSQL)
Six SSL modes matching libpq semantics: disable, allow (default), prefer, require, verify-ca, verify-full. Mutual TLS supported via server_tls_certificate / server_tls_private_key.
Configuration is per-pool with global defaults in [general]. Cancel requests use TLS when the main connection used TLS.
Breaking change: server_tls (bool) and verify_server_certificate (bool) are removed. They were parsed but non-functional. Replace with:
| Old config | New config |
|---|---|
server_tls: false | server_tls_mode: "disable" |
server_tls: true | server_tls_mode: "require" |
server_tls: true + verify_server_certificate: true | server_tls_mode: "verify-full" |
| (not set) | server_tls_mode: "allow" (new default) |
The new default allow tries plain TCP first. If the server rejects the connection (e.g. pg_hba.conf requires TLS), pg_doorman retries with TLS on a new TCP socket. This matches libpq sslmode=allow.
SHOW SERVERS now includes a tls column showing whether each backend connection uses TLS.
3.5.3 Apr 22, 2026
Prepared statement cache overflow under concurrent load
The pool-level prepared statement cache could grow well above its configured prepared_statements_cache_size under concurrent client traffic. Production showed 480 entries with a limit of 300. The check-then-insert sequence in the cache had a race: multiple clients passed the size check simultaneously, each inserted without evicting. Now insertion happens first, followed by eviction in a loop until the cache is within bounds.
3.5.2 Apr 21, 2026
Semaphore permit leak on direct handoff
Each return_object handoff (delivering a connection to a waiting client via oneshot channel) permanently consumed one semaphore permit. After max_size handoffs the pool semaphore was fully drained, blocking all new timeout_get callers. The pool could not create connections and stabilized at whatever size it reached during cold start (typically 4-8 out of 40).
Root cause: wrap_checkout calls permit.forget(), and the handoff path in return_object skipped add_permits(1). Now return_object restores the permit on both the handoff and idle-queue paths. Compensating add_permits(1) in pre_replace_one removed (no longer needed).
Burst gate select race
The tokio::select! in the burst gate loop randomly picked among ready branches. When sleep(5ms) or create_done won over an already-delivered oneshot, the connection was silently dropped, inflating slots.size without a live server. Fixed with biased; (oneshot checked first) and a try_recv drain that pushes orphaned connections to idle without double-counting the permit.
Migration fixes
-
Client ID collision after migration. The new process started its connection counter at 0, colliding with migrated client IDs. Now the counter advances past the highest migrated ID.
-
SCRAM passthrough state preserved. The ClientKey from the first client's SCRAM handshake is serialized in the migration payload (v2 format, backward compatible). The new process skips the
ScramPendingfallback toserver_password.
Session mode statistics fix
xact_time percentiles in session mode showed the entire session duration instead of individual transaction time. Now recorded per-transaction at each ReadyForQuery(Idle), matching transaction mode semantics.
query_time had the same accumulation bug: the timer was set once before the inner loop and never reset, so each subsequent query reported the cumulative session duration. Now reset per-query in session mode.
Adaptive anticipation budget
Anticipation wait (formerly fixed 300-500ms) scales with real transaction latency: xact_p99 * 2 +/- 20% jitter, clamped to [5ms, 500ms]. Cold start default: 100ms.
Diagnostic logging
Slow checkout warnings (>500ms) now include pool state: size, avail, waiting, inflight, creates, gate_waits, antic_ok, antic_to, fallback. Phase-specific warnings added for semaphore timeout, burst gate timeout, coordinator exhaustion, and create failure.
3.5.1 Apr 20, 2026
systemd Type=notify support
pg_doorman now sends sd_notify(READY=1) on startup and sd_notify(MAINPID=<child_pid>) during binary upgrade. With Type=notify in the systemd unit, systemctl reload performs a zero-downtime binary upgrade without PID tracking issues — systemd follows the new process correctly and does not restart the service.
The shipped pg_doorman.service changes from Type=forking + --daemon to Type=notify (foreground). Existing installations using --daemon continue to work but do not benefit from client migration.
Docker STOPSIGNAL changed from SIGINT to SIGTERM to prevent binary upgrade in containers (where PID 1 exit kills the container).
3.5.0 Apr 15, 2026
Client migration during binary upgrade
Idle clients now transfer to the new process via Unix socket (SCM_RIGHTS) without reconnecting. Active-transaction clients finish their transaction on the old process, then migrate. Prepared statement caches are serialized and transparently re-parsed on the new backend. The old process exits once all clients have migrated or shutdown_timeout expires.
TLS connection migration (opt-in)
Build with --features tls-migration to migrate TLS sessions without re-handshake. A patched vendored OpenSSL 3.5.5 exports/imports symmetric cipher state (keys, IVs, sequence numbers). Linux-only. Offline builds supported via OPENSSL_SOURCE_TARBALL env var with SHA-256 verification.
3.4.0 Apr 11, 2026
Pool Coordinator — database-level connection limits
When multiple user pools share one PostgreSQL database, the sum of their pool_size values can exceed max_connections. A spike in one pool starves the others, or PostgreSQL rejects connections outright.
max_db_connections caps total backend connections per database across all user pools. When the cap is reached, the coordinator frees capacity through three mechanisms, tried in order:
-
Reserve pool. If
reserve_pool_size > 0and the reserve has headroom, a permit is granted immediately — no eviction, no wait. The reserve is a burst buffer: idle reserve connections are upgraded to main permits by the retain cycle once pressure drops, and closed if they stay idle longer thanmin_connection_lifetime. -
Eviction. The coordinator closes one idle connection from a peer pool with the largest surplus above its
min_guaranteed_pool_sizefloor. Candidates are ranked by p95 transaction time — slow pools donate first, because a 1 ms reconnect cost is negligible against a 15 ms p95 but doubles a 0.96 ms one. Only connections older thanmin_connection_lifetime(default 30 s) are eligible, which suppresses cyclic reconnect between pools that take turns stealing slots. -
Wait. If nothing is evictable, the caller parks for up to
reserve_pool_timeout(default 3 s), waking on any peer connection return or permit drop. After the wait, the reserve is retried once more before the client receives an error.
Disabled by default (max_db_connections = 0) — zero overhead when not configured. The hot path (idle connection reuse) never touches the coordinator; only new connection creation does, at the cost of one atomic operation.
New pool-level config fields:
| Parameter | Default | Purpose |
|---|---|---|
max_db_connections | 0 (disabled) | Hard cap on backend connections per database |
min_connection_lifetime | 30000 ms | Eviction age floor — connections younger than this are immune |
reserve_pool_size | 0 (disabled) | Extra permits above the cap, granted on burst |
reserve_pool_timeout | 3000 ms | Coordinator wait budget before error |
min_guaranteed_pool_size | 0 | Per-user eviction protection floor |
New admin commands: SHOW POOL_COORDINATOR (per-database coordinator state), SHOW POOL_SCALING (per-pool checkout counters). Both are also exported as Prometheus metrics under pg_doorman_pool_coordinator{type, database} and pg_doorman_pool_scaling{type, user, database}.
See the pool pressure tutorial for acquisition phases, tuning recipes, and alert examples.
Connection checkout under pressure
Replaces scaling_cooldown_sleep (a fixed 10 ms delay before creating a backend connection) with a multi-phase checkout that reuses connections about to be returned before resorting to connect().
When the idle pool is empty and the pool is above its warm threshold (scaling_warm_pool_ratio, default 20%), a caller first spins briefly (scaling_fast_retries, default 10 yield iterations), then registers a direct-handoff waiter. Connections returned by other clients are delivered through the waiter channel — no idle-queue round-trip, no race with other checkout attempts. The waiter deadline is bounded by query_wait_timeout minus a 500 ms reserve for the create path. If no connection arrives, the caller proceeds to create.
Backend connect() calls are capped at scaling_max_parallel_creates (default 2) per pool. Callers above the cap wait for a peer create to finish or a connection to be returned. Background replenish (min_pool_size) respects the same cap and defers to the next retain cycle when the gate is full, so it does not compete with client-driven creates during spikes.
Connections nearing server_lifetime expiry (95% of age) trigger a pre-replacement: a background task creates a successor before the old connection fails recycle, so the next checkout hits the hot path.
The direct-handoff queue is FIFO. On a 500-client / 40-connection AWS Fargate benchmark, p99/p50 ratio is 1.08 (pg_doorman) vs 25.5 (Odyssey). Every client pays roughly the same queue cost.
Migration: remove scaling_cooldown_sleep from your config if present. Replace with scaling_max_parallel_creates (default 2) if you need to tune the concurrency cap.
Improvements:
-
Runtime log level control.
SET log_level = 'debug'changes the log filter without restart;SET log_level = 'warn,pg_doorman::pool::pool_coordinator=debug'targets specific modules.SHOW LOG_LEVELdisplays the current filter. Changes are ephemeral (lost on restart). -
Log readability overhaul. Consistent
[user@pool #cN]prefix. Durations as4m30sinstead of raw milliseconds. Stats line in logfmt. PG error newlines escaped. Expensive debug computations guarded bylog_enabled!()to avoid allocations at production log levels. -
Auth failure logs include client IP. SCRAM, MD5, JWT, and PAM failures show the source address.
-
Replenish failure noise suppression. Repeated
min_pool_sizefailures log once at warn, then a periodic reminder every ~10 minutes with the failure count. -
avg_xact_timecolumn inSHOW POOLS. Average transaction time per pool, visible alongside existing connection counts. -
Smart session cleanup in transaction mode. pg_doorman tracks which session state a client dirtied (
SET,DECLARE CURSOR, prepared statements) and sends the matching reset on checkin. If the client cleaned up after itself —RESET ALL,CLOSE ALL,DEALLOCATE ALL, orDISCARD ALL— pg_doorman sees the confirmation and skips its own reset. Drivers likejackc/pgxthat send a cleanup batch on disconnect no longer cause a redundant round-trip to PostgreSQL. ASETwithout a follow-up reset still triggers cleanup as before.
3.3.5 Mar 31, 2026
Bug Fixes:
- Prepared statement eviction during batch breaks buffered Bind. When a client sent a batch like
Parse(A), Bind(A), Parse(C), SyncandParse(C)triggered server-side LRU eviction of statement A, theClose(A)was sent to PostgreSQL immediately (out-of-band), deleting A before the client buffer was flushed.Bind(A)then failed withprepared statement "DOORMAN_X" does not exist(error 26000). Two fixes: (1)has_prepared_statement()now promotes entries in the LRU on access (get()instead ofcontains()), so actively-used statements resist eviction. (2) EvictionCloseis deferred until after the batch completes — the statement stays alive on PostgreSQL while Binds in the buffer are processed, thenCloseis sent as post-batch cleanup. If the client disconnects beforeSync,checkin_cleanupdetects the pending deferred closes and triggersDEALLOCATE ALL.
3.3.4 Mar 30, 2026
Bug Fixes:
- Prepared statement cache desync after client disconnect. When a client sent Parse but disconnected before Sync/Flush, pg_doorman registered the statement in the server-side LRU cache but never sent the actual Parse to PostgreSQL (it was still in the client buffer, which was dropped on disconnect). The next client that got the same server connection and used the same query saw the stale cache entry, skipped sending Parse, and received
prepared statement "DOORMAN_X" does not exist(error 26000) from PostgreSQL. Fixed by tracking ahas_pending_cache_entriesflag on the server connection: set when a statement is added to the cache without immediate Parse confirmation, cleared after successful buffer flush. If the client disconnects before flushing,checkin_cleanupdetects the flag and triggersDEALLOCATE ALLto re-synchronize the cache. Zero overhead on the normal path (one boolean check per checkin).
3.3.3 Mar 26, 2026
Bug Fixes:
-
Log spam from missing
/proc/net/tcp6when IPv6 disabled.get_socket_states_countfailed entirely if any of the three /proc files was absent, logging errors every 15 seconds and losing tcp/unix metrics that were available. Missing files are now skipped — counters stay at zero. Other I/O errors (permission denied) still propagate. -
Protocol violation when streaming large DataRow with cached prepared statements.
handle_large_data_rowwrote accumulated protocol messages (BindComplete, RowDescription) directly to the client socket, bypassingreorder_parse_complete_responses. When Parse was skipped (prepared statement cache hit), the client received BindComplete without the synthetic ParseComplete — causingReceived backend message BindComplete while expecting ParseCompleteMessagein Npgsql and similar drivers. Triggered whenmessage_size_to_be_stream≤ 64KB. Fixed by returning accumulated messages fromrecv()before entering the streaming path, so response reordering runs first. Same fix applied tohandle_large_copy_data.
3.3.2 Mar 1, 2026
Breaking Changes:
auth_queryconfig field renames: Two fields in theauth_querysection have been renamed for clarity.auth_query.pool_size(number of connections for running auth queries) is nowauth_query.workers.auth_query.default_pool_size(data pool size for dynamic users) is nowauth_query.pool_size, matching the same parameter name used in static pools. Migration: renamepool_sizetoworkersanddefault_pool_sizetopool_sizein yourauth_queryconfig. If you don't update, the oldpool_sizevalue (typically 1-2) will be interpreted as the data pool size, drastically reducing connection capacity. The olddefault_pool_sizekey is silently ignored and defaults to 40.
Bug Fixes:
-
Session mode: keep server connections alive after SQL errors. A query like
SELECT 1/0returns anErrorResponsefrom PostgreSQL but leaves the connection fully usable. Previously,handle_error_responsecalledmark_bad()unconditionally in async mode, so the connection was destroyed at session end. Nowmark_badis skipped when the pool runs in session mode. Transaction mode still callsmark_badbecause the connection returns to a shared pool where protocol desync is dangerous. -
Pool-level
server_lifetimeandidle_timeoutoverrides ignored: Pool-level overrides forserver_lifetimeandidle_timeoutwere silently ignored — the general (global) values were always used instead. Fixed in 6 places across 3 pool creation contexts (static pools, auth_query shared pools, dynamic pools). Nowpool.server_lifetimeandpool.idle_timeoutcorrectly override the general settings when specified. -
idle_timeoutdefault was 83 hours instead of 10 minutes: The defaultidle_timeoutwas set to 300,000,000ms (83 hours), effectively disabling idle connection cleanup. Idle server connections could accumulate indefinitely. Changed default to 600,000ms (10 minutes). -
retain_connections_maxquota exhaustion causing unlimited closure: Whenretain_connections_max > 0and the global counter reached the limit, the remaining quota became0viasaturating_sub. Since0means "unlimited" inretain_oldest_first(), pools processed after quota exhaustion lost ALL idle connections in a single retain cycle instead of none. With non-deterministic HashMap iteration order, this bug manifested as random pools losing all connections. Fixed by adding an early return when the quota is exhausted. -
retain_connections_maxdoc comment incorrectly stated default as0(unlimited): The actual default is3. -
server_lifetimedefault changed from 5 minutes to 20 minutes: The previous default of 5 minutes was shorter thanidle_timeout(10 minutes), which meantidle_timeoutcould never trigger — connections were always killed byserver_lifetimefirst. Changed to 20 minutes so thatidle_timeout(10 min) handles idle cleanup whileserver_lifetime(20 min) rotates long-lived connections. Note:idle_timeoutonly applies to connections that have been used at least once — prewarmed/replenished connections that were never checked out by a client are not subject toidle_timeoutand will only be closed whenserver_lifetimeexpires. -
idle_timeout = 0did not disable idle timeout: Settingidle_timeoutto0was supposed to disable idle connection cleanup (consistent with PgBouncer'sserver_idle_timeout = 0semantics and our ownserver_lifetime = 0behavior). Instead, it closed connections after ~1ms of being idle. Fixed by adding anidle_timeout_ms > 0guard before the elapsed time check. -
idle_timeouthad no jitter — synchronized mass closures: Unlikeserver_lifetimewhich applies ±20% per-connection jitter to prevent thundering herd,idle_timeoutused a single pool-wide value. When many connections became idle simultaneously (e.g., after a traffic burst), they all expired at the exact same moment, causing mass closures in one retain cycle. Nowidle_timeoutapplies the same ±20% per-connection jitter asserver_lifetime. -
retain_connections_maxunfair quota distribution across pools: The retain cycle iterated pools via HashMap, whose order is deterministic within a process (fixed RandomState seed). The same pool always got iterated first and consumed the entireretain_connections_maxquota, starving other pools. Expired connections in starved pools were never cleaned up by retain — clients had to discover them via failedrecycle()checks, adding latency. Fixed by shuffling pool iteration order each cycle. -
Retain and replenish used separate pool snapshots: The retain and replenish phases each called
get_all_pools()separately. IfPOOLSwas atomically updated between them (config reload, dynamic pool GC), retain operated on one set of pools and replenish on another, potentially missing pools that need replenishment. Fixed by using a single snapshot for both phases.
Testing:
- PHP PDO_PGSQL driver added to test infrastructure. PHP 8.4 with
pdo_pgsqlextension is now included in the Nix-based Docker test image. Two BDD scenarios verify basic connectivity (SELECT 1) and session mode behavior (SQL error does not change backend PID). Run withmake test-phpor--tags @php.
New Features:
-
pool_sizeobservability: Newpg_doorman_pool_sizePrometheus gauge exposes the configured maximum pool size per user/database. Thepool_sizecolumn is also added toSHOW POOLSandSHOW POOLS_EXTENDEDadmin commands (aftersv_login), allowing operators to compare current server connections against configured capacity directly from the admin console. Works for both static and dynamic (auth_query) pools. -
PAUSE, RESUME, RECONNECT admin commands: New admin console commands for managing connection pools.
PAUSE [db]blocks new backend connection acquisition (active transactions continue).RESUME [db]lifts the pause and unblocks waiting clients.RECONNECT [db]forces connection rotation by incrementing the pool epoch — idle connections are immediately closed and active connections are discarded when returned to the pool. Without arguments, all pools are affected; with a database name, only matching pools. Specifying a nonexistent database returns an error. UseSHOW POOLSto see thepausedstatus column. -
min_pool_sizefor dynamic auth_query passthrough pools: Newauth_query.min_pool_sizesetting controls the minimum number of backend connections maintained per dynamic user pool in passthrough mode. Connections are prewarmed in the background when the pool is first created and replenished by the retain cycle afterserver_lifetimeexpiry. Pools withmin_pool_size > 0are never garbage-collected. Default is0(no prewarm — backward compatible). Note: total backend connections scale asactive_users × min_pool_size.
3.3.1 Feb 26, 2026
Bug Fixes:
-
Fix Ctrl+C in foreground mode: Pressing Ctrl+C in foreground mode (with TTY attached) now performs a clean graceful shutdown instead of triggering a binary upgrade. Previously, each Ctrl+C would spawn a new pg_doorman process via
--inherit-fd, leaving orphan processes accumulating. SIGINT in daemon mode (no TTY) retains its legacy binary upgrade behavior for backward compatibility with existingsystemdunits. -
Minimum pool size enforcement (
min_pool_size): Themin_pool_sizeuser setting is now enforced at runtime. After each connection retain cycle, pg_doorman checks pool sizes and creates new connections to maintain the configured minimum. Previously,min_pool_sizewas accepted in config but never applied — pools started empty and could drop to 0 connections even withmin_pool_sizeset. Replenishment stops on the first connection failure to avoid hammering an unavailable server.
New Features:
-
SIGUSR2 for binary upgrade: New dedicated signal
SIGUSR2triggers binary upgrade + graceful shutdown in all modes (daemon and foreground). This is now the recommended signal for binary upgrades. Thesystemdservice file has been updated to useSIGUSR2forExecReload. -
UPGRADEadmin command: New admin console command that triggers binary upgrade via SIGUSR2. Use it frompsqlconnected to the admin database:UPGRADE;.
Improvements:
-
Pool prewarm at startup: When
min_pool_sizeis configured, pg_doorman now creates the minimum number of connections immediately at startup, before the first retain cycle. Previously, pools started empty and connections were only created lazily on first client request or after the first retain interval (default 60s). This eliminates cold-start latency for the first clients connecting after pg_doorman restart. -
Configurable connection scaling parameters: New
generalsettingsscaling_warm_pool_ratio,scaling_fast_retries, andscaling_cooldown_sleepallow tuning connection pool scaling behavior. All three can be overridden at the pool level.scaling_cooldown_sleepuses the human-readableDurationtype (e.g."10ms","1s") consistent with other timeout fields. -
max_concurrent_createssetting: Controls the maximum number of server connections that can be created concurrently per pool. Uses a semaphore instead of a mutex for parallel connection creation.
3.3.0 Feb 23, 2026
New Features:
-
Dynamic user authentication (
auth_query): PgDoorman can now authenticate users dynamically by querying PostgreSQL at connection time — no need to list every user in the config. Supportspg_shadow, custom tables, andSECURITY DEFINERfunctions. The query must return a column namedpasswdorpassword(or any single column) containing an MD5 or SCRAM-SHA-256 hash. -
Passthrough authentication: Default mode for both static and dynamic users — PgDoorman reuses the client's cryptographic proof (MD5 hash or SCRAM ClientKey) to authenticate to the backend automatically. No plaintext
server_passwordin config needed when the pool user matches the backend PostgreSQL user. -
Two auth_query modes:
- Passthrough mode (default) — each dynamic user gets their own backend connection pool and authenticates as themselves, preserving per-user identity on the backend.
- Dedicated mode (
server_userset) — all dynamic users share a single backend pool under one PostgreSQL role.
-
Auth query caching: DashMap-based cache with configurable TTL, double-checked locking, rate-limited refetch, and request coalescing. Supports separate TTLs for successful and failed lookups.
-
SHOW AUTH_QUERYadmin command: Displays per-pool metrics — cache entries/hits/misses, auth success/failure counters, executor stats, and dynamic pool count. -
Prometheus metrics for auth_query: New metric families
pg_doorman_auth_query_cache,pg_doorman_auth_query_auth,pg_doorman_auth_query_executor,pg_doorman_auth_query_dynamic_pools. -
Idle dynamic pool garbage collection: Background task cleans up expired dynamic pools when all connections have been idle beyond
server_lifetime. Zero overhead for static-only configs. -
Smart password column lookup: Password column resolved by name (
passwd→password→ single-column fallback), works withpg_shadow, custom tables, and arbitrary single-column queries.
Improvements:
-
server_username/server_passwordnow optional: Previously documented as required for MD5/SCRAM hash configs. Now only needed when the backend user differs from the pool user (username mapping, JWT auth). -
Data-driven config & docs generation:
fields.yamlis the single source of truth for all config field descriptions (EN/RU). Reference docs, annotated configs, and inline comments are all generated from it.
Testing:
- 39 new BDD scenarios (260+ steps) covering auth_query executor, end-to-end auth, HBA integration, passthrough mode, SCRAM-only auth, RELOAD/GC lifecycle, observability, and static user passthrough.
3.2.4 Feb 20, 2026
New Features:
-
Annotated config generation: The
generatecommand now produces well-documented configuration files with inline comments for every parameter by default. Previously it only did plain serde serialization without any documentation. -
--referenceflag: Generates a complete reference config with example values without requiring a PostgreSQL connection. The rootpg_doorman.tomlandpg_doorman.yamlare now auto-generated from this flag, ensuring they always stay in sync with the codebase. -
--format(-f) flag: Explicitly choose output format (yamlortoml). Default output format changed from TOML to YAML. When--outputis specified, format is auto-detected from file extension;--formatoverrides auto-detection. -
--russian-comments(--ru) flag: Generates comments in Russian for quick start guide. All ~100+ comment strings are translated to clear, simple Russian. -
--no-commentsflag: Disables inline comments for minimal config output (plain serde serialization, the old default behavior). -
Passthrough authentication documentation: Documents passthrough auth as the default mode —
server_username/server_passwordare no longer needed when the pool user matches the backend PostgreSQL user. PgDoorman reuses the client's MD5 hash or SCRAM ClientKey to authenticate to the backend automatically.
Testing:
-
Config field coverage guarantee: New test parses config struct source files (
general.rs,pool.rs,user.rs, etc.) at compile time and verifies everypubfield appears in annotated output. If someone adds a new config parameter but forgets to add it toannotated.rs, CI will fail with a clear message listing the missing fields. -
BDD tests for generate command: End-to-end tests that generate TOML and YAML configs, start pg_doorman with them, and verify client connectivity.
Bug Fixes:
-
Fixed protocol desynchronization on prepared statement cache eviction in async mode: When asyncpg/SQLAlchemy uses
Flush(instead ofSync) for pipelinedParse+Describebatches and the prepared statement LRU cache is full, eviction sendsClose+Syncto the server. In async mode,recv()was exiting immediately whenexpected_responses==0, leavingCloseCompleteandReadyForQueryunread in the TCP buffer. The nextrecv()call would then read these stale messages instead of the expected response, causing protocol desynchronization. Fixed by temporarily disabling async mode during eviction so thatrecv()waits forReadyForQueryas the natural loop terminator. -
Fixed generated config startup failure:
syslog_prog_nameanddaemon_pid_fileare now commented out by default in generated configs. Previously they were uncommented, causing pg_doorman to fail when started in foreground mode or when syslog was unavailable. -
Fixed Go test goroutine leak:
TestLibPQPreparednow usessync.WaitGroupto wait for all goroutines before test exit, fixing sporadic panics caused by logging after test completion. -
Fixed protocol violation on flush timeout — client now receives ErrorResponse: When the 5-second flush timeout fires (server TCP write blocks because the backend is overloaded or unreachable), the
FlushTimeouterror was propagating via?throughhandle_sync_flush→ transaction loop →handle()without sending any PostgreSQL protocol message to the client. The TCP connection was simply dropped, causing drivers like Npgsql to report "protocol violation" due to unexpected EOF. Now pg_doorman sends a properErrorResponsewith SQLSTATE58006and message containing "pooler is shut down now" before closing the connection, allowing client drivers to detect the error and reconnect gracefully.
3.2.3 Feb 10, 2026
Improvements:
- Jitter for
server_lifetime(±20%): Connection lifetimes now have a random ±20% jitter applied to prevent mass disconnections from PostgreSQL. When pg_doorman is under heavy load, it creates many connections simultaneously, which previously caused them all to expire at the same time, creating spikes of connection closures. Now each connection gets an individual lifetime calculated asbase_lifetime ± random(20%). For example, withserver_lifetime: 300000(5 minutes), actual lifetimes range from 240s to 360s, spreading connection closures evenly over time.
3.2.2 Feb 9, 2026
New Features:
-
Configuration test mode (
-t/--test-config): Added nginx-style configuration validation flag. Runningpg_doorman -torpg_doorman --test-configwill parse and validate the configuration file, report success or errors, and exit without starting the server. Useful for CI/CD pipelines and pre-deployment configuration checks. -
Configuration validation before binary upgrade: When receiving SIGINT for graceful shutdown/binary upgrade, the server now validates the new binary's configuration using
-tflag before proceeding. If the configuration test fails, the shutdown is cancelled and critical error messages are logged to alert the operator. This prevents accidental downtime from deploying a binary with invalid configuration. -
New
retain_connections_maxconfiguration parameter: Controls the maximum number of idle connections to close per retain cycle. When set to0, all idle connections that exceedidle_timeoutorserver_lifetimeare closed immediately. Default is3, providing controlled cleanup while preventing connection buildup. Previously, only 1 connection was closed per cycle, which could lead to slow connection cleanup when many connections became idle simultaneously. Connection closures are now logged for better observability. -
Oldest-first connection closure: When
retain_connections_max > 0, connections are now closed in order of age (oldest first) rather than in queue order. This ensures that the oldest connections are always prioritized for closure, providing more predictable connection rotation behavior. -
New
server_idle_check_timeoutconfiguration parameter: Time after which an idle server connection should be checked before being given to a client (default: 30s). This helps detect dead connections caused by PostgreSQL restart, network issues, or server-side idle timeouts. When a connection has been idle longer than this timeout, pg_doorman sends a minimal query (;) to verify the connection is alive before returning it to the client. Set to0to disable. -
New
tcp_user_timeoutconfiguration parameter: Sets theTCP_USER_TIMEOUTsocket option for client connections (in seconds). This helps detect dead client connections faster than keepalive probes when the connection is actively sending data but the remote end has become unreachable. Prevents 15-16 minute delays caused by TCP retransmission timeout. Only supported on Linux. Default is60seconds. Set to0to disable. -
Removed
wait_rollbackmechanism: The pooler no longer attempts to automatically wait for ROLLBACK from clients when a transaction enters an aborted state. This complex mechanism was causing protocol desynchronization issues with async clients and extended query protocol. Server connections in aborted transactions are now simply returned to the pool and cleaned up normally via ROLLBACK during checkin. -
Removed savepoint tracking: Removed the
use_savepointflag and related logic that was tracking SAVEPOINT usage. The pooler now treats savepoints as regular PostgreSQL commands without special handling.
Bug Fixes:
- Fixed protocol desynchronization in async mode with simple prepared statements: When
prepared_statementswas disabled but clients used extended query protocol (Parse, Bind, Describe, Execute, Flush), the pooler wasn't tracking batch operations, causingexpected_responsesto be calculated as 0. This led to the pooler exiting the response loop immediately without waiting for server responses (ParseComplete, BindComplete, etc.). Now batch operations are tracked regardless of theprepared_statementssetting.
Performance:
- Removed timeout-based waiting in async protocol: The pooler now tracks expected responses based on batch operations (Parse, Bind, Execute, etc.) and exits immediately when all responses are received. This eliminates unnecessary latency in pipeline/async workloads.
3.1.8 Jan 31, 2026
Bug Fixes:
-
Fixed ParseComplete desynchronization in pipeline on errors: Fixed a protocol desynchronization issue (especially noticeable in .NET Npgsql driver) where synthetic
ParseCompletemessages were not being inserted if an error occurred during a pipelined batch. When the pooler caches a prepared statement and skips sendingParseto the server, it must still provide aParseCompleteto the client. If an error occurs before subsequent commands are processed, the server skips them, and the pooler now ensures all missing syntheticParseCompletemessages are inserted into the response stream upon receiving anErrorResponseorReadyForQuery. -
Fixed incorrect
use_savepointstate persistence: Fixed a bug where theuse_savepointflag (which disables automatic rollback on connection return if a savepoint was used) was not reset after a transaction ended.
3.1.7 Jan 28, 2026
Memory Optimization:
-
DEALLOCATE now clears client prepared statements cache: When a client sends
DEALLOCATE <name>orDEALLOCATE ALLvia simple query protocol, the pooler now properly clears the corresponding entries from the client's internal prepared statements cache. Previously, synthetic OK responses were sent but the client cache was not cleared, causing memory to grow indefinitely for long-running connections using many unique prepared statements. This fix allows memory to be reclaimed when clients properly deallocate their statements. -
New
client_prepared_statements_cache_sizeconfiguration parameter: Added protection against malicious or misbehaving clients that don't callDEALLOCATEand could exhaust server memory by creating unlimited prepared statements. When the per-client cache limit is reached, the oldest entry is evicted automatically. Set to0for unlimited (default, relies on client callingDEALLOCATE). Example:client_prepared_statements_cache_size: 1024limits each client to 1024 cached prepared statements.
3.1.6 Jan 27, 2026
Bug Fixes:
-
Fixed incorrect timing statistics (xact_time, wait_time, percentiles): The statistics module was using
recent()(cached clock) without proper clock cache updates, causing transaction time, wait time, and their percentiles to show extremely large incorrect values (e.g., 100+ seconds instead of actual milliseconds). The root cause was that thequanta::Upkeephandle was not being stored, causing the upkeep thread to stop immediately after starting. Now the handle is properly retained for the lifetime of the server, ensuringClock::recent()returns accurate cached time values. -
Fixed query time accumulation bug in transaction loop: Query times were incorrectly accumulated when multiple queries were executed within a single transaction. The
query_start_attimestamp was only set once at the beginning of the transaction, causing each subsequent query's elapsed time to include all previous queries' durations (e.g., 10 queries of 100ms each would report the last query as ~1 second instead of 100ms). Nowquery_start_atis updated for each new message in the transaction loop, ensuring accurate per-query timing.
New Features:
-
New
clock_resolution_statisticsconfiguration parameter: Addedgeneral.clock_resolution_statisticsparameter (default:0.1ms= 100 microseconds) that controls how often the internal clock cache is updated. Lower values provide more accurate timing measurements for query/transaction percentiles, while higher values reduce CPU overhead. This parameter affects the accuracy of all timing statistics reported in the admin console and Prometheus metrics. -
Sub-millisecond precision for Duration values: Duration configuration parameters now support sub-millisecond precision:
- New
ussuffix for microseconds (e.g.,"100us"= 100 microseconds) - Decimal milliseconds support (e.g.,
"0.1ms"= 100 microseconds) - Internal representation changed from milliseconds to microseconds for higher precision
- Full backward compatibility maintained: plain numbers are still interpreted as milliseconds
- New
3.1.5 Jan 25, 2026
Bug Fixes:
- Fixed PROTOCOL VIOLATION with batch PrepareAsync
- Rewritten ParseComplete insertion algorithm
Performance:
- Deferred connection acquisition for standalone BEGIN: When a client sends a standalone
BEGIN;orbegin;query (simple query protocol), the pooler now defers acquiring a server connection until the next message arrives. SinceBEGINitself doesn't perform any actual database operations, this optimization reduces connection pool contention when clients are slow to send their next query after starting a transaction.- Micro-optimized detection: first checks message size (12 bytes), then content using case-insensitive comparison
- If client sends Terminate (
X) afterBEGIN, no server connection is acquired at all - The deferred
BEGINis automatically sent to the server before the actual query
3.1.0 Jan 18, 2026
New Features:
- YAML configuration support: Added support for YAML configuration files (
.yaml,.yml) as the primary and recommended format. The format is automatically detected based on file extension. TOML format remains fully supported for backward compatibility.- The
generatecommand now outputs YAML or TOML based on the output file extension. - Include files can mix YAML and TOML formats.
- New array syntax for users in YAML:
users: [{ username: "user1", ... }]
- The
- TOML backward compatibility: Full backward compatibility with legacy TOML format
[pools.*.users.0]is maintained. Both the legacy map format and the new array format[[pools.*.users]]are supported. - Username uniqueness validation: Added validation to reject duplicate usernames within a pool, ensuring configuration correctness.
- Human-readable configuration values: Duration and byte size parameters now support human-readable formats while maintaining backward compatibility with numeric values:
- Duration:
"3s","5m","1h","1d"(or milliseconds:3000) - Byte size:
"1MB","256M","1GB"(or bytes:1048576) - Example:
connect_timeout: "3s"instead ofconnect_timeout: 3000
- Duration:
- Foreground mode binary upgrade: Added support for binary upgrade in foreground mode by passing the listener socket to the new process via
--inherit-fdargument. This enables zero-downtime upgrades without requiring daemon mode. - Optional tokio runtime parameters: The following tokio runtime parameters are now optional and default to
None(using tokio's built-in defaults):tokio_global_queue_interval,tokio_event_interval,worker_stack_size, and the newmax_blocking_threads. Modern tokio versions handle these parameters well by default, so explicit configuration is no longer required in most cases. - Improved graceful shutdown behavior:
- During graceful shutdown, only clients with active transactions are now counted (instead of all connected clients), allowing faster shutdown when clients are idle.
- After a client completes their transaction during shutdown, they receive a proper PostgreSQL protocol error (
58006 - pooler is shut down now) instead of a connection reset. - Server connections are immediately released (marked as bad) after transaction completion during shutdown to conserve PostgreSQL connections.
- All idle connections are immediately drained from pools when graceful shutdown starts, releasing PostgreSQL connections faster.
Performance:
- Statistics module optimization: Major refactoring of the
src/statsmodule for improved performance:- Replaced
VecDequewith HDR histograms (hdrhistogramcrate) for percentile calculations — O(1) percentile queries instead of O(n log n) sorting, ~95% memory reduction for latency tracking. - Histograms are now reset after each stats period (15 seconds) to provide accurate rolling window percentiles.
- Replaced
3.0.5 Jan 16, 2026
Bug Fixes:
- Fixed panic (
capacity overflow) in startup message handling when receiving malformed messages with invalid length (less than 8 bytes or exceeding 10MB). Now gracefully rejects such connections withClientBadStartuperror.
Testing:
- Integration fuzz testing framework: Added comprehensive BDD-based fuzz tests (
@fuzztag) that verify pg_doorman's resilience to malformed PostgreSQL protocol messages. - All fuzz tests connect and authenticate first, then send malformed data to test post-authentication resilience.
CI/CD:
- Added dedicated fuzz test job in GitHub Actions workflow (without retries, as fuzz tests should not be flaky).
3.0.4 Jan 16, 2026
New Features:
- Enhanced DEBUG logging for PostgreSQL protocol messages: Added grouped debug logging that displays message types in a compact format (e.g.,
[P(stmt1),B,D,E,S]or[3xD,C,Z]). Messages are buffered and flushed every 100ms or 100 messages to reduce log noise. - Protocol violation detection: Added real-time protocol state tracking that detects and warns about protocol violations (e.g., receiving ParseComplete when no Parse was pending). Helps diagnose client-server synchronization issues.
Bug Fixes:
- Fixed potential protocol violation when client disconnects during batch operations with cached prepared statements: disabled fast_release optimization when there are pending prepared statement operations.
- Fixed ParseComplete insertion for Describe flow: now correctly inserts one ParseComplete before each ParameterDescription ('t') or NoData ('n') message instead of inserting all at once.
3.0.3 Jan 15, 2026
Bug Fixes:
- Improved handling of Describe flow for cached prepared statements: added a separate counter (
pending_parse_complete_for_describe) to correctly insert ParseComplete messages before ParameterDescription or NoData responses when Parse was skipped due to caching.
Testing:
- Added comprehensive .NET client tests for Describe flow with cached prepared statements (
describe_flow_cached.cs). - Added aggressive mixed tests combining batch operations, prepared statements, and extended protocol (
aggressive_mixed.cs).
3.0.2 Jan 14, 2026
Bug Fixes:
- Fixed protocol mismatch for .NET clients (Npgsql) using named prepared statements with
Prepare(): ParseComplete messages are now correctly inserted before ParameterDescription and NoData messages in the Describe flow, not just before BindComplete.
3.0.1 Jan 14, 2026
Bug Fixes:
- Fixed protocol mismatch for .NET clients (Npgsql): prevented insertion of ParseComplete messages between DataRow messages when server has more data available.
Testing:
- Extended Node.js client test coverage with additional scenarios for prepared statements, error handling, transactions, and edge cases.
3.0.0 Jan 12, 2026
Major Release — Complete Architecture Refactoring
This release represents a significant milestone with a complete codebase refactoring that dramatically improves async protocol support, making PgDoorman the most efficient connection pooler for asynchronous PostgreSQL workloads.
New Features:
- patroni_proxy — A new high-performance TCP proxy for Patroni-managed PostgreSQL clusters:
- Zero-downtime connection management — existing connections are preserved during cluster topology changes
- Hot upstream updates — automatic discovery of cluster members via Patroni REST API without connection drops
- Role-based routing — route connections to leader, sync replicas, or async replicas based on configuration
- Replication lag awareness with configurable
max_lag_in_bytesper port - Least connections load balancing strategy
Improvements:
- Complete codebase refactoring — modular architecture with better separation of concerns:
- Client handling split into dedicated modules (core, entrypoint, protocol, startup, transaction)
- Configuration system reorganized into focused modules (general, pool, user, tls, prometheus, talos)
- Admin, auth, and prometheus subsystems extracted into separate modules
- Improved code maintainability and testability
- Enhanced async protocol support — significantly improved handling of asynchronous PostgreSQL protocol, providing better performance than other connection poolers for async workloads
- Extended protocol improvements — better client buffering and message handling for extended query protocol
- xxhash3 for prepared statement hashing — faster hash computation for prepared statement cache
- Comprehensive BDD testing framework — multi-language integration tests (Go, Rust, Python, Node.js, .NET) with Docker-based reproducible environment
2.5.0 Nov 18, 2025
Improvements:
- Reworked the statistics collection system, yielding up to 20% performance gain on fast queries.
- Improved detection of
SAVEPOINTusage, allowing the auto-rollback feature to be applied in more situations.
Bug Fixes / Behavior:
- Less aggressive behavior on write errors when sending a response to the client: the server connection is no longer immediately marked as "bad" and evicted from the pool. We now read the remaining server response and clean up its state, returning the connection to the pool in a clean state. This improves performance during client reconnections.
2.4.3 Nov 15, 2025
Bug Fixes:
- Fixed handling of nested transactions via
SAVEPOINT: auto-rollback now correctly rolls back to the savepoint instead of breaking the outer transaction. This prevents clients from getting stuck in an inconsistent transactional state.
2.4.2 Nov 13, 2025
Improvements:
pg_hbarules now apply to the admin console as well; thetrustmethod can be used for admin connections when a matching rule is present (use with caution; restrict by address/TLS).
Bug Fixes:
- Fixed
pg_hbaevaluation:localrecords were mistakenly considered; PgDoorman only handles TCP connections, solocalentries are now correctly ignored.
2.4.1 Nov 12, 2025
Improvements:
- Performance optimizations in request handling and message processing paths to reduce latency and CPU usage.
pg_hbarules now apply to the admin console as well; thetrustmethod can be used for admin connections when a matching rule is present (use with caution; restrict by address/TLS).
Bug Fixes:
- Corrected logic where
COMMITcould be mishandled similarly toROLLBACKin certain error states; now transactional state handling is aligned with PostgreSQL semantics.
2.4.0 Nov 10, 2025
Features:
- Added
pg_hbasupport to control client access in PostgreSQL format. Newgeneral.pg_hbasetting supports inline content or file path. - Clients that enter the
aborted in transactionstate are detached from their server backend; the proxy waits for the client to sendROLLBACK.
Improvements:
- Refined admin and metrics counters: separated
cancelconnections and corrected calculation oferrorconnections in admin output and Prometheus metrics descriptions. - Added configuration validation to prevent simultaneous use of legacy
general.hbaCIDR list with the newgeneral.pg_hbarules. - Improved validation and error messages for Talos token authentication.
2.2.2 Aug 17, 2025
Features:
- Added new generate feature functionality
Bug Fixes:
- Fixed deallocate issues with PGX5 compatibility
2.2.1 Aug 6, 2025
Features:
- Improve Prometheus exporter functionality
2.2.0 Aug 5, 2025
Features:
- Added Prometheus exporter functionality that provides metrics about connections, memory usage, pools, queries, and transactions
2.1.2 Aug 4, 2025
Features:
- Added docker image
ghcr.io/ozontech/pg_doorman
2.1.0 Aug 1, 2025
Features:
- The new command
generateconnects to your PostgreSQL server, automatically detects all databases and users, and creates a complete configuration file with appropriate settings. This is especially useful for quickly setting up PgDoorman in new environments or when you have many databases and users to configure.
2.0.1 July 24, 2025
Bug Fixes:
- Fixed
max_memory_usagecounter leak when clients disconnect improperly.
2.0.0 July 22, 2025
Features:
- Added
tls_modeconfiguration option to enhance security with flexible TLS connection management and client certificate validation capabilities.
1.9.0 July 20, 2025
Features:
- Added PAM authentication support.
- Added
talosJWT authentication support.
Improvements:
- Implemented streaming for COPY protocol with large columns to prevent memory exhaustion.
- Updated Rust and Tokio dependencies.
1.8.3 Jun 11, 2025
Bug Fixes:
- Fixed critical bug where Client's buffer wasn't cleared when no free connections were available in the Server pool (query_wait_timeout), leading to incorrect response errors. #38
- Fixed Npgsql-related issue. Npgsql#6115
1.8.2 May 24, 2025
Features:
- Added
application_nameparameter in pool. #30 - Added support for
DISCARD ALLandDEALLOCATE ALLclient queries.
Improvements:
- Implemented link-time optimization. #29
Bug Fixes:
- Fixed panics in admin console.
- Fixed connection leakage on improperly handled errors in client's copy mode.
1.8.1 April 12, 2025
Bug Fixes:
- Fixed config value of prepared_statements. #21
- Fixed handling of declared cursors closure. #23
- Fixed proxy server parameters. #25
1.8.0 Mar 20, 2025
Bug Fixes:
- Fixed dependencies issue. #15
Improvements:
- Added release vendor-licenses.txt file. Related thread
1.7.9 Mar 16, 2025
Improvements:
- Added release vendor.tar.gz for offline build. Related thread
Bug Fixes:
- Fixed issues with pqCancel messages over TLS protocol. Drivers should send pqCancel messages exclusively via TLS if the primary connection was established using TLS. Npgsql follows this rule, while PGX currently does not. Both behaviors are now supported.
1.7.8 Mar 8, 2025
Bug Fixes:
- Fixed message ordering issue when using batch processing with the extended protocol.
- Improved error message detail in logs for server-side login attempt failures.
1.7.7 Mar 8, 2025
Features:
- Enhanced
show clientscommand with new fields:state(waiting/idle/active) andwait(read/write/idle). - Enhanced
show serverscommand with new fields:state(login/idle/active),wait(read/write/idle), andserver_process_pid. - Added 15-second proxy timeout for streaming large
message_size_to_be_streamresponses.
Bug Fixes:
- Fixed
max_memory_usagecounter leak when clients disconnect improperly.
Вклад в PgDoorman
Спасибо за интерес к развитию PgDoorman! Это руководство поможет настроить окружение для разработки и разобраться в процессе вклада в проект.
С чего начать
Зависимости
Чтобы запускать интеграционные тесты, нужно только:
Установка Nix НЕ требуется -- воспроизводимость тестового окружения обеспечивается Docker-контейнерами, собранными через Nix.
Для локальной разработки (опционально):
Настройка окружения для разработки
- Сделайте fork репозитория на GitHub.
- Склонируйте свой fork:
git clone https://github.com/YOUR-USERNAME/pg_doorman.git cd pg_doorman - Добавьте upstream-репозиторий:
git remote add upstream https://github.com/ozontech/pg_doorman.git
Локальная разработка
-
Сборка проекта:
cargo build -
Сборка для performance-тестов:
cargo build --release -
Настройка PgDoorman:
- Скопируйте пример конфигурации:
cp pg_doorman.toml.example pg_doorman.toml - Подправьте настройки в
pg_doorman.tomlпод ваше окружение.
- Скопируйте пример конфигурации:
-
Запуск PgDoorman:
cargo run --release -
Запуск 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-метрик |
@fuzz | Fuzz-тесты на устойчивость |
@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 bothwe send CopyFromStdin "COPY ..." with data "..." to bothwe 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_pidwe abort TCP connection for session "name"we sleep 100ms
Тестирование cancel-запросов:
we create session "name" ... and store backend keywe send SimpleQuery "SQL" to session "name" without waiting for responsewe 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
- Создайте новую ветку для своей фичи или багфикса.
- Внесите изменения и закоммитьте их с понятными, описательными сообщениями.
- Напишите или обновите тесты, если требуется.
- Обновите документацию, отражая изменения.
- Откройте pull request в основной репозиторий.
- Реагируйте на замечания code review.
Issues
Если нашли баг или хотите предложить новую функциональность, создайте issue в репозитории на GitHub с:
- Чётким, описательным заголовком.
- Подробным описанием проблемы или фичи.
- Шагами воспроизведения (для багов).
- Ожидаемым и фактическим поведением (для багов).
Где получить помощь
Если нужна помощь с вкладом в проект:
- Задавайте вопросы в GitHub issues.
- Заходите в Telegram-канал: @pg_doorman.
- Свяжитесь с maintainers.
Спасибо, что вносите вклад в PgDoorman!