PgDoorman
Многопоточный пулер соединений для PostgreSQL, написанный на Rust. Совместимая замена для PgBouncer и Odyssey, альтернатива PgCat. Три года в промышленной эксплуатации у Ozon под нагрузками Go (pgx), .NET (Npgsql), Python (asyncpg, SQLAlchemy) и Node.js.
Скачать PgDoorman 3.11.0 · Сравнение · Бенчмарки
Ключевые возможности
В extended protocol многие драйверы отправляют короткие параметризованные запросы как Parse без имени statement. PgDoorman переименовывает такой Parse на стороне PostgreSQL в служебный DOORMAN_<N> и хранит соответствие в пуле. Следующие Bind для той же формы запроса используют уже подготовленное состояние.
Это снижает повторную работу планировщика PostgreSQL на горячих OLTP-запросах без изменений в приложении. PgBouncer 1.21+ и Odyssey работают с именованными prepared statements, но анонимный Parse передают как есть; PgDoorman закрывает этот частый сценарий у драйверов по умолчанию.
Кеш не растёт бесконтрольно: анонимные записи истекают по таймауту бездействия, именованные освобождаются после последней ссылки. SHOW INTERNER показывает память текстов запросов, а метрики Prometheus — попадания, промахи и вытеснения.
Когда несколько пользовательских пулов работают с одной базой, общий лимит должен защищать PostgreSQL, а не просто ставить клиентов в очередь. PgDoorman держит потолок через max_db_connections: если лимит выбран, координатор закрывает свободное соединение у пула с запасом и отдаёт слот клиенту, который ждёт подключение к PostgreSQL.
Доноры ранжируются по излишку свободных соединений. При равном излишке первым уступает пул с более высоким p95 времени транзакции: быстрые пулы сохраняют больше шансов на переиспользование, а у медленных вытеснение свободного соединения меньше мешает работе. Резервный пул сглаживает короткие всплески, а min_guaranteed_pool_size защищает важные нагрузки от вытеснения.
В PgBouncer max_db_connections задаёт общий лимит, но не перераспределяет уже открытые свободные соединения между пулами. В Odyssey прямого аналога нет.
Если PgDoorman запущен рядом с PostgreSQL и локальный сервер пропадает во время Patroni switchover, новые серверные соединения временно уходят на другой живой узел кластера. PgDoorman выбирает узел через Patroni REST API (GET /cluster): сначала sync_standby, затем replica.
Локальный сервер уходит в период охлаждения, а fallback-соединения получают короткий lifetime. Когда локальный узел снова доступен, пул возвращается на него без отдельного HAProxy или consul-template перед пулером. Настройки patroni_api_urls и fallback_cooldown задаются в [general].
Перед SIGUSR2 или UPGRADE можно заменить бинарь и конфиг на диске. PgDoorman проверяет эту комбинацию через -t, запускает дочерний процесс уже с ней, передаёт ему слушающий сокет, а старый процесс продолжает обслуживать существующих клиентов и переносит мигрируемые сессии.
В новый процесс уходят connection_id, ключ отмены запроса, параметры PostgreSQL-сессии, состояние аутентификации к PostgreSQL и клиентский кеш prepared statements. Новый процесс восстанавливает клиента в том же пуле. Для приложения соединение не рвётся: без переподключения, повторного auth/SCRAM и потери prepared statements. Если на новом серверном соединении statement ещё не подготовлен, PgDoorman отправит нужный Parse при первом Bind.
В foreground-режиме TCP-сессии без TLS передаются через SCM_RIGHTS. TLS-сессии мигрируют только в Linux-сборке с фичей tls-migration и теми же tls_certificate/tls_private_key; в обычных пакетах и Docker-образе она выключена, поэтому TLS-клиенты дренируются. Клиенты внутри транзакции остаются на старом процессе и переезжают после COMMIT или ROLLBACK. У PgBouncer (-R, устарел с 1.20, или rolling restart через so_reuseport) и Odyssey (SIGUSR2 + bindwith_reuseport) старые сессии остаются в старом процессе до отключения клиентов.
PgDoorman отдаёт веб-консоль на том же адресе и порту, что и /metrics. Это локальная панель для разбора инцидента, а не замена долгосрочному мониторингу в Prometheus/Grafana.
На одном экране видны насыщение пулов, p95/p99 задержки запросов, транзакций и ожидания соединения, ошибки по SQLSTATE, долгие запросы, состояние prepared statements и query interner, хвост лога, CPU по потокам tokio-worker и память процесса (jemalloc, /proc/self/status, код/библиотеки, стеки, swap).
Из консоли можно выполнить Pause, Resume, Reconnect и Reload для одного пула или всего инстанса. Остальные экраны доступны только для чтения. Веб-интерфейс включается только при [web].ui = true и непустом general.admin_password, отличном от admin; иначе PgDoorman оставляет только /metrics и пишет WARN в лог.
Почему PgDoorman
- Кеш
Parseна горячих запросах. PgDoorman переиспользует подготовленное состояние в пределах пула, включая безымянныйParse, который многие драйверы отправляют для коротких параметризованных запросов. Это снижает CPU планировщика PostgreSQL на повторяющихся OLTP-запросах;SHOW INTERNERпоказывает память текстов запросов, а метрики Prometheus — попадания, промахи и вытеснения. - Один пул для всех рабочих потоков. Рабочие потоки используют общий набор серверных соединений. При масштабировании PgBouncer несколькими процессами за
so_reuseportкаждый процесс держит свой пул, поэтому свободные соединения могут распределяться неравномерно. - Контроль всплесков при создании соединений. Если много клиентов одновременно ждут несколько свободных серверных соединений, PgDoorman ограничивает параллельное создание новых соединений (
scaling_max_parallel_creates) и передаёт вернувшееся соединение самому старому ожидающему клиенту. - Предсказуемая задержка выдачи соединения. Ожидающие клиенты обслуживаются по FIFO. PgDoorman заранее заменяет серверные соединения перед истечением
server_lifetime, чтобы ротация поколения не превращалась во всплеск задержки при выдаче соединения. - Быстрое обнаружение обрыва PostgreSQL. Если серверное соединение пропадает во время транзакции, PgDoorman отслеживает это параллельно чтению клиента и возвращает SQLSTATE
08006, не дожидаясь системного TCP keepalive. - Операционные инструменты в бинаре. Конфиг пишется в YAML или TOML, длительности задаются как
30sили5m,pg_doorman generate --host ...собирает стартовый конфиг из существующего PostgreSQL,pg_doorman -tпроверяет конфиг без запуска сервера, а/metricsдоступен без отдельного экспортёра.
Сравнение
| Функция | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Общий пул соединений для всех рабочих потоков | Да | Нет, один рабочий поток | Рабочие процессы с отдельными пулами |
| Prepared statements в transaction pooling | Да | Да, с версии 1.21 | Да, через pool_reserve_prepared_statement |
Кеш анонимного Parse для горячих параметризованных запросов | Да, переиспользуется между клиентами пула | Нет, только именованные | Нет, только именованные |
| Общий лимит серверных соединений к базе | Да, с вытеснением свободных соединений | Нет | Нет |
| Переключение на резервный узел через Patroni | Да, встроено | Нет | Нет |
| Опережающая замена стареющих серверных соединений | Да | Нет | Нет |
| Обрыв серверного соединения во время транзакции | Да, возвращает 08006 без ожидания TCP keepalive | Нет, ждёт TCP keepalive | Нет, ждёт TCP keepalive |
| Горячая замена процесса с переносом свободных сессий | Да, через SCM_RIGHTS; TLS-состояние при сборке tls-migration | Нет, старые сессии остаются в старом процессе | Нет, старые сессии остаются в старом процессе |
| TLS-соединение от пулера к PostgreSQL | Да, 5 режимов и reload по SIGHUP | Да, server_tls_* и reload по RELOAD | Нет |
| SCRAM passthrough без открытого пароля в конфиге | Да, извлекает ClientKey из SCRAM proof | Да, зашифрованный SCRAM secret через auth_query/userlist.txt, с 1.14 | Да |
| JWT-аутентификация (RSA-SHA256) | Да | Нет | Нет |
PAM, pg_hba.conf и auth_query | Да | Да | Да |
| LDAP-аутентификация | Нет | Да, с версии 1.25 | Да |
| Формат конфигурации | YAML или TOML | INI | Собственный формат |
| Структурированные JSON-логи | Да | Нет | Да, log_format "json" |
| Перцентили задержек p50/p90/p95/p99 | Да, через встроенный /metrics | Нет, только средние значения | Да, через отдельный экспортёр на Go |
| Диагностическая web-консоль | Да, встроенная | Нет, только admin-консоль через psql | Нет, только admin-консоль через psql |
| Проверка конфига без запуска сервера | Да, -t | Нет | Нет |
| Генерация начального конфига из PostgreSQL | Да, generate --host | Нет | Нет |
| HTTP-эндпоинт Prometheus | Встроенный /metrics | Отдельный экспортёр | Отдельный экспортёр на Go |
Бенчмарки
AWS Fargate (16 vCPU), pool size 40, pgbench 30 с на тест:
| Сценарий | vs 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% |
Быстрый старт
Установите пакет через ваш дистрибутив:
# Ubuntu / Debian
sudo add-apt-repository ppa:vadv/pg-doorman
sudo apt update
sudo apt install pg-doorman
# Fedora / RHEL family
sudo dnf copr enable @pg-doorman/pg-doorman
sudo dnf install pg_doorman
Дистрибутивные пакеты и Docker-образ собраны без фич tls-migration и pam. Матрица TLS-фич и инструкции по сборке — в Установке.
Либо запустите через Docker:
docker run -p 6432:6432 \
-v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
ghcr.io/ozontech/pg_doorman \
pg_doorman /etc/pg_doorman/pg_doorman.yaml
Минимальный конфиг (pg_doorman.yaml):
general:
host: "0.0.0.0"
port: 6432
admin_username: "admin"
admin_password: "change_me"
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "app"
password: "md5..." # хеш из pg_shadow / pg_authid
pool_size: 40
server_username и server_password намеренно не указаны: PgDoorman использует MD5-хеш клиента или SCRAM ClientKey для аутентификации к PostgreSQL. В конфиге нет паролей в открытом виде.
Руководство по установке → · Справочник по конфигурации →
Куда дальше
- Впервые видите PgDoorman? Начните с Обзора, затем Установка и Базовое использование.
- Мигрируете с PgBouncer или Odyssey? Прочитайте Сравнение и Аутентификацию.
- Используете Patroni? См. Fallback через Patroni и
patroni_proxy. - Готовитесь к промышленной эксплуатации? Прочитайте Пул под нагрузкой и Координатор пулов.
- Эксплуатируете PgDoorman? См. Горячую замену процесса, Сигналы, Диагностику проблем.
PgDoorman vs PgBouncer vs Odyssey
Сравнительная матрица фич для выбора пулера соединений PostgreSQL. Каждое утверждение про PgBouncer привязано к config reference и changelog; каждое утверждение про Odyssey — к docs проекта.
PgCat намеренно опущен: у него центр тяжести — шардинг и балансировка, а не drop-in замена PgBouncer, поэтому построчное сравнение вводит в заблуждение. Если нужен горизонтальный шардинг, см. репозиторий PgCat.
Цифры из бенчмарков — Бенчмарки.
Аутентификация
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| MD5 password | Да | Да | Да |
| SCRAM-SHA-256 (клиент → пулер) | Да | Да | Да |
| Сквозной SCRAM-SHA-256 (без открытого пароля в конфиге) | Да (ClientKey извлекается из proof клиента) | Да (с 1.14, encrypted SCRAM secret в auth_query / userlist.txt) | Да |
| Сквозной MD5 | Да | Да | Да |
auth_query (динамические пользователи) | Да | Да | Да |
Сквозной режим auth_query (своя идентичность PostgreSQL для каждого пользователя) | Да | Нет (один auth_user на все lookup-запросы) | Да |
Файл в формате pg_hba.conf | Да (файл или inline) | Да (auth_hba_file) | Да (с 1.4) |
| PAM | Да (Linux) | Да (auth_type=pam или через HBA) | Да |
| JWT (RSA-SHA256) | Да | Нет | Нет |
| Talos (custom JWT с извлечением роли) | Да (специфика Ozon) | Нет | Нет |
| LDAP | Нет | Да (с 1.25) | Да |
SCRAM channel binding (scram-sha-256-plus) | Нет | Да | Да |
| User-name maps (cert/peer → DB user) | Нет | Да (с 1.23) | Да |
Тонкая настройка scram_iterations | Нет | Да (с 1.25) | Нет |
См. Аутентификация.
TLS
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
Client-side TLS (режимы: disable, allow, require, verify-full) | Да | Да (disable, allow, prefer, require, verify-ca, verify-full) | Да |
Server-side TLS к PostgreSQL (disable, allow, require, verify-ca, verify-full) | Да (5 режимов) | Да (server_tls_*, 6 режимов вкл. prefer) | Нет |
| mTLS к PostgreSQL (отправка клиентского сертификата на backend) | Да (server_tls_certificate + server_tls_private_key) | Да (server_tls_key_file + server_tls_cert_file) | Нет |
| Hot reload server-side TLS-сертификатов | Да (SIGHUP) | Да (через RELOAD / SIGHUP, "new file contents will be used for new connections") | Нет |
| Hot reload client-facing TLS-сертификатов | Нет (требуется restart или горячая замена процесса) | Да (через RELOAD / SIGHUP) | Нет |
| Минимальная версия TLS настраивается | Да (по умолчанию TLS 1.2) | Да (tls_protocols, default tlsv1.2,tlsv1.3) | Настраивается, дефолты другие |
Direct TLS handshake (PostgreSQL 17, без SSLRequest) | Нет | Да (с 1.25) | Нет |
| Контроль TLS 1.3 cipher suites | Нет | Да (с 1.25, client_tls13_ciphers/server_tls13_ciphers) | Нет |
| Миграция TLS-сессии при горячей замене процесса | Да (сборка tls-migration, Linux, по запросу) | Нет (TLS-соединения отбрасываются при online restart) | Нет |
См. TLS.
Маршрутизация и высокая доступность
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
Fallback через Patroni (встроенный lookup /cluster) | Да | Нет | Нет |
Bundled TCP-прокси с маршрутизацией по ролям (patroni_proxy) | Да | Нет | Нет |
| Защита от лага реплик | Да (max_lag_in_bytes в patroni_proxy) | Нет | Да (watchdog_lag_query + catchup_timeout) |
| Несколько хостов PostgreSQL с балансировкой | Да (patroni_proxy) | Да (с 1.24, load_balance_hosts) | Да |
target_session_attrs (read-write / read-only routing) | Да (через роли patroni_proxy) | Нет | Да |
| Sequential routing rules (правило-в-порядке-первое-совпадение) | Нет | Нет | Да |
| Маршрутизация по типу соединения (TCP vs UNIX) | Нет | Нет | Да |
| Выбор хоста с учётом availability zone | Нет | Нет | Да |
См. Fallback через Patroni, patroni_proxy.
Пулинг
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Режимы пула | session, transaction | session, transaction, statement | session, transaction |
| Координатор пулов (лимит на базу с приоритетным вытеснением) | Да (max_db_connections + вытеснение по p95) | Нет (max_db_connections ставит клиентов в очередь, пока существующие соединения не закроются по idle timeout) | Нет |
| Резервный пул | Да (reserve_pool_size) | Да (reserve_pool_size) | Нет |
Per-user min_guaranteed_pool_size | Да | Нет | Нет |
Опережающая замена при истечении server_lifetime (warm-up до экспирации старого) | Да (порог 95%, до 3 параллельных) | Нет | Нет |
Упреждающее ожидание и ограничение всплеска (scaling_warm_pool_ratio, быстрые повторы) | Да | Нет | Нет |
| Прямая передача (возвращающееся соединение уходит самому давно ждущему клиенту через in-process oneshot-канал) | Да | Нет | Нет |
| Строгий FIFO порядок ожидающих | Да | Нет (LIFO через server_round_robin = 0) | Нет |
min_pool_size (warm connections) | Да | Нет | Да |
| Prepared statements в transaction mode | Да (именованные и анонимные, двухуровневый кеш, query interner) | Да (именованные, с 1.21, max_prepared_statements) | Да (именованные, pool_reserve_prepared_statement) |
Кеш анонимного Parse для производительности | Да (DOORMAN_N, переиспользование между клиентами пула) | Нет (анонимный Parse проходит без изменений) | Нет (требуются именованные prepared statements) |
Умная очистка при возврате соединения (пропустить DEALLOCATE ALL, если кеш не менялся) | Да (RESET ALL / DEALLOCATE ALL по факту мутаций) | Нет (всегда DISCARD ALL, если задан server_reset_query) | Да (auto) |
| LISTEN / NOTIFY pinning в transaction mode | Нет | Нет | Экспериментально |
Cross-rule connection cap (shared_pool) | Нет | Нет | Да (с 1.5.1) |
Команды администратора PAUSE / RESUME / RECONNECT | Да | Да | Да (с 1.4.1) |
GUC PostgreSQL на уровне пула в backend StartupMessage | Да (startup_parameters: general → пул → passthrough auth_query; клиентские RESET ALL / DISCARD ALL возвращают эти значения; ошибки PG при запуске бэкенда доходят до клиента без переписывания) | Нет эквивалентных операторских значений по умолчанию; отдельные клиентские startup-параметры можно отслеживать или игнорировать | Нет (maintain_params сохраняет клиентские параметры при rebind; операторских GUC нет) |
См. Координатор пулов, Пул под нагрузкой.
Лимиты и таймауты
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
server_idle_check_timeout (probe перед checkout) | Да | Нет | Нет |
idle_timeout (server-side) | Да (idle_timeout) | Да (server_idle_timeout) | Да |
server_lifetime | Да | Да | Да |
query_wait_timeout | Да | Да | Да |
client_idle_timeout | Нет | Да (с 1.24) | Нет |
transaction_timeout (enforced пулером) | Нет | Да (с 1.25) | Нет |
max_user_client_connections | Нет | Да (с 1.24) | Нет |
max_db_client_connections | Нет | Да (с 1.24) | Нет |
Per-user query_timeout | Нет | Да (с 1.24) | Нет |
Per-user reserve_pool_size | Нет | Да (с 1.24) | Нет |
| Уведомление клиента, пока тот ждёт серверное соединение | Нет | Да (с 1.25, query_wait_notify) | Да (pool_notice_after_waiting_ms) |
См. Справочник по general-настройкам, Справочник по pool-настройкам.
Наблюдаемость
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Встроенная веб-консоль администратора | Да (HTML-консоль на том же порту, что и /metrics, включается через [web].ui) | Нет (только psql admin-консоль) | Нет (только psql admin-консоль) |
| Prometheus-эндпоинт | Встроенный /metrics | Внешний (pgbouncer_exporter) | Внешний (Go-exporter sidecar, опрашивает admin-консоль) |
| Перцентили задержки на пул (p50, p90, p95, p99) | Да (HDR Histogram) | Нет (только средние в SHOW STATS) | Да через exporter (TDigest, требует rule-опцию quantiles) |
Счётчики prepared statements в SHOW STATS | Да | Да (с 1.24) | Нет |
| Структурированные JSON-логи | Да (--log-format structured) | Нет | Да (log_format "json") |
Управление уровнем логов в рантайме (SET log_level) | Да | Нет | Нет |
SHOW POOL_COORDINATOR / SHOW POOL_SCALING / SHOW SOCKETS | Да | Нет | Нет |
SHOW PREPARED_STATEMENTS | Да | Нет | Нет |
SHOW INTERNER (записи / байты / предпросмотр по половинам) | Да | Нет | Нет |
| Ограниченный prepared-кеш (TTL у анонимных, клиентский LRU с разделением Named/Anonymous) | Да | Только named, с лимитом max_prepared_statements; анонимного кеша нет | Нет |
SHOW HOSTS (CPU/память хоста) | Нет | Нет | Да |
SHOW RULES (дамп активной маршрутизации) | Нет | Нет | Да |
| Метрики server-side TLS-соединений (длительность handshake, ошибки, активные) | Да | Нет | Нет |
| Метрики Patroni API | Да | Нет | Нет |
| Метрики fallback (active flag, текущий хост, hits) | Да | Нет | Нет |
См. Справочник Prometheus-метрик, Команды администратора.
Эксплуатация
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Горячая замена процесса с миграцией сессий (TCP-сокет, cancel keys, prepared cache) | Да (SCM_RIGHTS, плюс TLS state со сборкой tls-migration) | Нет: -R deprecated с 1.20; rolling restart через so_reuseport оставляет старые сессии на старом процессе | Нет: SIGUSR2 + bindwith_reuseport оставляет старые сессии на старом процессе |
| Формат конфига | YAML или TOML | INI | Свой формат (lex/yacc) |
Человекочитаемые длительности и размеры (30s, 1h, 256MB) | Да | Нет (целые микросекунды / байты) | Нет |
Режим проверки конфига (pg_doorman -t) | Да | Нет | Нет |
Авто-конфиг из PostgreSQL (pg_doorman generate --host) | Да | Нет | Нет |
Перезагрузка по SIGHUP | Да (серверные TLS-сертификаты включены; клиентский TLS требует рестарта) | Да (auth_file, auth_hba_file, server и client TLS certs) | Да |
systemd sd-notify (Type=notify) | Да | Нет | Нет |
Лимит памяти (max_memory_usage) | Да | Нет | Нет |
| Лимит TCP-буферов | Да (tcp_socket_buffer_size для клиентских TCP-сокетов и TCP-сокетов к PostgreSQL) | Да (tcp_socket_buffer) | Нет |
См. Горячая замена процесса с переносом сессий, Сигналы.
Протокол
| Возможность | PgDoorman | PgBouncer | Odyssey |
|---|---|---|---|
| Simple query | Да | Да | Да |
| Extended query | Да | Да | Частично |
| Pipelined batches | Да | Да | Частично |
| Async Flush | Да | Да | Нет |
| Cancel requests поверх TLS | Да | Да | Да |
COPY IN / COPY OUT | Да | Да | Да |
Replication passthrough (replication=true startup) | Нет | Да (с 1.23) | Нет |
| Согласование версии протокола (3.2) | Нет | Да (с 1.23) | Нет |
server_drop_on_cached_plan_error | Нет | Нет | Да (с 1.5.1) |
Когда PgDoorman не подойдёт
- Нужна LDAP-аутентификация. Используйте Odyssey или PgBouncer 1.25+.
- Нужен replication passthrough для logical replication tools. Используйте PgBouncer 1.23+.
- Нужен
transaction_timeout, который применяет сам пулер. Используйте PgBouncer 1.25+. - Нужен горизонтальный шардинг внутри пулера. Используйте PgCat.
Если нужны prepared statements в transaction mode, Patroni HA без внешних прокси, многопоточная пропускная способность с одним общим пулом и горячая замена процесса с миграцией живых сессий — PgDoorman ближе по профилю.
Обзор
Что делает PgDoorman
PgDoorman сидит между приложениями и PostgreSQL. Для приложения он выглядит как сервер PostgreSQL (тот же wire-протокол, та же строка подключения для psql); внутри он мультиплексирует много клиентских сессий на гораздо меньший набор реальных серверных соединений.
graph LR
App1[Приложение A] --> Pooler(PgDoorman)
App2[Приложение B] --> Pooler
App3[Приложение C] --> Pooler
Pooler --> DB[(PostgreSQL)]
Изначально PgDoorman был форкнут из PgCat, но с тех пор переписан вокруг других целей: prepared statements в transaction mode, многопоточные общие пулы, интеграция с Patroni и горячая замена процесса с миграцией живых сессий. Сейчас это самостоятельная кодовая база.
Зачем вообще пулер
Каждое соединение к PostgreSQL стоит серверу около 10 МБ RAM, отдельный процесс и время на каждый handshake (auth, SCRAM, разрешение search_path). Без пулера приложение, открывающее N короткоживущих соединений в секунду, платит N×время-handshake. Пулер позволяет тем же N клиентам переиспользовать небольшой набор долгоживущих серверных соединений, и стоимость handshake оплачивается один раз на соединение с PostgreSQL, а не один раз на клиента.
Конкретные эффекты:
pool_sizeравный 40 обычно обслуживает несколько тысяч клиентских сессий для коротких OLTP-транзакций.- PostgreSQL не платит per-process memory overhead за соединения, которые иначе пришлось бы держать открытыми.
- Failover, рестарт или rolling deploy не превращаются в лавину новых auth/SCRAM handshake-ов.
Режимы пула
Серверное соединение удерживается на время одной транзакции и возвращается в пул при COMMIT или ROLLBACK. Именно в этом режиме пулинг реально окупается.
Серверное соединение удерживается на всё время клиентской сессии и возвращается только при отключении клиента. Используйте для клиентов, зависящих от состояния уровня сессии (SET TIME ZONE вне транзакции, advisory-блокировки между транзакциями, WITH HOLD курсоры).
PgDoorman не реализует statement mode. См. режимы пула — точный контракт каждого режима и что работает в transaction mode у нас, чего нет у других пулеров.
Что есть для эксплуатации
- Консоль администратора — PostgreSQL-совместимый эндпоинт для
SHOW POOLS,SHOW CLIENTS,RELOAD,PAUSE,UPGRADEи т.д. - Prometheus
/metrics— встроенный HTTP-эндпоинт с per-pool latency-перцентилями, счётчиками prepared statements, состоянием fallback и метриками TLS. - Видимость prepared-кеша —
SHOW INTERNERиSHOW POOLS_MEMORYпоказывают объём интернера и клиентскую разбивку Named / Anonymous, с парными метриками в Prometheus. pg_doorman -t— валидация конфига без запуска сервера.pg_doorman generate --host …— собрать starter-конфиг через интроспекцию существующего PostgreSQL.
См. команды администратора и Справочник Prometheus.
Куда дальше
- Установка — установить pg_doorman из пакетов, исходников или Docker.
- Базовое использование — минимальный конфиг, первое подключение, типичные грабли.
- Координатор пулов — когда одна база делится между несколькими пользовательскими пулами.
- Горячая замена процесса с переносом сессий — заменить процесс в промышленной эксплуатации и перенести мигрируемые сессии.
Установка PgDoorman
PgDoorman работает на Linux и macOS. Для промышленной эксплуатации рекомендуем собирать самим — так вы контролируете версию Rust, целевую платформу и зависимости. Также доступны готовые пакеты из репозиториев и статические бинарники. Docker — только для тестов.
Системные требования
- Linux (рекомендуется) или macOS
- PostgreSQL 10 или новее (любая поддерживаемая версия)
- Память пропорциональна размеру пулов (несколько МБ на пул + кэш prepared statements)
- Rust 1.87 или новее, если собираете из исходников
Сборка из исходников (рекомендуется)
Соберите со своим toolchain — это даёт контроль над версией компилятора, целевой платформой и зависимостями:
git clone https://github.com/ozontech/pg_doorman.git
cd pg_doorman
cargo build --release
sudo install -m 0755 target/release/pg_doorman /usr/local/bin/pg_doorman
cargo build --release собирает оптимизированный бинарник в target/release/pg_doorman. Требования к окружению и процесс разработки описаны в Участие в проекте.
Фичи Cargo
| Фича | По умолчанию | Эффект |
|---|---|---|
tls-migration | выкл | Vendored OpenSSL 3.5.5 с патчем, позволяющим переносить TLS-клиентов при горячей замене процесса. Нужен, если TLS-клиенты должны мигрировать без переподключения. |
pam | выкл | Поддержка аутентификации PAM (Linux). |
Сборка с миграцией TLS-клиентов
По умолчанию TLS-клиенты не могут перейти на новый процесс при горячей замене процесса — они получают ошибку 58006 и переподключаются. Чтобы TLS-соединения переходили на новый процесс без разрыва, соберите с фичей tls-migration:
cargo build --release --features tls-migration
Сборка использует vendored OpenSSL 3.5.5 с патчем, который экспортирует и заново импортирует состояние TLS-шифров (ключи, IV, sequence numbers, TLS 1.3 traffic secrets) при передаче соединений между процессами. Зашифрованные клиенты остаются на том же TCP-соединении без повторного TLS handshake.
Требования:
- Только Linux (macOS и Windows используют системный TLS, не OpenSSL).
- Утилиты
perlиpatchвPATH. - Около 5 минут дополнительного времени сборки на компиляцию OpenSSL.
Сборка без доступа к интернету:
curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz
OPENSSL_SOURCE_TARBALL=$(pwd)/openssl-3.5.5.tar.gz \
cargo build --release --features tls-migration
Старый и новый процесс должны использовать одни и те же tls_certificate и tls_private_key. Полное описание процесса обновления, мониторинг и диагностика — в плавном обновлении бинаря.
Для упаковки в deb/rpm смотрите каталоги debian/ и pkg/ в репозитории.
Пакеты из репозиториев
Готовые deb- и rpm-пакеты публикуются с теми же релизными тегами. Используйте их, когда сборка из исходников нежелательна.
Пакеты из Ubuntu PPA и Fedora COPR собираются без поддержки TLS. Если нужен TLS — для клиентских соединений, серверных соединений к PostgreSQL или для горячей миграции TLS при обновлении бинарника — собирайте из исходников с включённой TLS-фичей. См. Сборка из исходников выше.
Ubuntu / Debian (PPA)
sudo add-apt-repository ppa:vadv/pg-doorman
sudo apt update
sudo apt install pg-doorman
Поддерживаемые релизы: jammy (22.04 LTS), noble (24.04 LTS), questing (25.10), resolute (26.04 LTS).
Fedora / RHEL / CentOS / Rocky / AlmaLinux (COPR)
sudo dnf copr enable @pg-doorman/pg-doorman
sudo dnf install pg_doorman
Поддерживаемые цели: Fedora 39, 40, 41; EPEL 8 и 9 для семейства RHEL.
Пакет ставит systemd-юнит, конфиг по умолчанию и пользователя pg_doorman.
Готовые бинарники с GitHub Releases
Если ни сборка из исходников, ни пакеты из репозиториев не подходят, скачайте статический бинарник со страницы релизов:
# Замените VERSION и TARGET на нужные значения со страницы релизов.
curl -L -o pg_doorman \
"https://github.com/ozontech/pg_doorman/releases/download/VERSION/pg_doorman-TARGET"
curl -L -o pg_doorman.sha256 \
"https://github.com/ozontech/pg_doorman/releases/download/VERSION/pg_doorman-TARGET.sha256"
sha256sum -c pg_doorman.sha256 # должно вывести "OK"
chmod +x pg_doorman
sudo mv pg_doorman /usr/local/bin/
Пропуск checksum-шага означает доверие сетевому пути между вами и objects.githubusercontent.com. Не делайте так.
Docker (только для тестов)
Docker поддерживается для разработки, CI и быстрых демо. Для промышленной эксплуатации не рекомендуется — упаковка и управление жизненным циклом проще через пакеты из репозиториев выше.
docker run -p 6432:6432 \
-v $(pwd)/pg_doorman.yaml:/etc/pg_doorman/pg_doorman.yaml \
ghcr.io/ozontech/pg_doorman \
pg_doorman /etc/pg_doorman/pg_doorman.yaml
CMD образа по умолчанию запускает pg_doorman без аргументов. При WORKDIR /etc/pg_doorman это означает конфиг /etc/pg_doorman/pg_doorman.toml. Если монтируете YAML, передайте путь явно, как в примере выше.
Пробрасывайте 6432 для PostgreSQL-протокола. Если в конфиге включён web.enabled, дополнительно пробросьте 9127 для /metrics и веб-консоли; без этого флага listener не поднимается. Путь к конфигу можно задать позиционным аргументом или переменной CONFIG_FILE; также доступны LOG_LEVEL (по умолчанию info), LOG_FORMAT (text, structured или debug) и NO_COLOR.
В Dockerfile задан STOPSIGNAL SIGTERM, поэтому docker stop отправляет pg_doorman обычный сигнал остановки контейнера. Не используйте SIGINT для остановки контейнера: вне TTY этот сигнал запускает горячую замену процесса, что для контейнерного запуска обычно приводит к завершению PID 1.
Публичный образ собран без фич tls-migration и pam. Обычный TLS для клиентских и серверных соединений не зависит от tls-migration; эта фича нужна только для переноса TLS-сессий при горячей замене процесса. Для TLS migration или PAM соберите свой образ из публичного Dockerfile, добавив --features tls-migration и/или pam в шаг cargo build --release.
docker-compose.yaml с PostgreSQL в качестве sidecar лежит в example/ — для smoke-тестов.
Проверка установки
pg_doorman --version
pg_doorman -t /etc/pg_doorman/pg_doorman.yaml # проверяет конфиг
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c "SHOW VERSION;"
pg_doorman -t проверяет конфиг до деплоя — у PgBouncer и Odyssey такой возможности нет.
Куда дальше
- Базовое использование — первый конфиг, admin-консоль, мониторинг.
- Аутентификация — выбор подходящего метода.
- Сигналы и перезагрузка — сигналы, reload, интеграция с systemd.
- Горячая замена процесса с переносом сессий — новый бинарь и конфиг с переносом мигрируемых клиентов.
Базовое использование PgDoorman
Сквозной разбор: ключи командной строки, минимальный конфиг, admin-консоль и эксплуатационные команды (PAUSE, RESUME, RECONNECT, RELOAD). Эта страница — вторая по порядку чтения после Обзора и Установки.
Параметры командной строки
$ pg_doorman --help
PgDoorman: Nextgen PostgreSQL Pooler (based on PgCat)
Usage: pg_doorman [OPTIONS] [CONFIG_FILE] [COMMAND]
Commands:
generate Generate configuration for pg_doorman by connecting to PostgreSQL and auto-detecting databases and users
help Print this message or the help of the given subcommand(s)
Arguments:
[CONFIG_FILE] [env: CONFIG_FILE=] [default: pg_doorman.toml]
Options:
-l, --log-level <LOG_LEVEL> [env: LOG_LEVEL=] [default: INFO]
-F, --log-format <LOG_FORMAT> [env: LOG_FORMAT=] [default: text] [possible values: text, structured, debug]
-n, --no-color force colors off in the log output
-d, --daemon run as daemon [env: DAEMON=]
-h, --help Print help
-V, --version Print version
Доступные параметры
| Параметр | Описание |
|---|---|
-d, --daemon | Запуск в фоне. Без этого параметра процесс работает на переднем плане. В daemon-режиме обязательны daemon_pid_file и syslog_prog_name. После ухода в фон сообщения в stderr больше не пишутся. |
-l, --log-level | Уровень логирования: INFO, DEBUG или WARN. |
-F, --log-format | Формат логов. Возможные значения: text, structured, debug. |
-n, --no-color | Принудительно отключить цвета в логах. Цвета также автоматически выключаются, если stderr не терминал (под systemd — это pipe в journald) и если переменная окружения NO_COLOR установлена в любое непустое значение. |
-V, --version | Показать информацию о версии. |
-h, --help | Показать справку. |
Установка и настройка
Структура файла конфигурации
PgDoorman поддерживает форматы YAML и TOML. Для новых установок рекомендуется YAML. Конфигурация разбита на несколько секций:
general: # Глобальные настройки сервиса PgDoorman
pools:
<name>: # Настройки конкретного пула базы данных
users:
- ... # Настройки пользователя для этого пула
Некоторые параметры обязательно должны быть указаны в файле конфигурации, чтобы PgDoorman запустился, даже если у них есть значения по умолчанию. Например, для доступа к admin-консоли нужно указать admin username и password.
Минимальный пример конфигурации
Минимальный пример конфигурации для старта:
YAML (рекомендуется)
general:
host: "0.0.0.0" # Слушать на всех интерфейсах
port: 6432 # Порт для клиентских соединений
admin_username: "admin"
admin_password: "admin" # Поменяйте это в промышленной эксплуатации!
pools:
exampledb:
server_host: "127.0.0.1" # Адрес сервера PostgreSQL
server_port: 5432 # Порт сервера PostgreSQL
pool_mode: "transaction" # Режим пулинга
users:
- pool_size: 40
username: "doorman"
password: "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="
TOML
[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "admin"
[pools.exampledb]
server_host = "127.0.0.1"
server_port = 5432
pool_mode = "transaction"
[pools.exampledb.users.0]
pool_size = 40
username = "doorman"
password = "SCRAM-SHA-256$4096:6nD+Ppi9rgaNyP7...MBiTld7xJipwG/X4="
Полный список параметров конфигурации можно получить командой pg_doorman generate --reference --output ref.yaml — она генерирует аннотированный конфиг со всеми параметрами и значениями по умолчанию.
Автоматическая генерация конфигурации
Команда generate создаёт файл конфигурации, подключаясь к серверу PostgreSQL и определяя базы данных и пользователей. По умолчанию сгенерированный конфиг содержит inline-комментарии, объясняющие каждый параметр.
# Посмотреть все доступные опции
pg_doorman generate --help
# Сгенерировать YAML-конфиг (рекомендуется)
pg_doorman generate --output pg_doorman.yaml
# Сгенерировать TOML-конфиг (для обратной совместимости)
pg_doorman generate --output pg_doorman.toml
# Сгенерировать reference-конфиг со всеми настройками (без подключения к PG)
pg_doorman generate --reference --output pg_doorman.yaml
# Сгенерировать reference-конфиг с русскими комментариями для быстрого старта
pg_doorman generate --reference --ru --output pg_doorman.yaml
# Сгенерировать конфиг без комментариев (plain serialization)
pg_doorman generate --no-comments --output pg_doorman.yaml
Команда generate поддерживает несколько опций:
| Опция | Описание |
|---|---|
--host | Хост PostgreSQL для подключения (если не указано, используется localhost) |
--port, -p | Порт PostgreSQL для подключения (по умолчанию: 5432) |
--user, -u | Пользователь PostgreSQL для подключения (нужны привилегии superuser, чтобы читать pg_shadow) |
--password | Пароль PostgreSQL для подключения |
--database, -d | База данных PostgreSQL для подключения (если не указано, используется имя пользователя) |
--ssl | Подключение к серверу PostgreSQL по SSL/TLS |
--pool-size | Размер пула в сгенерированной конфигурации (по умолчанию: 40) |
--session-pool-mode, -s | Сессионный режим пулинга в сгенерированной конфигурации |
--output, -o | Файл для сгенерированной конфигурации (если не указан, используется stdout) |
--server-host | Переопределить server_host в конфиге (если не указано, используется значение --host) |
--no-comments | Отключить inline-комментарии в сгенерированной конфигурации (по умолчанию они включены) |
--reference | Сгенерировать полный reference-конфиг с примерами значений, без подключения к PG |
--russian-comments, --ru | Сгенерировать комментарии на русском для быстрого старта |
--format, -f | Формат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение |
Команда подключается к PostgreSQL, обнаруживает базы данных и пользователей, затем создаёт документированный файл конфигурации.
Команда 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
Приложение разговаривает по PostgreSQL wire-протоколу; уровень пулинга для него прозрачен.
Администрирование
Admin-консоль
PgDoorman предоставляет административный интерфейс через специальную базу данных pgdoorman (или pgbouncer для обратной совместимости):
$ psql -h localhost -p 6432 -U admin pgdoorman
После подключения можно посмотреть доступные команды:
pgdoorman=> SHOW HELP;
NOTICE: Console usage
DETAIL:
SHOW HELP|CONFIG|DATABASES|POOLS|POOLS_EXTENDED|POOLS_MEMORY|POOL_COORDINATOR|POOL_SCALING
SHOW CLIENTS|SERVERS|USERS|CONNECTIONS|STATS|PREPARED_STATEMENTS|AUTH_QUERY
SHOW LISTS|SOCKETS|LOG_LEVEL|VERSION
SET log_level = '<filter>'
RELOAD
SHUTDOWN
UPGRADE
PAUSE [db]
RESUME [db]
RECONNECT [db]
Admin-консоль на данный момент поддерживает только simple query protocol.
Некоторые драйверы баз данных используют extended query protocol для всех команд, что делает их непригодными для работы с admin-консолью. В таких случаях используйте клиент psql для администрирования.
Войти в admin-консоль может только пользователь, указанный в admin_username файла конфигурации.
Если это разрешено правилами general.pg_hba, в admin-консоль можно входить методом trust (без запроса пароля), например:
# Разрешить только локальному admin доступ к admin-БД без пароля
host pgdoorman admin 127.0.0.1/32 trust
Используйте trust с предельной осторожностью. Всегда ограничивайте по адресу и, по возможности, требуйте TLS через hostssl. В промышленной эксплуатации предпочитайте методы с паролями, если только не понимаете последствия полностью.
Мониторинг PgDoorman
Admin-консоль предоставляет несколько команд для мониторинга текущего состояния PgDoorman:
SHOW STATS— посмотреть статистику производительности.SHOW CLIENTS— список текущих клиентских соединений.SHOW SERVERS— список текущих серверных соединений.SHOW POOLS— состояние пулов соединений.SHOW DATABASES— список настроенных баз данных.SHOW USERS— список настроенных пользователей.
Эти команды подробно описаны в разделе Команды admin-консоли ниже.
Перечитывание конфигурации
Если вы изменили pg_doorman.toml, изменения можно применить без перезапуска сервиса:
pgdoorman=# RELOAD;
При перечитывании конфигурации:
- 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, отключая всех клиентов. В промышленной эксплуатации используйте её с осторожностью.
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 применяет большинство параметров конфигурации без разрыва существующих соединений — то, что нужно в промышленной эксплуатации, где простой недопустим.
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 | Горячая замена процесса. Валидирует новый бинарь и конфиг флагом -t, запускает новый процесс, передаёт ему слушающий сокет и переносит сессии, где это возможно. Рекомендуется для обновлений. См. Горячая замена процесса с переносом сессий. |
| SIGINT | Foreground + TTY (Ctrl+C): только плавное завершение (без замены процесса). Режим демона / без TTY: горячая замена процесса для совместимости со старыми установками. |
| SIGTERM | Немедленное завершение. Активные соединения обрываются. |
В окружениях на базе systemd unit-файл по умолчанию использует ExecReload=/bin/kill -SIGUSR2 $MAINPID, чтобы запускать горячую замену процесса при systemctl reload.
Аутентификация
pg_doorman аутентифицирует клиентов, прежде чем перенаправить их к PostgreSQL. Поддерживаются шесть методов; они выбираются в порядке приоритета по тому, что присылает клиент и что задано в конфигурации пула.
Эта страница объясняет, как pg_doorman выбирает метод аутентификации. Подробности настройки смотрите по ссылкам каждого метода ниже.
Обзор методов
| Метод | Когда использовать | Хранит ли секрет в конфиге? |
|---|---|---|
| Сквозной режим (MD5 / SCRAM) | По умолчанию. Имя пользователя пула совпадает с пользователем PostgreSQL. | Хеш MD5 или SCRAM ClientKey, никогда — открытый пароль |
| auth_query | Много пользователей, динамическое заведение. Учётные данные ищутся в самом PostgreSQL. | Только секрет одного сервисного пользователя |
| PAM | Аутентификация на уровне OS (LDAP через pam_ldap, Kerberos, локальные учётки). Только Linux. | Нет |
| JWT | Доступ сервиса к базе по короткоживущим токенам, подписанным внешним IdP. | Только публичный ключ |
| 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: сквозная и явно заданная
pg_doorman аутентифицируется дважды: один раз как шлюз (клиент → pg_doorman) и один раз как бэкенд (pg_doorman → PostgreSQL). Возможны три варианта:
- Сквозной режим (по умолчанию). Хеш MD5 клиента или SCRAM
ClientKeyпереиспользуется для аутентификации на PostgreSQL. В конфиге нет открытого пароля. Требует, чтобыserver_usernameне был задан (или был равен имени клиента). - Заданный пользователь PostgreSQL. Установите
server_usernameиserver_passwordв блоке пользователя. pg_doorman будет аутентифицироваться на PostgreSQL под ними. Используйте, когда имя пользователя пула отвязано от пользователя базы (Talos, JWT, переименование). - auth_query в выделенном режиме. Установите
server_userвнутри блокаauth_query. Все динамически найденные пользователи разделят один пул серверных соединений, аутентифицированный какserver_user. Это размен идентичности PostgreSQL на эффективность переиспользования пула.
Подробности смотрите в сквозном режиме, а про выделенный режим — в auth_query.
Ограничение подключений
pg_hba.conf применяется до проверки учётных данных. Распространённые шаблоны:
- Отвергнуть всё кроме localhost:
host all all 0.0.0.0/0 reject, а перед нимhost all all 127.0.0.1/32 trust. - Требовать TLS для нелокальных соединений:
hostssl all all 0.0.0.0/0 scram-sha-256иhostnossl all all 127.0.0.1/32 trust. - ACL по базам:
host mydb appuser 10.0.0.0/8 scram-sha-256.
Смотрите pg_hba.conf.
Куда дальше
- Новая инсталляция? Прочтите сквозной режим и базовое использование.
- Много пользователей с ротируемыми учётными данными? Используйте auth_query.
- Идентичность сервиса по токенам? Используйте JWT.
- Аутентификация, интегрированная с OS? Используйте PAM.
- Ограничения на сетевом уровне? Настройте pg_hba.conf.
Passthrough-аутентификация (по умолчанию)
pg_doorman переиспользует криптографическое доказательство клиента — хеш MD5 или SCRAM ClientKey — чтобы аутентифицироваться на PostgreSQL. Открытый пароль никогда не покидает клиента и никогда не хранится в конфигурации пула.
Это рекомендуемая настройка, когда имя пользователя пула совпадает с пользователем PostgreSQL.
Как это работает
MD5
Протокол MD5-пароля PostgreSQL хранит на сервере md5(password + username). Клиент хеширует пароль тем же способом и присылает md5(stored_hash + salt). pg_doorman:
- Получает хешированный ответ клиента.
- Ищет сохранённый хеш 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:
- Сквозной режим (без
server_user): каждый аутентифицированный пользователь получает собственный пул серверных соединений, аутентифицированный под ним же. Сохраняет идентичность PostgreSQL дляcurrent_user, row-level security и audit logs. - Выделенный режим (с
server_user): все динамические пользователи разделяют один пул серверных соединений, аутентифицированный какserver_user. Это размен идентичности PostgreSQL на более высокое переиспользование пула и меньшее число соединений.
auth_query в стиле PgBouncer — это выделенный режим. Odyssey поддерживает оба. В pg_doorman сквозной режим включён по умолчанию.
Сквозной режим
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "postgres"
password: "md5..."
database: "postgres"
cache_ttl: "1h"
cache_failure_ttl: "30s"
Запрос должен возвращать колонку с именем passwd или password,
содержащую хеш MD5 или SCRAM. Дополнительные колонки игнорируются, кроме
необязательной startup_parameters. В сквозном режиме pg_doorman
читает эту колонку как text, json или jsonb; значение должно быть
JSON-объектом с параметрами запуска PostgreSQL для конкретного
пользователя. В выделенном режиме эта колонка игнорируется, а в лог
пишется предупреждение.
user и password — это учётные данные, под которыми pg_doorman выполняет lookup-запрос. У них должно быть право читать колонку с учётными данными. Либо выдайте доступ к специально созданному представлению (рекомендуется), либо используйте пользователя из группы pg_read_server_files.
Когда клиент подключается как alice:
- pg_doorman выполняет запрос с
$1 = 'alice'и получает её хеш. - Кэширует хеш в памяти на
cache_ttlсекунд. - Выполняет passthrough-аутентификацию MD5 или SCRAM (смотрите Passthrough).
- Открывает соединение с PostgreSQL, аутентифицированное как
aliceс тем же хешем.
Выделенный режим
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "auth_lookup"
password: "md5..."
database: "postgres"
server_user: "app"
server_password: "md5..."
pool_size: 40
min_pool_size: 5
cache_ttl: "1h"
Установка server_user переключает режим. Теперь:
- Клиент аутентифицируется как
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.
Мониторинг
SHOW AUTH_QUERY показывает статистику по базам:
database | cache_entries | cache_hits | cache_misses | cache_refetches | rate_limited | auth_success | auth_failure | executor_queries | executor_errors
Метрики Prometheus: pg_doorman_auth_query_cache, pg_doorman_auth_query_auth, pg_doorman_auth_query_executor, pg_doorman_auth_query_dynamic_pools. Смотрите команды администратора.
Аутентификация PAM
pg_doorman делегирует аутентификацию клиента сервису PAM на хосте. Используйте это для аутентификации, интегрированной с OS (LDAP через pam_ldap, Kerberos, локальные модули PAM), без хранения учётных данных на каждого пользователя в конфиге пула.
PAM работает только под Linux. Готовые бинарники собираются с поддержкой PAM.
Конфигурация
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "alice"
auth_pam_service: "pg_doorman"
server_username: "alice"
server_password: "md5..."
pool_size: 20
auth_pam_service — имя файла сервиса PAM в /etc/pam.d/. pg_doorman не проверяет имя сервиса при старте — убедитесь, что файл существует.
Поле password опускается, потому что проверкой занимается PAM. server_username и server_password обязательны: PAM аутентифицирует только клиента в pg_doorman; pg_doorman всё равно нужны учётные данные для соединения с бэкендом.
Пример сервиса PAM
/etc/pam.d/pg_doorman:
auth required pam_unix.so
account required pam_unix.so
Для аутентификации через LDAP:
auth required pam_ldap.so
account required pam_ldap.so
Настройте pam_ldap в /etc/ldap.conf (или /etc/nslcd.conf) под своё окружение.
Порядок выбора метода
PAM проверяется после Talos и HBA Trust, но до любого метода на основе пароля. Если у пользователя одновременно заданы auth_pam_service и статический password (с префиксом MD5, SCRAM или JWT), выигрывает PAM.
Смотрите Обзор.
Оговорки
- PAM блокирует поток-обработчик во время вызова аутентификации. Если ваш стек PAM делает сетевые вызовы (LDAP, Kerberos), ждите эпизодических всплесков задержки.
pam_unix.soтребует доступ на чтение к/etc/shadow— обычно только дляroot. Запускайте pg_doorman под пользователем с нужным членством в группе или используйте другой модуль PAM.- PAM не поддерживает passthrough SCRAM. Соединение с бэкендом всегда использует
server_usernameиserver_password. - Прямая поддержка LDAP без PAM в pg_doorman не реализована. Используйте Odyssey или PgBouncer 1.25+.
Аутентификация JWT
Аутентифицируйте клиентов JSON Web Token, подписанным внешним поставщиком идентификации. pg_doorman проверяет подпись токена RSA-SHA256 по публичному ключу с диска, сверяет claim preferred_username и открывает соединение PostgreSQL под заданной идентичностью бэкенда.
Этот метод подходит для доступа сервиса к базе, когда короткоживущие токены выпускает OIDC-провайдер, Vault или внутренний токен-сервис.
Конфигурация
Сгенерируйте (или получите) публичный RSA-ключ и сошлитесь на него в поле password пользователя через префикс jwt-pkey-fpath::
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "billing-service"
password: "jwt-pkey-fpath:/etc/pg_doorman/jwt-public.pem"
server_username: "billing"
server_password: "md5..."
pool_size: 40
То, что клиент пришлёт как пароль, будет считаться JWT и проверяться по /etc/pg_doorman/jwt-public.pem. Токен должен:
- Быть подписан RS256 (RSA-SHA256). HS256 и EC-варианты не поддерживаются.
- Иметь claim
preferred_username, равный заданномуusername(billing-serviceв примере выше). - Проходить стандартную валидацию
expиnbf.
Соединение с бэкендом открывается как billing с хешем из server_password. Идентичность клиента (billing-service) отвязана от идентичности базы (billing).
Генерация пары ключей
openssl genrsa -out jwt-private.pem 2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem
Храните jwt-private.pem у издателя токенов. Раздавайте jwt-public.pem в pg_doorman.
Выпуск токена
Подойдёт любая RS256 JWT-библиотека. Пример на Python (PyJWT):
import jwt
import time
private_key = open("jwt-private.pem").read()
token = jwt.encode(
{
"preferred_username": "billing-service",
"iat": int(time.time()),
"exp": int(time.time()) + 300, # 5 минут
},
private_key,
algorithm="RS256",
)
Клиент подключается к pg_doorman с user=billing-service и password=<token>. Большинство драйверов PostgreSQL принимают любую строку в поле пароля.
Ротация токенов
pg_doorman читает файл публичного ключа один раз при старте и по SIGHUP. Чтобы ротировать ключ:
- Добавьте новый публичный ключ во вторую запись пользователя с параллельным именем.
- Сделайте 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
Заблокировать устаревший 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 нет.
Горячая замена процесса может загрузить новый сертификат и ключ для новых входящих TLS-подключений, но это не бесшовная ротация для уже открытых TLS-сессий. Для переноса TLS-сессий оба процесса должны использовать те же tls_certificate и tls_private_key, а сборка должна быть Linux + tls-migration; при смене этих файлов TLS-клиенты дренируются и переподключаются.
Политика шифров
Минимум TLS 1.2 применяется на этапе handshake. PgDoorman не задаёт явный список шифров — эффективные шифры берутся из системной сборки OpenSSL. Если нужен жёсткий список, настройте его системно (/etc/ssl/openssl.cnf) или соберите OpenSSL с нужной политикой.
Direct TLS handshake (PostgreSQL 17, без SSLRequest) не поддерживается. Для управления шифрами TLS 1.3 или direct TLS из PostgreSQL 17 используйте PgBouncer 1.25+.
Серверный TLS
Шифрование соединений между pg_doorman и PostgreSQL. Появилось в 3.6.0.
Режимы
| Режим | Поведение |
|---|---|
disable | Обычный TCP. |
allow (по умолчанию) | Сначала пробовать обычный TCP; если сервер отказывает, повторить попытку на новом сокете с TLS. Соответствует libpq sslmode=allow. |
prefer | Отправить SSLRequest; если сервер отвечает 'N', откатиться к обычному TCP. |
require | Требовать TLS. Падать, если сервер его не поддерживает. |
verify-ca | Требовать TLS и проверять серверный сертификат против настроенного CA. |
verify-full | Требовать TLS, проверять CA и проверять hostname сервера против сертификата. |
allow — режим по умолчанию ради обратной совместимости: существующие развёртывания, где у PostgreSQL настроен TLS, автоматически переходят на TLS без изменения конфигурации. Для новых развёртываний, которым нужны явные гарантии, выбирайте require или verify-full.
Конфигурация
general:
server_tls_mode: "verify-full"
server_tls_ca_cert: "/etc/pg_doorman/tls/pg_ca_bundle.pem"
# Необязательно: клиентский сертификат для mTLS к PostgreSQL
server_tls_certificate: "/etc/pg_doorman/tls/pg_client.crt"
server_tls_private_key: "/etc/pg_doorman/tls/pg_client.key"
server_tls_ca_cert принимает PEM-bundle (несколько CA-сертификатов, склеенных подряд). Загружаются все.
Горячая перезагрузка
По SIGHUP серверные сертификаты перечитываются с диска. Существующие соединения продолжают пользоваться исходным TLS-контекстом; новые соединения используют перезагруженные сертификаты. Перезагрузка не берёт блокировку на горячем пути (Arc<ArcSwap<...>>): без обрыва соединений и без задержек на handshake.
kill -HUP $(pidof pg_doorman)
Это единственный путь перезагрузки TLS. Клиентские сертификаты по SIGHUP не перезагружаются.
mTLS к PostgreSQL
Задайте server_tls_certificate и server_tls_private_key. PostgreSQL должен быть настроен с ssl_ca_file, соответствующим подписанту клиентского сертификата, а у роли в pg_hba.conf со стороны PostgreSQL должно быть clientcert=verify-ca (или verify-full).
Мониторинг
Серверный TLS покрывают три серии Prometheus:
| Метрика | Тип | Назначение |
|---|---|---|
pg_doorman_server_tls_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-запросы к PostgreSQL минуют серверный TLS — они идут по свежему обычному TCP-соединению. Это совпадает с дизайном протокола PostgreSQL (cancel отправляется по отдельному сокету).
- Direct TLS handshake (быстрый handshake PostgreSQL 17 без
SSLRequest) не поддерживается ни на одной из сторон.
Куда дальше
- Настройка нового кластера? Смотрите Установку.
- Ротация сертификатов? Смотрите горячую замену процесса с переносом сессий и Сигналы. Для переноса TLS-сессий нужна Linux-сборка с
tls-migration. - Hardening существующего развёртывания? Сочетайте с pg_hba.conf: принудительный
hostsslдля нелокальных соединений.
Режимы пула
pg_doorman поддерживает два режима пула: transaction и session. Режим задаётся для пула, при необходимости переопределяется для конкретного пользователя.
Режима statement нет. В statement-пулинге серверное соединение ротируется после каждого оператора — это вынуждает клиентов отказаться от мульти-statement транзакций и полностью ломает протокол prepared statements. Все оптимизации pg_doorman (кеш prepared statements, прямая передача, строгий FIFO-планировщик) рассчитаны на транзакционный режим. PgBouncer оставляет statement для обратной совместимости; Odyssey его не реализует.
Транзакционный режим (рекомендуется)
pools:
mydb:
pool_mode: "transaction"
Backend-соединение удерживается в течение транзакции и возвращается в пул при COMMIT, ROLLBACK или неявном завершении.
Именно этот режим даёт ту самую эффективность соединений, ради которой существует pg_doorman: pool_size равный 40 обслуживает тысячи клиентов, пока транзакции остаются короткими.
Что работает в транзакционном режиме (там, где большинство пулеров проваливаются):
- Prepared statements. pg_doorman кеширует их в рамках пула, переименовывает имена statement между backend-соединениями и прозрачно повторно готовит запрос. Драйверы, привязанные к
unnamed-statement (Go pgx, .NET Npgsql, Python asyncpg), работают без настройки. - Pipelined-батчи и асинхронный поток
Flush. - Cancel-запросы поверх TLS.
LISTEN/NOTIFY— но только внутри транзакции.LISTENпослеCOMMITотпускает backend, и любые уведомления, доставленные ему после, попадут к тому клиенту, который следующим заберёт это соединение, а не к исходномуLISTEN-еру. PgBouncer ведёт себя так же; если вам нуженLISTENчерез несколько транзакций, для этого клиента используйте сессионный режим.
Что в транзакционном режиме не работает:
SETиRESETвне транзакции. Используйте сессионный режим для клиентов, опирающихся на изменение GUC уровня сессии (SET TIME ZONE,SET search_pathодин раз на соединение).- advisory-блокировки, удерживаемые между транзакциями. Используйте сессионный режим.
- курсоры, удерживаемые вне транзакций (
WITH HOLD). Используйте сессионный режим. SET LOCALработает как ожидается — он ограничен транзакцией.
Сессионный режим
pools:
legacy_app:
pool_mode: "session"
Backend-соединение удерживается в течение клиентской сессии. В пул возвращается только при отключении клиента.
Используйте, когда:
- Приложение использует состояние уровня сессии (
SET search_path,SET TIME ZONE). - Приложение использует курсоры WITH HOLD.
- Приложение использует advisory-блокировки между транзакциями.
- Вы переезжаете с немодифицированного PgBouncer-развёртывания, работавшего в сессионном режиме, и хотите подмену один-в-один.
В сессионном режиме pool_size фактически равен максимальному числу одновременных клиентов. Размер пула подбирается так, чтобы соответствовать max_connections PostgreSQL минус резервы.
Переопределение для конкретного пользователя
Режим пула можно переопределить для конкретного пользователя:
pools:
mydb:
pool_mode: "transaction"
users:
- username: "app"
password: "md5..."
pool_size: 40
- username: "admin_tools"
password: "md5..."
pool_size: 4
pool_mode: "session"
Полезно, когда одному пользователю (инструменты эксплуатации, миграции) нужна семантика сессии, а основное приложение остаётся в транзакционном режиме.
Очистка при возврате в пул
Очистка в транзакционном режиме отслеживает мутации, а не выполняется безусловно. pg_doorman следит за каждой транзакцией на предмет SET, PREPARE и DECLARE CURSOR, и только когда backend уходит в пул с одним из этих флагов, отправляет соответственно RESET ALL, DEALLOCATE ALL или CLOSE ALL. Транзакция только на чтение пропускает очистку целиком — это измеримый выигрыш на горячих OLTP-путях.
Что сбрасывается, когда сработал флаг:
- Флаг
SET→RESET ALLсбрасывает session-level GUCs и неявно вызываетpg_advisory_unlock_all. - Флаг
PREPARE→DEALLOCATE ALLудаляет PostgreSQL-side prepared statements, которые драйвер именовал явно. Собственный кеш prepared statements pg_doorman сохраняется после сброса: он индексируется текстом запроса, а не backend-именем. - Флаг
DECLARE CURSOR→CLOSE ALLзакрывает курсоры.
DEALLOCATE ALL и DISCARD ALL со стороны клиента очищают prepared-statement-кеш именно этого клиента (следующий Parse зарегистрируется заново). Pool-level shared cache не затрагивается; у других клиентов их записи сохраняются.
Полностью отключить очистку (ради производительности в жёстко контролируемых развёртываниях):
pools:
mydb:
pool_mode: "transaction"
cleanup_server_connections: false
Делайте так только если уверены, что приложение никогда не оставляет состояние сессии. Очистка по умолчанию уже дёшева на транзакциях без мутаций, поэтому отключение редко стоит риска.
Справочник
- Параметр
pool_mode: Настройки пула. cleanup_server_connections: Настройки пула.- Размер пула: Координатор пулов, Пул под нагрузкой.
Координатор пулов
Координатор пулов ограничивает суммарное число серверных соединений к одной базе по всем пользователям пула. Когда лимит выбран, он может освободить слот: закрыть свободное соединение у пользователя с запасом и отдать этот слот тому, кто сейчас ждёт подключение к PostgreSQL.
Эта страница объясняет модель и сценарии применения. Рецепты настройки и разбор вывода SHOW POOL_COORDINATOR смотрите в Пуле под нагрузкой.
Какую задачу решает
Без координатора каждый пользовательский пул независим. pool_size равный 40 у пяти пользователей означает до 200 серверных соединений, и PostgreSQL приходится защищать свои лимиты уже на своей стороне.
max_db_connections в PgBouncer ограничивает общую сумму, но как только лимит достигнут, новые клиенты просто становятся в очередь. Соединения освобождаются только тогда, когда их текущий владелец сам закроет их по server_idle_timeout. Кто первым занял слоты, тот и удерживает их независимо от интенсивности использования, а медленные нагрузки никогда не уступают быстрым.
Координатор в pg_doorman ограничивает сумму и:
- Вытесняет свободные соединения у пользователей с запасом, когда другому пользователю нужно вырасти.
- Ранжирует пользователей по p95 времени транзакции, чтобы самые медленные пулы уступали слоты первыми. Пулы с быстрыми транзакциями сохраняют преимущество переиспользования; пулы с длинными транзакциями простаивают большую долю времени, поэтому забрать у них слот стоит дешевле.
- Резервирует небольшое переполнение для коротких всплесков. Настраивается отдельно от основного лимита.
- Защищает минимальный размер пула на пользователя. Соединения ниже этого минимума не вытесняются.
Когда использовать
Включайте координатор, когда:
- На одной базе работают разные приложения или пользователи, и нужен верхний предел числа серверных соединений (
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 не нашлось свободного соединения.
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. Резерв поглощает короткие пики; если пик затягивается, включается вытеснение.
Как выбирается донор
Когда пользователь запрашивает новое серверное соединение, а лимит уже достигнут:
- Найти кандидатов со свободными соединениями. Пользователь, у которого все соединения активны, не может стать донором: его работа ещё выполняется.
- Пропустить защищённых. Пользователь ниже
min_guaranteed_pool_sizeисключается. - Пропустить недавно созданные соединения. Соединения младше
min_connection_lifetimeне вытесняются; это снижает колебания при коротких простоях. - Ранжировать по излишку. Пользователи с наибольшим числом свободных соединений сверх
min_guaranteed_pool_sizeполучают высший ранг. - Разрешить ничью по p95 времени транзакции. Среди пулов с одинаковым излишком первым уступает тот, у кого выше p95. Высокий p95 означает, что каждая транзакция дольше держит соединение, а значит одно вытеснение ломает меньше повторных выдач.
Выбранное свободное соединение закрывается; запрашивающий пользователь получает новое соединение из PostgreSQL.
Мониторинг
SHOW POOL_COORDINATOR показывает текущее состояние по каждой базе:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
shared_db | 80 | 78 | 16 | 2 | 142 | 18 | 0
evictionsбыстро растёт — один пользователь регулярно остаётся без слотов. Поднимитеmax_db_connectionsили задайте этому пользователюmin_guaranteed_pool_size.reserve_acqвысокий — всплески нормальны, но размер занижен; рассмотрите повышениеmax_db_connectionsвместо опоры на резерв.exhaustionsненулевой — резерв тоже был полным. Клиенты упёрлись вquery_wait_timeout, ожидая бэкенд. Поднимите лимит.
Prometheus: pg_doorman_pool_coordinator{type="..."} (gauge) и pg_doorman_pool_coordinator_total{type="evictions|reserve_acquisitions|exhaustions"} (counter). Смотрите Команды администратора и Справочник Prometheus.
Оговорки
- Координатор работает только внутри одного пула (одной базы). Кросс-пуловые / кросс-баз ограничения не поддерживаются.
- Вытеснение выбирает только свободные соединения. Пользователь, удерживающий все соединения в длинных транзакциях, не может стать донором, и другие пользователи могут ждать. Если ваша нагрузка устроена именно так, поднимите
max_db_connectionsили разделите её по пулам. min_guaranteed_pool_size— это нижняя граница для вытеснения, а неmin_pool_sizeдля прогрева. Эти соединения пул всё равно создаёт по требованию.- Задавать
max_db_connectionsбезmin_guaranteed_pool_size— это режим PgBouncer: работает, но мелкие пользователи голодают под давлением. Для общих баз всегда задавайте оба.
Куда дальше
- Как подобрать размер пула с разобранными примерами: Пул под нагрузкой → Согласование лимита с PostgreSQL.
- Тюнинг под нагрузкой: Пул под нагрузкой → Параметры тюнинга.
- Чтение вывода администратора: Команды администратора → SHOW POOL_COORDINATOR.
- Режимы пула (транзакционный против сессионного): Режимы пула.
Кеширование анонимных prepared statements
PgDoorman кеширует анонимные сообщения Parse в transaction pooling.
Многие драйверы отправляют короткие параметризованные запросы как
Parse с пустым именем. Без подмены PostgreSQL заново строит план на
каждом Bind, поэтому горячие OLTP-запросы каждый раз тратят CPU
планировщика.
PgDoorman прозрачно переписывает каждый анонимный Parse в
служебное имя DOORMAN_<N> на серверном соединении. План попадает в
реестр именованных prepared statements и переиспользуется между Bind
одного клиента и между разными клиентами одного пула. Главный эффект —
меньше CPU на планирование и меньше повторных Parse для одинаковых
форм запроса.
Подмена прозрачна для драйвера: клиент шлёт и получает пустые имена точно так же, как при работе с обычным PostgreSQL.
PgBouncer (1.21+) и Odyssey поддерживают prepared statements в
transaction mode, но только для именованных statements. Анонимный
Parse они передают без изменений. Отличие PgDoorman — подмена
анонимного Parse. Ограничения кеша, LRU, TTL и метрики нужны, чтобы
эта подмена не превращалась в рост памяти на динамическом SQL.
Что ускоряется
Кеш анонимного Parse убирает повторную работу на горячих
параметризованных запросах:
- бэкенд PostgreSQL не получает одинаковый
Parseпри каждом повторном обращении к уже известной форме запроса; - PostgreSQL может использовать prepared statement, который уже создан на этом серверном соединении;
- разные клиенты одного пула используют одну пуловую запись, а не прогревают одинаковый запрос независимо друг от друга;
- при попадании в кеш бэкенда PgDoorman синтезирует
ParseCompleteбез обращения к PostgreSQL.
Поэтому это в первую очередь оптимизация производительности для OLTP-нагрузок с повторяющимися формами запросов. Ограничение размеров кеша, TTL и интернер нужны затем, чтобы выигрыш не превращался в бесконтрольный рост памяти на пулере или бэкенде.
Базовая модель PostgreSQL
В сообщении Parse имя prepared statement задаёт его тип: пустое
имя соответствует анонимному statement, любое непустое —
именованному:
Время жизни в PG Кеширование плана
───────────────────── ───────────────── ─────────────────────
Анонимный (name="") До следующего Нет именованной
анонимного Parse записи; каждый новый
или конца сессии анонимный Parse
строит план заново
Именованный До Close / Сначала custom-планы;
(name="stmt_42") DEALLOCATE / после 5 custom-
конца сессии выполнений возможен
generic-план
Большинство современных драйверов по умолчанию используют
анонимные prepared для разовых параметризованных запросов:
lib/pq (Go), libpq PQexecParams (C), часть режимов в pgjdbc и
psycopg. Прикладной код выглядит как обычный параметризованный
запрос, но в wire protocol уходит пустое имя.
Почему это проблема в transaction pooling
В transaction pooling одно серверное соединение по очереди обслуживает разных
клиентов. Если пулер отправляет пустой Parse как есть, каждый
Bind клиента приходит на соединение, у которого плана для этого
запроса нет. Горячие OLTP-пути платят CPU планировщика на каждом
обращении.
Именованные prepared решают проблему производительности планирования, но перекладывают учёт на пулер:
- Пулер должен помнить именованные statements каждого клиента до его отключения, даже если общий кеш пула уже выселил запись.
- На каждом
Bindпулер должен проверить, знает ли текущее серверное соединение это имя, и при необходимости заново сделатьParse. - При отключении клиента пулер должен отправить
CloseилиDEALLOCATEна правильное серверное соединение. - Драйверы, которые генерируют отдельное имя
stmt_<seq>под каждый уникальный запрос, раздувают клиентский кеш: сотни записей на клиента, при тысячах подключений превращающиеся в миллионы записей в памяти.
Остаются два варианта: отказаться от кеширования плана для анонимных запросов или взять на себя полный учёт именованных. PgDoorman выбирает третий путь.
Что делает PgDoorman
На каждый анонимный Parse от клиента PgDoorman:
- Считает хеш по тексту запроса, OID типов параметров и короткому
отпечатку параметров планировщика, которые клиент закрепил при
подключении (
search_path,default_transaction_isolation,default_transaction_read_only,default_text_search_config,role). Поэтому два клиента с одинаковым запросом и OID параметров, но с разными значениямиsearch_pathвStartupMessage, получат разные записи кэша и разные планы PostgreSQL. Другие параметры, которые тоже могут влиять на план (TimeZone,DateStyle,plan_cache_mode,enable_*, настройки стоимости JIT), в этот отпечаток не входят. Перед тем как использовать один и тот же prepared-запрос с разными значениями таких параметров, посмотрите ограничения в справке поsync_server_parameters. - Ищет хеш в общем кеше пула. При промахе выделяет новое имя
DOORMAN_<counter>и регистрирует записьArc<Parse>. - Записывает в клиентский кеш ключ
Anonymous(hash), чтобы следующийBindнашёл тот жеDOORMAN_<N>. - Отправляет
Parseна бэкенд с переписанным именем. - На соответствующем
Bind(с пустым именем) переписывает имя statement вDOORMAN_<N>и проверяет, что текущий бэкенд уже держит запись; если нет, отправляетParseповторно.
Клиент никогда не видит DOORMAN_<N>: имя живёт только на участке
между PgDoorman и бэкендом. Когда нужный бэкенд уже держит запись,
PgDoorman синтезирует ParseComplete сам и не делает round-trip.
Пример wire-protocol
Go-приложение, выполняющее
db.Query("SELECT * FROM t WHERE name = $1", "vasya")
через lib/pq, отправляет такой обмен:
Клиент PgDoorman Backend
────── ───────── ───────
Parse("", q) ───►│ hash, miss → DOORMAN_42
│ pool_cache[hash] = Arc<Parse>
│ client_cache[Anon(hash)] = ...
│ Parse("DOORMAN_42") ─────►
│ ◄── ParseComplete
◄────│ ParseComplete
Bind("", "vasya") ───►│ rewrite "" → "DOORMAN_42"
│ Bind("DOORMAN_42") ──────►
│ Execute, Sync ───────────►
│ ◄── BindComplete, ...
│ ◄── ReadyForQuery
◄────│ BindComplete, ...
Второй клиент с тем же запросом в том же пуле попадает в кеш пула
и не отправляет Parse на бэкенд:
Клиент B PgDoorman Backend (тот же)
──────── ───────── ────────────────
Parse("", q) ───►│ hash hit → DOORMAN_42
│ server_cache содержит "DOORMAN_42"
◄────│ синтетический ParseComplete (на бэкенд ничего)
Bind("", v) ───►│ rewrite "" → "DOORMAN_42"
│ Bind("DOORMAN_42") ────►
│ ...
Слои кеша
PgDoorman держит состояние prepared statements на трёх уровнях:
Pool-level DashMap<hash, CacheEntry>
Один на пул. Хранит Arc<Parse> с именем DOORMAN_N.
Размер: prepared_statements_cache_size (по умолчанию 8192).
Выселение: приближённый LRU.
Client-level Named: AHashMap<String, CachedStatement>, без лимита.
Anonymous: LruCache<u64, CachedStatement> ограничен
client_anonymous_prepared_cache_size (если не задан,
наследует prepared_statements_cache_size),
или AHashMap при размере 0.
Выселение Anonymous локальное: Arc<Parse> отбрасывается,
DOORMAN_<N> на бэкенде остаётся.
Server-level LruCache<String, ()>, на серверное соединение.
Запоминает, какие DOORMAN_N этот бэкенд уже держит.
Точный LRU; при выселении отправляет Close на бэкенд.
При вытеснении записи из Anonymous LRU PgDoorman отбрасывает локальную
ссылку и не отправляет Close на бэкенд. Соответствующий
DOORMAN_<N> будет переиспользован server-level LRU или закроется по
server_lifetime (по умолчанию 20 минут) — что наступит раньше.
Текст запроса интернируется через Arc<str>: десять клиентов с
одним и тем же анонимным запросом делят одну аллокацию в памяти.
Когда подмена помогает
- API-нагрузки с малым набором горячих запросов. Десяток
уникальных форм
SELECT/INSERTна тысячи клиентов. Доля попаданий в кеш пула близка к 100 %, планировщик работает один раз на серверное соединение для каждой формы запроса, а следующие обращения идут черезBindк уже подготовленному statement. - Драйверы, привязанные к анонимным prepared.
lib/pq,libpqPQexecParams, pgjdbc до достиженияprepareThreshold. Без подмены они каждый раз перепланируют. - Смешанные пулы, где именованные и анонимные statements соседствуют. Анонимные получают тот же выигрыш от кеша планов, что и именованные, без роста клиентского кеша именованных statements.
Когда подмена не помогает
- Разовый / OLAP-трафик. Каждый запрос уникален. Когда кеш пула
заполнен, каждая новая форма запроса ищет старую запись для
вытеснения через O(N)-обход. Если инстанс обслуживает только такую
нагрузку, отключите remap через
prepared_statements: false. - Скрипты с одним statement. Цепочка connect →
Parse→ 1Bind→ disconnect не накапливает достаточно попаданий, чтобы окупить учёт. Накладные расходы наParse~700 нс — небольшие, но измеримые. - Асинхронные драйверы в режиме pipeline. Каждая сессия получает
уникальное имя
DOORMAN_async_<N>, чтобы избежать коллизий между одновременными незавершёнными операциями. Серверный кеш между сессиями не переиспользуется. Кеш пула по-прежнему делит текст запроса между сессиями; планировщик на бэкенде срабатывает один раз на сессию.
Эффективность этого ускорения измеряйте по
rate(pg_doorman_servers_prepared_hits_total[5m]) и
rate(pg_doorman_servers_prepared_misses_total[5m]). Устойчивая доля
промахов выше 30 % означает, что подмена расходует CPU и память, но
почти не даёт переиспользования планов. Тогда либо отключите её, либо
увеличьте prepared_statements_cache_size.
Как это устроено у других пулеров
| Пулер | Кеш Parse/плана для анонимного prepared statement |
|---|---|
| PgDoorman | Да: прозрачная подмена на DOORMAN_<N> |
| PgBouncer 1.21+ | Нет: только named, анонимный проходит as-is |
| Odyssey | Нет: только named, pool_reserve_prepared_statement |
| PgCat | Нет: только named |
В PgBouncer поддержка prepared statements появилась в 1.21, но
ограничена именованными: анонимный Parse проходит без
изменений, и каждый Bind запускает планировщик. Флаг
pool_reserve_prepared_statement в Odyssey требует именованных
statement; на анонимный трафик он не влияет. PgCat ведёт себя
так же.
Кешировать план анонимных prepared сегодня умеет только PgDoorman.
Конфигурация
| Параметр | По умолчанию | Эффект |
|---|---|---|
prepared_statements | true | Включает подмену и кеширование prepared statements. false отключает функцию. |
prepared_statements_cache_size | 8192 | Размер кеша пула в записях. Должен быть больше 0, пока prepared_statements = true. |
server_prepared_statements_cache_size | наследует prepared_statements_cache_size | Размер LRU на серверное соединение для имён DOORMAN_<N>. 0 отключает хранение на бэкенде, но не подмену на уровне пула. |
client_anonymous_prepared_cache_size | наследует prepared_statements_cache_size | Размер Anonymous LRU на клиента. 0 снимает ограничение. Named-часть всегда без лимита. |
Named-часть клиентского кеша всегда без лимита и не зависит от
client_anonymous_prepared_cache_size.
Полностью отключить подмену prepared statements (редко, для установок только с OLAP):
general:
prepared_statements: false
Отдельного переключателя только для анонимных statements нет. Не
используйте prepared_statements_cache_size: 0 как выключатель:
pg_doorman отклоняет такое общее значение, пока prepared_statements
включён.
Отличия от семантики PostgreSQL
Подмена меняет несколько протокольных деталей, на которые могут полагаться строгие приложения:
- Один и тот же анонимный
Parse, отправленный дважды, не стирает предыдущий. Каждая пара(query, param_types)живёт независимо в кеше пула под своимDOORMAN_<N>. Closeс пустым именем ничего не делает с кешами PgDoorman. СоответствующийDOORMAN_<N>живёт до выселения из LRU кеша пула или до закрытия пула.- Решение PostgreSQL между custom- и generic-планом становится общим
для всех клиентов, использующих один
DOORMAN_<N>. Именованный statement сначала получает custom-планы; после пяти custom-выполнений PostgreSQL может перейти на generic-план, если его оценочная стоимость достаточно близка. С PgDoorman эти выполнения могут прийти от разных клиентов, поэтому выбор generic-плана может отражать смешанное распределение параметров.
Приложения, которые опираются на PG-семантику "анонимный Parse
стирает предыдущий", должны переключиться на именованные statement
с явным Close.
Тюнинг
Размер кеша
Кеш prepared statements в PgDoorman состоит из трёх слоёв. Ими управляют три связанных параметра:
prepared_statements_cache_size(по умолчанию8192) задаёт размер общего кеша пула — одна хеш-таблица на пул, ключом служит хеш запроса. Это верхняя граница на число различных форм запроса, которые пул помнит сразу для всех клиентов. Приближённый LRU: вытеснение проходит за O(N) по всему кешу и не отправляетCloseна бэкенд (другие клиенты могут ещё держатьArc).server_prepared_statements_cache_size(по умолчанию наследуетprepared_statements_cache_size) задаёт размер серверного кеша — отдельный LRU на каждое серверное соединение, ключом служит имяDOORMAN_<N>. Это верхняя граница на число prepared statements, которое PgDoorman позволит держать одному бэкенду PostgreSQL. Точный LRU за O(1); при выселении в очередь бэкенда кладётсяClose, который отправляется ближайшим Sync или Flush — представлениеpg_prepared_statementsможет временно показывать больше строк, чем потолок, пока не придёт следующий Sync.client_anonymous_prepared_cache_size(по умолчанию наследуетprepared_statements_cache_size) задаёт размер Anonymous LRU на клиента.0отключает LRU и снимает потолок: кеш растёт без ограничения. Любое положительное число задаёт лимит независимо от размера кеша пула.
Пуловый и серверный лимиты можно переопределить на уровне пула:
general:
prepared_statements_cache_size: 8192
server_prepared_statements_cache_size: 1024 # потолок на бэкенд жёстче
pools:
oltp:
# наследует оба значения из general
pool_mode: "transaction"
reporting:
# у этого пула шире разнообразие запросов; пусть серверный кеш
# вмещает больше
server_prepared_statements_cache_size: 4096
pool_mode: "transaction"
prepared_statements: false отключает подмену целиком и заодно
обнуляет кеши на уровне пула и серверного соединения. Указать
server_prepared_statements_cache_size: 0 при положительном
размере кеша пула допустимо, но смысла мало: серверный кеш превратится
в pass-through, и каждое попадание на другой бэкенд приведёт к
повторному Parse.
Когда уменьшать server_prepared_statements_cache_size ниже размера
кеша пула:
- На бэкендах копится слишком много строк
DOORMAN_<N>(pg_prepared_statementsупирается в потолок, память планов растёт). - Хочется ускорить вытеснение через
Close, не урезая попадания в кеш пула.
Когда оставить значения равными (поведение по умолчанию):
- Нет измеренной проблемы с памятью на бэкендах. Достаточно наследования.
Размер client_anonymous_prepared_cache_size
Если параметр не задан, Anonymous LRU на клиента наследует
вычисленный prepared_statements_cache_size пула (по умолчанию 8192).
Явное значение перекрывает наследование: 0 отключает LRU, и кеш
растёт без ограничения; любое положительное число задаёт потолок
LRU.
Каждая запись хранит лёгкую структуру
(hash, async_name?, Arc<Parse>) — сам Arc<Parse> делится с
кешем пула, поэтому накладные расходы на клиента ≈ 80 байт
на запись. На 10 000 подключённых клиентов × 256 записей × ~80 байт
получаем около 200 МБ предсказуемого потолка на пулере.
Поднимайте лимит, когда:
- ORM или генератор SQL выдаёт
stmt_<seq>под каждый запрос и Anonymous LRU постоянно вытесняет записи (видно по устойчиво ненулевой скоростиpg_doorman_clients_prepared_anonymous_evictions_total). - Приложение заведомо имеет широкое рабочее множество в одной сессии и скорость вытеснений соответствует этой нагрузке.
Уменьшайте лимит при очень больших числах подключений (50 000+
клиентов): на таком масштабе clients × cache_size × 80 байт учётной
памяти на пулере может пересечь 1 ГБ, и урезание лимита уполовинивает
её. max_memory_usage не ограничивает служебную память кеша prepared
statements; этот параметр защищает буферы запросов в полёте.
Named всегда без лимита
Named-часть клиентского кеша не ограничена. PgDoorman держит
Arc<Parse> для каждого именованного statement, который создал
клиент, до его отключения или явного DEALLOCATE /
DEALLOCATE ALL. Это согласуется с собственным контрактом
PostgreSQL — именованные prepared живут до конца сессии — и
исключает сценарий, при котором вытеснение Named-записи под нагрузкой
ломает следующий Bind ошибкой prepared statement does not exist.
Обратная сторона: драйверы, генерирующие отдельное имя под каждый
запрос (часть режимов pgjdbc и Hibernate, отдельные конфигурации
.NET Npgsql), могут раздуть Named-часть без потолка. PgDoorman не
может ограничить её безопасно; ответственность за переиспользование
имён или явный DEALLOCATE лежит на приложении.
Сигнал давления есть только для Anonymous LRU — счётчик вытеснений
pg_doorman_clients_prepared_anonymous_evictions_total. Для Named
такого сигнала нет: следите за колонкой client_named_count в
SHOW POOLS_MEMORY и метрикой pg_doorman_clients_prepared_named_entries
на предмет неожиданного роста.
Окно роста памяти на бэкенде
При вытеснении записи из Anonymous LRU на стороне клиента PgDoorman
отбрасывает только локальный Arc<Parse>. Соответствующий
DOORMAN_<N> остаётся живым на каждом бэкенде, который когда-либо
его обслуживал. Очищают его два механизма:
- Server-level LRU. На каждом бэкенде ведётся свой
LruCache<String, ()>имёнDOORMAN_<N>, ограниченныйserver_prepared_statements_cache_size(илиprepared_statements_cache_size, если отдельный серверный лимит не задан). При достижении лимита бэкенд отправляетCloseна наименее давно использованное имя и освобождает план. - Ротация бэкенда. Бэкенд достигает
server_lifetime(по умолчанию 20 мин), и pg_doorman закрывает его; новый бэкенд стартует с пустым кешем планов.
Худший случай по памяти на одном бэкенде — это
server_prepared_statements_cache_size × средний размер плана
(8192 × ~100 КБ — около 800 МБ) на стороне PostgreSQL. Чтобы сжать
окно:
- Снизьте
server_prepared_statements_cache_size, чтобы серверный LRU быстрее вытесняла планы. - Снизьте
server_lifetime, чтобы бэкенды ротировались чаще.
Системное представление pg_prepared_statements в PostgreSQL
показывает имена, которые держит текущий бэкенд. Подсчёт строк там
показывает, насколько близко бэкенд подошёл к лимиту.
Мониторинг
Команды администратора:
-
SHOW PREPARED_STATEMENTS— pool, hash, name, query,count_used,kind. Топ записей поcount_usedпоказывает горячие запросы, на которых кеш окупается. Колонкаkind— последняя в наборе и принимает значенияnamed,anonymousилиmixedв зависимости от того, как клиенты использовали запись за её жизнь.Пример:
pool | hash | name | query | count_used | kind --------------+--------------------+-------------+-------------------+------------+----------- sharded.user | 1234567890123456 | DOORMAN_1 | SELECT * FROM t1 | 150234 | anonymous sharded.user | 2345678901234567 | DOORMAN_2 | INSERT INTO t2 .. | 87654 | named sharded.user | 3456789012345678 | DOORMAN_3 | SELECT * FROM t3 | 45678 | mixed -
SHOW POOLS_MEMORY—pool_prepared_count,client_prepared_count,pool_prepared_bytes,client_prepared_bytesплюс разбивка по kind:client_named_count,client_anonymous_count,client_anonymous_evictions_alive. Последняя колонка суммирует счётчики вытеснений только по подключённым сейчас клиентам — отключившиеся клиенты выпадают из суммы, поэтому колонка не монотонна. Для накопительного счётчика читайте Prometheus-метрикуpg_doorman_clients_prepared_anonymous_evictions_total.
Prometheus-метрики (полный список в Prometheus):
pg_doorman_pool_prepared_cache_entries{user, database}pg_doorman_pool_prepared_cache_bytespg_doorman_clients_prepared_cache_entriespg_doorman_clients_prepared_cache_bytespg_doorman_clients_prepared_named_entries{user, database}pg_doorman_clients_prepared_anonymous_entries{user, database}pg_doorman_clients_prepared_anonymous_evictions_total{user, database}pg_doorman_servers_prepared_hits{user, database}pg_doorman_servers_prepared_misses{user, database}pg_doorman_servers_prepared_hits_total{user, database}pg_doorman_servers_prepared_misses_total{user, database}pg_doorman_async_clients_count
Для rate() и алертов используйте метрики с суффиксом _total.
Метрики серверного кеша без _total — текущая сумма по живым
бэкендам; она может уменьшаться при ротации бэкендов.
Алертинг
Скорость вытеснений в Anonymous LRU
Устойчиво ненулевая скорость на счётчике вытеснений означает, что LRU вытесняет записи быстрее, чем приложение переиспользует их. Шаблон алерта:
rate(pg_doorman_clients_prepared_anonymous_evictions_total[5m]) > 10
for 10m
Порог в 10 вытеснений/с на пул — отправная точка, реальное значение
зависит от формы трафика и числа подключений. Срабатывание алерта
читайте как "лимит слишком мал или рабочее множество приложения
шире, чем ожидалось"; решение — либо поднять
client_anonymous_prepared_cache_size, либо разобраться, не
генерирует ли приложение уникальные запросы на горячем пути.
Интерпретация kind = mixed
Каждая запись кеша пула помнит, использовали ли её клиенты как
именованный statement, как анонимный, или обоими способами. kind = mixed
означает, что одна и та же пара (query, param_types) была
обработана хотя бы одним клиентом как named и хотя бы одним другим
как anonymous за её текущую жизнь. У большинства нагрузок строк
mixed нет; если в пуле их большинство, у клиентов разные драйверы
или разные конфигурации драйверов против одной БД, и эту разнородность
стоит проверить — иногда она задумана, иногда сигналит, что один из
клиентов настроен неверно.
Число prepared statements на бэкенде
PostgreSQL отдаёт pg_prepared_statements для текущего бэкенда.
Если память пулера в норме, но RSS бэкендов PostgreSQL растёт,
посчитайте строки на каждом бэкенде:
SELECT count(*) FROM pg_prepared_statements;
Цифры около server_prepared_statements_cache_size означают, что
серверный LRU работает на потолке, а ротация — второй способ
освободить память планов. Если серверный лимит наследует
prepared_statements_cache_size, используйте это значение как потолок.
Снижение серверного лимита или server_lifetime уменьшает давление на
память планов ценой более частых повторных Parse на бэкенде.
Ограниченный query interner
Глобальный интернер, в котором дедуплицируются тексты Parse, разделён на две независимые хеш-таблицы.
- NAMED — тексты именованных prepared. Запись жива, пока
пуловой или клиентский кеш держит ссылку через
Arc<str>. Сборщик мусора убирает запись, когда её перестали удерживать снаружи интернера; двухтактовая отсрочка защищает запись, на которую сослались между двумя циклами обхода. - ANON — тексты анонимных prepared. Запись истекает по
query_interner_anon_idle_ttl_secondsбездействия (60 секунд по умолчанию).0отключает TTL и возвращает поведение pg_doorman до 3.7 — оставлено для существующих установок.
Если anonymous Bind или Describe приходит после того, как
pg_doorman потерял соответствующее состояние anonymous prepared,
pg_doorman отвечает ERROR: unnamed prepared statement does not exist
(SQLSTATE 26000). Типичные причины: вытеснение из клиентского
Anonymous LRU, RESET INTERNER, TTL-вытеснение из interner или паттерн
драйвера, который переиспользует unnamed prepared statements между
пачками. Это та же ошибка, что PostgreSQL отдаёт напрямую в аналогичной
ситуации; стандартные драйверы реагируют повторным Parse.
Бинарное обновление (SIGUSR2) переносит и NAMED, и ANON в новый
процесс. Анонимные записи попадают в новый ANON-интернер со
свежим last_used, и TTL отсчитывается заново с момента
обновления.
Команды для оператора
SHOW INTERNER (через admin-сессию) выводит количество и объём
текста для каждой половины:
kind | entries | bytes
named | 420 | 87654
anonymous | 1337 | 234567
SHOW INTERNER N показывает N самых тяжёлых записей: hash,
kind, bytes, idle_ms (-1 для named — там нет
последнего-использования, вместо него отслеживается состояние
сборщика) и 120 первых символов текста запроса.
RESET INTERNER чистит обе половины. Активные клиенты заново
сделают Parse при следующем использовании. Команда
диагностическая.
Метрики Prometheus отражают SHOW INTERNER плюс гистограмму
длительности обхода и счётчик синтетических 26000. Увеличивайте
query_interner_anon_idle_ttl_seconds только когда synthetic misses
совпадают с TTL-вытеснениями anonymous-записей или с подтверждённым
паттерном cross-batch unnamed statements. Если промахи идут вместе с
pg_doorman_clients_prepared_anonymous_evictions_total, увеличивайте
client_anonymous_prepared_cache_size.
Справочник
- Режимы пула — transaction mode, где работает подмена prepared statements.
- Общие настройки —
prepared_statements_cache_size,server_prepared_statements_cache_size,client_anonymous_prepared_cache_size,query_interner_gc_interval_seconds,query_interner_anon_idle_ttl_seconds. - Команды администратора —
SHOW PREPARED_STATEMENTS,SHOW POOLS_MEMORY,SHOW INTERNER,RESET INTERNER. - Prometheus — полный список метрик.
Параметры запуска PostgreSQL
PgDoorman может задавать параметры PostgreSQL при открытии серверного
соединения, не меняя postgresql.conf, ALTER ROLE или
ALTER DATABASE. Это полезно, например, в таких случаях:
- В горячем OLTP-пуле план переключается на generic после решения
эвристики
plan_cache_mode = autoи обратно уже не возвращается.ALTER ROLE SET plan_cache_mode = force_custom_planзатронет любую другую нагрузку под этой ролью, а изменить нужно только один пул. - Приложение не задаёт
statement_timeoutилиidle_in_transaction_session_timeout, а быстро доработать его нельзя. Администратору БД нужно сессионное значение по умолчанию, которое сохранится после клиентскогоRESET ALL. - Одно приложение должно стабильно показывать конкретный
application_name, независимо от значения, которое передаст драйвер, чтобыpg_stat_activityи аудит оставались читаемыми.
Для этого используется startup_parameters: набор GUC PostgreSQL,
который pg_doorman добавляет в StartupMessage новых серверных
соединений пула.
Конфигурация
Значения применяются в три слоя. Более узкий слой переопределяет ключ из предыдущего.
[general.startup_parameters]
statement_timeout = "5s"
[pools.checkout.startup_parameters]
plan_cache_mode = "force_custom_plan"
work_mem = "64MB"
После SIGHUP или RELOAD через консоль администратора каждое новое
серверное соединение пула checkout открывается со значениями
statement_timeout = 5s, plan_cache_mode = force_custom_plan и
work_mem = 64MB. В других пулах остаётся только
statement_timeout = 5s из general; остальные значения берутся из
настроек PostgreSQL по умолчанию. Уже открытые бэкенды не меняются:
новые значения вступают в силу по мере ротации соединений.
В сквозном режиме auth_query, когда server_user не задан, запрос
аутентификации может вернуть необязательную колонку startup_parameters
типа text, json или jsonb с JSON-объектом. Значения из этой
колонки переопределяют general и настройки пула только для
конкретного пользователя.
SELECT
rolpassword AS passwd,
CASE rolname
WHEN 'vip' THEN '{"work_mem":"256MB"}'::text
ELSE NULL::text
END AS startup_parameters
FROM pg_authid
WHERE rolname = $1;
pg_doorman выбирает декодер по типу колонки, поэтому приведение ::text
не требуется. Содержимое должно быть JSON-объектом, а значения —
строками. Для других типов PostgreSQL, в том числе доменов поверх
jsonb, pg_doorman пишет предупреждение и игнорирует пользовательские
параметры из этой строки.
Выделенный режим auth_query, когда server_user задан, игнорирует эту
колонку и один раз пишет предупреждение на пару (пул, пользователь).
В этом режиме один серверный пул обслуживает разных пользователей,
поэтому пользовательские значения применить нельзя.
Изменения в пользовательской строке startup_parameters применяются
только к новым серверным подключениям и только после того, как
pg_doorman перечитает строку. Кеш auth_query хранит положительные
записи в течение auth_query.cache_ttl (по умолчанию час); при
обновлении кеша pg_doorman замечает изменение overlay и сбрасывает
динамический пул, чтобы следующий логин пересоздал его с новыми
значениями. Пока запись кеша не истекла, новые подключения клиентов
получают прежний overlay. Чтобы выкатить изменения немедленно: уменьшить
cache_ttl и сделать reload конфигурации, перезапустить pg_doorman или
дождаться истечения TTL. Бэкенды, которые уже выданы клиентам,
продолжают работать со значениями, сохранёнными при создании пула.
Что pg_doorman делает со значениями
pg_doorman добавляет итоговый набор параметров в StartupMessage
каждого нового бэкенда. PostgreSQL сохраняет эти значения как
сессионные значения по умолчанию (pg_settings.reset_val и
pg_settings.source = 'client'). Поэтому клиентские RESET ALL и
DISCARD ALL возвращают операторские значения, а не исходные
значения PostgreSQL.
Значение видно со стороны клиента:
checkout=> SHOW plan_cache_mode;
plan_cache_mode
-------------------
force_custom_plan
checkout=> SET plan_cache_mode = 'auto'; RESET ALL; SHOW plan_cache_mode;
plan_cache_mode
-------------------
force_custom_plan
Валидация
При загрузке конфигурации pg_doorman проверяет:
- Имена ключей должны соответствовать маске GUC PostgreSQL:
^[A-Za-z_][A-Za-z0-9_.]*$. Составные имена вродеauto_explain.log_min_durationдопустимы; произвольная пунктуация нет. - Зарезервированные ключи (
user,database,replication,options,role,session_authorizationи всё, что начинается с_pq_.) отклоняются. pg_doorman управляет ими сам, либо PostgreSQL обрабатывает их вStartupMessageособым образом. - Значения не должны содержать нулевой байт.
- Каждый уровень (
generalилиpool) должен помещаться в лимит для операторских параметров:MAX_STARTUP_PACKET_LENGTH(10000 байт) минус 512 байт, зарезервированных под служебные ключи pg_doorman.
Перед запуском каждого бэкенда pg_doorman снова проверяет объединённый
набор параметров по тому же лимиту. Слои, которые помещаются по
отдельности, могут превысить лимит после объединения: general + pool
может быть слишком большим сам по себе, а строка auth_query может
переполнить уже допустимый базовый набор. Любое превышение теперь
возвращается клиенту как PostgreSQL-ошибка с SQLSTATE 53400; пустой
или урезанный StartupMessage не отправляется. В предупреждении
записываются размеры в байтах, а
pg_doorman_startup_parameters_dropped_total увеличивается на каждой
отклонённой попытке запуска бэкенда.
Что происходит, если PG отвергает параметр
Если PostgreSQL отвергает заданный оператором параметр при запуске
бэкенда, pg_doorman возвращает клиенту ErrorResponse PostgreSQL без
изменений. Клиент видит тот же SQLSTATE (22023, 42704, 42501,
55P02 или любой другой код, который PostgreSQL вернул при отклонении
StartupMessage) и то же сообщение, что увидел бы при прямом
подключении к PostgreSQL.
pg_doorman не пробует повторить подключение без отклонённого параметра
и не отключает этот ключ автоматически для пула. Следующее подключение
клиента отправит тот же StartupMessage и получит ту же ошибку, пока
оператор не исправит конфигурацию.
Наблюдаемость
Итоговые параметры по каждому пулу видны через административную SQL-консоль:
admin> SHOW STARTUP_PARAMETERS;
user | database | parameter | value | source | state
------+----------+-------------------+-------------------+---------+--------
shop | checkout | plan_cache_mode | force_custom_plan | pool | applied
shop | reports | statement_timeout | 10s | general | applied
Веб-интерфейс показывает эти же строки на странице пула в секции «Startup parameters (operator-injected)».
В Prometheus:
pg_doorman_backend_startup_parameter_errors_total{pool, sqlstate}считает попытки запуска бэкенда, которые PostgreSQL отклонил из-за параметра, заданного оператором. Имя параметра и пользователя пишутся в строку лога уровняwarn; в лейблы они не включены, чтобы динамическиеauth_query-пулы не раздували количество серий.pg_doorman_startup_parameters_dropped_total{pool, reason}считает случаи, когда pg_doorman отклонилstartup_parametersдо отправкиStartupMessage: превышение лимита, неподдерживаемый тип или неверный JSON изauth_query, недопустимые ключи или значения, а также пользовательские значения, проигнорированные в выделенном режиме.
Практичное условие для алерта: если
pg_doorman_backend_startup_parameter_errors_total растёт по одному и
тому же пулу несколько минут подряд, новые подключения к этому пулу
падают на одном и том же GUC. Конфигурацию нужно исправить до возврата
трафика.
Когда это не нужно
- Приложение само задаёт параметр на каждом подключении. Дублирование в
startup_parametersдобавляет ещё одну настройку без изменения поведения. - Тюнинг на одну транзакцию (
SET LOCAL).startup_parametersзадают сессионные значения по умолчанию; параметры уровня транзакции должно выставлять приложение. - Значения, которые зависят от текущего запроса. Параметры запуска действуют для всех транзакций бэкенда на протяжении его жизни; режима «на один statement» нет.
Справочник
- Общие настройки:
startup_parameters. - Настройки пула:
pools.<name>.startup_parameters. - auth_query: сквозной и выделенный
режимы, чтение колонки
startup_parameters. - Команды администратора:
SHOW STARTUP_PARAMETERS. - Метрики Prometheus: полный список.
Давление на пул
Давление на пул — это поведение pg_doorman в момент, когда много
клиентов одновременно просят серверное соединение, а очередь свободных
соединений пуста. В этот момент пулер должен решить, кто получает
соединение, кто ждёт, кто запускает новый connect() к PostgreSQL, а
кому возвращается ошибка.
Внутри каждого пула (database, user) работают два механизма:
упреждающее ожидание (anticipation) ждёт возврата соединения от
соседа перед новым connect(), а ограничитель всплесков задаёт
жёсткий лимит на одновременные подключения к PostgreSQL внутри одного
пула. Если включён max_db_connections, поверх них работает
координатор (coordinator) — общий лимит серверных соединений к
одной базе данных для всех её пулов.
Аудитория: DBA или инженер эксплуатации, который уже знает PgBouncer и хочет понять, чем pg_doorman отличается и за чем нужно следить.
Зачем нужно давление на пул
Возьмём пул с pool_size = 40 и нагрузкой в 200 коротких транзакций,
приходящих в одну и ту же миллисекунду. В пуле 4 свободных соединения. В
наивном пулере первые 4 клиента забирают свободные соединения, а оставшиеся
196 независимо вызывают connect() к PostgreSQL. PostgreSQL получает
196 одновременных TCP-попыток подключения, на каждую из которых нужно
выполнить SCRAM-аутентификацию и согласование параметров, только чтобы
обнаружить, что пул разрешает ещё 36 соединений. Обращения к
pg_authid взлетают всплеском, потолок max_connections пробивается,
очередь accept() ядра насыщается, а хвостовая задержка (p99/p99.9,
то есть редкие, но самые медленные запросы) уже подключённых клиентов
растёт, потому что postmaster PostgreSQL занят порождением процессов
вместо выполнения запросов.
Это лавина одновременных подключений: ситуация, когда множество
независимых задач одновременно реагируют на одно состояние и
устраивают шторм одинаковых запросов к общему ресурсу. В нашем
сценарии это 196 одновременных connect() к одному слушателю PostgreSQL
вместо размазанного по времени потока.
Time: ----------------------------------------->
Client_1 -[idle hit]--[query]-----[done]
Client_2 -[idle hit]--[query]-----[done]
Client_3 -[idle hit]--[query]-----[done]
Client_4 -[idle hit]--[query]-----[done]
Client_5 -[connect]-[auth]-[query]-[done]
Client_6 -[connect]-[auth]-[query]-[done]
. ^
. 196 одновременных подключений к PostgreSQL
. в один момент
Client_200 -[connect]-[auth]-[query]-[done]
PostgreSQL: 196 новых backend-процессов + 4 выполняющихся запроса
Давление на пул подавляет это поведение. pg_doorman заставляет
большинство из этих 196 вызовов переиспользовать соединение, которое
другой клиент вот-вот вернёт, либо подождать несколько миллисекунд за
небольшим числом уже идущих подключений к PostgreSQL. Частота connect() к
PostgreSQL остаётся ограниченной даже при всплесках клиентов.
Режим независимых пулов
Этот режим работает, когда max_db_connections не задан. Пулы
независимы, общей координации между ними нет, давление управляется
внутри каждого пула (database, user). Это режим по умолчанию, и
большинство инсталляций находятся именно в нём.
Прогрев пула с холодного старта
Пул с pool_size = 40 и min_pool_size = 0 стартует с нулём
соединений. Первый пришедший клиент не ждёт: pg_doorman сразу создаёт
серверное соединение. Второй делает то же самое, третий тоже,
пока пул не достигнет порога прогрева (warm threshold).
Порог прогрева равен pool_size × scaling_warm_pool_ratio / 100. При
значении по умолчанию 20% и pool_size = 40 порог равен 8 соединениям.
Ниже этого порога pg_doorman создаёт соединения сразу: пул
холодный, цена ожидания выше цены нового подключения, и клиенты не могут
конкурировать за свободные соединения, которых не существует.
Выше порога активируется зона упреждающего ожидания (anticipation zone):
pg_doorman считает, что пул уже разогрет и кто-то
из параллельных клиентов скоро вернёт занятое соединение, поэтому имеет
смысл подождать его возврата вместо того, чтобы тратить ресурсы на
новый connect(). Когда клиент не находит соединения в очереди свободных,
pg_doorman сначала пытается перехватить такой возврат.
Третья зона накладывается поверх обеих: при любом размере пула, если
число соединений, создаваемых прямо сейчас, достигает
scaling_max_parallel_creates (по умолчанию 2), пул упирается в
лимит ограничителя всплесков. Дополнительные вызовы ждут свободный слот независимо
от того, сколько свободных соединений существует.
Three pressure zones
--------------------
Pool size: 0 ----------- 8 ---------------------------- 40
^ ^ ^
| | |
| WARM ZONE | ANTICIPATION ZONE |
| | |
| size < | size >= warm_threshold |
| warm_thr | |
| | |
| Пропускает | Фаза 3: быстрый опрос |
| фазы 3 | Фаза 4: прямая передача |
| и 4. | (ожидание возврата от |
| Сразу к | соседа, дедлайн = |
| фазе 5 | query_wait_timeout |
| (огранич. | − 500 ms) |
| + connect) | Затем фаза 5 |
Ограничение параллельных создаваемых соединений
(ортогонально размеру пула)
-----------------------------------------------
Создаваемых сейчас: 0 ---- 1 ---- 2 (= scaling_max_parallel_creates)
^
| На лимите: новый вызов встаёт
| в очередь на получение
| возвращённого соединения и ждёт
| завершения чужого создания.
Зоны прогрева и упреждающего ожидания отслеживают текущий размер пула. Отдельно
работает ограничитель всплесков: жёсткий лимит на число одновременно
идущих connect() к PostgreSQL внутри одного пула. Этот лимит
срабатывает независимо от размера пула: пул может одновременно
находиться в зоне упреждающего ожидания, при этом все слоты
ограничителя всплесков уже заняты параллельными connect()-ами. Под
нагрузкой это типичная ситуация.
Пул ниже порога прогрева тоже может упереться в этот лимит, если во
время холодного заполнения одновременно приходит много клиентов.
Получение соединения
Когда клиент запрашивает соединение через pool.get(), pg_doorman
проходит по следующим фазам. Каждая фаза либо возвращает соединение,
либо передаёт управление следующей фазе.
Фаза 1 — горячий путь recycle. Берём первое соединение из
очереди свободных соединений (двусторонняя очередь свободных серверных соединений
«голова очереди» — это то соединение, которое было возвращено раньше
всех). Если оно проходит проверку пригодности (recycle), отдаём его
клиенту.
Проверка пригодности откатывает любые открытые транзакции, запускает
проверку живости (liveness probe) если соединение простояло дольше
server_idle_check_timeout, и сверяет поколение соединения с текущим
поколением пула (reconnect-эпоха — счётчик, который увеличивается при
admin-команде RECONNECT и при обнаруженных сбоях бэкенда).
Соединения, созданные до увеличения счётчика, эту проверку не проходят
и удаляются вместо возврата клиенту. Здоровый пул в установившемся
режиме идёт только этим путём.
Фаза 2 — порог прогрева. Если размер пула ниже порога прогрева, пропускаем упреждающее ожидание и сразу переходим к созданию нового серверного соединения. Холодные пулы заполняются быстро.
Фаза 3 — быстрый опрос. Выше порога прогрева повторяем
проверку пригодности до 10 раз в плотном цикле без пауз (контролируется
параметром scaling_fast_retries). Так перехватывается случай, когда
другой клиент завершил свой запрос в том же микросекундном диапазоне и
вот-вот вернёт соединение. Полная стоимость порядка 10–50
микросекунд: без sleep и без блокирующего I/O.
Фаза 4 — прямая передача. Если быстрый опрос не поймал возврат, задача встаёт в очередь ожидающих. Когда любой клиент возвращает соединение, оно отправляется напрямую старейшему ожидающему, минуя очередь свободных соединений. Получатель забирает соединение без конкуренции с другими задачами — соединение никогда не попадает в общую очередь свободных соединений и достаётся ровно одному адресату.
Если передача удалась, соединение проходит проверку пригодности. При успехе соединение возвращается вызывающему. При ошибке (устаревший бэкенд) пул уменьшает текущий размер, и вызов переходит в путь создания нового соединения.
Если соединение не пришло до дедлайна, ожидающий снимается с очереди. При попытке доставки пул обнаруживает снятого ожидающего, пропускает его и пробует следующего. Таким образом устаревшие ожидающие вычищаются по мере обработки очереди, без отдельного прохода.
Дедлайн адаптивный: min(query_wait_timeout - 500 ms, adaptive_cap),
где adaptive_cap вычисляется из реальной задержки транзакций:
| Состояние пула | Бюджет | Пример |
|---|---|---|
| Холодный старт (нет данных) | 100ms ± 20% jitter | 80-120ms |
| Устойчивый режим (steady state) | xact_p99 × 2 ± 20% jitter | p99=0.7ms → 5ms (min); p99=50ms → 100ms |
| Высокая задержка | Ограничено 500ms | p99=300ms → 500ms |
Фаза 1/2 ожидания семафора тратит из того же бюджета, поэтому
суммарное ожидание не может увести клиента за его query_wait_timeout.
Случайный разброс (jitter) ±20% предотвращает обрыв таймаутов (timeout cliff): без него N клиентов, одновременно вошедших в Фазу 4, одновременно выходят из ожидания и лавиной входят в ограничитель всплесков, создавая N новых серверных соединений для пула, которому нужно значительно меньше. Со случайным разбросом клиенты выходят порциями — первые создают соединения, и к моменту выхода последних эти соединения уже использованы и возвращены в очередь свободных соединений для переиспользования.
Если дедлайн истёк без получения соединения, переходим к фазе 5.
Фаза 5 — ограничитель всплесков. Перед тем
как пойти на новый connect(), задача должна получить слот в
ограничителе — механизме, который параллельно пропускает не больше
scaling_max_parallel_creates (по умолчанию 2) задач на пул. Если
слот свободен, задача забирает его и идёт вызывать connect(). Если
все слоты заняты, задача встаёт в очередь на прямую передачу и
одновременно ожидает завершения чужого создания. Приоритет отдаётся
прямой передаче: если соединение вернулось, пока задача ждала
освобождения слота, оно доставляется напрямую — задача проверяет его
пригодность и возвращает клиенту. Если передачи не было, задача снова
пробует проверку пригодности и захват слота. Так лимитируется скорость
рождения новых серверных соединений в одном пуле, а не размер самого
пула.
Адаптивный таймаут ограничителя всплесков. Цикл ограничителя ограничен адаптивным бюджетом:
xact_p99 × 2 ± 20% jitter (min 20 ms, max 500 ms). Если задача
провела в цикле дольше бюджета, она прекращает ожидать прямую
передачу и переходит к захвату слота ограничителя напрямую. Без этого
механизма пул мог застрять на пороге прогрева навсегда: клиенты
бесконечно получали переиспользованные соединения через прямую
передачу, а до создания нового соединения дело не доходило. Счётчик
burst_gate_budget_exhausted отслеживает срабатывания.
Фаза 6 — подключение к бэкенду. Запускаем connect(), аутентифицируемся,
отдаём соединение клиенту. Слот ограничителя освобождается автоматически по
завершении этой фазы независимо от исхода.
Получение соединения в режиме независимых пулов
-----------------------------------------------
pool.get()
|
v
+--------------+
| Фаза 1: | --- есть ----> вернуть свободное соединение
| recycle pop |
+------+-------+
| нет
v
+--------------+
| Фаза 2: | --- ниже порога ---> к фазе 5
| warm gate |
+------+-------+
| выше порога
v
+--------------+
| Фаза 3: | --- есть ----> вернуть свободное соединение
| опрос |
+------+-------+
| нет
v
+--------------+
| Фаза 4: | --- передача ----> вернуть соединение
| anticipate | --- таймаут -----> дальше
| передача |
+------+-------+
|
v
+--------------+
| Фаза 5: | --- слот взят --> к фазе 6
| burst gate | --- нет слота --> ждать, повторить recycle
+------+-------+
|
v
+--------------+
| Фаза 6: |
| connect() | ----> вернуть новое соединение
+--------------+
Подавление всплеска в действии
Тот же сценарий с 200 клиентами, но теперь в режиме независимых пулов и
с scaling_max_parallel_creates = 2:
Time: t=0ms t=5ms t=10ms t=15ms t=20ms t=25ms
C_1 [idle]--[query]-[done]
C_2 [idle]--[query]-[done]
C_3 [idle]--[query]-[done]
C_4 [idle]--[query]-[done]
C_5 [spin/wait]------[recycled C_1]--[query]-[done]
C_6 [spin/wait]------[recycled C_2]--[query]-[done]
C_7 [gate=1]-[connect]----[auth]--[query]-[done]
C_8 [gate=2]-[connect]----[auth]--[query]-[done]
C_9 [gate full, wait]---[recycled C_3]--[query]
C_10 [gate full, wait]---[recycled C_4]--[query]
.
. [...196 clients use a mix of recycle, anticipation, and at
. most 2 in-flight connects...]
.
C_200 [gate=2]-[connect]--[auth]--[query]--[done]
PostgreSQL: at most 2 spawning backends at any moment
+ the 4 connections that were already there
Тот же пул обслуживает все 200 клиентов, но PostgreSQL никогда не видит
больше scaling_max_parallel_creates (по умолчанию 2) одновременных
запусков backend-процессов из этого пула. Большинство клиентов попадают на
переиспользованное соединение от соседа, который завершил работу
мгновением раньше, а не на свежий connect().
Получение соединения без ожидания
Когда клиент устанавливает query_wait_timeout = 0, он просит либо
сразу получить свободное соединение, либо запустить новый connect(),
без ожидания. Фаза упреждающего ожидания и ожидание у ограничителя
всплесков пропускаются. pg_doorman выполняет recycle на горячем пути,
один раз пробует ограничитель всплесков, затем
либо создаёт соединение, либо возвращает ошибку wait timeout.
Ограничение при включённом координаторе. Режим без ожидания
пропускает только упреждающее ожидание и ожидание у ограничителя всплесков внутри пути в
рамках одного пула. Если max_db_connections задан и фазы ожидания
координатора (B–D) занимают время, такой вызов всё равно блокируется
внутри coordinator.acquire() вплоть до reserve_pool_timeout (по
умолчанию 3000 ms). Чтобы на базах под координатором соблюсти строгий
дедлайн без ожидания, поставьте reserve_pool_timeout достаточно
низким, чтобы он помещался в ваш бюджет.
Фоновый replenish
Когда задан min_pool_size, фоновая задача периодически дополняет пул
до его минимума. Она использует тот же ограничитель всплесков, что и клиентский
трафик. Эта задача не встаёт в очередь за занятым ограничителем: если
ограничитель занят, она немедленно сдаётся и повторяет попытку
на следующем retain-цикле (по умолчанию каждые 30 секунд, контролируется
параметром retain_connections_time).
Логика такова: во время всплеска нагрузки клиенты уже насыщают ограничитель,
создавая соединения, которые им нужны прямо сейчас. Если фоновый
replenish будет конкурировать с ними за слоты, толку не будет: пул всё равно
поднимется выше min_pool_size за счёт клиентских запросов на создание.
При каждом таком отступлении фоновой задачи увеличивается счётчик
replenish_deferred.
Следствие: под нагрузкой min_pool_size поддерживается в режиме
по возможности: pg_doorman старается, но не обязуется
держать прогретыми ровно столько соединений, если клиенты съедают весь
бюджет. Если нужна жёсткая гарантия минимума, см. раздел
Диагностика.
Прямая передача при возврате
Когда соединение возвращается, пул первым делом проверяет очередь ожидающих (direct-handoff). Если хотя бы один ожидающий зарегистрирован, соединение отправляется напрямую старейшему из них, минуя очередь свободных соединений. Ожидающие, чей вызывающий уже отвалился по таймауту, пропускаются: пул обнаруживает недоступного получателя и пробует следующего в очереди.
Если зарегистрированных ожидающих нет (типичный случай при высокой пропускной способности, когда каждая выдача соединения попадает в горячий путь), соединение кладётся в очередь свободных соединений и будит ближайшего клиента, ожидающего на Фазе 1/2.
В обоих случаях координатор (если настроен) уведомляется о возврате, чтобы ожидающие Фазы C из соседних пулов могли просканировать кандидатов для выселения. Ожидающие внутри того же пула получают соединения напрямую, а не через общее уведомление.
FIFO и распределение задержки
Очередь ожидающих — двусторонняя очередь (FIFO): новые ожидающие добавляются в конец, соединения доставляются из начала. Старейший ожидающий всегда получает следующее возвращённое соединение.
Это даёт измеримо другой профиль задержки по сравнению с пулерами, использующими широковещательное пробуждение (broadcast-notify) или LIFO-планирование. При 500 клиентах на пул из 40 соединений (AWS Fargate):
| Пулер | p50 (мс) | p95 (мс) | p99 (мс) | p99/p50 |
|---|---|---|---|---|
| pg_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. SLO «p99 < 15 мс» достижим с pg_doorman при этой нагрузке. С Odyssey те же настройки пула его нарушают. Единственный выход — запас по соединениям: увеличивать число соединений, пока даже невезучие клиенты укладываются.
-
Отсутствие голодания. При broadcast-notify клиент может проиграть гонку за пробуждение многократно. При прямой передаче соединение идёт ровно одному получателю, ожидающие с истёкшим дедлайном пропускаются. Нет лавины одновременных подключений, нет повторных проигрышей.
-
Предсказуемое планирование ёмкости. Когда p50 ≈ p99, удвоение числа клиентов примерно удваивает задержку. При соотношении хвоста 25x изменение нагрузки вызывает непредсказуемые всплески p99.
Теория очередей подтверждает: среди невытесняющих дисциплин планирования FIFO минимизирует дисперсию времени ожидания при том же среднем, что и LIFO. Среднее одинаково — разница только в хвосте.
Упреждающая замена при истечении lifetime
Когда настроен server_lifetime, серверные соединения закрываются по
достижении индивидуального лимита (базовый ± 20% jitter). Закрытие
означает, что в пуле на одно свободное соединение меньше — последующие
выдачи соединения могут попасть в зону упреждающего ожидания или путь
создания нового соединения, добавляя несколько миллисекунд к p99 во
время кластеров истечения lifetime.
Упреждающая замена (pre-replacement) убирает этот всплеск задержки. Когда выдача соединения проверяет пригодность соединения и обнаруживает, что оно достигло 95% своего lifetime, фоновая задача создаёт замену и помещает её в очередь свободных соединений. Когда старое соединение отклоняется при 100% lifetime, следующая выдача соединения находит предсозданную замену через горячий путь — ноль ожидания.
Параллельно может работать до 3 упреждающих замен на пул. Во время
окна перекрытия пул временно держит max_size + 3 соединений. Когда
старые соединения умирают, текущий размер пула возвращается к
max_size.
Условия, предотвращающие неконтролируемый рост:
| Условие | Предотвращает |
|---|---|
| Пул не под давлением | Создание лишних при насыщении пула (старое соединение выживет, пропустив закрытие по lifetime) |
| Доля свободных соединений < 25% | Замену в переразмеренном пуле, который должен сжаться |
| Запас координатора >= 2 | Захват последнего слота координатора у соседнего пула |
| Lifetime >= 60 s | Срабатывание на коротких lifetime, где окно перекрытия слишком мало |
| Текущий размер <= max_size + cap | Накопление нескольких параллельных превышений |
| Лимит параллельных фоновых создаваемых (cap=3) | Неограниченное число фоновых создаваемых соединений |
Упреждающая замена срабатывает только при выдаче соединения (при проверке пригодности), не из фоновой задачи обслуживания. Свободные соединения, истекающие без выдачи клиенту, закрываются фоновой задачей без замены — так пул естественно сжимается при падении нагрузки.
Согласование лимита с PostgreSQL
Перед чтением про координатор проверьте, что число серверных соединений
в худшем случае умещается в PostgreSQL. Без max_db_connections верхняя
граница для одной базы:
N pools (users) × pool_size = верхняя граница серверных соединений
Пример с расчётом: три пула, у каждого pool_size = 40, без
max_db_connections. В худшем случае получится 120 одновременных
серверных соединений к этой базе, ограниченных только
scaling_max_parallel_creates на пул (по умолчанию 2 на каждый, то
есть до 6 одновременно идущих вызовов connect()). Если в PostgreSQL
выставлен max_connections = 100, база начнёт отказывать в новых
соединениях во время общего всплеска нагрузки, и клиенты получат
FATAL: too many connections.
Два решения:
- Понизить
pool_sizeтак, чтобыN × pool_sizeукладывалось нижеmax_connectionsс запасом наsuperuser_reserved_connections, слоты репликации и любые прямые коннекторы в обход pg_doorman. - Задать
max_db_connectionsкак жёсткий лимит (см. следующий раздел).
Эмпирическое правило: суммарная потребность pg_doorman не должна
превышать 80% от PostgreSQL max_connections - superuser_reserved_connections.
Оставшиеся 20% оставьте под admin-соединения, репликацию и всплески.
Режим координатора
Режим координатора активируется, когда у пула задан max_db_connections.
Он добавляет второй слой давления поверх того, что работает внутри
каждого пула: общий семафор, который ограничивает суммарное число
серверных соединений к базе по всем обслуживающим её пользовательским пулам. Без
него единственным ограничением остаётся потолок N × pool_size из
предыдущего раздела. С max_db_connections = 80 к базе одновременно
может существовать только 80 соединений независимо от конфигурации
пулов, и координатор решает, какие пулы могут расти.
При max_db_connections = 0 (по умолчанию) координатор не создаётся.
Когда параметр задан, все механизмы режима независимых пулов, описанные выше,
продолжают работать; координатор добавляет один шаг получения
permit (разрешения на удержание слота в общем лимите на базу:
пока permit не получен, серверное соединение создавать нельзя) на
пути создания нового соединения. Переиспользование idle координатора
не касается.
Что добавляет координатор
Три вещи:
-
Жёсткий лимит общего числа соединений к базе. Если 80 уже занято, 81-й запрос ждёт или падает независимо от того, какой пул его подал.
-
Резервный пул (reserve pool). Когда общий лимит достигнут и у
reserve_pool_sizeесть свободное место, координатор сразу выдаёт permit из резерва — небольшого дополнительного пула поверхmax_db_connections, работающего как буфер под всплеск. Это Фаза R (reserve-first) в схеме ниже: ни один соседний бэкенд не закрывается, ожидания тоже нет. Резерв ограниченreserve_pool_size(по умолчанию 0, то есть выключен) и приоритизирован: голодающие пользователи (те, кто ниже своего эффективного минимума) и пользователи с большим числом ожидающих клиентов обслуживаются первыми через арбитра. -
Выселение (eviction). Запасной путь на случай, когда резерв выключен (
reserve_pool_size = 0) или уже полностью занят: координатор закрывает свободное соединение из пула другого пользователя, чтобы освободить основной слот. Кандидаты сортируются по p95 времени транзакции (по убыванию): медленные пулы отдают первыми, потому что лучше переносят стоимость пересоздания (1 ms pool wait добавляет 6.7% к 15 ms p95, но 104% к 0.96 ms p95). Излишек над эффективным минимумом — дополнительный признак среди пулов с похожим p95. Только соединения старшеmin_connection_lifetime(по умолчанию 30 000 ms) попадают в список. 30-секундный порог подавляет циклический reconnect между соседними пулами, которые по очереди воруют слоты друг у друга.Эффективный минимум для user-пула равен
max(user.min_pool_size, pool.min_guaranteed_pool_size). Оба параметра защищают соединения от выселения; побеждает больший. Эффективный минимум уменьшается только при снижении того параметра, который сейчас задаёт максимум; снижение меньшего значения ничего не меняет.
Фазы получения слота в координаторе
Когда путь внутри пула доходит до шага создания нового соединения, координатор проходит шесть фаз. Первая фаза, выдавшая permit, завершает последовательность.
Фаза A — неблокирующая попытка (try-acquire). Вернуть permit сразу, если есть свободный слот, иначе немедленно сообщить об отсутствии. Если лимит не достигнут, забираем слот и возвращаемся.
Фаза R — сначала резерв (reserve-first). Фаза A установила, что база заполнена. До
того как закрыть хоть одно соседнее серверное соединение, координатор
проверяет, есть ли место в резервном пуле (reserve_in_use < reserve_pool_size). Если есть — сразу запрашивает permit у арбитра
резерва. При успехе вызывающий получает reserve-permit без выселения,
без закрытия соседнего бэкенда и без ожидания на connection_returned.
В обычных условиях арбитр отвечает за доли миллисекунды.
Это путь, который держит p99 задержки низкой: reserve-permit стоит
одного round-trip с арбитром, тогда как старая последовательность
(Фаза B + Фаза C) могла заблокировать клиента на полный
reserve_pool_timeout, даже если в резерве было свободно. Фаза R не
работает при reserve_pool_size = 0 и проваливается в Фазу B, если
арбитр отказывает (все reserve-permit-ы уже заняты, либо идёт гонка
с другим вызывающим).
Фаза B — выселение. Выполняется, когда Фаза R не выдала permit:
reserve_pool_size = 0, либо резервный семафор был полностью занят
на момент проверки (reserve_in_use == reserve_pool_size), либо
арбитр отказал. Обходим все остальные user-пулы той же базы,
сортируем по p95 времени транзакции (по убыванию, медленные
первые) с излишком (spare) как tiebreaker, и закрываем одно
свободное соединение старше min_connection_lifetime у верхнего кандидата.
Permit выселенного соединения освобождается синхронно, слот становится
доступен сразу. Повторяем захват семафора. Если два вызова конкурируют,
проигравший переходит к следующей фазе. p95 кешируется каждые 15
секунд (stats cycle) — сканирование читает одно кешированное значение
на кандидата без блокировки гистограммы.
Фаза C — ожидание (wait). Выполняется, когда резерв отключён или полностью занят и Фаза B не нашла что выселить. Регистрируется подписка на уведомления, которая срабатывает на двух событиях:
- Был освобождён permit координатора соседнего пула — серверное
соединение физически закрыто (истёк
server_lifetime, ошибка проверки пригодности,RECONNECT), и слот семафора теперь свободен. - Соседний пул вернул соединение в свою очередь свободных соединений — слот семафора НЕ освободился, но излишек над минимумом этого соседа только что вырос.
На каждое пробуждение Фаза C сначала пытается захватить свободный слот неблокирующей проверкой, и только если дешёвый путь не сработал — пробует выселение. Пробуждение от освобождения permit-а оставляет свободный слот в семафоре — дешёвый путь берёт его, и ни один соседний бэкенд не закрывается. Пробуждение от idle-return не освобождает слот напрямую, но могло вырастить излишек соседа, поэтому повторная попытка выселения находит кандидата, которого мгновение назад не было, освобождает permit соседа, и следующая неблокирующая проверка срабатывает. Этот порядок (дешёвый путь сначала, выселение потом) закреплён регрессионным тестом: будущий рефакторинг не сможет случайно вернуть закрытия соседних бэкендов на пробуждениях от drop permit.
Ждём до reserve_pool_timeout (по умолчанию 3000 ms) либо пробуждения,
либо дедлайна. Этот таймаут применяется даже при
reserve_pool_size = 0: он задаёт бюджет всей фазы wait, а не только
окно входа в резерв. Если ваш query_wait_timeout короче, чем
reserve_pool_timeout, клиент сдаётся первым, и вы видите ошибки
wait timeout вместо более наглядной
all server connections to database 'X' are in use. Разбор симптома
есть в разделе «Диагностика».
Фаза D — повторная попытка резерва (reserve retry). Фаза R уже пробовала этот путь один раз.
Фаза D повторяет попытку после того, как Фаза C исчерпала свой
бюджет ожидания — на случай, если за время ожидания соседний
держатель reserve-permit отпустил свой permit. Запросы ранжируются по паре
(starving, queued_clients), где starving означает, что пул сейчас
ниже своего эффективного минимума. Арбитр — это отдельный фоновый
процесс, который раздаёт permit-ы резервного пула из приоритетной
очереди.
Фаза E — ошибка (error). Если Фаза D тоже не выдала permit или резерв не
настроен, клиент получает ошибку:
all server connections to database 'X' are in use (max=N, ...).
Перенос соединения из резерва в основной лимит
reserve-permit — это буфер под всплеск, а не постоянное состояние.
После того как всплеск прошёл, бэкенд с reserve-permit продолжает
жить как обычное свободное соединение, но его permit координатора
по-прежнему учитывается в reserve_in_use — даже когда
current < max_db_connections и в основном семафоре есть свободные
слоты. Без активного обслуживания SHOW POOL_COORDINATOR показывал
бы занятый резерв при том, что реальная ёмкость для всплеска пустая, и
следующему всплеску некуда расти.
retain-задача запускается каждые retain_connections_time (по умолчанию
30 секунд) и делает бухгалтерскую перестановку: для каждого пула,
который не находится под давлением (см. определение ниже), он
обходит очередь свободных соединений и для каждого бэкенда с reserve-permit пытается
забрать permit из основного семафора.
Пул считается под давлением, когда его семафор пула имеет
ноль свободных permit-ов. Одной колонки в SHOW POOLS, которая
напрямую показывала бы состояние семафора, нет, и наблюдаемые
колонки отстают от внутреннего состояния:
- Строгий признак:
sv_active == pool_size. Каждое активное серверное соединение держит permit, поэтому когда все серверы в пуле активны, все permit-ы заняты. Это направление строгое. - Слабый признак:
cl_waiting > 0означает, что как минимум один клиент находится внутриtimeout_get— это часто означает, что семафор пуст, но клиент, который уже взял permit и припарковался в Фазе 4 упреждающего ожидания или Фазе C координатора, тоже числится ожидающим. Используйте как индикатор, не как доказательство.
retain-задача пропускает пулы под давлением по двум причинам: повышение
в такой момент просто отдаёт слот ожидающему клиенту и не меняет
reserve_used, а закрытие reserve-соединения заставит этого
клиента делать свежий connect(). Очистка отработает на следующем
цикле. При успехе
reserve-permit возвращается в резервный семафор, reserve_in_use
уменьшается на единицу, а тип permit у этого бэкенда переключается с
резервного на основной. Никакого переподключения, никакого дёргания
соседа. Обход прерывается на первом неудачном повышении в пуле: это
доказывает, что основной семафор заполнен, и остальные reserve-permit-ы
этого пула проверять бессмысленно. Тот же retain-цикл затем закрывает
reserve-бэкенды, которые не удалось повысить до основного и которые
простаивают дольше min_connection_lifetime.
При такой схеме reserve_in_use > 0 означает ровно одно: либо
всплеск сейчас идёт, либо он закончился не более чем
retain_connections_time назад. Исторический остаток резервной ёмкости
сходится к нулю, как только в основном лимите появляется свободное место.
Получение слота координатора по требованию
Внутри пути получения соединения ограничитель всплесков работает до
координатора. Это порядок just-in-time (JIT): permit координатора
берётся только тогда, когда вызов уже занял слот ограничителя всплесков и готов вызвать
connect().
Предыдущий порядок (координатор первый, потом ограничитель) вызывал
фантомные permit-ы (phantom permits): N вызовов захватывали по
permit координатора и вставали в очередь за ограничителем всплесков
(cap=2). Реально соединения создавали только 2 вызова, но координатор
видел N permit-ов в использовании и начинал выдавать reserve-permit-ы,
хотя БД была далеко от предела.
С JIT-порядком в каждый момент permit-ы координатора держат не более
max_parallel_creates вызовов.
Остальные ждут слот ограничителя без расходования бюджета координатора.
Блокировка головы очереди (head-of-line blocking) снимается разделением координатора на быстрый и медленный путь. Быстрый — неблокирующая проверка доступности слота координатора внутри слота ограничителя (мгновенно). Если не прошла — вызов освобождает слот ограничителя, ждёт координатора (выселение / возврат от соседа), и затем снова занимает слот ограничителя.
Координатор + получение соединения (JIT)
----------------------------------------
pool.get()
|
v
Фаза 1: recycle --- есть ---> вернуть
| нет
v
Фаза 2: порог прогрева --- ниже ----+
| выше порога |
v |
Фаза 3: быстрый опрос --- есть ---> вернуть
| нет |
v |
Фаза 4: прямая передача --- есть ---> вернуть
| дедлайн |
v |
| <----------------------------------+
v
Фаза 5: ограничитель всплесков (scaling_max_parallel_creates)
| слот получен
v
+-------------------------------+
| JIT-захват координатора | только при max_db_connections > 0
| fast: неблокир. проверка | мгновенный ответ
| slow: отпустить слот | ожидание координатора (evict/return)
| → взять заново | затем продолжить create
+------------+------------------+
| permit получен
v
Фаза 6: server_pool.create()
|
v
return new connection
Фазы пронумерованы так же, как в режиме независимых пулов. Захват координатора
работает внутри слота ограничителя всплесков, когда max_db_connections > 0.
В режиме независимых пулов он не работает.
Когда координатор настроен, но лимит не достигнут
Если max_db_connections = 80, а текущее использование 30, фаза A
координатора всегда успешна. Фазы B–E никогда не запускаются. Поведение
идентично режиму независимых пулов плюс одна атомарная инкрементация семафора на
каждое новое соединение. Горячий путь (повторное использование свободного соединения)
координатора вообще не касается, поэтому там у него нет измеримой
стоимости. Платят только новые создания соединений, и платят ровно
одной атомарной операцией.
По устройству координатор работает как лимит, а не очередь: он стоит ресурсов только когда вы упираетесь в потолок.
Фоновое пополнение под координатором
replenish берёт permit координатора через неблокирующую проверку
доступности. Если база уже на лимите, replenish сдаётся и
повторяет попытку на следующем retain-цикле. Та же логика, что и у
ограничителя всплесков: фоновая задача не должна бороться с клиентским трафиком
за скудные permit-ы.
Параметры тюнинга
Параметры масштабирования по умолчанию глобальные. Для
scaling_warm_pool_ratio и scaling_fast_retries можно задать
переопределение в каждом пуле. scaling_max_parallel_creates
настраивается только глобально, переопределений на уровне пула для
него нет.
| Параметр | По умолчанию | Где | Что делает |
|---|---|---|---|
scaling_warm_pool_ratio | 20 (процент) | general, пул | Порог, ниже которого соединения создаются без упреждающего ожидания. Ниже pool_size × ratio / 100 каждый запрос нового соединения идёт сразу к connect(). |
scaling_fast_retries | 10 | general, пул | Число быстрых повторных проверок пригодности перед переходом к прямой передаче (ожиданию возврата от соседа). |
scaling_max_parallel_creates | 2 | general | Жёсткий лимит одновременно идущих connect() к бэкенду на пул. Задачи сверх лимита ждут возврата свободного соединения или завершения чужого создания. Должен быть >= 1. |
max_db_connections | не задан (выключено) | пул | Лимит суммарного числа серверных соединений к базе по всем пользовательским пулам. Когда не задан, координатор не создаётся. |
min_connection_lifetime | 30000 (ms) | пул | Минимальный возраст свободного соединения, после которого координатор может выселить его в пользу другого пула. 30-секундный порог подавляет циклический reconnect между соседними пулами. |
reserve_pool_size | 0 (выключено) | пул | Дополнительные permit-ы координатора поверх max_db_connections, выдаваемые по приоритету при исчерпании основного пула. |
reserve_pool_timeout | 3000 (ms) | пул | Максимальное время Фазы C перед повторной попыткой резерва или ошибкой. |
min_guaranteed_pool_size | 0 | пул | Минимум на пользователя, защищённый от выселения координатором. Соединения пользователя, у которого current_size <= min_guaranteed_pool_size, не могут быть выселены другими пользователями. |
Когда повышать scaling_max_parallel_creates
Повышайте, если:
burst_gate_waitsустойчиво растёт между скрейпами, иreplenish_deferredтоже ненулевой: клиентский трафик и фоновая задача оба борются за слоты, которых нет;connect()к бэкенду быстрый (< 50 ms), и у PostgreSQL есть запас поmax_connections;- скачки задержки соединения совпадают с ростом частоты
burst_gate_waits.
Жёсткий потолок. Не поднимайте scaling_max_parallel_creates выше
ни одного из этих лимитов:
pool_size / 4для самого маленького пула, в котором используется этот параметр. Выше этого значения лимит теряет смысл: одновременно создаётся половина пула, и сглаживание всплеска ломается.(PostgreSQL max_connections - superuser_reserved_connections) / (10 × N pools), гдеN poolsобозначает все пулы, делящие этот инстанс PostgreSQL. Выше этого значения суммарное число одновременных подключений превышает то, что бэкенд успевает обрабатывать, не переполняя очередьaccept().
Понижайте, если:
connect()к PostgreSQL дорогой (> 200 ms, например, SSL с проверкой сертификата или медленный поиск вpg_authid);- в логах PostgreSQL появляется конкуренция за
pg_authid; - бэкенд жалуется на переполнение очереди
accept().
Симптом слишком низкого значения: частота burst_gate_waits растёт
быстрее, чем частота прихода клиентов. Симптом слишком высокого:
задержка connect() к PostgreSQL растёт, и возвращается шторм
подключений: много одновременных connect() к бэкенду после
лавины клиентских запросов.
Размер при множестве пулов. Суммарный потолок одновременных
подключений равен N pools × scaling_max_parallel_creates. Если за
одним PostgreSQL стоит 10 пулов и в любой момент суммарно по ним всем
вам нужно не более 8 одновременных подключений к PostgreSQL, поставьте
scaling_max_parallel_creates около desired_aggregate / N pools,
округляя вниз. Ниже 1 нельзя; если арифметика даёт меньше единицы,
уменьшайте N pools, объединяя пользователей.
Когда повышать scaling_warm_pool_ratio
Повышайте, если:
- пулы медленно прогреваются на старте, и
min_pool_sizeне используется; - клиенты ждут anticipation в момент, когда пул в основном пуст (anticipation активируется только выше порога прогрева, поэтому такого быть не должно, но более высокий ratio дополнительно сужает окно, в котором это может случиться).
Понижайте, если:
- пулы избыточны по размеру, и вы хотите, чтобы anticipation начинала подавлять создания на меньших значениях текущего размера.
Этот параметр редко требует вмешательства. Значение по умолчанию 20% подходит большинству нагрузок.
Когда задавать max_db_connections
Задавайте, если:
- один хост PostgreSQL обслуживает несколько пулов
(database, user), и суммаpool_sizeпо всем пулам превышаетmax_connectionsбазы; - нужен жёсткий потолок, который сохранится даже при неправильной конфигурации какого-то отдельного пула;
- нужна общая честность между пулами через eviction.
Оставляйте незаданным, если:
- один пул обслуживает одну базу, и
pool_sizeостаётся единственным ограничением; - вам не нужно выселение между пулами (некоторые нагрузки предпочитают жёсткую изоляцию между пользователями).
reserve_pool_size и reserve_pool_timeout
Резерв работает как временный клапан переполнения, а не как
дополнительная постоянная ёмкость. Он предотвращает видимые клиенту
ошибки исчерпания во время коротких всплесков. В нормальном режиме
reserve_in_use большую часть времени должен быть равен нулю.
Эмпирическое правило размера: reserve_pool_size ≤ 0.25 × max_db_connections.
За этим порогом резерв перестаёт вести себя как буфер. Если половина
нагрузки постоянно лежит в резерве, поднимайте max_db_connections,
а не расширяйте клапан переполнения.
reserve_pool_timeout задаёт, сколько клиент ждёт в Фазе C координатора
перед повторной попыткой резерва или ошибкой. Значение 3000 ms по умолчанию консервативное.
Понижайте его, если ваш query_wait_timeout короткий: в этом случае
лучше быстро дойти до повторной попытки резерва или ошибки, чем
блокировать клиентов на ожидании координатора.
Рецепт тюнинга: снизить p99 выдачи соединения на базе под координатором
Профиль нагрузки: PostgreSQL отвечает за ~1 ms (низкий p99 длительности запроса), но клиенты видят p99 задержки выдачи соединения 100–500 ms на базе под координатором. Задержка идёт от координатора, а не от PostgreSQL.
- Подтвердите фазу. Запустите
SHOW POOL_COORDINATORв момент всплеска задержки. Вычислитеmain_used = current - reserve_used:currentвключаетreserve-permit-ы, а рецепт зависит от того, заполнен ли именно основной семафор.main_used == max_db_connиexhaustionsне растёт → доминирует фаза ожидания. Клиент тратит свой бюджет в Фазе C перед переходом в Фазу D. Переходите к шагу 2.main_used < max_db_connбез exhaustions → задержка идёт не от координатора. СмотритеSHOW POOL_SCALINGcreate_fallbackи раздел «Диагностика» для режима независимых пулов.
- Включите reserve-first, если он ещё не включён. Задайте
reserve_pool_sizeкак минимумmax(2, 0.1 × max_db_connections). Reserve-first выдаёт permit меньше чем за миллисекунду, когда в резерве есть место, так что клиент, раньше сидевший в Фазе C, платит только один round-trip к арбитру. - Уменьшите
reserve_pool_timeoutдо2 × p99 задержки запроса, но не ниже. Для запроса в 1 ms нижняя граница обычно 20 ms; начните с 50 ms и неделю наблюдайтеreserve_acqиevictions. - Оставьте
min_connection_lifetimeна дефолте 30 000 ms, если у вас нет явной цели ускорить кросс-пуловую ребалансировку; понижение увеличивает частоту выселений и повторных подключений.
За чем следить после каждого изменения (все в SHOW POOL_COORDINATOR):
| До | После | Вердикт |
|---|---|---|
reserve_acq не растёт | reserve_acq растёт | Reserve-first подхватил — задержка выдачи соединения должна упасть; ожидаемо |
evictions стабилен | evictions падает | Фаза B перестала срабатывать, потому что Фаза R ловит вызывающего раньше; ожидаемо |
exhaustions 0 | exhaustions > 0 | Перетянули: reserve_pool_timeout ниже реального времени возврата от соседа |
reserve_used колеблется > 0 | reserve_used возвращается к 0 за 30 с | Путь повышения из резерва в основной лимит работает; делать ничего не надо |
Если p99 выдачи соединения не упал после шагов 2–3, путь не ограничен
координатором. Перечитайте SHOW POOL_SCALING по пострадавшему пулу:
create_fallback > 0 означает, что сам пул не может обслужить нагрузку
из возвратов, и лечить нужно pool_size, а не reserve_pool_size.
Нижняя граница. Не опускайте reserve_pool_timeout ниже
2 × ваш p99 задержки запроса. Ниже этого порога фаза ожидания всегда
истекает раньше, чем соседний пул вернёт соединение, и резерв
превращается из клапана переполнения в обязательный permit для
каждого нового соединения. reserve-permit-ов по замыслу мало, и
использовать их как постоянный источник означает сломать их
назначение.
Ловушка: query_wait_timeout < reserve_pool_timeout. Когда дедлайн
клиента короче фазы ожидания координатора, клиент сдаётся первым, и вы
видите ошибки wait timeout вместо более наглядной
all server connections to database 'X' are in use. Фазы wait и
reserve координатора отрабатывают полностью, но к этому моменту
клиента, которому нужен результат, уже нет. На старте валидатор
конфига pg_doorman выдаёт предупреждение; реагируйте на него.
Мониторинг
pg_doorman экспортирует состояние давления на пул через admin-консоль и через Prometheus. Оба показывают одни и те же счётчики; выбирайте то, что подходит вашему стеку мониторинга.
Консоль администратора: SHOW POOL_SCALING
Счётчики пути упреждающего ожидания и ограничителя всплесков в разрезе каждого пула.
Подключитесь к admin-базе pgdoorman и выполните:
pgdoorman=> SHOW POOL_SCALING;
| Колонка | Тип | Значение |
|---|---|---|
user | text | Пользователь пула |
database | text | База пула |
inflight | gauge | Вызовы connect() к бэкенду, выполняемые в этом пуле прямо сейчас. Ограничено scaling_max_parallel_creates. |
creates | counter | Сколько всего серверных соединений пул начинал создавать с момента старта. В паре с gate_waits используется для расчёта частоты попаданий в ограничитель. |
gate_waits | counter | Сколько раз вызов наткнулся на заполненный ограничитель всплесков и был вынужден ждать слот. Высокие значения говорят, что scaling_max_parallel_creates слишком низкий. |
gate_budget_ex | counter | Сколько раз адаптивный бюджет ограничителя истёк, и клиент перестал ждать прямую передачу перед созданием соединения. |
antic_notify | counter | Попытки упреждающего ожидания в Фазе 4, где прямая передача удалась. Инкрементируется один раз на успешное получение, до проверки пригодности. Высокий antic_notify при низком create_fallback — хороший признак: прямая передача ловит возвраты, клиенты не платят за connect(). |
antic_timeout | counter | Попытки упреждающего ожидания в Фазе 4, где ожидание истекло без получения соединения, либо бюджет был нулевой. Инкрементируется ровно один раз при каждом провале Фазы 4 в путь создания. Высокий antic_timeout означает, что клиенты упираются в query_wait_timeout, не успев получить соединение через прямую передачу. |
create_fallback | counter | Фаза 4 не получила соединение через прямую передачу: дедлайн исчерпан или бюджет был нулевой. Именно эти ожидания превращаются в новый connect(). Стабильно ненулевой create_fallback значит, что клиентского бюджета не хватает на перехват возвратов: пул либо мал, либо запросы длиннее query_wait_timeout. |
replenish_def | counter | Запуски фонового replenish, упёршиеся в лимит ограничителя всплесков и отложенные до следующего retain-цикла. Устойчиво ненулевые значения означают, что min_pool_size нельзя поддержать при текущей нагрузке. |
Все счётчики монотонные с момента старта. Считайте дельты между скрейпами; абсолютные значения полезны только для расчёта соотношений.
Консоль администратора: SHOW POOL_COORDINATOR
Состояние координатора в разрезе каждой базы. Присутствует только
для баз с max_db_connections > 0.
pgdoorman=> SHOW POOL_COORDINATOR;
| Колонка | Тип | Значение |
|---|---|---|
database | text | Имя базы |
max_db_conn | gauge | Сконфигурированное max_db_connections |
current | gauge | Сколько всего серверных соединений сейчас удерживается под этим координатором (по всем пользовательским пулам) |
reserve_size | gauge | Сконфигурированное reserve_pool_size |
reserve_used | gauge | Сколько reserve-permit-ов используется прямо сейчас. Сходится обратно к 0, когда в основном лимите есть свободное место — retain-задача каждые retain_connections_time повышает свободные reserve-permit-ы до основных. Устойчивое ненулевое значение означает либо активный всплеск, либо базу, постоянно упёртую в max_db_connections. |
evictions | counter | Сколько раз координатор выселил свободное соединение соседнего пула, чтобы освободить слот. С включённым reserve-first этот счётчик растёт только при реальном кросс-пуловом давлении — когда резерв заполнен и у соседа есть что выселить. |
reserve_acq | counter | Сколько всего reserve-permit-ов выдал арбитр (Фаза 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 нет. Алерты здесь должны молчать.
После всплеска, повышение в процессе:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
mydb | 80 | 65 | 10 | 3 | 0 | 12 | 0
Всплеск занял большую часть max_db_connections и три соединения
перелились в резерв. current < max_db_conn означает, что в основном лимите
есть место, поэтому retain-задача повысит эти три permit-а до основных
на следующем цикле; reserve_used должен упасть до 0 в течение
retain_connections_time (по умолчанию 30 секунд). Если не падает,
смотрите раздел «Диагностика» ниже. evictions = 0 и
reserve_acq > 0 вместе подтверждают, что reserve-first поглотил
всплеск без закрытия соседних бэкендов.
Устойчивая перегрузка:
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
----------+-------------+---------+--------------+--------------+-----------+-------------+-------------
mydb | 80 | 95 | 20 | 15 | 300 | 500 | 0
Основной лимит полностью занят (main_used = current - reserve_used = 80,
равно max_db_conn), резерв использован на 75%, много выселений,
много выдач из резерва. База не просто иногда под давлением — она
постоянно недоразмерена и выживает только за счёт того, что
выселение ротирует соединения между пользователями, а reserve-first
поглощает каждого нового вызывающего. exhaustions = 0 означает,
что арбитр пока успевает, но любой переходный пик опрокинет
ситуацию. Действие: поднимите max_db_connections (сначала
проверьте запас PostgreSQL) или найдите жадный пул через
SHOW POOLS и уменьшите его pool_size.
Метрики Prometheus
Два семейства метрик в разрезе пула и два в разрезе координатора. Все
четыре живут в namespace pg_doorman_pool_scaling* и
pg_doorman_pool_coordinator*.
| Метрика | Тип | Лейблы | Источник |
|---|---|---|---|
pg_doorman_pool_scaling{type="inflight_creates"} | 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 — сработал адаптивный таймаут, клиент перешёл к созданию |
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).
Каждый алерт ниже содержит блок Проверка с одной диагностической командой и двумя-тремя ветвями, привязанными к конкретным значениям счётчиков.
Исчерпан лимит координатора (page). Клиент получил ошибку "database exhausted". Жёсткий отказ: не сработали ни резерв, ни выселение.
rate(pg_doorman_pool_coordinator_total{type="exhaustions"}[5m]) > 0
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
current — это суммарное число соединений main + reserve
(current == max_db_conn + reserve_size означает, что оба семафора
пусты).
current == max_db_conn + reserve_size→ оба семафора полностью пусты. Поднимитеmax_db_connections(сначала проверьте, что у PostgreSQL есть запасmax_connections) или увеличьте резерв.reserve_size == 0иcurrent == max_db_conn→ резерв выключен, main полностью занят. Задайтеreserve_pool_size, чтобы поглощать всплески, и, еслиexhaustionsпродолжит расти, поднимайтеmax_db_connections.current < max_db_conn + reserve_size, ноexhaustionsрастёт → гонка в Фазе R/D, такого не должно быть устойчиво; заведите баг со снапшотомSHOW POOL_COORDINATOR.- В
SHOW POOLSу одного пользователяsv_idleсильно больше, чем у остальных → один пул захватывает соединения. Уменьшите егоpool_sizeили задайтеmin_guaranteed_pool_size, чтобы защитить соседей.
Ограничитель всплесков насыщен (warn). Ограничитель ждёт чужие создания
чаще, чем проходит напрямую. Короткие всплески выше порога при
failover или рестарте нормальны. Устойчивые значения означают,
что scaling_max_parallel_creates слишком низкий.
rate(pg_doorman_pool_scaling_total{type="burst_gate_waits"}[5m])
> 0.5 * rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m])
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
inflight_createsсидит на лимите и вSHOW POOLSвидны клиенты вcl_waiting→connect()медленный со стороны бэкенда. Смотрите раздел «Ограничитель всплесков как узкое место даже при низком трафике» перед тем, как поднимать лимит.inflight_createsходит ниже лимита, ноgate_waitsрастёт → много коротких всплесков. Поднимайтеscaling_max_parallel_creates, оставаясь в пределах потолка из раздела тюнинга.- Горит только один пул → рассмотрите
min_guaranteed_pool_sizeдля соседей или уменьшитеpool_sizeу горячего.
Частый переход к созданию после упреждающего ожидания (warn). Фаза 4 упреждающего ожидания
сдаётся, не поймав возврат, и проваливается в свежий connect().
В устойчивом состоянии должно быть близко к нулю.
rate(pg_doorman_pool_scaling_total{type="create_fallback"}[5m]) > 0.1
and
rate(pg_doorman_pool_scaling_total{type="creates_started"}[5m]) > 0.1
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW STATS' | grep -E 'database|avg_xact_time|avg_query_time'
create_fallbackвысокий на одном пуле иavg_xact_timeэтой базы растёт → долгие запросы держат соединения вне ротации. Сначала лечите долгий запрос; пул рассчитан на обычную длительность, а не на эту транзакцию.create_fallbackвысокий у всех пулов иcreates_startedтоже растёт → нагрузка превышает то, что возвраты могут обслужить в пределах дедлайна. Поднимайтеpool_size.create_fallbackвысокий, ноquery_wait_timeoutкороткий (< 1 секунды) → дедлайн упреждающего ожидания (query_wait_timeout − 500 msс верхней границей 500 ms) слишком короткий даже для нормальных возвратов. Поднимайтеquery_wait_timeoutминимум до2 × p99 длительности запроса.
Фоновое пополнение постоянно откладывается (warn). Фоновая задача не
может поддерживать min_pool_size, потому что ограничитель всплесков занят
клиентским трафиком.
increase(pg_doorman_pool_scaling_total{type="replenish_deferred"}[1h]) > 60
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_SCALING'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
- Пострадавший пул показывает
sv_idle + sv_active < min_pool_sizeодновременно с растущимgate_waits→ replenish проигрывает клиентскому трафику. Поднимайтеscaling_max_parallel_creates, чтобы у фоновой задачи был запас, или принимайте defer как косметическую проблему (под нагрузкой клиенты сами поднимут пул вышеmin_pool_size). inflight_createsстабильно стоит на лимите → ограничитель полон по другой причине (медленныйconnect()); сначала это.
Резервный пул постоянно используется (warn). Метрика reserve-permit
не возвращался к нулю в течение 15 минут. retain-задача повышает
свободные reserve-permit-ы до основных каждые retain_connections_time
(по умолчанию 30 секунд), поэтому алерт означает, что путь повышения
не в состоянии отработать или получить успех, а не что он забыл
запуститься.
min_over_time(pg_doorman_pool_coordinator{type="reserve_in_use"}[15m]) > 0
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
Вычислите main_used = current - reserve_used из строки — current
это суммарное число permit-ов main и reserve, а не только main.
main_used == max_db_conn→ основной лимит полностью занят, повышению негде забрать слот. База недоразмерена; поднимайтеmax_db_connections.main_used < max_db_connи каждый пул вSHOW POOLSимеетsv_active == pool_size(илиcl_waiting > 0как индикатор) → все пулы под давлением, retain-задача пропускает повышение. Поднимайтеpool_sizeу того пула, у которого самый высокийcl_waitingили самое плотное соотношениеsv_active / pool_size.main_used < max_db_connи ни у одного пула нет ни того, ни другого признака, а метрика всё равно ненулевая → заведите баг со снапшотамиSHOW POOL_COORDINATORиSHOW POOLS; этого не должно быть.
Координатор приближается к лимиту (warn). Ранний сигнал: координатор подходит к потолку.
pg_doorman_pool_coordinator{type="max_connections"} > 0
and
pg_doorman_pool_coordinator{type="connections"}
/ pg_doorman_pool_coordinator{type="max_connections"} > 0.85
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOLS'
currentмонотонно растёт часами → проблема планирования ёмкости. Поднимайтеmax_db_connections(сначала проверьте запас PostgreSQL) до следующего всплеска.currentколеблется рядом с лимитом → burst-driven. Поднимайтеreserve_pool_size, чтобы всплески поглощались без касанияmax_db_connections, потом следите заreserve_acq.- Один пул доминирует в
SHOW POOLS(sv_active + sv_idleсильно больше, чем у соседей) → один пул захватывает соединения; уменьшите егоpool_sizeили задайтеmin_guaranteed_pool_sizeсоседям.
Создание соединений застряло на лимите (warn). inflight_creates, сидящий на
сконфигурированном лимите больше 5 минут, означает, что вызовы
connect() не завершаются.
min_over_time(pg_doorman_pool_scaling{type="inflight_creates"}[5m])
>= 2 # подставьте своё scaling_max_parallel_creates
Проверка:
time psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c 'SELECT 1'
psql -h $PG_HOST -p $PG_PORT -c \
"SELECT state, count(*) FROM pg_stat_activity GROUP BY state"
time psqlпоказываетconnect()> 500 ms → подключение к PostgreSQL медленный. Проверьтеpg_stat_sslна стоимость SSL-handshake,pg_authidна конкуренцию за поиск роли и время DNS-резолва с хоста pg_doorman.pg_stat_activityпоказывает много сессий вstartupилиauthenticating→ бэкенд спавнит процессы, но не разгружает очередь handshake. Вероятно, упёрлись вmax_connectionsна стороне базы — выполнитеSELECT setting FROM pg_settings WHERE name = 'max_connections'и сравните с активными сессиями.pg_stat_activityпо пользователю pg_doorman пустой → сетевая проблема или firewall между pg_doorman и PostgreSQL.
Координатор постоянно выселяет соединения (warn). Лимит исчерпан и идут выселения: координатор постоянно закрывает соседние соединения, чтобы освободить место. Недоразмеренный пул, а не "иногда под давлением".
pg_doorman_pool_coordinator{type="connections"}
/ pg_doorman_pool_coordinator{type="max_connections"} > 0.95
and
rate(pg_doorman_pool_coordinator_total{type="evictions"}[5m]) > 0
Проверка:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman -c 'SHOW POOL_COORDINATOR'
evictionsрастёт иreserve_used == 0→ резерв выключен или исчерпан, выселение — единственный клапан сброса. Включите / поднимитеreserve_pool_size, чтобы всплеск поглощался без закрытия соседних бэкендов.evictionsиreserve_acqрастут одновременно → резерва тоже не хватает. Поднимайтеmax_db_connectionsилиreserve_pool_size; сначала проверьтеmax_connectionsу PostgreSQL.
Чтение admin-вывода во время инцидента
Admin-консоль принимает только SHOW <subcommand>, SET, RELOAD,
SHUTDOWN, UPGRADE, PAUSE, RESUME и RECONNECT. SHOW это не
виртуальная таблица, поэтому к admin-базе нельзя выполнить SELECT.
Чтобы запрашивать счётчики в shell-пайплайнах, запускайте SHOW из
psql и обрабатывайте вывод.
Шаблоны ниже используют psql к admin-листенеру (по умолчанию
учётные данные admin/admin):
# Сначала пул с максимальной долей ожиданий на ограничителе.
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f inflight=%d defer=%d\n", $1, $2, $5/$4, $3, $10 }' \
| sort -k3 -nr | head
# Сначала пул с максимальной долей fallback после дедлайна.
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_SCALING' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $4>0 { printf "%-20s %-20s %.3f creates=%d fallback=%d\n", $1, $2, $9/$4, $4, $9 }' \
| sort -k3 -nr | head
# Координатор: базы, ближайшие к исчерпанию.
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman \
-c 'SHOW POOL_COORDINATOR' --no-align --field-separator='|' \
| awk -F'|' 'NR>1 && $2>0 { printf "%-30s %.3f used=%d/%d reserve=%d exhaustions=%d\n", $1, $3/$2, $3, $2, $5, $8 }' \
| sort -k2 -nr
Позиции полей в awk соответствуют порядку колонок, описанному выше:
для POOL_SCALING это user|database|inflight|creates|gate_waits|gate_budget_ex|antic_notify|antic_timeout|create_fallback|replenish_def,
для POOL_COORDINATOR это database|max_db_conn|current|reserve_size|reserve_used|evictions|reserve_acq|exhaustions.
Сравнение с PgBouncer
PgBouncer и pg_doorman оба пулят соединения, но давление обрабатывают по-разному.
| Аспект | PgBouncer | pg_doorman |
|---|---|---|
| Лимит размера в каждом пуле | pool_size | pool_size |
| Лимит на уровне БД, общий для пулов | max_db_connections (жёсткий лимит, без выселения; для изоляции есть переопределения pool_size на базу или на пользователя) | max_db_connections (жёсткий лимит плюс выселение между пулами и резервный пул) |
| Резервный пул | reserve_pool_size, reserve_pool_timeout | reserve_pool_size, reserve_pool_timeout (плюс приоритизация в арбитре по starving/queued) |
| Выселение между пользователями | Не поддерживается. Пользователь, удерживающий свободные соединения, морит голодом соседа, которому они нужны. | Координатор выселяет свободные соединения у пользователя с наибольшим излишком над эффективным минимумом (max(user.min_pool_size, min_guaranteed_pool_size)). |
Одновременные connect() к бэкенду в одном пуле | Однопоточный, обрабатывает события последовательно в пределах пула, вызовы connect() выпускаются по одному. | Ограничено scaling_max_parallel_creates (по умолчанию 2 на пул): не больше N одновременных подключений к PostgreSQL в пуле, лишние задачи ждут на ограничитель всплесков. |
| Упреждающее ожидание возвратов | Нет. Клиенты ждут следующего доступного соединения в порядке прихода, в пределах wait_timeout. | Возвращающееся соединение будит ровно одного из ожидающих в очереди, часто ещё до того, как будет выпущен хоть один новый connect(). |
Прогрев min_pool_size | Поддерживается на каждом такте цикла событий (без отдельной задачи replenish). | Периодический фоновый replenish (retain_connections_time, по умолчанию 30 s), который отступает, когда ограничитель всплесков занят. |
| Повторный логин после ошибки | server_login_retry (по умолчанию 15 s) блокирует новые попытки логина после отказа бэкенда. | Аналога нет. Ошибки логина бэкенда возвращаются клиенту на каждую попытку. |
| Jitter на lifetime | Нет. server_lifetime точный. | ±20% jitter на server_lifetime и idle_timeout, чтобы избежать одновременного массового закрытия. |
| Ключ поиска пула | (database, user, auth_type) | (database, user) |
| Честность между пользователями на общем лимите | First come first served на max_db_connections. | Арбитр резерва оценивает запросы по (starving, queued_clients). |
| Мониторинг давления на новые соединения | SHOW POOLS, SHOW STATS. Текущие inflight-коннекты и результаты anticipation не видны вовсе. | SHOW POOL_SCALING и SHOW POOL_COORDINATOR показывают каждый счётчик нового кодового пути. |
В промышленной эксплуатации важнее всего три различия:
-
Ограничитель всплесков. Размер пула в PgBouncer ограничивает, сколько соединений у вас есть, но не ограничивает, сколько вызовов
connect()выпускается одновременно, когда в один момент приходит много клиентов. pg_doorman ограничивает частоту одновременныхconnect()к бэкенду независимо от размера пула, поэтому внезапный всплеск трафика не превращается в шторм подключений к PostgreSQL. -
Выселение между пулами.
max_db_connectionsв PgBouncer задаёт жёсткий потолок и не умеет перераспределять. Если пользователь A держит 80 свободных соединений, а пользователю B нужно одно, но лимит уже выбран, пользователь B ждёт или падает. Координатор pg_doorman может закрыть одно из соединений A (если оно старшеmin_connection_lifetime) и отдать слот B. -
FIFO прямая передача. PgBouncer ставит клиентов в порядке прихода и отдаёт следующее свободное соединение, но PgBouncer обрабатывает события на одном потоке — под нагрузкой порядок зависит от обработчиков libevent. pg_doorman отправляет возвращённые соединения напрямую каждому ожидающему в строгом FIFO-порядке. Результат — p99/p50 в пределах 1.1x при любом числе клиентов, тогда как пулеры без строгого FIFO показывают 10-25x раздувание хвоста при той же нагрузке.
Диагностика
В логах одновременно несколько подключений к бэкенду
Симптом. В логах сервера (или в debug-логах pg_doorman) видно
5 или больше событий connect() к бэкенду в одну и ту же миллисекунду.
Кажется, что ограничитель всплесков не работает.
Причина. Либо scaling_max_parallel_creates выставлен слишком
высоко (проверьте SHOW CONFIG или ваш pg_doorman.yaml), либо
существует 5 или больше пулов, каждый из которых независимо
открывает одновременные подключения (ограничитель работает в пределах одного
пула, а не глобально).
Исправление. Понизьте scaling_max_parallel_creates. Значение по
умолчанию 2 подходит большинству нагрузок. Когда пулов много,
суммарная частота одновременных коннектов равна
pools × scaling_max_parallel_creates, и это ожидаемо. Чтобы
ограничить и сумму, задайте max_db_connections на базу; тогда
координатор поставит в очередь создания сверх лимита.
min_pool_size не поддерживается
Симптом. Пул с min_pool_size = 10 показывает sv_idle = 4 в
SHOW POOLS и держится так минутами.
Причина. Фоновый replenish откладывается, потому что ограничитель всплесков
занят клиентским трафиком. Проверьте replenish_def в
SHOW POOL_SCALING. Если он продолжает расти, replenish пропускает
каждый retain-цикл.
Исправление. Так задумано: под нагрузкой ограничитель отдан клиентским
запросам на создание. Пул дойдёт до min_pool_size, когда клиентский
трафик ослабнет. Если нужна жёсткая гарантия минимума, поднимайте
scaling_max_parallel_creates, чтобы у replenish была свободная
ёмкость, либо сократите retain_connections_time, чтобы replenish
запускался чаще.
Для transaction pooling (pool_mode = transaction) значение
min_pool_size выше, чем pool_size / 2, обычно указывает на то,
что пул мал: большинство соединений должно быть доступно для выдачи
клиентам, а не пришпилено к минимуму. Для session pooling
эвристика не работает: min_pool_size = pool_size это нормальная
настройка, чтобы держать всё состояние сессий в горячем виде.
p99 задержки растёт без видимой причины
Симптом. p99 клиентской задержки растёт, p50 остаётся ровным. Размер пула выглядит нормально, в логах нет ошибок.
Причина. Фаза 4 упреждающего ожидания (прямая передача) держит клиентов
в ожидании возврата до query_wait_timeout - 500 ms, но возвраты
приходят медленнее, чем клиент готов ждать. Ожидающие либо получают
соединение через прямую передачу, либо проваливаются в путь создания
по истечении бюджета. Проверьте create_fallback в
SHOW POOL_SCALING: если он ненулевой и растёт, клиенты переходят
к новому connect() после исчерпания бюджета Фазы 4.
Исправление. Сверьте query_wait_timeout с p95 длительности
запросов, обслуживаемых этим пулом. Два случая.
query_wait_timeoutсопоставим или короче p95: клиент не успевает дождаться освобождения соединения, каждый такой клиент платит полный бюджет ожидания. Либо поднимайтеquery_wait_timeout, либо увеличивайтеpool_size, чтобы освобождения приходили чаще.query_wait_timeoutзаметно длиннее p95, ноcreate_fallbackвсё равно растёт: пул слишком мал для своей нагрузки, все соединения заняты дольше p95 из-за конкуренции. Поднимайтеpool_size(иmax_db_connections, если задан координатор).create_fallbackнулевой, аantic_notifyрастёт пропорционально обороту пула: прямая передача работает, возвраты перехватываются, лишнихconnect()нет. Задержка приходит откуда-то ещё: PostgreSQL, сеть, клиентская сторона. ПроверьтеSHOW STATS avg_wait_time.
max_db_connections исчерпан, клиенты получают ошибки
Симптом. Клиенты видят ошибки вида all server connections to database 'X' are in use (max=80, ...).
pg_doorman_pool_coordinator_total{type="exhaustions"} растёт.
Причина. Все пять фаз координатора провалились: неблокирующая попытка не
взял слот, выселять нечего, ожидание истекло, а резерв либо исчерпан,
либо reserve_pool_size = 0.
Исправление. Пройдите по фазам по порядку.
- Сравните
currentиmax_db_connвSHOW POOL_COORDINATOR. Еслиcurrentстабильно стоит на лимите, ваша нагрузка его превышает. Либо поднимайтеmax_db_connections, либо ищите разогнавшийся пул. - Посмотрите на частоту
evictions. Если она нулевая или близка к нулю, выселение не помогает: либо свободные соединения каждого пула моложе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-permit до основного
permit держался за своим бэкендом, пока тот не доживёт до
min_connection_lifetime и retain-цикл не поймает его свободным. При
устойчивом клиентском трафике last_used() на бэкенде обновлялся
быстрее, чем min_connection_lifetime, и permit никогда не
отпускался.
Исправление. В текущих сборках это решается автоматически: retain-задача
каждые retain_connections_time (по умолчанию 30 секунд)
запускает повышение reserve-permit-ов до основных. Для каждого
reserve-бэкенда в пуле без давления permit меняется с резервного на основной,
если в основном семафоре есть свободное место. Метрика reserve_used должна
упасть до нуля в течение одного retain-цикла.
Если reserve_used всё равно не уходит, значит пул либо под
устойчивым давлением (повышение пропускается, когда пул под давлением —
и это правильно, иначе ожидающий клиент тут же заберёт освободившийся
слот), либо current == max_db_connections (нет main-слота, который
можно забрать). Оба случая означают, что база честно исчерпана;
лечение — больше ёмкости, а не обход.
Ограничитель всплесков как узкое место даже при низком трафике
Симптом. Частота gate_waits заметная, но частота creates
низкая, а inflight_creates всё время стоит на лимите.
Причина. connect() к бэкенду медленный. Каждое создание держит
слот по нескольку секунд; даже с двумя слотами вы создадите всего
около 2 / connect_seconds соединений в секунду.
Исправление. Разберитесь, почему connect() медленный со стороны
PostgreSQL (слишком много SCRAM-итераций, конкуренция за блокировки
pg_authid, медленный DNS, SSL handshake). Когда connect() станет
быстрым, ограничитель перестанет быть узким местом. Поднятие
scaling_max_parallel_creates лишь маскирует проблему и перенесёт
шторм на PostgreSQL. Сначала разбирайтесь, потом поднимайте лимит.
Один пользователь забирает весь резерв
Симптом. reserve_acquisitions_total продолжает расти. Большую
часть резервов забирает один и тот же небольшой пользователь.
Причина. Пользователь ниже своего эффективного минимума
(max(user.min_pool_size, min_guaranteed_pool_size)), и координатор
не может удовлетворить этот минимум, не выселяя соединения у соседей.
Каждый клиентский запрос от этого пользователя попадает в Фазу R
(reserve-first), как только база заполнена, и захватывает
reserve-permit — арбитр даёт starving-пользователям наивысший балл,
поэтому они выигрывают грант. Более глубокий вопрос: почему
пользователю постоянно нужны свежие соединения? Либо его pool_size
слишком мал, чтобы поглотить собственную нагрузку, либо его трафик
всплесковый, и резерв делает ровно то, для чего нужен.
Исправление. Три варианта, выбор зависит от настоящей причины:
- Если
pool_sizeпользователя действительно мал для постоянной нагрузки, поднимитеpool_sizeи (если нужно)max_db_connections, чтобы увеличенный пул туда поместился. - Если у пользователя высокий эффективный минимум, который координатор
не может удовлетворить, понизьте тот параметр, который реально
задаёт границу (проверьте оба:
user.min_pool_sizeиmin_guaranteed_pool_size). - Если трафик действительно всплесковый, а резервы ловят всплески, оставьте как есть. Кратковременное использование резерва это и есть его назначение.
Клиенты получают wait timeout, а не database exhausted
Симптом. Под давлением координатора клиенты видят ошибку
wait timeout (во внутренних логах pg_doorman она записана как
PoolError::Timeout(Wait)), но
pg_doorman_pool_coordinator_total{type="exhaustions"} стоит на нуле.
Координатор так и не объявил исчерпание, при этом каждый клиент уходит
по таймауту.
Причина. query_wait_timeout короче, чем reserve_pool_timeout.
Клиент сдаётся раньше, чем фаза ожидания координатора успевает
завершиться. Счётчик exhaustions никогда не увеличивается, потому
что координатор в итоге получает permit для запроса, у которого
ожидающего клиента больше нет.
Исправление. Либо поднимите query_wait_timeout выше
reserve_pool_timeout плюс типичное время connect(), либо понизьте
reserve_pool_timeout (в пределах нижней границы из раздела тюнинга).
На старте валидатор конфига выдаёт предупреждение для такой
конфигурации; реагируйте на него.
Восстановление пула после рестарта PostgreSQL
Симптом. Мастер PostgreSQL перезапустился (failover, краш,
плановое окно). Видна толпа клиентов, бьющих в ограничитель всплесков,
inflight_creates стоит на лимите, частота creates_started резко
растёт.
Причина. Когда pg_doorman замечает непригодный бэкенд (через
server_idle_check_timeout или упавший запрос), он повышает
reconnect-эпоху пула и сразу сливает все свободные соединения. Горячий
путь recycle становится пустым, и каждый пришедший после слива
клиент идёт по маршруту anticipation, ограничитель всплесков, connect. При
scaling_max_parallel_creates = 2 каждый пул прирастает максимум
двумя соединениями за раз, и скорость ограничена задержкой
connect() к PostgreSQL.
Как выглядит здоровое восстановление. В первые несколько секунд
inflight_creates = 2 непрерывно, creates_started быстро растёт,
burst_gate_waits растёт вместе с ней. По мере того как новые
соединения начинают циркулировать, anticipation_wakes_notify
разгоняется, а create_fallback перестаёт расти: прямая передача
ловит возвраты внутри клиентского query_wait_timeout и новые
connect() уже не нужны. За время pool_size / 2 × connect()
секунд пул возвращается в норму.
Исправление. Обычно никакого. Ограничитель всплесков делает свою
работу: гасит шторм подключений к восстанавливающемуся primary, чтобы
тот не получил сотню одновременных коннектов на и без того перегруженный
postmaster. Если connect() действительно быстрый (< 50 ms), и у
вашего max_connections есть запас, поднимите
scaling_max_parallel_creates до 4 или 8, чтобы сократить
восстановление, но оставайтесь в пределах жёсткого потолка из раздела
тюнинга.
Глоссарий
- ограничитель всплесков — ограничитель
на уровне пула, пропускающий не более
scaling_max_parallel_createsодновременных вызововconnect()к бэкенду. Задачи сверх лимита встают в очередь на прямую передачу и ожидают завершения чужого создания, пока слот не освободится. - permit координатора (coordinator permit) — разрешение на удержание одного слота в общем лимите координатора. Может быть основным (main) или резервным (reserve). Освобождается, когда серверное соединение физически уничтожается (а не когда оно возвращается в очередь свободных соединений); при освобождении слот возвращается либо в основной, либо в резервный семафор.
- эффективный минимум — нижняя граница для выселения у пользовательского пула,
равная
max(user.min_pool_size, pool.min_guaranteed_pool_size). Координатор защищает именно столько соединений на пользователя от выселения соседями. - прямая передача — механизм доставки в Фазе 4. При возврате соединение отправляется напрямую старейшему зарегистрированному ожидающему, минуя очередь свободных соединений. Соединение достаётся ровно одному адресату, конкуренции нет.
- Фаза R (reserve-first) — короткое замыкание координатора,
вставленное между Фазой A и Фазой B. Когда база заполнена, а в
резерве есть место, Фаза R выдаёт
reserve-permit напрямую через арбитра, вместо того чтобы закрывать соседний бэкенд или парковать клиента в Фазе C. - жёсткий потолок Фазы 4 — каждая выдача соединения выбирает случайный
потолок в диапазоне 300–500 ms. Верхняя граница времени ожидания
Фазы 4 упреждающего ожидания, независимо от
query_wait_timeout. Не настраивается. Случайный разброс предотвращает синхронные таймауты, вызывающие лавину запросов в ограничитель всплесков. - арбитр резерва (reserve arbiter) — отдельный фоновый процесс,
владеющий
reserve-permit-ами. Запросы на резерв ранжируются по паре(starving, queued_clients)и разгружаются из приоритетной очереди так, чтобы самые нуждающиеся пользователи получали permit первыми. - повышение из резерва в основной (reserve → main upgrade) —
периодическая бухгалтерская перестановка. Когда свободный бэкенд
удерживает
reserve-permit, а в основном семафоре есть запас, retain-задача забирает основной permit, возвращает резервный слот и переключает тип permit. Без переподключения. - излишек над минимумом (spare_above_min) — текущий размер пула минус эффективный минимум, где текущий размер — это количество всех соединений пула (активные + свободные вместе, а не только свободные). Координатор сначала сортирует кандидатов по p95 времени транзакции, а излишек использует как дополнительный признак среди похожих пулов. Само соединение, чтобы его можно было выселить, всё равно должно быть свободным — излишек выбирает пул, а не конкретное соединение.
- голодающий пользователь (starving) — пользовательский пул, у которого текущее число соединений ниже эффективного минимума. Арбитр резерва даёт starving-пользователям абсолютный приоритет перед обычными.
- пул под давлением (under pressure) — состояние, при котором все permit-ы пула заняты, то есть каждый слот сейчас используется. retain-задача пропускает повышение/закрытие на таких пулах, потому что иначе слот просто перейдёт ожидающему клиенту.
- порог прогрева (warm threshold) —
pool_size × scaling_warm_pool_ratio / 100. Ниже этого размера пул пропускает упреждающее ожидание и идёт сразу вconnect(). Выше — упреждающее ожидание активно, и пул пытается ловить возвраты, прежде чем создавать новые бэкенды.
Fallback через Patroni
Когда pg_doorman работает на одной машине с PostgreSQL и подключён через Unix-сокет, switchover в Patroni или аварийное падение PostgreSQL оставляют doorman без локального сервера. Пока Patroni не закончит promote реплики или не перезапустит локальный PostgreSQL, все клиентские запросы падают.
Fallback через Patroni перекрывает этот промежуток. Когда локальный PostgreSQL перестаёт отвечать, pg_doorman запрашивает Patroni REST API, выбирает другого члена кластера и направляет новые соединения туда. Существующие соединения к мёртвому серверу закрываются при штатной ротации.
Это краткосрочная мера. Она перекрывает 10-30 секунд, пока Patroni завершает свой failover. Когда Patroni восстановит локальный PostgreSQL (как реплику нового primary или как восстановленный primary), pg_doorman сам вернётся к локальному сокету.
Быстрый старт
Рекомендуемая схема — pg_doorman рядом с PostgreSQL на одной машине,
ходит к нему через Unix-сокет. Patroni REST API тоже на localhost,
поэтому fallback включается одной строкой в [general]:
general:
patroni_api_urls: ["http://localhost:8008"]
Каждый пул подхватывает это автоматически. Когда Unix-сокет перестаёт
отвечать, pg_doorman запрашивает /cluster, выбирает кандидата по
приоритету sync_standby > replica > leader и направляет новые
соединения на выбранный хост, пока локальный PostgreSQL не вернётся в
строй. Значения по умолчанию: период охлаждения 30s, HTTP-таймаут 5s, TCP-таймаут
5s, lifetime fallback-соединений 30s. Переопределить их можно через
параметры настройки.
Когда это помогает
Плановый switchover. DBA запускает patroni switchover --candidate node2.
Patroni промотирует node2, затем останавливает PostgreSQL на node1.
Между остановкой и тем, как Patroni перезапустит node1 как реплику node2,
doorman на node1 не имеет локального сервера. С включённым fallback следующий
клиентский запрос, не сумевший дойти до локального сокета, триггерит
/cluster lookup, и новое соединение открывается к node2.
Аварийное падение. PostgreSQL на node1 убит OOM killer. Patroni ещё
не обнаружил сбой. doorman получает connection refused на Unix-сокете,
запрашивает Patroni API и подключается к sync_standby (вероятному
следующему лидеру).
Когда это не помогает
Падение машины. Если вся машина недоступна, doorman мёртв вместе с ней. Для этого сценария нужна внешняя маршрутизация (HAProxy, patroni_proxy, DNS failover, VIP).
Ошибки аутентификации. Если PostgreSQL отклоняет учётные данные doorman, сервер жив. Fallback не активируется.
Как это работает
Штатный режим:
клиент --unix--> doorman --unix--> PostgreSQL (локальный)
Fallback:
клиент --unix--> doorman --TCP---> PostgreSQL (удалённый, из /cluster)
|
+-- GET /cluster --> Patroni API
- doorman пробует локальный Unix-сокет.
Connection refusedили ошибка сокета: doorman помечает локальный сервер как недоступный наfallback_cooldown(по умолчанию 30 секунд).- doorman отправляет
GET /clusterко всем Patroni URL из конфига параллельно и берёт первый успешный ответ. - Из списка участников doorman отбрасывает находящихся в периоде охлаждения и
делит остальных на две волны по роли: волна 1 — все
sync_standby; волна 2 — все остальные (replica + leader, в порядке discovery). - Волна 1 (строгий приоритет sync_standby). doorman параллельно
запускает
Server::startupдля каждого sync_standby, каждый подfallback_connect_timeout(по умолчанию 5 секунд). Первый sync_standby, успешно прошедший startup, выигрывает и его соединение отдаётся клиенту. Пока хоть один sync_standby ещё в процессе startup, replica/leader не учитываются — даже если replica уже готова. Цель — сохранить пишущий трафик: sync_standby это кандидат на promote с минимальной потерей данных. - Волна 2 (без приоритета). Запускается, только если все sync_standby упали (или их нет в кластере). doorman параллельно стартует остальных кандидатов под тем же per-candidate timeout; выигрывает первый, кто успешно завершит startup — replica и leader идут на равных.
- Все кандидаты исчерпаны. Если обе волны кончились без победителя, в лог
doorman пишется
all fallback candidates rejected (3 startup_error, 1 timeout)с детерминированной разбивкой по причинам. Клиент получает очищенную FATAL-ошибку —Unable to retrieve server parameters … may be unavailable or misconfigured; разбор смотрите в логе doorman. - Успешное соединение попадает в пул со сниженным lifetime
(по умолчанию 30 секунд, совпадает с
fallback_cooldown). На него действуют все обычные правила пула: лимиты координатора, idle timeout, ротация. - Последующие соединения в рамках периода охлаждения идут к тому же fallback-хосту напрямую, без повторного запроса к Patroni API. Если кэшированный host позже отказывает на startup, doorman очищает кэш и выполняет один дополнительный раунд discovery.
- Когда период охлаждения истекает, doorman снова пробует локальный сокет. Если работает — штатный режим. Если нет — цикл повторяется.
Отказ отдельного кандидата (auth, database is starting up, таймаут) ставит
кандидата в период охлаждения с экспоненциальным backoff; следующие раунды
discovery пропускают эти хосты до истечения окна.
Ограничение времени ожидания клиента
Клиент никогда не ждёт fallback дольше query_wait_timeout
(по умолчанию 5 секунд). Если дедлайн срабатывает, doorman прерывает
fallback с записью fallback: outer deadline {ms}ms exceeded в лог,
а клиент получает очищенную FATAL-ошибку — ту же, что и на любую другую
startup-time ошибку. Дедлайн мягкий: жёсткую защиту от зависаний
обеспечивает per-candidate fallback_connect_timeout, а внешний
дедлайн — это верхняя граница того, сколько клиент сам готов ждать.
Период охлаждения для отдельных хостов
Кандидат, отказавший на startup, исключается из ближайшего discovery
на fallback_connect_timeout (по умолчанию 5 секунд). Каждый
последовательный отказ того же хоста удваивает период охлаждения, с верхним
пределом 60 секунд. После окончания окна запись удаляется (lazy
cleanup на следующем discovery-цикле), счётчик сбрасывается на
следующем отказе. Это не даёт застрявшему кандидату (postgres в
recovery, постоянная ошибка auth, медленная сеть) повторно
тестироваться на каждый клиентский запрос и заваливать одновременно
и кандидата, и Patroni API.
Запись на реплике
Если fallback-хост — реплика, которая ещё не промотирована, запросы на запись получат ошибку от PostgreSQL:
ERROR: cannot execute INSERT in a read-only transaction
Запросы на чтение работают нормально. При типичном switchover sync_standby
промотируется раньше, чем doorman обнаруживает отказ, поэтому
большинство запросов на запись проходит. В худшем случае ошибки записи
длятся до истечения сниженного lifetime (30 секунд), после чего
следующее соединение через свежий /cluster найдёт нового master.
Конфигурация
Добавьте patroni_api_urls к любому пулу, который должен
использовать fallback. Без этого параметра фича отключена,
doorman работает как раньше.
pools:
mydb:
pool_mode: transaction
server_host: "/var/run/postgresql"
server_port: 5432
# Адреса Patroni REST API. Укажите минимум 2 для отказоустойчивости.
# Первый ответивший URL выигрывает; порядок не важен.
patroni_api_urls:
- "http://10.0.0.1:8008"
- "http://10.0.0.2:8008"
- "http://10.0.0.3:8008"
TOML-эквивалент:
[pools.mydb]
pool_mode = "transaction"
server_host = "/var/run/postgresql"
server_port = 5432
patroni_api_urls = [
"http://10.0.0.1:8008",
"http://10.0.0.2:8008",
"http://10.0.0.3:8008",
]
Параметры настройки
Все параметры опциональны и имеют разумные значения по умолчанию.
| Параметр | По умолчанию | Описание |
|---|---|---|
fallback_cooldown | "30s" | Сколько локальный сервер остаётся помеченным как недоступный после ошибки соединения. В течение этого окна все новые соединения идут на fallback-хост. |
patroni_api_timeout | "5s" | HTTP-таймаут запросов к Patroni API. Действует на каждый URL; так как все URL опрашиваются параллельно, реальный таймаут равен этому значению, а не умноженному на количество URL. |
fallback_connect_timeout | "5s" | Дедлайн Server::startup для каждого кандидата (покрывает TCP-connect плюс StartupMessage round-trip) и база per-host cooldown после отказа startup. Один параметр на две роли — обе имеют одинаковую семантику «кандидат не отвечает». |
fallback_lifetime | = fallback_cooldown | Время жизни fallback-соединений. Короче штатного server_lifetime, чтобы doorman быстро вернулся к локальному серверу после восстановления. |
connect_timeout ([general]) | "3s" | Дедлайн Server::startup для локального сервера, в дополнение к существующим ролям для alive-check и TCP probe. Поднимите этот параметр, если ваш локальный PostgreSQL имеет медленный startup (большой WAL replay, прогрев shared_buffers). |
query_wait_timeout ([general]) | "5s" | Внешний дедлайн всего fallback-пути. Клиент никогда не ждёт серверное соединение дольше этого значения, независимо от количества перебираемых кандидатов. |
Что указывать в patroni_api_urls
Перечислите адреса Patroni REST API ваших узлов кластера. Эндпоинт
/cluster на любом узле Patroni возвращает полную топологию кластера,
поэтому даже одного URL достаточно для перечисления всех участников.
Два и более URL рекомендуется: если первый URL указывает на ту же машину что и мёртвый PostgreSQL, он тоже не ответит. doorman опрашивает все URL параллельно и берёт первый ответ.
Prometheus-метрики
| Метрика | Тип | Описание |
|---|---|---|
pg_doorman_patroni_api_requests_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, пока локальный сервер в периоде охлаждения и пул использует fallback |
pg_doorman_fallback_host | gauge | Текущий активный fallback-хост (1 = активен). Лейблы: pool, host, port |
pg_doorman_fallback_cache_hits_total | counter | Повторное использование кешированного fallback-хоста без запроса к Patroni API |
pg_doorman_fallback_candidate_failures_total | counter | Отказ конкретного кандидата на startup. Labels: pool, reason (connect_error, startup_error, server_unavailable, timeout, other). По разбивке по reason при exhaustion видно, что произошло — auth-фейлы на всех нодах или сетевая проблема. |
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, и в Patroni
member.host, не hostname. Дедлайн startup покрывает DNS-резолв
через TcpStream::connect, но 5-секундный DNS hang съест весь
бюджет fallback_connect_timeout для конкретного кандидата прежде
чем будет проверен следующий.
Объём логов при failure storm. Per-candidate
<host>:<port> rejected (...) WARN ограничен одной строкой в
10 секунд на (pool, host, port). Подавленные строки уходят в
DEBUG. Если вы видите один WARN там, где ожидали много — это
rate-limit, не потерянные данные; проверяйте счётчик
pg_doorman_fallback_candidate_failures_total для реального
количества попыток.
Switchover whitelist и pg_doorman_fallback_host. Когда
fallback-цель меняется (cooldown истёк, retry-раунд выбрал другой
host), gauge для предыдущего (host, port) удаляется в той же
операции, что устанавливает gauge для нового. Дашборды не видят
два хоста одновременно помеченных как активные во время
переключения.
standby_leader. В standby-кластерах Patroni используется роль
standby_leader. doorman обрабатывает её как «other» (наименьший
приоритет, после sync_standby и replica). Для primary-кластерного
развёртывания это то, что нужно; если же pg_doorman работает на
standby-кластере, fallback вам, скорее всего, не нужен вообще —
писать всё равно некуда.
Связь с patroni_proxy
patroni_proxy и fallback через Patroni решают разные задачи.
patroni_proxy — TCP-балансировщик, разворачивается рядом с клиентскими приложениями. Маршрутизирует соединения к нужному узлу PostgreSQL по роли (leader, sync, async). Пулинг соединений не выполняет.
Fallback через Patroni — встроен в pooler doorman, который разворачивается рядом с PostgreSQL. Обрабатывает ситуацию, когда локальный backend умер и doorman нуждается во временной альтернативе. Пулинг соединений выполняет.
В рекомендуемой архитектуре (patroni_proxy → pg_doorman → PostgreSQL) fallback сохраняет read-трафик на уровне doorman при падении локального backend, не затрагивая маршрутизацию patroni_proxy.
Patroni Proxy
patroni_proxy — TCP-балансировщик для кластеров PostgreSQL под управлением Patroni. Слушает один или несколько портов, спрашивает у Patroni REST API кто сейчас leader / sync / async, и направляет новые соединения на нужную роль по стратегии least-connections. Не выполняет пулинг соединений, не парсит wire-протокол, не знает что за SQL внутри — это работа pg_doorman, развёрнутого ниже по цепочке.
Что он делает
- Открытие членов кластера через опрос
/clusterс интерваломcluster_update_interval(по умолчанию 3 с) и по запросуGET /update_clusters. - Маршрутизация по ролям. Каждый listen-порт привязан к одной или нескольким ролям (
leader,sync,async,any). Соединения с этого порта попадают на члена, у которого совпадает одна из указанных ролей. - Least-connections. Для портов с несколькими допустимыми членами прокси держит счётчик соединений на каждого члена и отправляет новое соединение туда, где их меньше. Обновление кластера не сбрасывает эти счётчики.
- Отбрасывает реплики со старыми данными.
max_lag_in_bytesper-port исключает членов, у которыхreplication_lag(из/cluster) выше порога. Leader по лагу никогда не исключается. - Пропускает не-running. Допускаются только члены со
state: "running";starting,stopped,crashedи узлы с тегомnoloadbalanceфильтруются.
Операционно важно следующее: при изменении топологии patroni_proxy обновляет таблицу маршрутизации только для новых соединений. Существующие TCP-соединения к ещё живому backend не трогаются. По сравнению с HAProxy + confd, где reload рвёт все соединения через затронутую backend-секцию, это значит, что cluster_update_interval не воюет с долгоживущими транзакциями.
Роли
| Роль | Описание |
|---|---|
leader | Primary / master |
sync | Синхронные standby-реплики |
async | Асинхронные реплики |
any | Любой running-член кластера |
Рекомендуемое развёртывание
graph TD
App1[Приложение A] --> PP(patroni_proxy<br/>TCP-балансировка)
App2[Приложение B] --> PP
App3[Приложение C] --> PP
PP --> D1(pg_doorman<br/>пулинг)
PP --> D2(pg_doorman<br/>пулинг)
PP --> D3(pg_doorman<br/>пулинг)
D1 --> PG1[(PostgreSQL<br/>leader)]
D2 --> PG2[(PostgreSQL<br/>sync-реплика)]
D3 --> PG3[(PostgreSQL<br/>async-реплика)]
- pg_doorman живёт на хостах PostgreSQL. Делает пулинг, ведёт кеш prepared statements и парсит протокол — работа, которой выгодна низкая задержка до локального сокета.
- patroni_proxy живёт рядом с приложением. Маршрутизирует TCP, владеет role-aware failover-решением и не лезет в дела пулера.
Если поток приложения небольшой и одного pg_doorman на кластер достаточно, схема сжимается до одного pg_doorman с включённым fallback через Patroni, а patroni_proxy можно вообще не разворачивать.
Конфигурация
Пример patroni_proxy.yaml:
# Интервал обновления кластера в секундах (по умолчанию: 3)
cluster_update_interval: 3
# Адрес HTTP API для health checks и ручных обновлений (по умолчанию: 127.0.0.1:8009)
listen_address: "127.0.0.1:8009"
clusters:
my_cluster:
# Endpoint'ы Patroni API (несколько — для отказоустойчивости)
hosts:
- "http://192.168.1.1:8008"
- "http://192.168.1.2:8008"
- "http://192.168.1.3:8008"
# Опционально: TLS-конфигурация для Patroni API
# tls:
# ca_cert: "/path/to/ca.crt"
# client_cert: "/path/to/client.crt"
# client_key: "/path/to/client.key"
# skip_verify: false
ports:
# Соединения к primary/master
master:
listen: "0.0.0.0:6432"
roles: ["leader"]
host_port: 5432
# Read-only соединения к репликам
replicas:
listen: "0.0.0.0:6433"
roles: ["sync", "async"]
host_port: 5432
max_lag_in_bytes: 16777216 # 16MB
Параметры конфигурации
| Параметр | По умолчанию | Описание |
|---|---|---|
cluster_update_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-логирование, чтобы увидеть детальный жизненный цикл соединений.
Горячая замена процесса с переносом сессий
Перед SIGUSR2 или admin-командой UPGRADE можно заменить бинарь и
конфиг на диске. Работающий процесс проверит эту комбинацию через -t,
запустит дочерний процесс с теми же аргументами запуска, передаст ему
слушающий сокет и переведёт на него новые подключения.
Старый процесс продолжает обслуживать уже подключённых клиентов и переносит свободные TCP-сессии без TLS в новый процесс через Unix-сокет. Клиент остаётся на том же TCP-соединении: вместе с файловым дескриптором передаются ключ отмены запроса, параметры PostgreSQL-сессии и кеш prepared statements.
Клиенты внутри транзакции продолжают работать на старом процессе и
переезжают, когда транзакция завершится. Для приложений это убирает
волну переподключений, а для PostgreSQL — всплеск auth/SCRAM.
TLS-сессии мигрируют только в Linux-сборке с фичей tls-migration.
Дистрибутивные пакеты и Docker-образ собраны без неё, поэтому TLS-клиенты
при такой замене дренируются и переподключаются к новому процессу.
PgBouncer (-R, устарел с 1.20, или rolling restart через
so_reuseport) и Odyssey (SIGUSR2 + bindwith_reuseport) переводят
новые подключения на новый процесс, а старые сессии оставляют в старом
до отключения клиентов. Сессии, prepared statements и TLS-состояние
между процессами не переносятся.
pg_doorman передаёт живой клиентский сокет через SCM_RIGHTS, а в
Linux-сборке с tls-migration может перенести и состояние OpenSSL.
Быстрый старт
Если pg_doorman поставлен через apt install pg-doorman или
dnf install pg-doorman, заменяйте бинарник через пакетный менеджер.
Обычный путь — apt-get install --only-upgrade pg-doorman или
dnf upgrade pg-doorman. Ручной install ниже нужен только там, где
бинарник раскладывают без пакетного менеджера.
# 1. Установите новый бинарник по пути, из которого запущен сервис,
# и при необходимости обновите конфиг на диске.
install -m 0755 pg_doorman_new /usr/bin/pg_doorman
# 2. Проверьте, что новый процесс стартует с этим конфигом, до того
# как запускать замену. SIGUSR2 тоже прогоняет `-t` и отменяет
# замену при ошибке, но проверка здесь даёт шанс починить конфиг,
# не трогая работающий сервер.
/usr/bin/pg_doorman -t /etc/pg_doorman/pg_doorman.toml
# 3. Запустите замену процесса. С `ExecReload=/bin/kill -SIGUSR2 $MAINPID`
# в юните `systemctl reload` отправляет SIGUSR2 и запускает
# горячую замену процесса. Дальше pg_doorman проверяет конфиг,
# запускает дочерний процесс, мигрирует состояние где возможно и
# дренирует старый процесс. systemd шлёт сигнал в MainPID, поэтому
# выбирается ровно один процесс, даже если на хосте есть
# другие инстансы pg_doorman. Прямой `kill -USR2 $(pgrep -f
# /usr/bin/pg_doorman)` работает, но pgrep ищет по командной
# строке и может попасть во все инстансы сразу — пакетные
# установки используют systemctl.
sudo systemctl reload pg_doorman.service
# Успешный reload означает только, что systemd отправил SIGUSR2.
# Проверка конфига, запуск дочернего процесса, MAINPID handoff и
# миграция клиентов происходят внутри pg_doorman. Проверьте их следующим
# шагом и по логам.
# 4. Проверьте: systemd подхватил новый MainPID. При Type=notify
# дочерний процесс отправляет `MAINPID=<new_pid>` во время передачи
# управления. Состояние active и админ-консоль подтверждают, что
# клиенты на месте.
systemctl show -p MainPID --value pg_doorman.service
psql -h pgdoorman -p 6432 -c 'SHOW POOLS;' # отвечает новый процесс
Если процесс не управляется systemd, читайте PID-файл, который пишет
режим демона (daemon_pid_file, по умолчанию /tmp/pg_doorman.pid),
вместо парсинга pgrep: kill -USR2 "$(cat /var/run/pg_doorman/pg_doorman.pid)".
Для запуска в foreground-режиме без systemd храните PID
процесса-супервизора и шлите сигнал ему напрямую.
Ту же замену процесса можно запустить из консоли администратора:
UPGRADE;
UPGRADE шлёт SIGUSR2 запущенному процессу — тот же путь, что и
kill -USR2. Успешный ответ команды означает, что сигнал отправлен, а
не что проверка и миграция уже закончились.
Как работает замена
SIGUSR2
|
v
+-----------------------+
| 1. Проверка конфига |
| (pg_doorman -t) | -- ошибка --> отмена
+-----------+-----------+
|
v
+-----------------------+
| 2. Запуск нового |
| socketpair() |
| inherit-fd |
| readiness pipe | — ожидание до 10 с
+-----------+-----------+
|
+-------------+-------------+
| |
v v
+---------------------+ +---------------------+
| СТАРЫЙ процесс | | НОВЫЙ процесс |
| | | |
| 3. Свободные | | migration_receiver |
| сериализация +--->+ восстановление |
| dup() + SCM_RIGHTS | запуск клиента |
| | | handle() |
| 4. Клиенты в tx | | |
| дождаться COMMIT +--->+ Принимает новые |
| мигрировать | | соединения |
| | | |
| 5. Таймер выхода | +---------------------+
| опрос 250 мс |
| выход при 0 |
+---------------------+
Фаза 1: Проверка конфига
Работающий процесс берёт путь, из которого стартовал сам, и запускает
его с -t и текущим конфигом. После install и правки конфига из
быстрого старта этот путь уже указывает на новый бинарник и новый файл
конфига, поэтому проверяется именно процесс, который должен принять
трафик. Если проверка проваливается,
замена отменяется, а старый процесс продолжает обслуживать трафик.
В логах появляется баннер:
!!! BINARY UPGRADE ABORTED - SHUTDOWN CANCELLED !!!
!!! FIX THE CONFIGURATION BEFORE ATTEMPTING BINARY UPGRADE AGAIN !!!
!!! THE SERVER WILL CONTINUE RUNNING WITH THE CURRENT BINARY !!!
Фаза 2: Запуск нового процесса
Foreground-режим:
- Создаётся Unix
socketpair()для миграции клиентов. - Файловый дескриптор слушающего сокета передаётся дочернему
процессу через
--inherit-fd. - Канал готовности: родитель ждёт до 10 секунд байт от дочернего процесса. Дочерний процесс пишет в канал, когда начинает принимать соединения.
- Родитель закрывает свой слушающий сокет — новые соединения идут в дочерний процесс.
Режим демона:
Запускается новый фоновый процесс. Старый закрывает слушающий сокет.
Миграция клиентов через socketpair() не используется: клиенты
остаются на старом процессе. По истечении shutdown_timeout старый
процесс выходит, а оставшиеся клиентские сокеты закрываются. Если
клиенты должны мигрировать в новый процесс, используйте foreground-режим.
Фаза 3: Миграция свободных клиентов (foreground-режим)
Когда установлен флаг MIGRATION_IN_PROGRESS, каждый свободный клиент
(нет активной транзакции, нет отложенного BEGIN, нет
буферизованных данных на чтение) мигрирует:
- Сериализация:
connection_id,secret_key, имя пула, username, параметры сервера, полный кеш prepared statements. dup()+SCM_RIGHTS: дескриптор клиентского TCP-сокета дублируется и передаётся новому процессу через Unixsocketpair().- Восстановление: новый процесс заново создаёт структуру клиента,
подключает к нужному пулу и запускает
handle().
Клиент не замечает миграции: нет повторного подключения, ошибки или повторной аутентификации. TCP-соединение остаётся тем же физическим сокетом.
Фаза 4: Доработка клиентов внутри транзакции
Клиент внутри BEGIN ... COMMIT продолжает работать на старом
процессе. Его серверное соединение остаётся живым. После завершения
транзакции (COMMIT или ROLLBACK) клиент становится свободным и мигрирует
на следующей итерации цикла.
Отложенный BEGIN (сервер ещё не выделен) тоже блокирует миграцию.
Клиент должен отправить запрос, тем самым материализовать BEGIN,
затем выполнить COMMIT; после этого соединение можно перенести.
Фаза 5: Таймер завершения
Таймер завершения опрашивает CURRENT_CLIENT_COUNT каждые 250 мс.
Когда все клиенты мигрировали или отключились — старый процесс
вызывает process::exit(0).
Если shutdown_timeout истекает раньше, старый процесс выходит
принудительно, а оставшиеся соединения закрываются.
Во время миграции drain_all_pools() откладывается: клиентам внутри
транзакций нужны их серверные соединения. Дренирование пулов начинается
только после завершения миграции или сброса MIGRATION_IN_PROGRESS.
Кеш prepared statements при миграции
Клиентский кеш prepared statements сериализуется при миграции:
- Ключ записи: имя statement или хеш анонимного
Parse - Хеш запроса
- Полный текст запроса
- OID типов параметров
В новом процессе:
- Каждая запись регистрируется в общем кеше пула (
DashMap). - На новых серверных соединениях ещё нет prepared statements.
- При первом
Bindк мигрированной записи pg_doorman прозрачно отправляетParseна новое серверное соединение. Клиент не видит дополнительного обмена.
Ограничения:
- Если
client_anonymous_prepared_cache_sizeнового конфига меньше, лишние анонимные записи вытесняются по LRU. Именованная часть не ограничена и переносится полностью. Оставшиеся записи работают нормально. - Анонимные prepared statements (
Parseс пустым именем) переносятся в новый процесс, но требуют повторногоParseпередBind. DEALLOCATE ALLпосле миграции очищает переданный кеш. ПовторныйParseс тем же именем использует новый текст запроса.
Миграция TLS-сессий
По умолчанию TLS-клиенты не мигрируют — зашифрованная сессия
требует ключевой материал, который живёт внутри автомата OpenSSL.
Такие клиенты дренируются при замене процесса: соединение закрывается при
истечении shutdown_timeout, клиент переподключается к новому процессу.
Опциональная сборка с tls-migration решает эту проблему. Патченный
OpenSSL экспортирует состояние симметричного шифрования, передаёт его
вместе с файловым дескриптором через Unix-сокет, а новый процесс
импортирует состояние и продолжает шифрование. Клиент не выполняет
повторное TLS-рукопожатие.
Что экспортируется
Патч добавляет SSL_export_migration_state() и
SSL_import_migration_state() в OpenSSL 3.5.5. Экспортируемые данные:
- Версия TLS-протокола
- ID набора шифров и длина тега
- Симметричные ключи для чтения/записи (входные данные для AES key schedule, не развёрнутые ключи)
- IV (nonce) для чтения/записи
- Номера записей для чтения/записи (по 8 байт)
- Для TLS 1.3: секреты клиентского и серверного трафика приложения
Этого достаточно для восстановления слоя TLS-записей в новом процессе и продолжения шифрования/дешифрования на том же TCP-соединении.
Сборка с миграцией TLS
cargo build --release --features tls-migration
Требует perl и patch в окружении сборки. Vendored OpenSSL 3.5.5
собирается из исходников с наложенным патчем.
Сборка без доступа к интернету
# Скачайте архив заранее
curl -fLO https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz
# Собрать с указанием пути
OPENSSL_SOURCE_TARBALL=./openssl-3.5.5.tar.gz \
cargo build --release --features tls-migration
SHA-256 архива проверяется автоматически.
Ограничения
- Только Linux. На macOS/Windows миграция TLS не поддерживается (native-tls использует Security.framework / SChannel, не OpenSSL).
- Одинаковые сертификаты. Старый и новый процесс должны
использовать одни и те же
tls_private_keyиtls_certificate. Состояние шифрования привязано кSSL_CTX, созданному из сертификата. Изменённые сертификаты приводят к ошибке импорта и отключению клиента. - FIPS несовместимо. Vendored OpenSSL не проходит FIPS-валидацию.
Для FIPS используйте сборку без
tls-migration(TLS-клиенты будут дренироваться вместо миграции). - Нет HSM/PKCS#11. Vendored OpenSSL собирается с
no-engine.
Известные ограничения
-
TLS 1.3 KeyUpdate меняет ключи шифрования. Если любая сторона отправит KeyUpdate после экспорта состояния шифрования, импортированные ключи станут невалидными — соединение упадёт с ошибкой AEAD.
Поведение драйверов (проверено апрель 2026):
Драйвер Автоматический KeyUpdate? Риск libpq (psql, pgbench) Нет — OpenSSL не отправляет Нет asyncpg (Python) Нет — Python ssl = OpenSSL Нет node-postgres Нет — Node.js tls = OpenSSL Нет Npgsql (.NET) Нет — SslStream без KeyUpdate API Нет pgjdbc (Java) Да — JSSE после ~128 GB ( jdk.tls.keyLimits)Высокий tokio-postgres (rustls) Да — rustls при лимите AEAD Средний PostgreSQL server Нет — renegotiation отключён, KeyUpdate не вызывается Нет Java-клиенты: JSSE автоматически отправляет KeyUpdate после ~128 GB зашифрованных данных. Баг JDK-8329548 может вызвать шторм сообщений KeyUpdate. Для Java с долгоживущими высоконагруженными соединениями миграция TLS может потерять соединения. Обходной путь: увеличить порог через
jdk.tls.keyLimitsвjava.security, или отключить TLS между Java-клиентом и pg_doorman.Rust-клиенты с rustls: rustls ротирует ключи при лимитах AEAD (очень высокий порог, ~2^36 записей для AES-GCM). Для типичных PostgreSQL-нагрузок этот порог практически недостижим. Использование
native-tls(OpenSSL) вместо rustls устраняет риск.Все OpenSSL-драйверы безопасны. OpenSSL явно не выполняет автоматический KeyUpdate (openssl#23566).
-
Данные
SSL_pendingне проверяются. Миграция происходит в точке простоя, где нет буферизованных данных приложения. Этот инвариант гарантируется состоянием клиента, но явная проверкаSSL_pending()не выполняется. -
Привязка к OpenSSL 3.5.5. Патч модифицирует внутренние структуры OpenSSL (
ssl_local.h,rec_layer_s3.c,ssl_lib.c). При обновлении OpenSSL нужно проверить и переложить патч на новую версию.
Сигналы
| Сигнал | Поведение |
|---|---|
SIGUSR2 | Горячая замена процесса + дренирование старого процесса. Рекомендуемый для всех режимов. |
SIGINT | В foreground + TTY (Ctrl+C): только завершение, без замены процесса. В режиме демона или без TTY: горячая замена процесса для совместимости со старыми установками. |
SIGTERM | Немедленный выход. Транзакции обрываются. Все клиенты отключаются. |
SIGHUP | Перечитать конфигурацию без перезапуска. Без простоя. |
UPGRADE (admin) | Отправляет SIGUSR2 текущему процессу. Тот же эффект. |
Поведение
SIGINTдля совместимости:SIGINTзапускает горячую замену процесса в режиме демона или без TTY (например, под systemd). В интерактивном терминале Ctrl+C останавливает процесс без запуска нового. Используйтеkill -USR2илиUPGRADEв консоли администратора для горячей замены в foreground-режиме.
Режим демона и foreground-режим
| Foreground | Режим демона | |
|---|---|---|
| Миграция клиентов через передачу fd | Да (socketpair) | Нет |
| Свободные клиенты сохраняются | Да | Нет (закрываются при выходе старого процесса) |
| Клиенты внутри транзакции | Завершают транзакцию, затем мигрируют | Работают до таймаута, затем закрываются |
| Запуск нового процесса | Наследует файловый дескриптор слушающего сокета | Запускается независимо |
| Рекомендуется для | systemd, контейнеры, Kubernetes | Старые установки |
Для горячей замены процесса с миграцией клиентов запускайте pg_doorman
в foreground-режиме. systemd управляет жизненным циклом процесса.
Используйте Type=notify: юнит становится active только после
сигнала готовности от pg_doorman, а новый процесс при SIGUSR2
обновляет MainPID на себя:
[Service]
Type=notify
# Дочерний процесс, который принимает трафик после SIGUSR2, должен
# отправить READY=1 и MAINPID=<new_pid> во время передачи управления.
NotifyAccess=exec
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.toml
# `systemctl reload` запускает горячую замену процесса: проверка конфига,
# новый процесс, миграция клиентов где возможно, затем дренирование
# старого процесса по shutdown_timeout из конфига pg_doorman.
ExecReload=/bin/kill -SIGUSR2 $MAINPID
# `systemctl stop` — немедленное завершение. Это не путь горячей замены,
# и он не ждёт миграции активных транзакций.
ExecStop=/bin/kill -SIGTERM $MAINPID
# При горячей замене новый дочерний процесс становится MainPID через sd_notify.
# KillMode=mixed при обычном stop шлёт SIGTERM только в MainPID, а
# оставшиеся процессы cgroup добивает SIGKILL только после TimeoutStopSec.
KillMode=mixed
TimeoutStopSec=60
# Не перезапускать сервис после чистой ручной остановки или после выхода
# старого процесса, который успешно передал трафик новому при замене.
Restart=on-failure
Nice=-15
# pg_doorman активно использует дескрипторы: каждый клиент и каждый
# бэкенд — отдельный fd, плюс служебные pipe. 65536 покрывает большинство
# OLTP-пулов; считается как `general.pool_size * num_pools` плюс
# несколько тысяч на клиентов.
LimitNOFILE=65536
# Сервисная учётка, которой принадлежит PID-файл. На многих установках
# postgres уже существует — переиспользование оставляет владение
# файлов согласованным с самим PostgreSQL.
User=postgres
Group=postgres
SyslogIdentifier=pg_doorman
systemctl reload pg_doorman отправляет SIGUSR2; нулевой код выхода
означает только, что сигнал доставлен. Затем pg_doorman прогоняет -t
на новом бинарнике, отменяет замену при битом конфиге, иначе
порождает новый процесс и дренирует старый. UPGRADE; из админ-консоли
идёт по тому же пути. Окно дренирования задаёт shutdown_timeout в
pg_doorman.toml; TimeoutStopSec относится к обычному
systemctl stop, а не к тому, сколько systemctl reload ждёт
мигрировавшие сессии.
Продакшен-установки часто добавляют сверху ограничения ресурсов:
MemoryMax=, CPUAffinity=2,3,4,5,6,7,8,9. Это зависит от нагрузки
и не меняет контракт горячей замены.
Конфигурация
shutdown_timeout
Максимальное время ожидания завершения транзакций перед принудительным закрытием соединений. Старый процесс завершается по истечении этого таймаута вне зависимости от оставшихся клиентов.
По умолчанию: 10 секунд.
Рекомендация для продакшена с длинными аналитическими запросами: 30–60 секунд.
[general]
shutdown_timeout = 60000 # миллисекунды
Слишком маленькое значение — риск оборвать активные транзакции. Слишком большое — задержка выхода старого процесса при зависшем клиенте (например, idle-in-transaction). Выбирайте значение, покрывающее самую длинную ожидаемую транзакцию, с запасом.
tls_private_key / tls_certificate
Чтобы миграция TLS отработала, оба процесса (старый и новый) должны
загрузить один и тот же сертификат и приватный ключ для входящих
клиентских TLS-соединений. Состояние шифрования привязано к SSL_CTX,
созданному из этих файлов; при несовпадении импорт падает, и затронутые
клиенты отключаются и переподключаются.
Клиентский TLS-материал не перезагружается по SIGHUP (это
делает только серверный TLS, см. горячую перезагрузку TLS).
Не совмещайте ротацию сертификата для входящих клиентских TLS-соединений
с горячей заменой, где вы ожидаете миграцию TLS-сессий. Если файлы
отличаются между старым и новым процессом, импорт TLS-состояния падает,
и затронутые клиенты переподключаются даже с включённым
tls-migration. Ротируйте этот сертификат в окно, где переподключения
допустимы, либо оставьте те же файлы на время горячей замены процесса и
меняйте сертификат позже через рестарт.
prepared_statements_cache_size
Кеш prepared statements на уровне пула. Напрямую на миграцию не влияет, но кеш в новом процессе должен быть достаточного размера для записей от мигрированных клиентов.
client_anonymous_prepared_cache_size
LRU-кеш анонимных записей на клиента. Клиентский кеш (и именованная, и анонимная части) сериализуется полностью при миграции. Если новый конфиг имеет меньшее значение, LRU вытесняет лишние анонимные записи. Именованные записи не ограничены этим параметром и мигрируют целиком.
Откат
У горячей замены процесса нет отдельного пути отмены. Для отката положите
предыдущий бинарник на тот же путь и запустите ещё одну замену через
SIGUSR2. Если проверка конфига провалится, текущий процесс продолжит
обслуживать трафик. Если новый процесс уже принял трафик, откат
проходит как обычная горячая замена в обратную сторону.
Не используйте systemctl restart или SIGTERM для отката, если
переподключения недопустимы: оба пути закрывают клиентские сессии, а не
мигрируют их.
Мониторинг
Логи
Ключевые строки в логах при миграции:
INFO Got SIGUSR2, starting binary upgrade and graceful shutdown
INFO Validating configuration with: /usr/bin/pg_doorman -t pg_doorman.toml
INFO Configuration validation successful
INFO Starting new process with inherited listener fd=5
INFO New process signaled readiness
INFO Client migration enabled
INFO [user@pool #c42] client 10.0.0.1:51234 migrated to new process
INFO waiting for 3 clients in transactions
INFO All clients disconnected, shutting down
INFO Migration sender finished
В новом процессе:
INFO migration receiver: listening for migrated clients
INFO [user@pool #c42] migrated client accepted from 10.0.0.1:51234
INFO migration receiver done: migration socket closed
INFO migration receiver: stopped
Prometheus-метрики
| Метрика | Значение при замене процесса |
|---|---|
pg_doorman_pools_clients{status="active"} | Должна упасть до 0 на старом процессе |
pg_doorman_pools_clients{status="idle"} | Падает по мере миграции клиентов |
pg_doorman_connections_total{type="total"} | Новый процесс принимает свежие подключения; используйте rate() / increase() |
pg_doorman_clients_prepared_cache_entries | Подтверждает перенос кэша |
Консоль администратора
-- На новом процессе (старый отклоняет не-admin соединения)
SHOW POOLS;
SHOW CLIENTS;
Диагностика
Клиент получил 58006 или отключился вместо миграции
Ctrl+C в foreground-режиме. SIGINT в TTY означает завершение без
горячей замены. Используйте kill -USR2 или UPGRADE в консоли
администратора.
Режим демона. В режиме демона нет миграции через файловые дескрипторы. Существующие клиенты остаются на старом процессе и закрываются, когда он выходит. Переключитесь на foreground-режим.
PG_DOORMAN_CI_SHUTDOWN_ONLY=1 установлен. Эта переменная
окружения принудительно включает режим только завершения (используется
в CI-тестах). Уберите её.
Старый процесс не завершается
Длинная транзакция. Клиент застрял в BEGIN без COMMIT.
Дождитесь shutdown_timeout или завершите транзакцию вручную.
Административные соединения. Они не мигрируют. Закройте административную сессию на старом процессе.
Принудительный выход: kill -TERM <old_pid> отправляет SIGTERM
и завершает процесс без ожидания миграции.
TLS-соединение оборвалось после замены процесса
Бинарник собран без --features tls-migration. TLS-клиенты
дренируются вместо миграции. Пересоберите с --features tls-migration.
Запуск не на Linux. Миграция TLS работает только на Linux.
Сертификат или ключ изменились. Старый процесс экспортировал состояние шифрования, привязанное к старому сертификату для входящих клиентских TLS-соединений. Если нужна миграция TLS-сессий, используйте те же файлы в обоих процессах. Ротация этого сертификата требует рестарта или окна, где переподключения допустимы.
"TLS migration not available" в логах
Новый процесс получил пакет миграции с TLS-данными, но собран
без --features tls-migration или запущен не на Linux. Клиент
отключается. Пересоберите новый бинарник с --features tls-migration.
"migration channel not ready" в логах
Канал MIGRATION_TX ещё не инициализирован. Новый процесс не
завершил запуск, когда клиент попытался мигрировать. Клиент
повторит попытку на следующей итерации простоя (через миллисекунды).
"migration channel send failed" в логах
Канал миграции переполнен (capacity: 4096). Возможно при одновременной миграции тысяч клиентов. Клиент повторит попытку на следующей итерации простоя.
"prepare_migration failed" в логах
Исходный fd клиента недоступен или dup() не удался. Возможные причины:
исчерпание файловых дескрипторов, или клиент подключился через
ветку кода, которая не сохраняет исходный fd. Проверьте ulimit -n.
Совместимость с клиентскими библиотеками: Библиотеки вроде
github.com/lib/pqили Godatabase/sqlмогут потребовать настройки для обработки переподключения, если клиент не мигрировал и получил 58006 или закрытие соединения. См. issue.
Чек-лист перед продакшеном
Перед выкатом горячей замены процесса:
-
Запуск в foreground-режиме (не
daemon) для миграции через файловые дескрипторы -
shutdown_timeoutпокрывает самую длинную ожидаемую транзакцию (рекомендация: 30–60 секунд для OLTP, больше для аналитики) -
Если используете TLS: сборка с
--features tls-migration, оба процесса используют одинаковые файлы сертификата и ключа - Протестировать замену на staging: открыть сессию, отправить SIGUSR2, убедиться что сессия продолжает работать
-
В systemd-юните указано
Type=notifyсNotifyAccess=exec,ExecReload=/bin/kill -SIGUSR2 $MAINPID(тогдаsystemctl reloadзапускает горячую замену процесса с проверкой конфига),KillMode=mixedиRestart=on-failure - Мониторинг логов на ошибки миграции после первой замены в продакшене
-
Подтвердить что старый процесс завершился (PID-файл или
pgrep) - Проверить Prometheus-метрики: клиенты на новом процессе
HTTP-слушатель, который отдаёт /metrics, использует SO_REUSEPORT. Пока
старый процесс дренируется, а новый принимает клиентов, оба процесса
делят один порт, и ядро распределяет scrape-запросы между ними. На
отдельном scrape счётчики могут выглядеть так, будто откатились назад,
пока старый процесс не завершится. Это окно длится не дольше
shutdown_timeout.
Сигналы и перезагрузка
pg_doorman реагирует на четыре POSIX-сигнала: SIGHUP, SIGINT, SIGUSR2 и SIGTERM. Каждый делает одну конкретную вещь.
Краткая справка
| Сигнал | Эффект | Существующие соединения | Когда применять |
|---|---|---|---|
SIGHUP | Перезагрузить конфиг с диска. | Сохраняются. | Подкрутить пулы, ротировать серверные TLS-сертификаты, отредактировать pg_hba.conf. |
SIGTERM | Немедленное завершение. | Закрываются. | Остановка сервиса, когда переподключения допустимы. |
SIGUSR2 | Горячая замена процесса: новый бинарь и конфиг, затем дренирование старого процесса. | Мигрируют в новый процесс, где это возможно; TLS требует tls-migration. | Раскатить изменения, которым нужен новый процесс. |
SIGINT | Зависит от TTY (см. ниже). | По-разному. | Ctrl+C при разработке; устарело для промышленной эксплуатации. |
Перезагрузка (SIGHUP)
kill -HUP $(pidof pg_doorman)
Перечитывает файл конфига и применяет изменения. Что перезагружается:
- Определения пулов (добавлены, удалены, изменён размер).
- Списки пользователей, пароли, блоки
auth_query. - Правила
pg_hba.conf(файл или встроенное содержимое). - Серверные TLS-сертификаты и CA-бандлы (подмена без блокировок; существующие TLS-соединения сохраняют исходный контекст).
- Публичные ключи Talos и JWT.
- Уровень логирования и формат логов.
Что не перезагружается:
general.host,general.port— слушающий сокет фиксируется при старте.general.tcp_socket_buffer_sizeдля уже открытых сокетов — новое значение применяется только при приёме нового клиентского TCP-сокета или открытии нового TCP-сокета к PostgreSQL.- TLS-сертификаты для входящих клиентских подключений — нужен перезапуск процесса. Не ротируйте их во время горячей замены, где нужна миграция TLS-сессий.
- Число рабочих потоков и параметры рантайма Tokio.
После SIGHUP SHOW CONFIG показывает новые значения. Уже открытые клиентские соединения не проверяются заново по pg_hba.conf; новые правила действуют только для новых подключений. Уже открытые TCP-сокеты сохраняют размер буфера, заданный при их создании.
Немедленное завершение (SIGTERM)
kill -TERM $(pidof pg_doorman)
pg_doorman логирует, сколько клиентов ещё в транзакциях, и выходит. Он
не ждёт shutdown_timeout и не мигрирует активные транзакции. Все
клиентские соединения закрываются при выходе процесса.
shutdown_timeout относится к дренированию при горячей замене процесса
через SIGUSR2, а не к обычному завершению по SIGTERM.
Горячая замена процесса (SIGUSR2)
kill -USR2 $(pidof pg_doorman)
Рекомендуемый способ заменить процесс, не теряя мигрируемых клиентов:
- Замените бинарник и/или конфиг на диске.
- Отправьте
SIGUSR2работающему процессу. - Текущий процесс проверяет новый бинарник с конфигом через
-t. - Текущий процесс порождает дочерний процесс с теми же аргументами запуска, передаёт ему слушающий сокет и продолжает обслуживать существующих клиентов до миграции или завершения.
- Новые клиенты сразу подключаются к дочернему процессу.
- Старый процесс выходит, когда клиенты мигрировали или отключились (или по
shutdown_timeout).
Дочерний процесс отправляет sd_notify MAINPID=<new_pid>, чтобы юниты
systemd с Type=notify корректно отслеживали новый главный PID.
Если при горячей замене изменён general.tcp_socket_buffer_size, новый
процесс применяет его к мигрировавшим клиентским TCP-сокетам при
восстановлении клиента. Backend-сокеты через SCM_RIGHTS не передаются:
новые backend-соединения получают настройку при открытии, а старые
остаются в старом процессе до дренирования.
Полный протокол, миграцию TLS и откат смотрите в горячей замене процесса с переносом сессий.
SIGINT (Ctrl+C)
SIGINT зависит от контекста:
- На переднем плане с TTY (разработка,
cargo run): только завершение работы. - В режиме демона или без TTY: запускает горячую замену процесса и
дренирование старого процесса, как
SIGUSR2, для совместимости со старыми установками.
Путь горячей замены через SIGINT существует ради обратной совместимости с инсталляциями, которые отправляют SIGINT из init-скриптов. Новые инсталляции должны явно использовать SIGUSR2 для горячей замены и SIGTERM для остановки.
Интеграция с systemd
pg_doorman поддерживает Type=notify. Поставляемый юнит pg_doorman.service запускает бинарник в основном процессе и уведомляет systemd через sd_notify:
[Service]
Type=notify
NotifyAccess=exec
ExecStart=/usr/bin/pg_doorman /etc/pg_doorman/pg_doorman.toml
ExecReload=/bin/kill -SIGUSR2 $MAINPID
ExecStop=/bin/kill -SIGTERM $MAINPID
SyslogIdentifier=pg_doorman
KillMode=mixed
TimeoutStopSec=60
Restart=on-failure
Nice=-15
User=postgres
Group=postgres
LimitNOFILE=65536
sd_notify READY=1 отправляется после привязки слушающего сокета и инициализации пулов. sd_notify MAINPID=<child> отправляется при горячей замене процесса, чтобы systemd корректно отслеживал новый процесс.
С таким юнитом systemctl reload pg_doorman означает горячую замену
процесса (SIGUSR2), а не перечитывание конфига (SIGHUP). Если нужно
только перечитать конфиг, используйте kill -HUP <pid>.
Если переходите с Type=forking + --daemon, уберите --daemon и переключитесь на Type=notify — меньше движущихся частей и корректное отслеживание готовности. Старые инсталляции с --daemon продолжают работать, но не получают преимуществ sd_notify.
Режим демона
pg_doorman --daemon ответвляется в фон и пишет PID в daemon_pid_file (по умолчанию /tmp/pg_doorman.pid). Под systemd предпочитайте Type=notify вместо --daemon.
general:
daemon_pid_file: "/var/run/pg_doorman.pid"
Куда дальше
- Горячая замена процесса с переносом сессий — полный протокол замены с миграцией TLS.
- Диагностика — что проверить, когда перезагрузка не подхватывает изменения.
- TLS — семантика перезагрузки серверных сертификатов по
SIGHUP.
Fastpath и large objects
Используйте эту страницу, если pgjdbc или Hibernate работают с PostgreSQL large objects через pg_doorman в режиме transaction pool.
pgjdbc LargeObjectManager вызывает функции PostgreSQL large object через
Fastpath FunctionCall (F): lo_creat, lo_open, lo_read, lo_write,
lo_close и другие. PostgreSQL отвечает FunctionCallResponse (V), а затем
ReadyForQuery (Z). В V лежит результат функции. Состояние транзакции
приходит в следующем ReadyForQuery.
До 3.10.7 pg_doorman не передавал FunctionCall в transaction pooling. Клиент
мог отправить вызов функции large object и навсегда ждать ответа. Начиная с
3.10.7 pg_doorman передаёт вызов в PostgreSQL, возвращает клиенту
FunctionCallResponse и освобождает backend только после ReadyForQuery, если
PostgreSQL сообщил idle-состояние.
Transaction pooling
Дескрипторы large object живут внутри PostgreSQL-транзакции. Если после
fastpath-вызова ReadyForQuery вернул статус T или E, pg_doorman оставляет
за клиентом тот же backend. Backend освобождается только после idle-статуса
I, обычно после COMMIT или ROLLBACK.
Fastpath-вызовы в autocommit освобождают backend сразу после ReadyForQuery со
статусом idle.
Это соответствует поведению PgBouncer в transaction pooling для трафика
FunctionCall.
Размер пула
Каждый активный вызов функции large object занимает один backend до
ReadyForQuery. Считайте размер пула по числу одновременных чтений и записей
large objects, а не только по обычному темпу SQL-запросов.
После включения такого трафика следите за:
SHOW POOLS: активные клиенты, активные серверы и ожидающие клиенты.- Ошибками
query_wait_timeout. - Перцентилями задержек для пулов с large object трафиком.
Если всплески вызовов large object подводят клиентов к query_wait_timeout,
увеличьте пул для нужной пары user/database или уменьшите параллелизм large
object операций в приложении.
Большие чтения
pg_doorman передаёт большие DataRow, CopyData и FunctionCallResponse
потоково, если они превышают general.message_size_to_be_stream. Большой
fastpath-ответ lo_read уходит клиенту без предварительного буферизования
всего ответа в памяти pg_doorman.
Потоковая передача ограничивает расход heap в pg_doorman, но не делает большие
одиночные чтения бесплатными. Большой lo_read всё равно удерживает backend и
сокетные буферы, пока PostgreSQL отправляет ответ. Лимиты сообщений протокола
PostgreSQL тоже остаются. Читайте large objects порциями на стороне приложения.
Таймауты
server_lifetime применяется к idle backend в пуле. Он не прерывает backend,
который выполняет чтение или запись large object.
Дескрипторы large object зависят от состояния PostgreSQL-транзакции. Если
приложение оставляет large object транзакцию idle между fastpath-вызовами,
PostgreSQL idle_in_transaction_session_timeout может закрыть backend.
pg_doorman вернёт клиенту ошибку соединения. Держите large object транзакции
короткими или настройте PostgreSQL-таймауты для сессий, которые работают с
large objects.
Мониторинг query interner
Query interner дедуплицирует тексты Parse в памяти процесса
pg_doorman. Хранилище разделено на две независимые хеш-таблицы —
для именованных prepared (NAMED) и анонимных (ANON); каждая
работает по своей политике. NAMED чистит пассивный сборщик по
Arc::strong_count. ANON вытесняет по бездействию через
query_interner_anon_idle_ttl_seconds. Обе половины публикуют
метрики Prometheus: текущие значения, счётчики вытеснений и
гистограмму длительности уборки. Плюс счётчик синтетических
ошибок SQLSTATE 26000 — этот код pg_doorman возвращает клиентам,
чей анонимный prepared statement выпал из всех кешей.
Эта страница помогает оператору пользоваться этими метриками: готовый дашборд, правила алертов и приёмы настройки.
Дашборд
Главные панели (на первом экране)
- Сводка — общий объём интернера.
sum(pg_doorman_query_interner_bytes)в разрезе инстансов. Красный порог 1.5 ГиБ, жёлтый — 500 МиБ. Главный сигнал по памяти. - График — записи по типу. Две линии:
pg_doorman_query_interner_entries{kind="named"}pg_doorman_query_interner_entries{kind="anonymous"}Окно шесть часов. Устойчивый рост любой из линий — повод открыть панели детализации.
- График — частота синтетических 26000.
rate(pg_doorman_query_interner_synthetic_misses_total[5m]). Норма — плоский ноль. Любой всплеск означает: либо TTL вытеснил запись, на которую сослался клиент, либо драйвер рассчитывает на безымянный prepared statement, который остаётся доступным после Sync.
Детализация
- Скорость вытеснений с разбивкой по причине:
sum by (kind, reason) (rate(pg_doorman_query_interner_evictions_total[5m])). - Тепловая карта длительности уборки:
histogram_quantile(0.5, rate(pg_doorman_query_interner_gc_duration_seconds_bucket[5m])), с P99 поверх. - Среднее число байт на запись по типу:
pg_doorman_query_interner_bytes / pg_doorman_query_interner_entries.
Корреляции
- Скорость вытеснений ANON в сравнении с общим темпом запросов. Линейная корреляция говорит о здоровом трафике; нелинейная — о взрыве динамического SQL от ORM.
- Частота синтетических 26000 в сравнении с P99 latency запросов. Корреляция означает, что TTL режет живой трафик; разбираться с медленной веткой.
Переменные дашборда
instance— сравнивать реплики.kind— отфильтровать значения и счётчик до одной из половин.
Лейблы pool, user и database к интернеру неприменимы — он один на процесс. На панелях интернера они только введут читателя в заблуждение.
Правила алертов
Готовый блок groups: лежит в
monitoring/prometheus-rules/query-interner.yaml. Пять
алертов.
PgDoormanAnonInternerMemoryHigh(critical) — байты ANON выше 1.5 ГиБ. Уменьшить TTL или проверить ORM на динамический SQL.PgDoormanAnonTTLTooShort(critical) — синтетические 26000 чаще 1/с в течение 10 минут. Сначала определить источник: клиентский LRU,RESET INTERNER, TTL-вытеснение anonymous-записей или поведение драйвера.PgDoormanAnonInternerNotShrinking(warning) — ANON растёт, а вытеснение по TTL не идёт. TTL слишком велик, либо поток уникальных запросов превышает скорость их истечения.PgDoormanInternerGCSlow(warning) — P99 уборки выше 50 мс на 15-минутном окне. Увеличитьquery_interner_gc_interval_seconds(этот параметр работает только при перезапуске: reload не изменит частоту проходов у работающего процесса) или сделатьRESET INTERNERи уменьшить размеры кешей.PgDoormanNamedInternerGrowsUnbounded(warning) — больше 100 000 записей в NAMED при почти нулевом вытеснении. Почти всегда баг: ссылка наArc<str>удерживается навсегда.
Защита от холодного старта: у всех алертов for: > 5m. Пустой
интернер сразу после запуска процесса их не зажигает.
Размеры
Стационарный объём ANON-интернера при условии, что 50% запросов идут через prepared, а средний SQL — 2 КиБ:
| RPS | TTL = 60 с | TTL = 300 с |
|---|---|---|
| 100 | ~12k записей / ~24 МиБ | ~60k / ~120 МиБ |
| 1 000 | ~120k / ~240 МиБ | ~600k / ~1.2 ГиБ |
| 10 000 | ~1.2M / ~2.4 ГиБ | размер не рекомендуется |
Интернер глобален на процесс, поэтому объём кластера растёт линейно
с числом реплик pg_doorman. Берите это как стартовую оценку для
query_interner_anon_idle_ttl_seconds и бюджета RAM на хост;
фактическое значение даёт pg_doorman_query_interner_bytes.
Реальный TTL
Сборщик ходит в два прохода. На первом помечает запись как
кандидата, на втором удаляет — но только если за это время к
записи никто не обратился. Проход выполняется каждые
gc_interval / 4 секунд. По умолчанию (gc_interval = 60 с,
anon_idle_ttl = 60 с) это раз в 15 секунд. Пометка ставится
через 60–75 секунд после последнего использования, удаление — на
следующем проходе. Итого запись простаивает 75–120 секунд, прежде
чем сборщик её выкинет. Снижать anon_idle_ttl ниже 60 секунд
смысла мало: чаще, чем раз в gc_interval / 4, сборщик не
запускается.
Приёмы настройки
Уменьшить TTL, когда давит память
Когда: горит PgDoormanAnonInternerNotShrinking, байты ANON
подходят к лимиту памяти хоста.
Действие: уменьшить query_interner_anon_idle_ttl_seconds в
general (например, с 60 до 30). Перечитать конфиг pg_doorman.
Скорость вытеснений догонит новый порог.
Разобрать синтетические 26000 перед увеличением TTL
Когда: горит PgDoormanAnonTTLTooShort.
Действие: понять, какой клиент и какой запрос. У synthetic_misses
нет меток, поэтому смотреть WARN-лог — он пишется на каждый
промах и содержит client, pool, connection_id. Перед изменением
конфига проверьте pg_doorman_clients_prepared_anonymous_evictions_total
и pg_doorman_query_interner_evictions_total{kind="anonymous"}. Если
промахи идут из клиентского Anonymous LRU, увеличьте
client_anonymous_prepared_cache_size. Если они идут из TTL anonymous
interner или от драйвера, который законно переиспользует безымянный
Bind в следующей пачке, поднимите TTL (с 60 до 300, например). Если
нет — переключите клиента на именованный prepared.
Сбросить интернер
Когда: разовая диагностика или жёсткое сжатие памяти под инцидентом.
Действие: psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman" -c "RESET INTERNER". Возвращает
CommandComplete RESET. Активные клиенты заново разберут запрос
при следующем использовании. Короткоживущие клиенты эффекта не
заметят: их last_anonymous_hash всё ещё помнит хеш, который они
зарегистрировали до сброса, и следующий Bind увидит отсутствие
записи и один раз получит 26000. Драйвер на это отреагирует
повторным Parse.
Правила предвычислений (recording rules)
Кластерные агрегаты, которые имеет смысл предвычислять, чтобы дашборды были дешевле:
groups:
- name: pg_doorman_query_interner_recording
interval: 30s
rules:
- record: pg_doorman:query_interner_total_bytes:5m
expr: sum without (instance) (pg_doorman_query_interner_bytes)
- record: pg_doorman:query_interner_eviction_rate:5m
expr: |
sum without (instance) (rate(pg_doorman_query_interner_evictions_total[5m]))
Первое правило позволяет общей сводной панели читать одну серию.
Второе рисует темп вытеснений с разбивкой по причине без
пересчёта rate() на каждой перерисовке дашборда.
Диагностика
Симптомы, на которые вы скорее всего наступите в первую неделю работы PgDoorman, и куда смотреть, когда наступили.
Ошибки аутентификации при подключении к PostgreSQL
Симптом: PgDoorman принимает клиентское соединение, но первый же запрос возвращает password authentication failed от PostgreSQL.
Когда username пула совпадает с ролью на backend
PgDoorman по умолчанию использует passthrough authentication — криптографическое доказательство клиента (MD5-хеш или SCRAM ClientKey) повторно используется для аутентификации к PostgreSQL. Поле password в конфиге должно содержать именно тот хеш, что хранится в pg_authid / pg_shadow:
SELECT usename, passwd FROM pg_shadow WHERE usename = 'your_user';
Для SCRAM оба процесса должны видеть одни и те же salt и iteration count — даже один отличающийся символ в сохранённом verifier ломает passthrough.
Когда username пула отличается от роли на backend
Когда username, под которым клиент подключается к pg_doorman, не совпадает с реальной ролью PostgreSQL, passthrough работать не может: у pg_doorman нет пароля для backend-роли. Укажите явные credentials:
users:
- username: "app_user" # имя для клиента
password: "md5..." # хеш для аутентификации client → pg_doorman
server_username: "pg_app_user" # реальная роль в PostgreSQL
server_password: "plaintext_pwd" # plaintext-пароль для этой роли
pool_size: 40
Это же путь для JWT-аутентификации, где клиент не присылает пароль.
pg_doorman generate --host … интроспектирует PostgreSQL и собирает конфиг с уже подставленными хешами. Быстрее, чем копировать руками из pg_shadow.
Файл конфигурации не найден
Симптом: PgDoorman при запуске завершается с configuration file not found.
По умолчанию бинарник ищет pg_doorman.toml в текущем рабочем каталоге. Либо назовите файл так и cd в его каталог, либо передайте путь явно:
pg_doorman /etc/pg_doorman/pg_doorman.yaml
Проверка перед запуском:
pg_doorman -t /etc/pg_doorman/pg_doorman.yaml
Клиенты получают 58006 (pooler is shut down now)
Пул выключается, либо горячая замена процесса была запущена в режиме демона. Посмотрите серверные логи рядом с временем ошибки:
Got SIGUSR2, starting binary upgrade …— идёт горячая замена процесса. В foreground-режиме свободные TCP-клиенты без TLS должны мигрировать прозрачно; TLS-клиенты мигрируют только со сборкойtls-migration.58006получают клиенты, оставшиеся в транзакции послеshutdown_timeout. В режиме демона миграции через файловые дескрипторы нет, и каждый клиент получает58006при закрытии соединения. См. Горячая замена процесса с переносом сессий → Диагностика.- Нет строки
SIGUSR2в логе — кто-то прислалSIGTERMилиSIGINT, и пулер выключился без замены. Проверьте systemd-юнит, конкретный pid и operator runbook.
Если 58006 пришёл во время плановой замены — для этой подмножества клиентов это ожидаемое поведение. Настройте connection pool приложения на retry при transient-ошибках.
Размер пула слишком мал
Симптом: запросы ходят заметно дольше end-to-end, чем при прямом обращении к PostgreSQL.
Смотрите SHOW POOLS и SHOW POOLS_EXTENDED:
cl_waiting — сколько клиентов сейчас в очереди за серверным соединением
maxwait — самое долгое ожидание любого клиента, секунд
sv_idle — свободные серверные соединения в пуле
sv_active — серверные соединения, выданные клиентам
Если cl_waiting > 0 стабильно и sv_idle == 0, пул мал для нагрузки. Либо поднимайте pool_size для этого пользователя, либо разбирайтесь, почему sv_active не падает — длинные транзакции, idle-in-transaction сессии, медленный downstream-вызов, который держит серверное соединение.
Если у вас включён max_db_connections, смотрите ещё SHOW POOL_COORDINATOR на evictions (доноры под давлением отдают соединения) и exhaustions (лимит достигнут даже после вытеснений). См. Координатор пулов.
Куда подавать остальное
Если вашей проблемы здесь нет — откройте issue на GitHub: версия pg_doorman, релевантный конфиг (с замазанными паролями), драйвер клиента и его версия, и совпадающие по времени строки логов из pg_doorman и PostgreSQL.
Команды администратора
pg_doorman предоставляет административную базу, совместимую с протоколом Postgres. Подключайтесь к тому же порту, что и обычные клиенты, но с dbname=pgdoorman и административной учётной записью из конфига:
psql -h 127.0.0.1 -p 6432 -U admin pgdoorman
Или через connection string psql:
psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman"
Команды администратора читаются через SHOW <subcommand> или выполняются голыми глаголами (PAUSE, RESUME, RECONNECT, RELOAD, SHUTDOWN, RESET INTERNER, SET <param> = <value>).
Команды SHOW
| Команда | Назначение |
|---|---|
SHOW HELP | Список доступных команд. |
SHOW CONFIG | Текущая активная конфигурация. Только для чтения. |
SHOW DATABASES | По одной строке на пул: host, port, database, размер пула, режим. |
SHOW POOLS | Снимок утилизации пула на пару user×database: idle/active/waiting клиенты, idle/active серверы. |
SHOW POOLS_EXTENDED | SHOW POOLS плюс полученные/отправленные байты и среднее время ожидания. |
SHOW POOLS_MEMORY | Учёт памяти на пул для кэша prepared statements (клиентский и серверный). |
SHOW POOL_COORDINATOR | Состояние координатора пулов на базу: текущие соединения, использование резерва, число вытеснений. См. Координатор пулов. |
SHOW POOL_SCALING | Метрики anticipation/burst: in-flight create-операции, ожидания на воротах, anticipation notifies/timeouts. |
SHOW PREPARED_STATEMENTS | Закэшированные prepared statements на пул: hash, имя, текст запроса, число попаданий. |
SHOW INTERNER | Сводка query interner: число записей и байты для named- и anonymous-половины. |
SHOW INTERNER <N> | N самых крупных интернированных текстов запросов: hash, kind, idle age и предпросмотр SQL. |
SHOW CLIENTS | Активные клиенты: ID, database, user, имя приложения, адрес, состояние TLS, счётчики transaction/query/error, возраст. |
SHOW SERVERS | Активные соединения с бэкендом: ID сервера, PID бэкенда, database, user, TLS, состояние, счётчики transaction/query, попадания/промахи кэша prepare, байты. |
SHOW CONNECTIONS | Число соединений по типу: total, errors, TLS, plain, cancel. |
SHOW STATS | Агрегированная статистика на пару user×database: всего транзакций, запросов, времени, байт, средние. |
SHOW LISTS | Счётчики по категориям (databases, users, pools, clients, servers). |
SHOW USERS | Список пользователей и их режимы пула. |
SHOW AUTH_QUERY | Кэш auth_query: попадания/промахи/перезапросы, успехи/отказы аутентификации, ошибки исполнителя, счётчики динамических пулов. |
SHOW STARTUP_PARAMETERS | Итоговые startup_parameters по каждому пулу: параметр, значение, источник и состояние применения. |
SHOW SOCKETS | Счётчики TCP- и Unix-сокетов по состоянию (только Linux — читает /proc/net/). |
SHOW LOG_LEVEL | Текущий уровень логирования. |
SHOW VERSION | Версия pg_doorman. |
SHOW POOL_COORDINATOR и SHOW POOL_SCALING не имеют аналогов в PgBouncer или Odyssey — они показывают внутренние механизмы pg_doorman.
Управляющие команды
| Команда | Эффект |
|---|---|
PAUSE | Прекратить принимать новые клиентские запросы. Существующие клиенты завершают свои транзакции. |
PAUSE <database> | Поставить на паузу один пул. |
RESUME / RESUME <database> | Возобновить после PAUSE. |
RECONNECT / RECONNECT <database> | Принудительно пересоздать соединения с PostgreSQL (закрыть простаивающие, дренировать активные). Новые соединения берутся из PostgreSQL. |
RELOAD | То же, что и SIGHUP — перезагрузить конфиг с диска. |
SHUTDOWN | Отправляет SIGINT текущему процессу. Перед использованием в daemon mode см. Сигналы. |
KILL <database> | Сбросить всех клиентов, подключённых к конкретному пулу. |
RESET INTERNER | Очистить named- и anonymous-записи query interner. Диагностическая команда; активные клиенты заново делают Parse при следующем использовании. |
SET log_level = '<level>' | Изменить уровень логирования в рантайме (error, warn, info, debug, trace). |
PAUSE/RESUME полезны при failover или окнах обслуживания. RECONNECT после ротации учётных данных в pg_authid гарантирует, что бэкенды используют новый пароль.
Чтение типового вывода
SHOW POOLS
database | user | cl_idle | cl_active | cl_waiting | sv_active | sv_idle | sv_used | maxwait
mydb | app | 12 | 4 | 0 | 4 | 36 | 0 | 0.0
cl_waiting > 0означает, что клиенты застряли в ожидании серверного соединения. Либо поднимитеpool_size, либо проверьте медленные запросы.sv_idleсоответствует свободным серверным соединениям;sv_active— занятым;sv_used— зарезервированным координатором (см. ниже).maxwait— самое долгое текущее ожидание в секундах. Если оно вырастает заquery_wait_timeout, клиенты получают ошибки.
SHOW STARTUP_PARAMETERS
user | database | parameter | value | source | state
app | mydb | statement_timeout | 5s | general | applied
app | mydb | plan_cache_mode | force_custom_plan | pool | applied
sourceпоказывает источник значения:general,poolилиauth_query.stateпоказывает, будет ли значение отправлено в ближайшийStartupMessage:applied,dropped_due_to_budgetилиstale.
SHOW POOL_COORDINATOR
database | max_db_conn | current | reserve_size | reserve_used | evictions | reserve_acq | exhaustions
mydb | 80 | 78 | 16 | 2 | 142 | 18 | 0
evictionsбыстро растут: какой-то пользователь голодает раз за разом. Задайте или поднимитеmin_guaranteed_pool_sizeдля этого пользователя.reserve_acqвысокий: всплески — норма, но возможно вы недооценили размер. Подумайте о повышенииmax_db_connections, а не о ставке на резерв.exhaustionsненулевые: даже резерв был полным. Клиенты упёрлись вquery_wait_timeout. Поднимите потолок.
Тонкости настройки см. в Координатор пулов.
SHOW POOL_SCALING
user | database | inflight | creates | gate_waits | burst_gate_budget_ex | antic_notify | antic_timeout | create_fallback | replenish_def
app | mydb | 1 | 12345 | 87 | 3 | 142 | 8 | 22 | 0
inflight— текущие создания соединений с бэкендом в процессе.gate_waitsрастут:scaling_max_parallel_createsпридушивает вас. Допустимо, если PostgreSQL под нагрузкой; поднимите, если PG может обработать больше параллельных вызововconnect().- Соотношение
antic_notifyиantic_timeout: высокий счётчик timeout означает, что упреждающее ожидание не успевает поймать возвращающееся соединение. Поднимитеscaling_warm_pool_ratio, чтобы пул рос с опережением спроса. create_fallbackрастёт — срабатывает предзамена: соединения истекают раньше, чем естественным образом возвращаются.
См. Пул под нагрузкой → Параметры тюнинга.
Аутентификация
Административная база использует учётку из general.admin_username и general.admin_password:
general:
admin_username: "admin"
admin_password: "change_me"
Административные соединения не проходят через правила pg_hba.conf — они идут напрямую в обработчик администратора. Ограничивайте административный доступ на сетевом уровне (listen_addresses, фаервол) или используйте Unix-сокеты.
Куда дальше
- Справочник Prometheus — те же данные в машинно-читаемом виде.
- Координатор пулов — что говорит вам
SHOW POOL_COORDINATOR. - Пул под нагрузкой — что говорит вам
SHOW POOL_SCALING. - Диагностика — типичные сбои и их вывод в
SHOW.
Веб-консоль
В pg_doorman встроена операторская веб-консоль. Она работает на том же HTTP-сервере, что отдаёт Prometheus-метрики; собранный фронтенд лежит внутри бинарника. Запуск консоли не добавляет внешних зависимостей: один процесс, один бинарь, один TCP-порт.
Включение
Консоль настраивается в секции [web]. Старое имя секции [prometheus]
тоже принимается как алиас.
[web]
enabled = true
host = "0.0.0.0"
port = 9127
# Операторская консоль (по умолчанию выключена)
ui = true
ui_anonymous = false
log_tap_max_entries = 8192
При web.ui = true и general.admin_password, равном пустой строке или
литералу "admin", консоль на старте переходит в режим «только
метрики». HTTP-сервер продолжает отдавать /metrics, но административные эндпоинты
иначе оказались бы открыты любому. Задайте настоящий пароль до того, как
включать ui = true. Срабатывание этой проверки видно в логе по строке
web.ui = true ignored: admin_password is default/empty.
| Параметр | Описание | По умолчанию |
|---|---|---|
enabled | Запускать ли HTTP-сервер. /metrics работает независимо от ui. | false |
host | Адрес, на котором слушает HTTP-сервер. | "0.0.0.0" |
port | Порт HTTP-сервера. | 9127 |
ui | Отдавать SPA по / и публичные API-эндпоинты. | false |
ui_anonymous | При true публичные API-эндпоинты принимают запросы без авторизации. См. Роли доступа. | false |
log_tap_max_entries | Размер кольцевого буфера в памяти для /api/logs. 0 отключает эндпоинт. | 8192 |
URL-карта
| URL | Требуемая роль | Назначение |
|---|---|---|
/, /pools, любой путь вне API | нет | Оболочка приложения. Отдаётся анонимно даже при ui_anonymous = false, чтобы прямая ссылка не открывала системный диалог Basic-авторизации браузера до того, как появится форма входа React. |
/assets/* | нет | Хэшированные JS, CSS, шрифты и SVG. Cache-Control: public, max-age=31536000, immutable. |
/metrics | нет | Prometheus exposition format. От ui не зависит. |
GET /api/auth/config | нет | Сообщает SPA, подключён ли SSO и какая роль у текущего запроса. |
GET /api/version, /api/overview, /api/pools, /api/clients, /api/servers, /api/connections, /api/stats, /api/databases, /api/users, /api/auth_query, /api/config, /api/log_level, /api/pool_coordinator, /api/pool_scaling, /api/sockets, /api/prepared, /api/interner, /api/top/clients, /api/top/prepared, /api/apps, /api/events | Anonymous, когда ui_anonymous = true, иначе Sso | JSON только для чтения, повторяет формат SHOW <admin-команда>. |
GET /api/logs, /api/prepared/text/{hash}, /api/interner/top, /api/top/queries | Sso | Эндпоинты только для чтения с персональными данными. /api/logs подключает буфер логов на первом запросе и отключает его через 2 минуты простоя. /api/top/queries возвращает первые ~120 символов SQL-текста из кеша. Эти данные не вынесены в публичную поверхность, потому что превью могут содержать литералы и идентификаторы клиентов. |
POST /api/admin/{reload,pause,resume,reconnect} | Admin | Управляющие операции администратора. Семантика та же, что и у admin-протокола через psql. |
Роли доступа
Сервер на каждом запросе вычисляет одну из трёх ролей. Проверка работает на стороне сервера; SPA дублирует её на клиенте только для того, чтобы не показывать действия, недоступные текущему оператору.
| Роль | Как запрос её получает | Что роль даёт |
|---|---|---|
Anonymous | Учётных данных нет, [web].ui_anonymous = true. | Публичные /api/* только для чтения из таблицы выше плюс /metrics. На пути с персональными данными и /api/admin/* возвращается 401. |
Sso | Валидный JWT в Authorization: Bearer, в cookie sso_access_token= или в query ?token=, который не попадает в группу администраторов. | Все эндпоинты чтения, включая пути с персональными данными. На POST /api/admin/* отдаётся 403. |
Admin | Либо корректная пара Basic из [general].admin_username / admin_password, либо валидный JWT, у которого значение [web].sso_groups_claim пересекается с [web].sso_admin_groups. | Полный доступ, включая POST /api/admin/{reload,pause,resume,reconnect}. |
Когда в одном запросе есть и Basic, и SSO-токен, приоритет у Basic.
Корректный admin-пароль даёт Admin независимо от состояния SSO.
Неверный Basic-пароль не блокирует SSO-ветку: SSO-источники всё равно
проверяются, и валидный JWT даёт роль Sso (или Admin, если совпала
группа администраторов). Это покрывает типичный случай: в localStorage лежит
просроченный JWT рядом с рабочим Basic-паролем.
Basic-пароль сравнивается за постоянное время, чтобы по длительности
сравнения нельзя было угадывать символы. JWT проверяются по публичному
ключу из [web].sso_public_key_file; разобранный ключ кэшируется на
время жизни процесса и перечитывается на RELOAD.
fetch-обёртка SPA шлёт Accept: application/json, и сервер на ней
отдаёт чистый 401 без WWW-Authenticate: Basic. Без этого браузер
закешировал бы то, что оператор ввёл в системном диалоге Basic, и
подставлял этот пароль поверх формы входа React. Инструменты с
Accept: */* (curl, gh) получают challenge как обычно.
401 Unauthorized отдаётся, когда учётных данных не пришло или ни
один вариант не прошёл парсинг и валидацию. 403 Forbidden — когда
данные валидны, но роли не хватает для пути; тело —
{"error":"forbidden","message":"admin role required"}. SPA на 401
повторно открывает форму входа, на 403 показывает неблокирующий
баннер «admin role required», не уводя на форму входа.
Настройка SSO
SSO опциональный. По умолчанию ([web].sso_enabled = false) сервер
обслуживает только роли Anonymous и Admin через Basic. Чтобы подключить
внешний SSO-прокси:
-
Получите от SSO-провайдера публичный RSA-ключ, которым он подписывает JWT, и сохраните его в PEM-файле (например,
/etc/pg_doorman/sso-public.pem). Для oauth2-proxy ключ извлекается из приватного:openssl rsa -in private.pem -pubout -out public.pem. Для Keycloak — см. Keycloak ниже. -
Добавьте SSO-поля в
[web]:[web] enabled = true ui = true host = "127.0.0.1" port = 9127 ui_anonymous = false sso_enabled = true sso_proxy_url = "https://sso.example.com/oauth2/start" sso_public_key_file = "/etc/pg_doorman/sso-public.pem" sso_audience = ["pg_doorman"] sso_allowed_users = ["*"] -
Перечитайте конфиг:
kill -SIGHUP <pid>илиpsql -h <host> -p 6432 -U admin -d pgbouncer -c 'RELOAD'. -
Проверьте:
curl http://<host>:9127/api/auth/configдолжен вернуть"sso_enabled":trueи заданныйsso_proxy_url.
| Поле | Назначение | По умолчанию |
|---|---|---|
sso_enabled | Включает SSO-ветку. Без неё JWT не валидируются. | false |
sso_proxy_url | URL, на который SPA уводит браузер по кнопке «Sign in via SSO». Бэкенд этот URL сам не вызывает. | null |
sso_public_key_file | Путь к PEM-файлу с публичным RSA-ключом. Читается на старте и при RELOAD. | null |
sso_audience | Допустимые значения JWT claim aud. Токен принимается, если совпадает хотя бы одно. Обязательное поле при sso_enabled = true. | [] |
sso_allowed_users | Список разрешённых значений JWT claims preferred_username или sub. ["*"] принимает любой валидный токен; явный список пропускает только перечисленные имена. | ["*"] |
sso_groups_claim | Имя JWT claim, в котором лежат группы пользователя. Читается вместе с sso_admin_groups. | "groups" |
sso_admin_groups | Группы, которые поднимают SSO-пользователя до Admin. Пустой список оставляет каждый SSO-логин на роли Sso только для чтения. | [] |
sso_require_https | Отклонять Bearer/cookie/query SSO-учётные данные, пришедшие по обычному HTTP. Запрос считается защищённым только если TCP-пир входит в trusted_proxies и прокси прислал X-Forwarded-Proto: https. По умолчанию выключено, чтобы SSO продолжал работать в схеме «TLS терминирует прокси → pg_doorman слушает HTTP во внутренней сети». | false |
trusted_proxies | CIDR доверенных обратных прокси для X-Forwarded-For, Forwarded и X-Forwarded-Proto. При пустом списке pg_doorman игнорирует эти заголовки и берёт адрес прямого TCP-пира. Если sso_require_https = true работает за прокси, который завершает TLS, добавьте CIDR этого прокси, чтобы доверять X-Forwarded-Proto: https. См. Журнал доступа. | [] |
Поднятие SSO-пользователя до Admin через claim с группами
По умолчанию SSO-логин получает роль Sso — доступ только для чтения к логам
и SQL-текстам, но без POST /api/admin/*. Чтобы операторы могли
запускать управляющие операции администратора через SSO без раздачи Basic-пароля,
настройте sso_groups_claim и sso_admin_groups:
[web]
sso_enabled = true
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"]
sso_groups_claim = "groups"
sso_admin_groups = ["pg-doorman-admins"]
Когда в валидном JWT приходит "groups": [..., "pg-doorman-admins"],
запрос получает роль Admin. В access-логе это выглядит как
auth_role=admin auth_source=sso, и SSO-админы по-прежнему отличимы от
Basic-админов. /api/auth/config отдаёт
sso_admin_groups_configured = true, и SPA убирает из формы входа
обещание «SSO grants read-only access».
Keycloak
Keycloak подписывает каждый JWT RSA-ключом realm'а. Публичную часть этого ключа нужно один раз выгрузить в PEM-файл, который читает pg_doorman.
Без UI — через JWKS-эндпоинт realm'а:
REALM=https://kc.example.com/realms/operators
curl -s "$REALM/protocol/openid-connect/certs" \
| jq -r '.keys[] | select(.alg=="RS256") | "-----BEGIN CERTIFICATE-----\n" + .x5c[0] + "\n-----END CERTIFICATE-----"' \
| openssl x509 -pubkey -noout \
> /etc/pg_doorman/sso-public.pem
Через админ-консоль: Realm settings → Keys → строка с
Algorithm = RS256 и Use = SIG → Public key → скопированное
base64-тело завернуть в PEM-файл с заголовками
-----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY-----.
Секция [web] под Keycloak выглядит так:
[web]
sso_enabled = true
sso_proxy_url = "https://kc.example.com/realms/operators/protocol/openid-connect/auth"
sso_public_key_file = "/etc/pg_doorman/sso-public.pem"
sso_audience = ["pg_doorman"] # client_id, заданный в Keycloak
sso_groups_claim = "groups" # значение по умолчанию для маппера «groups»
sso_admin_groups = ["pg-doorman-admins"]
Чтобы Admin через group claim работал, добавьте клиенту маппер
Group Membership (Clients → нужный client → Mappers). Без
этого маппера Keycloak выдаёт токены без groups, и каждый оператор
остаётся в роли Sso.
После ротации ключа realm'а заново выгрузите PEM и сделайте
RELOAD — pg_doorman подхватит новый ключ без рестарта.
Когда SSO-конфигурация сломана
Опечатка в SSO-секции не должна выводить операторскую консоль из строя.
При sso_enabled = true, но не загружаемом рантайме (нет PEM-файла,
пустой audience, нечитаемый PEM) сервер пишет причину в лог на уровне
error, оставляет SSO выключенным на этот запуск и обслуживает только
Basic и Anonymous. Та же причина видна в двух точках, чтобы оператор
заметил поломку, а не тихий откат на Basic:
/api/auth/config.sso_config_errorсодержит человекочитаемое сообщение. SPA показывает баннер с этим текстом в форме входа.- Метрика
pg_doorman_web_sso_config_errorравна1, пока SSO запрошен, но не загружен. В паре сpg_doorman_web_sso_enabledподходит для alert-правила.
Логин из браузера
При первом заходе SPA получает /api/auth/config и показывает форму
входа. Если в ответе пришёл sso_proxy_url, рядом с Basic-формой
появляется кнопка Sign in via SSO; иначе — только Basic.
Клик по Sign in via SSO уводит браузер на
${sso_proxy_url}?redirect_to=<текущий URL>. Внешний proxy выполняет
OAuth/OIDC-обмен и возвращает браузер обратно с ?token=<jwt>. SPA
сохраняет токен в localStorage, чистит URL от параметра и шлёт
Authorization: Bearer <jwt> на каждом следующем запросе.
В нижней части боковой панели отображается имя текущего пользователя: admin
для Basic или sso: <preferred_username> для SSO. Кнопка Sign out
очищает в localStorage оба ключа (pgdoorman.admin-auth и
pgdoorman.sso-token) и заново открывает форму входа.
Тихое обновление токена запускается раз в 60 секунд. Когда до exp остаётся
меньше 90 секунд, SPA открывает скрытый iframe с URL
${origin}/?sso_silent=1. Внутри iframe запускается минимальный
SilentCallback без обычных polling-эффектов. Он
через window.postMessage отдаёт новый токен parent-окну. Если
тихое обновление не сработало:
- при наличии Basic-данных SPA удаляет SSO-токен без редиректа, и дальнейшие запросы идут под Basic;
- иначе SPA уходит на полный редирект через SSO-proxy.
Срок жизни JWT задавайте не меньше 5 минут — более короткие токены успевают истечь до тихого обновления.
SPA cookie не шлёт (credentials: "omit" на каждом fetch). Путь с
cookie sso_access_token существует для сайдкаров, curl и oauth2-proxy
вариантов, которые кладут токен в cookie на общем домене.
Basic-пароль по умолчанию живёт только в памяти React и пропадает после
полной перезагрузки страницы. Галочка Remember me on this device в форме входа
сохраняет его в localStorage, поэтому консоль открывается без повторного ввода.
Очистка хранилища сайта в браузере удаляет и Basic, и SSO-запись.
Журнал доступа
После каждого ответа (200/401/403/404/5xx, включая запросы к /metrics)
консоль пишет одну logfmt-строку в канал pg_doorman::web::access:
INFO pg_doorman::web::access method=GET path=/api/admin/reload query=false status=200 bytes=42 latency_ms=12 peer=10.0.1.5:42312 auth_role=admin auth_source=basic auth_user=admin
Поля:
method,path— HTTP-метод и URL-путь. Тела запросов и ответов в лог не пишутся.query=true|false— была ли в запросе строка параметров. Сама строка не логируется, чтобы JWT в?token=не попадал в журнал.status,bytes,latency_ms— статус ответа, размер тела и полная задержка ответа.peer— адрес инициатора запроса. По умолчанию это непосредственный TCP-peer. Если он попадает в[web].trusted_proxies, сервер разбираетX-Forwarded-For(илиForwarded, RFC 7239), идёт по цепочке справа налево, пропускает доверенные адреса и берёт первый недоверенный. Недоверенный клиент не может подделать поле — если TCP-peer не в списке доверенных, заголовки прокси игнорируются.auth_role—admin,sso,anonymousилиrejected.auth_source—basic,ssoили-.auth_user— имя пользователя из учётных данных или-для анонимов и отклонённых запросов.
Уровни:
info— все управляющие действия (POST /api/admin/*), все чтения персональных данных (/api/logs,/api/prepared/text/*,/api/interner/top,/api/top/queries), все эндпоинты аутентификации и SSO (/api/auth/*,/api/sso/*) и все ответы с кодом, отличным от 2xx.debug— любые остальные успешные чтения (2xx), анонимные или под авторизованной ролью. SPA опрашивает/api/overview,/api/pools,/api/clients,/api/processраз в 1,5–3 секунды; при старом правиле «любой 2xx с авторизацией = info» оператор на странице Logs видел собственные polling-запросы. Рутинные чтения логируются наdebug, аRUST_LOG=infoостаётся для управляющих действий, auth-трафика и ошибок.
Отдельный канал pg_doorman::web::access позволяет фильтровать поток
журнала доступа независимо от остальных логов. В выпадающем фильтре на
странице Logs этот канал включается или исключается одним кликом.
Реальный IP клиента за обратным прокси
По умолчанию peer фиксирует TCP-адрес, который соединился с
сервером. За обратным прокси это адрес самого прокси. Чтобы видеть
реальный IP клиента, добавьте CIDR прокси в [web].trusted_proxies:
[web]
trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"]
Распознаются и X-Forwarded-For, и Forwarded. Несколько доверенных
переходов в цепочке пропускаются. X-Forwarded-For, пришедший от
недоверенного клиента, игнорируется, поэтому через эту настройку
произвольный вызывающий не может управлять полем access-лога.
Метрики
| Метрика | Тип | Лейблы | Назначение |
|---|---|---|---|
pg_doorman_web_sso_enabled | gauge | — | 1, когда SSO загружен успешно, иначе 0. |
pg_doorman_web_sso_config_error | gauge | — | 1, когда sso_enabled = true, но рантайм не поднялся. |
pg_doorman_web_auth_attempts_total | counter | role, source | Попытки авторизации в разрезе итоговой роли (admin/sso/anonymous/rejected) и источника (basic/sso/none). |
pg_doorman_web_requests_total | counter | status_class, role | Запросы к веб-консоли в разрезе HTTP-статуса (1xx–5xx) и роли. |
pg_doorman_web_sso_validation_errors_total | counter | reason | Отказы валидации JWT по причине: signature, expired, audience, no_username, allowlist. |
Устойчивый рост signature означает, что SSO-прокси ротировал ключ, а
sso_public_key_file остался старый. Рост allowlist — кто-то снаружи
sso_allowed_users упорно пытается войти. Рост 4xx для роли sso
обычно указывает на сломанный прокси перед pg_doorman.
Диагностика
401 на JWT, который должен быть валиден. Проверьте, что aud
совпадает хотя бы с одним значением из sso_audience и exp ещё не
истёк. PEM проверяется через openssl rsa -pubin -in <pem> -text -noout.
Счётчик pg_doorman_web_sso_validation_errors_total{reason} показывает,
какая именно проверка не прошла.
403 на JWT, который должен быть валиден. Путь требует роли
Admin (например, POST /api/admin/reload). Войдите по Basic
admin-паролю или добавьте группу пользователя в
[web].sso_admin_groups и перечитайте конфиг.
SPA не показывает Sign in via SSO. /api/auth/config не возвращает
sso_proxy_url. Либо [web].sso_enabled = false, либо sso_proxy_url
не задан, либо рантайм не поднялся (ищите sso_config_error в том же
ответе).
Тихое обновление токена не срабатывает. SSO-прокси должен возвращать свежий
токен без полного экрана логина, когда iframe приходит с активной
сессией. У oauth2-proxy это включается флагом --silent-refresh=true.
JWT в cookie игнорируется. Cookie должна попасть на pg_doorman с
того же домена, и aud обязан входить в sso_audience. SPA сама
cookie не шлёт; cookie-аутентификация рассчитана на curl, сайдкары и
oauth2-proxy-варианты, которые проставляют токен в cookie на общем
домене.
Страницы
В боковой панели восемь пунктов. Экран инцидента открывается из
Обзора. Страницы с SQL-текстом и логами требуют роли Sso или
Admin; анонимные пользователи их не видят.
Обзор (/overview)
Страница по умолчанию. Показывает состояние всего процесса, основные метрики, очереди, насыщение пулов, частые SQLSTATE и свёрнутый блок ресурсов. Если пул ушёл на резервный бэкенд через Patroni, сверху появляется баннер со списком затронутых пулов.
Пулы (/pools)
Таблица всех пулов user@database: размер, активные соединения,
ожидание, p95, ошибки, насыщение и флаг резервного бэкенда. Строка
открывает Детали пула.
Детали пула (/pools/:poolId)
Подробности одного пула: режим, лимиты, текущие соединения, TLS,
резервный бэкенд, SQLSTATE, параметры запуска PostgreSQL и причины
сработавших порогов. Здесь же находятся действия PAUSE, RESUME,
RECONNECT и общий RELOAD.
Клиенты (/clients)
Таблица клиентов с фильтрами в URL:
/clients?pool=shop_checkout&state=waiting&user=app
Фильтры: pool, database, user, state, application_name, peer-адрес.
Сортировка: запросы, ошибки, возраст соединения и возраст текущего
запроса. Вместе со страницей Серверы помогает связать клиента с
PostgreSQL pid.
Серверы (/servers)
Таблица бэкенд-соединений из SHOW SERVERS: server_id,
process_id, база, пользователь, приложение, состояние, возраст
активного запроса, счётчики запросов и ошибок, трафик и TLS.
Используйте server_id из строки клиента, чтобы найти pid в
pg_stat_activity.
Приложения (/apps)
Одна строка на каждый application_name: активные клиенты, qps, tps,
суммарные запросы, транзакции, ошибки и err / 1k q.
Кеши (/caches)
Две вкладки: кеш подготовленных запросов по пулам и кеш SQL-текста в
процессе. Обе могут показывать SQL, поэтому доступны только Sso и
Admin.
Логи (/logs)
Лента LogTap с фильтрами в URL:
/logs?level=ERROR&q=53300
Пауза останавливает только отображение; серверный буфер продолжает
заполняться. При [web].log_tap_max_entries = 0 страница показывает,
что поток логов выключен. Доступ: Sso / Admin.
Конфиг и состояние (/config)
Зеркало команд SHOW CONFIG, SHOW DATABASES, SHOW USERS,
SHOW AUTH_QUERY, SHOW LOG_LEVEL, SHOW STARTUP_PARAMETERS,
SHOW SOCKETS, SHOW POOL_SCALING, SHOW POOL_COORDINATOR.
Страница помогает проверить текущую конфигурацию и увидеть, какие
ключи применяются на RELOAD, а какие требуют рестарта. Кнопка
Reload config доступна только Admin.
Экран инцидента (/wall)
Версия Обзора для большого экрана: карта насыщения пулов, крупные
метрики и последние управляющие действия. По Esc возвращает на
/overview.
Управляющие действия
Четыре управляющие команды доступны в SPA:
| Действие | Область | Где найти | Подтверждение |
|---|---|---|---|
RELOAD | все пулы | Конфиг и состояние · Детали пула | RELOAD |
PAUSE | один user@database | Детали пула | имя базы |
RESUME | один user@database | Детали пула, когда пул паузирован | имя базы |
RECONNECT | один user@database | Детали пула | имя базы |
Семантика та же, что в admin-протоколе через psql. PAUSE прекращает
выдачу бэкендов в нужном пуле; уже идущие транзакции продолжают
выполняться. RESUME снова разрешает выдачу. RECONNECT закрывает
idle-бэкенды и отказывает активным при возврате. RELOAD перечитывает
pg_doorman.toml; размер пула уменьшается по мере освобождения
соединений.
Подтверждение вводом защищает от случайного RELOAD или PAUSE не
того пула. Результат показывается уведомлением, а само действие
пишется в access-лог и список последних событий администратора.
Горячие клавиши
Сочетания работают вне текстовых полей.
| Сочетание | Действие |
|---|---|
| ⌘ K / Ctrl K | Поиск по страницам и пулам. |
| ? | Список горячих клавиш. |
| Esc | Закрыть подсказку или окно. На /wall возвращает на Обзор. |
Тема
Внизу боковой панели есть переключатель Light / System /
Dark. По умолчанию используется Light. Выбор хранится в
localStorage.
Встроенная справка
Рядом с заголовками метрик и секций есть значок (i). Подсказка объясняет, что означает число, откуда оно взято, как считается и какие пороги считаются нормой.
Сборка из исходников
Собранный веб-интерфейс лежит в git по пути frontend/dist/, чтобы
RPM-, DEB- и Docker-сборки не зависели от Node.js.
Разработчикам, правящим веб-интерфейс, нужно пересобирать его перед
коммитом:
cd frontend
npm ci
npm run install-hooks # одноразово: ставит pre-commit hook для синхронизации dist
npm run lint
npm run typecheck
npm run build
npm run install-hooks опционален. CI его не требует: проверка
.github/workflows/frontend.yml запускает npm run check-dist и
блокирует слияние, если исходники меняли без пересборки dist/. Она
же запускает lint и typecheck на каждом PR, который трогает
frontend/.
Развёртывание
/metrics доступен без авторизации на том же HTTP-сервере, что и
консоль. Так задумано: иначе сломались бы существующие настройки
Prometheus. Авторизация на /api/* не распространяется на
/metrics — метрики раскрывают имена пулов, пользователей и БД,
давление на пул, состояние auth_query и форму нагрузки. Либо держите
секцию [web] на приватном host:port, доступном только системе
сбора метрик, либо ставьте перед HTTP-сервером прокси, который добавляет
авторизацию на /metrics отдельно.
Структурированное JSON-логирование
pg_doorman пишет структурированные JSON-логи при запуске с --log-format structured. Каждая строка — самодостаточный JSON-объект с timestamp, уровнем, местом в исходниках и сообщением, готовый к приёму в Loki, Elasticsearch, Datadog или любой пайплайн логов, ожидающий JSON.
Включение
Три равноценных способа:
# Флаг командной строки
pg_doorman -F structured /etc/pg_doorman/pg_doorman.yaml
# Длинная форма
pg_doorman --log-format structured /etc/pg_doorman/pg_doorman.yaml
# Переменная окружения
LOG_FORMAT=structured pg_doorman /etc/pg_doorman/pg_doorman.yaml
По умолчанию — text (человекочитаемый). Флаг --log-format принимает значения text, structured или debug; последнее пока работает как text.
Формат вывода
{"timestamp":"2026-04-25T08:32:14.512Z","level":"INFO","file":"src/app/server.rs","line":357,"message":"Server is up at 0.0.0.0:6432"}
{"timestamp":"2026-04-25T08:32:14.514Z","level":"INFO","file":"src/pool/mod.rs","line":421,"message":"Pool 'mydb' initialized: 1 user, pool_size=40"}
{"timestamp":"2026-04-25T08:32:18.103Z","level":"WARN","file":"src/server/protocol_io.rs","line":189,"message":"Backend connection lost: connection reset by peer"}
Поля:
| Поле | Тип | Примечания |
|---|---|---|
timestamp | строка RFC 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 обратно.
Оговорки
- Для промышленной эксплуатации выбирайте
text(терминалы, syslog) илиstructured(log shippers).debugзарезервирован под будущее использование и сейчас равенtext. fileиlineберутся из мест вызова макросаlog. Они доступны в release-сборках, потому что pg_doorman поставляется с включённой отладочной информацией.- Логгер не включает trace-идентификаторы и корреляцию запросов. Для трассировки на запрос используйте
SHOW CLIENTSи Prometheus-метрики.
Куда дальше
- Prometheus reference — для машинно-читаемых метрик.
- Latency Percentiles — для сигналов о производительности.
- Команды администратора — для интроспекции в рантайме.
Перцентили задержек
Переход на гистограммы. Метрики
pg_doorman_pools_queries_percentile,pg_doorman_pools_transactions_percentileиpg_doorman_pools_avg_wait_timeустарели и будут удалены в 3.10. Для новых запросов PromQL используйте гистограммы:
pg_doorman_pools_query_duration_secondspg_doorman_pools_transaction_duration_secondspg_doorman_pools_wait_duration_secondsПерцентили считайте через
histogram_quantile(q, sum by (le, ...) (rate(_bucket[5m]))). Такой запрос можно агрегировать между репликами; усреднение заранее посчитанных перцентилей корректного результата не даёт.
pg_doorman измеряет задержки запросов и транзакций на пул, используя HDR Histogram. В Prometheus экспортируются четыре перцентиля: p50, p90, p95, p99.
Эта страница объясняет, откуда берутся числа и как их читать.
Что измеряется
Три серии задержек на пару user×database:
| Серия | Что покрывает |
|---|---|
query_histogram | Время от старта запроса до его завершения на бэкенде. Измеряет время выполнения PostgreSQL так, как его видит pg_doorman. |
xact_histogram | Время от BEGIN (или первого оператора неявной транзакции) до COMMIT / ROLLBACK. |
wait_histogram | Время, которое клиент провёл в ожидании, пока соединение с бэкендом не освободится. |
wait_histogram — собственный вклад пула в задержку. Если p99 у wait_histogram высокий, а p99 у query_histogram низкий, узкое место — получение соединения, а не PostgreSQL.
Детали гистограммы
pg_doorman использует HDR Histogram с параметрами:
- Максимальное значение: 10 минут (600 секунд).
- Значащих цифр: 2 (около 0,1% относительной погрешности).
Расход памяти: около 10 KB на гистограмму. Три гистограммы на пару user×database — это ~30 KB на пул, что комфортно для сотен пулов.
По умолчанию горизонт отчёта — время жизни процесса. Гистограммы сбрасываются на SIGHUP (перезагрузка конфига) и при явном RECONNECT.
Odyssey использует TDigest, PgBouncer перцентили не экспортирует. HDR предпочтителен, когда вы знаете верхнюю границу (10 минут — щедрый запас для пула соединений); TDigest работает с неограниченными потоками.
Экспорт в Prometheus
# HELP pg_doorman_pools_queries_percentile Query latency percentiles in milliseconds
# TYPE pg_doorman_pools_queries_percentile gauge
pg_doorman_pools_queries_percentile{percentile="50",user="app",database="mydb"} 1.2
pg_doorman_pools_queries_percentile{percentile="90",user="app",database="mydb"} 4.7
pg_doorman_pools_queries_percentile{percentile="95",user="app",database="mydb"} 8.1
pg_doorman_pools_queries_percentile{percentile="99",user="app",database="mydb"} 24.5
# HELP pg_doorman_pools_transactions_percentile Transaction latency percentiles in milliseconds
# TYPE pg_doorman_pools_transactions_percentile gauge
pg_doorman_pools_transactions_percentile{percentile="50",user="app",database="mydb"} 3.8
# ... (90, 95, 99)
# HELP pg_doorman_pools_avg_wait_time Average client wait time in milliseconds
# TYPE pg_doorman_pools_avg_wait_time gauge
pg_doorman_pools_avg_wait_time{user="app",database="mydb"} 0.05
avg_wait_time — среднее, а не перцентиль (HDR для ожиданий тоже отслеживается, но сейчас экспортируется только среднее).
Чтение метрик
Здоровый пул
queries: p50=1.2 p90=4.7 p95=8.1 p99=24.5
xacts: p50=3.8 p90=11.2 p95=18.5 p99=42.7
wait avg: 0.05ms
p99 укладывается в 20× от p50 — типично для OLTP-нагрузок с редкими медленными запросами. Время ожидания — микросекунды, пул узким местом не работает.
Пул под давлением
queries: p50=1.5 p90=4.9 p95=8.5 p99=25.0
xacts: p50=215 p90=1850 p95=2400 p99=4900
wait avg: 180ms
С задержкой запросов всё в порядке — PostgreSQL здоров. Но транзакции медленные, а время ожидания 180 мс. Клиенты выстраиваются в очередь за серверными соединениями. Проверьте SHOW POOLS на cl_waiting > 0 и SHOW POOL_COORDINATOR на вытеснения или исчерпания. Вероятное лечение: поднять pool_size или max_db_connections. См. Координатор пулов.
Один медленный пользователь
user "fast_app": queries p99=12 xacts p99=35
user "report_job": queries p99=4500 xacts p99=8000
report_job тянет общую базу вниз. С включённым координатором пулов медленные транзакции report_job приводят к тому, что под давлением он первым отдаёт свои соединения (вытеснение смещено по p95-времени транзакций). Без координатора выделите report_job его собственный min_guaranteed_pool_size, чтобы он не голодил fast_app.
Grafana
Пример запроса задержки запросов по перцентилю:
pg_doorman_pools_queries_percentile{database="mydb"}
Пример алерта: p99 запросов выше 100 мс в течение 5 минут:
pg_doorman_pools_queries_percentile{percentile="99"} > 100
Пример алерта на насыщение очереди:
pg_doorman_pools_avg_wait_time > 50
JSON дашборда лежит в директории grafana/ проекта.
Оговорки
- Перцентили считаются на пул, а не на запрос. pg_doorman не скажет вам, какой именно запрос медленный, — для этого используйте
pg_stat_statementsв PostgreSQL. - HDR-гистограммы хранят значения, а не события. Один и тот же запрос, выполненный 100 тысяч раз, добавляет 100 тысяч сэмплов; частоту сэмплирования настроить нельзя.
- Экспорт всех четырёх перцентилей на серию сделан намеренно — экспортировать сырые корзины гистограммы в Prometheus было бы значительно тяжелее и редко полезно.
Куда дальше
- Admin Commands — читать перцентили напрямую через
SHOW POOLS_EXTENDED. - Prometheus reference — полный список метрик с метками.
- Pool Pressure — диагностические рецепты, когда перцентили выглядят неправильно.
- Бенчмарки — эталонные распределения перцентилей под нагрузкой.
Настройки
Формат конфигурационного файла
pg_doorman поддерживает два формата конфигурационного файла:
- YAML (
.yaml,.yml) — основной и рекомендуемый формат для новых конфигураций. - TOML (
.toml) — поддерживается для обратной совместимости с уже существующими конфигурациями.
Формат определяется автоматически по расширению файла. Оба формата поддерживают одни и те же опции конфигурации и могут использоваться взаимозаменяемо.
Пример конфигурации YAML (рекомендуется)
general:
host: "0.0.0.0"
port: 6432
admin_username: "admin"
admin_password: "change_me_to_a_long_random_secret"
pools:
mydb:
server_host: "localhost"
server_port: 5432
pool_mode: "transaction"
users:
- username: "myuser"
password: "md5..." # хеш из pg_shadow / pg_authid
pool_size: 40
Пример конфигурации TOML (для совместимости)
[general]
host = "0.0.0.0"
port = 6432
admin_username = "admin"
admin_password = "change_me_to_a_long_random_secret"
[pools.mydb]
server_host = "localhost"
server_port = 5432
pool_mode = "transaction"
[[pools.mydb.users]]
username = "myuser"
password = "md5..." # хеш из pg_shadow / pg_authid
pool_size = 40
Команда generate
Команда generate может выводить конфигурацию в любом из форматов. Формат определяется по расширению выходного файла. По умолчанию сгенерированная конфигурация содержит подробные inline-комментарии, поясняющие каждый параметр.
# Сгенерировать конфигурацию YAML (рекомендуется)
pg_doorman generate --output config.yaml
# Сгенерировать конфигурацию TOML (для обратной совместимости)
pg_doorman generate --output config.toml
# Сгенерировать полную справочную конфигурацию без подключения к PG
pg_doorman generate --reference --output config.yaml
# Сгенерировать справочную конфигурацию с комментариями на русском
pg_doorman generate --reference --ru --output config.yaml
# Сгенерировать конфигурацию без комментариев (только сериализация)
pg_doorman generate --no-comments --output config.yaml
| Флаг | Описание |
|---|---|
--no-comments | Отключить inline-комментарии в сгенерированной конфигурации (по умолчанию комментарии включены) |
--reference | Сгенерировать полную справочную конфигурацию с примерами значений, без подключения к PostgreSQL |
--russian-comments, --ru | Генерировать комментарии на русском языке для быстрого старта |
--format, -f | Формат вывода: yaml (по умолчанию) или toml. Если задан --output, формат определяется по расширению файла. Этот флаг переопределяет автоопределение |
Подключаемые файлы
Подключаемые файлы могут быть в любом из форматов, форматы можно смешивать. Например, основная конфигурация в YAML может подключать TOML-файлы и наоборот:
include:
files:
- "pools.yaml"
- "users.toml"
Человекочитаемые значения
pg_doorman поддерживает человекочитаемые форматы для значений продолжительности и размера в байтах, сохраняя обратную совместимость с числовыми значениями.
Формат продолжительности
Значения продолжительности можно задавать как:
- Простые числа: интерпретируются как миллисекунды (например,
5000= 5 секунд) - Строка с суффиксом:
ms— миллисекунды (например,"100ms")s— секунды (например,"5s"= 5000 миллисекунд)m— минуты (например,"5m"= 300000 миллисекунд)h— часы (например,"1h"= 3600000 миллисекунд)d— дни (например,"1d"= 86400000 миллисекунд)
Примеры:
general:
# Все эти варианты эквивалентны (3 секунды):
# connect_timeout: 3000 # обратная совместимость (миллисекунды)
# connect_timeout: "3s" # человекочитаемый формат
# connect_timeout: "3000ms" # явные миллисекунды
connect_timeout: "3s"
idle_timeout: "10m" # 10 минут
server_lifetime: "1h" # 1 час
Формат размера в байтах
Значения размера в байтах можно задавать как:
- Простые числа: интерпретируются как байты (например,
1048576= 1 MB) - Строка с суффиксом (регистр не важен):
B— байты (например,"1024B")KилиKB— килобайты (например,"1K"или"1KB"= 1024 байта)MилиMB— мегабайты (например,"1M"или"1MB"= 1048576 байт)GилиGB— гигабайты (например,"1G"или"1GB"= 1073741824 байт)
Примечание: используются двоичные префиксы (1 KB = 1024 байта, не 1000 байт).
Примеры:
general:
# Все эти варианты эквивалентны (256 MB):
# max_memory_usage: 268435456 # обратная совместимость (байты)
# max_memory_usage: "256MB" # человекочитаемый формат
# max_memory_usage: "256M" # короткая форма
max_memory_usage: "256MB"
unix_socket_buffer_size: "1MB" # 1 MB
worker_stack_size: "8MB" # 8 MB
Общие настройки
host
Хост, на котором сервер будет принимать соединения (только TCP v4).
По умолчанию: "0.0.0.0".
port
Порт для входящих соединений.
По умолчанию: 5432.
backlog
TCP backlog для входящих соединений. При значении ноль в качестве TCP backlog используется значение max_connections.
По умолчанию: 0.
max_connections
Максимальное число клиентов, которые могут одновременно подключиться к пулеру. При достижении лимита:
- Клиент, подключающийся без SSL, получит ожидаемую ошибку (код:
53300, сообщение:sorry, too many clients already). - Клиент, подключающийся через SSL, увидит сообщение о том, что сервер не поддерживает протокол SSL.
По умолчанию: 8192.
max_concurrent_creates
Максимальное число серверных соединений, которые могут создаваться параллельно в одном пуле. Параметр использует семафор для ограничения параллельного создания соединений, что заметно повышает производительность при холодном старте и пиковых сценариях.
Большие значения ускоряют прогрев пула, но могут увеличить нагрузку на сервер PostgreSQL во время штормов соединений. Меньшие значения дают более плавное создание соединений.
По умолчанию: 4.
tls_mode
Режим TLS для входящих соединений. Может принимать одно из следующих значений:
allow— TLS-соединения разрешены, но не обязательны. pg_doorman попытается установить TLS-соединение, если клиент его запросит.disable— TLS-соединения запрещены. Все соединения устанавливаются без шифрования TLS.require— TLS-соединения обязательны. pg_doorman принимает только соединения, использующие TLS-шифрование.verify-full— TLS-соединения обязательны, и pg_doorman проверяет клиентский сертификат. Этот режим обеспечивает максимальный уровень безопасности.
По умолчанию: "allow".
tls_ca_cert
Файл с CA-сертификатом для проверки клиентского сертификата. Обязателен, когда tls_mode установлен в verify-full.
По умолчанию: None.
tls_private_key
Путь к файлу приватного ключа для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_certificate.
По умолчанию: None.
tls_certificate
Путь к файлу сертификата для TLS-соединений. Требуется для включения TLS для входящих клиентских соединений. Должен использоваться вместе с tls_private_key.
По умолчанию: None.
tls_rate_limit_per_second
Ограничение числа одновременных попыток создания TLS-сессии. Любое значение, отличное от нуля, означает, что клиенты должны проходить через очередь для установки TLS-соединения. В некоторых случаях это необходимо, чтобы запустить приложение, которое открывает много соединений при старте (так называемый «горячий старт»).
По умолчанию: 0.
daemon_pid_file
Включение этого параметра активирует режим демона. Закомментируйте, если хотите запускать pg_doorman в foreground с флагом -d.
По умолчанию: "/tmp/pg_doorman.pid".
syslog_prog_name
Если задан, pg_doorman начинает отправлять сообщения в syslog (через /dev/log или /var/run/syslog). Закомментируйте, если хотите логировать в stdout.
По умолчанию: None.
log_client_connections
Логировать подключения клиентов для мониторинга.
По умолчанию: true.
log_client_disconnections
Логировать отключения клиентов для мониторинга.
По умолчанию: true.
worker_threads
Число worker-потоков Tokio runtime (потоков ОС) для обслуживания клиентских соединений.
Производительность масштабируется линейно до числа ядер CPU.
Также определяет число шардов для внутренних concurrent hash maps (worker_threads * 4, округлённо до ближайшей степени двойки, минимум 4).
В Kubernetes задавайте этот параметр явно — автоматическое определение CPU может вернуть число ядер хоста, а не лимит контейнера.
По умолчанию: 4.
worker_cpu_affinity_pinning
Привязывать каждый worker-поток к отдельному ядру CPU (sched_setaffinity). Отключается, если доступно меньше 3 ядер.
По умолчанию: false.
tokio_global_queue_interval
Настройки Tokio runtime.
Задаёт, как часто шедулер проверяет глобальную очередь задач.
Параметры группы tokio_* и worker_stack_size, max_blocking_threads опциональны: на современных версиях tokio дефолты разумны, и их обычно не нужно трогать.
По умолчанию: not set (uses tokio's default).
tokio_event_interval
Настройки Tokio runtime. Задаёт, как часто шедулер проверяет внешние события (I/O, таймеры).
По умолчанию: not set (uses tokio's default).
worker_stack_size
Настройки Tokio runtime. Размер стека для worker-потоков.
По умолчанию: not set (uses tokio's default).
max_blocking_threads
Настройки Tokio runtime. Максимальное число потоков для блокирующих операций.
По умолчанию: not set (uses tokio's default).
connect_timeout
Максимальное время ожидания при установке нового соединения с сервером PostgreSQL. Если соединение не удаётся установить за это время, попытка прерывается. Аналог server_connect_timeout из PgBouncer.
По умолчанию: 3000 (3 sec).
query_wait_timeout
Максимальное время ожидания клиентом серверного соединения, когда пул полностью занят. Если за это время серверное соединение не освобождается, клиент получает ошибку. Аналог query_wait_timeout из PgBouncer.
По умолчанию: 5000 (5 sec).
idle_timeout
Закрывать серверное соединение, которое простаивает (не выдано ни одному клиенту) дольше этого значения.
Применяется только к соединениям, обслужившим хотя бы один клиентский запрос. Прогретые или дополненные
соединения, никогда не выдававшиеся клиенту, под действие idle_timeout не попадают — они закрываются
только по истечении server_lifetime. Каждое соединение получает джиттер ±20%, чтобы избежать
синхронных массовых закрытий. Установите 0, чтобы отключить. Аналог server_idle_timeout из PgBouncer.
По умолчанию: 600000 (10 min).
server_lifetime
Максимальный возраст серверного соединения. Когда соединение превышает этот возраст и переходит
в idle, оно закрывается на ближайшем цикле retain. Активные транзакции не прерываются.
Применяется ко всем соединениям, включая прогретые, которые никогда не выдавались клиенту.
Каждое соединение получает джиттер ±20%, чтобы избежать лавины одновременных закрытий. Установите 0, чтобы отключить.
Аналог server_lifetime из PgBouncer.
По умолчанию: 1200000 (20 min).
retain_connections_time
Интервал проверки и закрытия idle-соединений, превысивших idle_timeout или server_lifetime.
Задача retain выполняется периодически с этим интервалом и удаляет просроченные соединения.
По умолчанию: 30000 (30 sec).
retain_connections_max
Максимальное число idle-соединений, закрываемых за один цикл retain.
При значении 0 все idle-соединения, превысившие idle_timeout или server_lifetime, закрываются немедленно.
При положительном значении за цикл по всем пулам закрывается не более указанного числа соединений.
Этот параметр управляет агрессивностью закрытия idle-соединений в pg_doorman. При значении по умолчанию 3
за цикл retain закрывается до 3 соединений, что обеспечивает контролируемую очистку. Если нужна
более быстрая очистка просроченных соединений, установите 0 (без ограничений), чтобы закрывать все
просроченные соединения за каждый цикл retain.
По умолчанию: 3.
server_idle_check_timeout
Время, после которого idle-серверное соединение должно быть проверено перед выдачей клиенту. Это помогает обнаружить мёртвые соединения, возникшие из-за рестарта PostgreSQL, сетевых проблем или серверных idle-таймаутов.
Когда соединение простояло в пуле дольше этого таймаута, pg_doorman отправляет минимальный запрос (;),
чтобы проверить, что соединение живо, прежде чем выдать его клиенту. Если проверка не проходит,
соединение отбрасывается и берётся новое.
Установите 0, чтобы отключить проверку (не рекомендуется для промышленной эксплуатации с возможной
сетевой нестабильностью или рестартами PostgreSQL).
По умолчанию: 60s (60 seconds).
server_round_robin
Задаёт, какое idle-серверное соединение выбирается для следующей транзакции.
false (MRU, LIFO): берётся последнее возвращённое в пул соединение. Горячих соединений меньше, локальность shared buffer в PostgreSQL выше.
true (Round Robin): равномерная ротация по всем idle-соединениям.
Аналог server_round_robin из PgBouncer.
По умолчанию: false.
sync_server_parameters
В транзакционном режиме разные транзакции одного клиента могут выполняться на разных серверных
соединениях. Если включить sync_server_parameters, pg_doorman перед началом транзакции применяет
к выбранному серверному соединению параметры сессии, которые запросил клиент.
Есть два источника таких параметров:
-
Сообщения PostgreSQL
ParameterStatus:client_encoding,DateStyle,IntervalStyle,TimeZone,standard_conforming_strings,application_name. PostgreSQL сам сообщает об изменении этих параметров по протоколу. -
Параметры из клиентского
StartupMessage(начиная с pg_doorman 3.10): любой безопасный параметр, который клиент прислал при подключении. pg_doorman отбрасывает служебные имена и имена только для чтения (is_superuser,server_version,lc_collate,transaction_isolation, ...) и префикс_pq_.. Так клиент может задатьsearch_path,default_transaction_isolation,roleи другие параметры, важные для планировщика, один раз при подключении. После этого pg_doorman применит их к любому серверному соединению, на котором окажется транзакция. Значения из операторской настройкиstartup_parametersвсегда имеют приоритет над клиентским пакетом.
Ограничения, которые важно учитывать:
-
pg_doorman отслеживает только параметры, заданные при подключении, и параметры, о которых PostgreSQL сам присылает
ParameterStatus. Если клиент после подключения выполнитSET search_path = ...или изменит другой параметр планировщика, о котором PostgreSQL не сообщает, pg_doorman не узнает об этом изменении. В таком случае повторное использование prepared statement может попасть на план, построенный при старом состоянии. Для таких клиентов лучше задавать параметры вStartupMessage, после изменения выполнятьDISCARD ALLили переподключаться, либо отключитьprepared_statementsдля пула. -
В ключ кэша prepared statement входят текст запроса, OID параметров и отпечаток только этих параметров планировщика из
StartupMessage:search_path,default_transaction_isolation,default_transaction_read_only,default_text_search_config,role. Другие параметры, которые тоже могут влиять на план (TimeZone,DateStyle,plan_cache_mode,enable_*, настройки стоимости JIT, параметры расширений), в ключ не входят. Если один и тот же запрос готовится под разными значениями таких параметров, отключитеprepared_statementsдля пула или закрепите эти параметры на уровне роли или базы.
Когда состояние серверного соединения отличается от состояния клиента, pg_doorman делает
дополнительный обмен с PostgreSQL для SET или RESET. Если вам нужна только видимость
application_name в pg_stat_activity, проще задать application_name на уровне пула.
По умолчанию: false.
tcp_so_linger
По умолчанию pg_doorman отправляет RST вместо того, чтобы держать соединение открытым долгое время.
По умолчанию: 0.
tcp_no_delay
TCP_NODELAY для отключения алгоритма Нэйгла ради более низкой задержки.
По умолчанию: true.
tcp_keepalives_count
Число неподтверждённых TCP keepalive probes, после которого соединение считается мёртвым и закрывается.
По умолчанию: 5.
tcp_keepalives_idle
Keepalive включён по умолчанию и переопределяет дефолты ОС.
По умолчанию: 5.
tcp_keepalives_interval
Интервал в секундах между отдельными TCP keepalive probes после прохождения начального периода простоя (tcp_keepalives_idle).
По умолчанию: 5.
tcp_user_timeout
Задаёт опцию сокета TCP_USER_TIMEOUT для клиентских соединений (в секундах). Эта опция указывает
максимальное время, в течение которого переданные данные могут оставаться неподтверждёнными,
прежде чем TCP принудительно закроет соединение. Это помогает быстрее обнаруживать мёртвые
клиентские соединения, чем keepalive probes, когда соединение активно отправляет данные, но удалённый
конец стал недоступен (например, сбой сети, падение клиента).
При ненулевом значении, если данные остаются неподтверждёнными в течение указанного времени, соединение будет разорвано. Это особенно полезно, чтобы избежать задержек в 15–16 минут, вызванных TCP retransmission timeout, когда keepalive не помогает (например, при активной передаче данных).
Примечание: опция поддерживается только в Linux. На других ОС параметр игнорируется.
Установите 0, чтобы отключить (использовать значение по умолчанию ОС).
По умолчанию: 60.
tcp_socket_buffer_size
Лимиты буферов ядра SO_RCVBUF и SO_SNDBUF для принятых клиентских TCP-сокетов и исходящих TCP-сокетов к PostgreSQL.
При значении по умолчанию 0 pg_doorman не вызывает setsockopt(SO_RCVBUF/SO_SNDBUF), поэтому буферами управляет автонастройка TCP в Linux. Буфер приёма каждого соединения может расти по требованию до net.ipv4.tcp_rmem[2] (обычно 6 MiB в Ubuntu/RHEL). Эта память не входит в RSS процесса; в зависимости от ядра и режима cgroup она может отображаться отдельно как socket memory, например как sock в cgroup v2 memory.stat, или быть заметна в основном как память ядра на хосте. Если MemFree заметно растёт после рестарта pg_doorman, проверьте источник через ss -m, /proc/net/sockstat, cgroup v2 memory.current и ключ sock в memory.stat.
Ненулевое значение вызывает setsockopt(SO_RCVBUF/SO_SNDBUF) один раз для каждого настраиваемого TCP-сокета. Это отключает автонастройку для этого сокета и задаёт фиксированные лимиты буферов отправки и приёма. Linux внутренне удваивает запрошенные значения — см. man 7 socket — и может ограничить их через net.core.rmem_max / net.core.wmem_max. Перед настройкой значений выше системного дефолта проверьте /proc/sys/net/core/rmem_max и /proc/sys/net/core/wmem_max. Применённые ядром значения видны через getsockopt, ss -m и DEBUG-логи pg_doorman.
Примерная верхняя граница в Linux: 4 * tcp_socket_buffer_size * число_TCP_сокетов. Pg_doorman задаёт буферы отправки и приёма, а Linux внутренне удваивает каждое значение. Например, tcp_socket_buffer_size = 65536 даёт около 256 KiB лимитов на один TCP-сокет, то есть примерно 15 MiB на 60 TCP-сокетов без учёта накладных расходов sk_buff. Считайте и клиентские TCP-сокеты, и TCP-сокеты к PostgreSQL. Фактически занятая память всё равно зависит от данных в очередях.
Этот параметр в первую очередь ограничивает память. Стартовый диапазон для OLTP внутри одного датацентра: 64 KiB – 256 KiB. Не ставьте меньше 64 KiB без замеров. Для WAN, межзонального трафика, больших результатов запроса и массовой передачи данных может понадобиться большее значение или поведение по умолчанию с автонастройкой TCP.
Значение применяется, когда pg_doorman настраивает TCP-сокет: к новым клиентским подключениям, новым TCP-соединениям к PostgreSQL и клиентским сокетам, восстановленным в новом процессе через SCM_RIGHTS. SIGHUP не пересматривает уже открытые сокеты. Чтобы применить новое значение к существующим сессиям, используйте горячую замену процесса для мигрирующих клиентов, RECONNECT/дренирование пулов для backend-сокетов или обычный рестарт, если переподключения допустимы.
Это аналог параметра tcp_socket_buffer в PgBouncer. В Odyssey и PgCat аналога нет, они наследуют поведение автонастройки TCP в Linux.
По умолчанию: 0.
unix_socket_buffer_size
Размер буфера для операций чтения и записи при подключении к PostgreSQL через unix-сокет.
По умолчанию: 1048576.
admin_username
Имя пользователя для виртуальной admin-базы.
По умолчанию: "admin".
admin_password
Пароль для виртуальной admin-базы. Замените на свой секрет.
По умолчанию: "admin".
prepared_statements
Включает подмену и кеширование prepared statements. Когда параметр
выключен, pg_doorman передаёт Parse и Bind без переписывания через
пуловый кеш prepared statements.
Если значение true, prepared_statements_cache_size должен быть
больше 0.
По умолчанию: true.
prepared_statements_cache_size
Размер кеша prepared statements на уровне пула (общий для всех клиентов, подключающихся к одному пулу). Кеш хранит соответствие между хешем запроса и переписанным именем prepared statement.
Это не выключатель. Чтобы отключить подмену prepared statements,
задайте prepared_statements: false; pg_doorman отклоняет общее
значение prepared_statements_cache_size = 0, пока prepared_statements
включён.
Полная картина того, как этот параметр взаимодействует с server_prepared_statements_cache_size,
client_anonymous_prepared_cache_size и query interner — в туториале
Кеширование анонимных prepared statements.
По умолчанию: 8192.
server_prepared_statements_cache_size
Размер LRU DOORMAN_<N> на каждое серверное соединение, независимо от кеша уровня пула.
Если параметр не задан, наследуется итоговый prepared_statements_cache_size для пула:
сначала проверяется per-pool override, затем общее значение. Per-pool override этого
параметра имеет приоритет над общим.
Уменьшайте значение ниже размера пула, когда бэкенды накапливают слишком много
DOORMAN_<N> (pg_prepared_statements упирается в лимит, плановая память растёт)
или когда нужна более быстрая утилизация через Close без уменьшения hit rate
на уровне пула. При prepared_statements: false принудительно равен 0.
По умолчанию: not set (наследует prepared_statements_cache_size).
client_anonymous_prepared_cache_size
Ограничивает Anonymous-часть per-client кеша prepared statements. Анонимные statements отправляются без явного имени и обычно короткоживущие; LRU задаёт верхнюю границу того, сколько таких записей один клиент может накопить, прежде чем будет выселена самая старая.
Если параметр не задан, наследуется итоговый prepared_statements_cache_size для пула.
Установите 0, чтобы отключить LRU и хранить Anonymous в неограниченной хеш-таблице;
задайте число, чтобы ограничить кеш на каждого клиента независимо от размера пула.
Named-часть per-client кеша (statements, созданные с явным именем через PREPARE или extended-query
Parse) всегда без ограничения — этот параметр на неё не влияет. Named statements живут в кеше до
закрытия клиентского соединения.
По умолчанию: not set (наследует prepared_statements_cache_size).
query_interner_gc_interval_seconds
Интернер запросов запускает двухцикловый mark-and-sweep сборщик. Named-записи вытесняются,
когда вне интернера никто не держит Arc<str>; анонимные — когда простаивают дольше
query_interner_anon_idle_ttl_seconds.
Этот параметр задаёт, как часто запускается коллектор. Реальный sweep тикает
gc_interval / 4, поэтому помеченная на цикле N запись имеет примерно четверть
интервала, чтобы её прочитали (и сняли пометку), пока цикл N+1 её не вытеснит.
Меньшие значения быстрее уменьшают интернер после волн отключений ценой большей
нагрузки на CPU. Значение 0 отклоняется при старте.
Restart-only: изменения подхватываются только после рестарта; config reload не меняет частоту проходов работающего процесса.
По умолчанию: 60.
query_interner_anon_idle_ttl_seconds
Ограничивает верхнюю границу памяти, которую pg_doorman тратит на хранение SQL-текста анонимного prepared statement после последнего Bind или Parse, ссылающегося на тот же hash. Когда анонимная запись простаивает дольше указанных секунд, она помечается и вытесняется на следующем проходе, который всё ещё видит её бездействующей.
Значение 0 полностью отключает TTL: анонимные записи живут до перезапуска процесса.
Это соответствует поведению до 3.7 и нужно для старых развёртываний, которые опираются
на кросс-batch unnamed prepared statements; в остальных случаях оставляйте значение по умолчанию.
Live-reloadable: перечитывается на каждом проходе, поэтому config reload меняет эффективный TTL без рестарта.
По умолчанию: 60.
message_size_to_be_stream
Когда сообщение DataRow PostgreSQL превышает этот порог, pg_doorman переключается в потоковый режим:
данные пересылаются клиенту чанками по 4 KB вместо буферизации всего сообщения целиком.
Это предотвращает OOM на запросах, возвращающих очень большие строки (например, таблицы с большими
колонками bytea/text). Сам порог по умолчанию равен 1 MB.
По умолчанию: 1048576 (1 MB).
scaling_warm_pool_ratio
Доля прогретого пула в процентах (0–100). Когда размер пула ниже этого порога
от max_size, новые соединения создаются немедленно. Выше порога пул сначала
крутится через быстрые ретраи, затем переходит в event-driven цикл упреждающего ожидания,
который ждёт возврата idle-соединения. Цикл ограничен оставшимся query_wait_timeout
клиента минус резерв 500 ms на путь создания, поэтому он не может вытолкнуть
вызывающего за его собственный дедлайн ожидания.
По умолчанию: 20.
scaling_fast_retries
Число быстрых ретраев через yield_now() для ожидания с низкой задержкой при выдаче
соединений выше порога прогрева пула. Каждый ретрай занимает примерно 1–5 мкс.
После исчерпания быстрых ретраев пул переходит в event-driven цикл упреждающего ожидания,
ограниченный оставшимся query_wait_timeout клиента.
По умолчанию: 10.
scaling_max_parallel_creates
Ограничитель всплесков для создания соединений. Без этого лимита N параллельных
вызывающих timeout_get, не нашедших соединения в idle-пуле, независимо инициируют
подключение к PostgreSQL, создавая лавину подключений под нагрузкой. С лимитом одновременно
выполняется не больше указанного числа creates на пул; остальные кратко ждут на Notify
и затем либо подхватывают только что возвращённое idle-соединение, либо занимают следующий
слот для create. Значение по умолчанию 2 — компромисс между пропускной способностью
и сглаживанием всплесков.
По умолчанию: 2.
max_memory_usage
Общий бюджет памяти для внутренних буферов, хранящих данные in-flight запросов по всем клиентским соединениям. При достижении лимита pg_doorman отклоняет новые запросы с ошибкой, пока существующие запросы не завершатся и не освободят свои буферы. Защищает процесс пулера от OOM при тяжёлой нагрузке или больших результирующих наборах.
По умолчанию: 268435456 (256 MB).
shutdown_timeout
При graceful shutdown (SIGTERM) pg_doorman ждёт до этого времени завершения in-flight транзакций перед принудительным закрытием соединений.
По умолчанию: 10000 (10 sec).
proxy_copy_data_timeout
Максимальное время ожидания операций копирования данных при проксировании, в миллисекундах.
По умолчанию: 15000 (15 sec).
server_tls_mode
Режим TLS для исходящих соединений к серверам PostgreSQL.
allow— Сначала пробовать без TLS; если сервер отказывает, повторить с TLS. Соответствует libpq sslmode=allow (по умолчанию).disable— TLS не используется.prefer— TLS используется, если сервер его поддерживает; иначе обычное соединение.require— TLS обязателен, но сертификат сервера не проверяется.verify-ca— TLS обязателен, и сертификат сервера проверяется поserver_tls_ca_cert.verify-full— TLS обязателен, сертификат проверяется, и hostname сервера должен совпадать с сертификатом.
По умолчанию: "allow".
server_tls_ca_cert
CA-сертификат для проверки сертификатов серверов PostgreSQL. Обязателен, когда server_tls_mode равен verify-ca или verify-full.
По умолчанию: None.
server_tls_certificate
Клиентский сертификат для mTLS с серверами PostgreSQL. Используйте в паре с server_tls_private_key.
По умолчанию: None.
server_tls_private_key
Приватный ключ для клиентского сертификата mTLS. Используйте в паре с server_tls_certificate.
По умолчанию: None.
hba
Список IP-сетей в нотации CIDR, с которых разрешено подключение к pg_doorman. Для тонкого контроля доступа per-database и per-user используйте pg_hba (см. ниже).
По умолчанию: [].
pg_hba
Новый стиль контроля доступа клиентов в формате pg_hba.conf PostgreSQL. Позволяет задавать тонкие правила доступа аналогично PostgreSQL: per-database, per-user, диапазоны адресов, требования TLS.
Указать general.pg_hba можно тремя способами:
- Как многострочную строку с содержимым файла
pg_hba.conf - Как объект с
path, указывающий на файл на диске - Как объект с
content, содержащий правила в виде строки
Примеры:
[general]
# Inline-содержимое (TOML-строка в тройных кавычках)
pg_hba = """
# type database user address method
host all all 10.0.0.0/8 md5
hostssl all all 0.0.0.0/0 scram-sha-256
hostnossl all all 192.168.1.0/24 trust
"""
# Или загрузить из файла
# pg_hba = { path = "./pg_hba.conf" }
# Или встроить как однострочную строку
# pg_hba = { content = "host all all 127.0.0.1/32 trust" }
Поддерживаемые поля и методы:
- Типы соединений:
local,host,hostssl,hostnossl(TLS-зависимое сопоставление учитывается) - Сопоставитель базы: имя или
all - Сопоставитель пользователя: имя или
all - Адрес: CIDR-форма вроде
1.2.3.4/32или::1/128(обязателен для правил, отличных отlocal) - Методы:
trust,md5,scram-sha-256(неизвестные методы парсятся, но трактуются проверяющим как «не разрешено»)
Приоритет и совместимость:
general.pg_hbaимеет приоритет над устаревшим спискомgeneral.hba. Нельзя задавать оба одновременно; валидация конфигурации отклонит такую комбинацию.- Правила вычисляются по порядку; первое совпавшее правило определяет результат.
Поведение method = trust:
- Когда совпавшее правило имеет
trust, PgDoorman принимает соединение без запроса пароля. Это повторяет поведение PostgreSQL. - В частности, при срабатывании
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не совпал.
startup_parameters
Базовые параметры PostgreSQL, которые pg_doorman добавляет в
StartupMessage каждого нового бэкенда. startup_parameters уровня
пула переопределяют эти значения по ключу, а passthrough auth_query
может переопределить их для конкретного пользователя.
При загрузке конфигурации pg_doorman проверяет зарезервированные
протокольные ключи (user, database, replication, options,
_pq_.*), имена GUC, нулевые байты и размер этого уровня. Перед каждым
запуском бэкенда объединённый набор параметров снова проверяется по
лимиту MAX_STARTUP_PACKET_LENGTH PostgreSQL (10 000 байт). Любое
переполнение отклоняет запуск бэкенда с SQLSTATE 53400
(configuration_limit_exceeded), вместо отправки частичного или пустого
StartupMessage.
Если PostgreSQL отвергает параметр при запуске бэкенда, pg_doorman
возвращает клиенту ErrorResponse PostgreSQL без изменений: повторной
попытки без этого ключа не будет, сам ключ автоматически не отключается.
Накопительный счётчик отказов экспортируется как
pg_doorman_backend_startup_parameter_errors_total{pool, sqlstate};
имя параметра и пользователя остаются в строке лога уровня warn.
Итоговые параметры по каждому пулу видны через SHOW STARTUP_PARAMETERS
в административной SQL-консоли и через /api/pools в веб-интерфейсе.
По умолчанию: {}.
pooler_check_query
Когда клиент отправляет ровно этот запрос как SimpleQuery, pg_doorman обслуживает его через кеш ответа на уровне пула. Первая совпадающая проба за время жизни каждого пула отправляется в PostgreSQL, и полный ответ сохраняется. Последующие совпадающие пробы отвечаются из кеша без обращения к бэкенду.
Кеш индексируется по строке запроса. RELOAD с другим значением pooler_check_query
инвалидирует кеш на следующей пробе; новое значение вызывает одну свежую пробу к бэкенду
и затем обслуживается из кеша, пока значение снова не изменится. RELOAD с тем же значением
не сбрасывает кеш. ErrorResponse от бэкенда передаётся клиенту без изменений и не кешируется,
поэтому следующая проба снова идёт в PostgreSQL.
Поведение на холодном пуле изменилось: первая проба в каждом пуле теперь делает один
round-trip к PostgreSQL даже для значения по умолчанию ;. Если PostgreSQL в этот момент
недоступен, клиент-пробер увидит ошибку пробы вместо безусловного OK. Прежний хардкод
локального ответа сообщал, что пулер здоров, даже когда PostgreSQL лежал, и для непустых
значений вроде select 1 возвращал пустой ответ.
Контракт для оператора. Запрос должен быть детерминированным: один и тот же ввод
обязан давать один и тот же набор байт, без побочных эффектов. Безопасные значения:
;, select 1, select 'pg_doorman', select version().
Небезопасные значения, которые кеш молча заморозит:
select now(),select clock_timestamp()— закешированный timestamp перестанет идти вперёд.select pg_is_in_recovery()— failover поменяет роль на PostgreSQL, но закешированный ответ всё ещё будет показывать прежнюю роль.select count(*) from <table>— закешированное число останется тем, что увидела первая проба.UPDATE,INSERT,DELETE,CALL,DO— побочный эффект выполнится один раз, а ответ об успехе закешируется навсегда.
Доля попаданий в кеш экспортируется двумя счётчиками без меток:
pg_doorman_pooler_check_query_backend_total (пробы, отправленные в PostgreSQL) и
pg_doorman_pooler_check_query_cache_total (пробы, обслуженные из кеша). Отношение
cache_total / (cache_total + backend_total) — это hit rate.
По умолчанию: ";".
Настройки пула
Каждая запись в pool — это имя виртуальной базы данных, к которой может подключиться клиент pg_doorman.
[pools.exampledb] # Объявление базы данных 'exampledb'
server_host
Каталог с unix-сокетами или IPv4-адрес сервера PostgreSQL, обслуживающего этот пул.
Пример: "/var/run/postgresql" или "127.0.0.1".
По умолчанию: "127.0.0.1".
server_port
Порт, через который сервер PostgreSQL принимает входящие соединения.
По умолчанию: 5432.
server_database
Опциональный параметр, определяющий, к какой базе нужно подключаться на сервере PostgreSQL.
application_name
Параметр application_name, отправляемый серверу при открытии соединения с PostgreSQL. Может быть полезен при настройке sync_server_parameters = false.
connect_timeout
Максимальное время на установку нового серверного соединения для этого пула, в миллисекундах. Если не задано, используется глобальная настройка connect_timeout.
По умолчанию: None (uses global setting).
idle_timeout
Время жизни idle-соединения в пуле в миллисекундах; соединения старше указанного значения закрываются. Если не задано, берётся глобальный idle_timeout.
По умолчанию: None (uses global setting).
server_lifetime
Время жизни серверного соединения в пуле в миллисекундах; idle-соединения старше указанного значения закрываются. Если не задано, берётся глобальный server_lifetime.
По умолчанию: None (uses global setting).
pool_mode
Когда бэкенд-соединение возвращается в пул.
transaction: освобождается после каждой транзакции. session: удерживается до отключения клиента.
То же, что pool_mode в PgBouncer.
По умолчанию: "transaction".
log_client_parameter_status_changes
Логировать информацию о любой команде SET в лог.
По умолчанию: false.
cleanup_server_connections
Сбрасывать ли состояние сессии при возврате соединения в пул.
Когда параметр включён и сессия была изменена, pg_doorman отправляет: RESET ROLE, плюс при необходимости
RESET ALL (если использовался SET), DEALLOCATE ALL (если использовался PREPARE), CLOSE ALL
(если открывались курсоры). Замечание: ROLLBACK для открытых транзакций выполняется всегда, независимо
от этой настройки. Отключайте только если ваше приложение никогда не использует SET, prepared statements
или курсоры и вы хотите сэкономить roundtrip на очистке.
По умолчанию: true.
scaling_warm_pool_ratio
Переопределяет глобальный scaling_warm_pool_ratio для этого пула. Если не задано, используется глобальная настройка.
scaling_fast_retries
Переопределяет глобальный scaling_fast_retries для этого пула. Если не задано, используется глобальная настройка.
max_db_connections
Жёсткий потолок суммарного числа серверных соединений к этой базе, разделяемый между всеми
пользовательскими пулами. При достижении лимита и запросе нового соединения координатор сначала
пытается вытеснить idle-соединения у других пользователей (учитывая их min_pool_size), затем ждёт
возврата соединения и наконец откатывается к резервному пулу. Установите 0 (или опустите), чтобы
отключить координацию — каждый пользовательский пул работает независимо, ограниченный только своим
pool_size. Аналог max_db_connections из PgBouncer.
По умолчанию: 0 (disabled).
min_connection_lifetime
Минимальный возраст (в миллисекундах), которого должно достичь соединение, прежде чем оно сможет быть
вытеснено координатором пула. Предотвращает циклическое переподключение между пользовательскими пулами,
разделяющими одну базу: без этого порога idle-слот одного пользователя становится доступен для вытеснения
сразу же, как только сосед запрашивает разрешение, и под устойчивой многопользовательской нагрузкой
каждый пул отбирает слот у соседа каждые несколько секунд. Имеет значение только при max_db_connections > 0.
По умолчанию: 30000 (30 seconds).
reserve_pool_size
Число дополнительных соединений, разрешённых сверх max_db_connections в качестве крайней меры.
Когда вытеснение не удаётся и соединения не возвращаются за reserve_pool_timeout, резервное
соединение выдаётся запрашивающему с наивысшим приоритетом. Пользователи ниже своего min_pool_size
получают абсолютный приоритет. Имеет значение только при max_db_connections > 0.
По умолчанию: 0.
reserve_pool_timeout
Сколько времени (в миллисекундах) ждать освобождения обычного соединения, прежде чем
обратиться к резервному пулу. В этом окне координатор слушает возвраты соединений.
Имеет значение только при max_db_connections > 0 и reserve_pool_size > 0.
По умолчанию: 3000 (3 seconds).
min_guaranteed_pool_size
Дефолт уровня пула для минимального числа соединений на пользователя, защищённых от вытеснения координатором. Когда координатору нужно освободить слот соединения для другого пользователя, он не будет вытеснять соединения у пользователя, у которого их количество меньше или равно этому значению.
Это отличается от min_pool_size (уровня пользователя): min_pool_size управляет прогревом
и пополнением (проактивное создание соединений), тогда как min_guaranteed_pool_size влияет
только на решения о вытеснении (никогда не создаёт соединения).
Эффективная защита для пользователя — max(user.min_pool_size, pool.min_guaranteed_pool_size).
Установите 0 (или опустите), чтобы отключить защиту от вытеснения. Имеет значение только при max_db_connections > 0.
По умолчанию: 0 (no protection).
startup_parameters
Параметры PostgreSQL уровня пула, которые pg_doorman добавляет в
StartupMessage каждого нового бэкенда. Значения переопределяют
general.startup_parameters по ключу. В passthrough-пулах auth_query
колонка startup_parameters может переопределить и этот уровень для
конкретного пользователя.
При загрузке конфигурации pg_doorman проверяет зарезервированные
протокольные ключи, имена GUC, нулевые байты и размер этого уровня. Перед
каждым запуском бэкенда объединённый набор параметров снова проверяется
по лимиту MAX_STARTUP_PACKET_LENGTH PostgreSQL; если он не помещается,
pg_doorman отклоняет запуск бэкенда с SQLSTATE 53400
(configuration_limit_exceeded) вместо отправки частичного или пустого
StartupMessage.
По умолчанию: {}.
Настройки auth_query
Секция auth_query включает динамическую аутентификацию пользователей через запрос учётных данных
у базы PostgreSQL во время подключения. Это позволяет pg_doorman аутентифицировать пользователей
без статического перечисления их в конфигурационном файле.
pools:
mydb:
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "doorman_auth"
password: "auth_password"
Существуют два режима работы:
- Dedicated mode (задан
server_user): все динамически аутентифицированные пользователи разделяют один пул соединений, который подключается к PostgreSQL какserver_user. Это самая простая настройка, хорошо подходящая, когда всем пользователям нужен одинаковый бэкенд-доступ. - Passthrough mode (
server_userне задан): каждый динамически аутентифицированный пользователь получает собственный пул соединений, подключающийся к PostgreSQL по его собственным учётным данным (MD5 pass-the-hash или SCRAM ClientKey passthrough). Это сохраняет идентичность каждого пользователя на бэкенде.
Статические пользователи (определённые в секции users) всегда проверяются первыми. auth_query используется только когда имя пользователя не найдено среди статических.
user, выполняющий auth-запросы, должен иметь доступ к хешам паролей (например, из pg_shadow). Не используйте суперпользователя для этой цели. Вместо этого создайте функцию SECURITY DEFINER, принадлежащую суперпользователю, и выделенную роль с минимальными привилегиями:
-- Создаём выделенную роль для auth-запросов
CREATE ROLE doorman_auth LOGIN PASSWORD 'strong_password';
-- Создаём функцию SECURITY DEFINER (выполняется с привилегиями владельца)
CREATE OR REPLACE FUNCTION pg_doorman_get_auth(p_usename TEXT)
RETURNS TABLE (usename name, passwd text)
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS
$$
SELECT usename, passwd FROM pg_shadow WHERE usename = p_usename;
$$;
-- Выдаём execute только выделенной роли
REVOKE ALL ON FUNCTION pg_doorman_get_auth(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION pg_doorman_get_auth(TEXT) TO doorman_auth;
Затем используйте эту функцию в параметре query:
auth_query:
query: "SELECT * FROM pg_doorman_get_auth($1)"
user: "doorman_auth"
password: "strong_password"
query
SQL-запрос для получения учётных данных. Он должен возвращать колонку с
именем passwd или password, содержащую MD5- или SCRAM-хеш. Если
запрос возвращает ровно одну колонку, pg_doorman использует её независимо
от имени.
Дополнительные колонки игнорируются, кроме необязательной
startup_parameters типа text. В passthrough-режиме pg_doorman читает
её как JSON-объект с параметрами запуска PostgreSQL для этого пользователя.
Dedicated-режим игнорирует эту колонку и пишет предупреждение. В
качестве плейсхолдера для имени пользователя используйте $1.
Пример: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user
Имя пользователя PostgreSQL для executor-соединения, выполняющего auth-запросы.
password
Пароль executor-пользователя (открытым текстом). Может быть пустым, если сервер PostgreSQL использует аутентификацию trust для этого пользователя.
По умолчанию: "".
database
База для executor-соединений. Если не задана, используется имя пула.
По умолчанию: None (uses pool name).
workers
Число постоянных соединений с PostgreSQL, выделенных под выполнение SQL auth_query.
Эти соединения открываются при старте и поддерживаются живыми. Они обрабатывают только поиск
учётных данных — клиентский трафик данных идёт через отдельные соединения пула данных (pool_size).
Увеличьте, если видите всплески задержки auth при высокой частоте подключений.
По умолчанию: 2.
server_user
Бэкенд-пользователь PostgreSQL для соединений с данными в dedicated mode. Когда задан, все динамически аутентифицированные пользователи разделяют один пул соединений, подключающийся под этим пользователем. Когда не задан, используется passthrough mode.
По умолчанию: None (passthrough mode).
server_password
Пароль server_user открытым текстом. Имеет смысл только когда задан server_user.
По умолчанию: None.
pool_size
Максимальное число бэкенд-соединений на каждый пул данных, создаваемый auth_query.
Та же концепция, что и users[].pool_size для статически заданных пользователей.
Сколько пулов будет создано, зависит от режима: server_user определяет, разделяют ли
все динамические пользователи один пул или у каждого свой.
По умолчанию: 40.
min_pool_size
Минимальное число бэкенд-соединений, поддерживаемое в каждом пуле динамического пользователя в passthrough mode. Соединения прогреваются при первом создании пула и пополняются циклом retain. Установите 0, чтобы отключить (по умолчанию). Замечание: пулы с min_pool_size > 0 никогда не подвергаются garbage collection, и общее число бэкенд-соединений масштабируется как active_users × min_pool_size.
По умолчанию: 0.
cache_ttl
Максимальное время жизни кеша для успешно полученных учётных данных. Принимает строки продолжительности вроде "1h", "30m", "300s".
По умолчанию: "1h".
cache_failure_ttl
TTL кеша для записей «пользователь не найден» (negative cache). Предотвращает повторные запросы для несуществующих пользователей.
По умолчанию: "30s".
min_interval
Минимальный интервал между повторными запросами для одного и того же имени пользователя после неудачной аутентификации. Защищает бэкенд от избыточных запросов при попытках перебора.
По умолчанию: "1s".
Настройки пользователей пула
[pools.exampledb.users.0]
username = "exampledb-user-0" # Виртуальный пользователь, который может подключаться к этой виртуальной базе.
username
Имя пользователя, под которым клиенты подключаются к этому пулу. Должно быть уникальным в рамках пула.
password
Верификатор пароля для аутентификации клиента. Поддерживает форматы MD5, SCRAM-SHA-256 и JWT.
Хеши паролей можно скопировать напрямую из PostgreSQL: SELECT usename, passwd FROM pg_shadow.
auth_pam_service
PAM-сервис, отвечающий за авторизацию клиента. В этом случае pg_doorman игнорирует значение password.
server_username
Реальное имя пользователя PostgreSQL, используемое для подключения к серверу базы.
По умолчанию PgDoorman использует одно и то же username и для аутентификации клиента, и для серверных соединений, применяя passthrough authentication: криптографический материал из аутентификации клиента (MD5-хеш или SCRAM ClientKey) переиспользуется для аутентификации на бэкенде. Это устраняет потребность в server_password открытым текстом.
Passthrough mode (рекомендуется для пользователей с совпадающей идентичностью):
- Опустите и
server_username, иserver_password - pg_doorman переиспользует доказательство аутентификации клиента для подключения к PostgreSQL
- Для MD5: хеш из
passwordиспользуется напрямую - Для SCRAM: ClientKey извлекается из первой SCRAM-аутентификации клиента и кешируется
- Требование: верификатор
passwordдолжен совпадать сpg_authidна бэкенде (та же соль/итерации для SCRAM, тот же хеш для MD5)
Explicit credentials mode (когда идентичности различаются):
- Установите
server_usernameиserver_passwordв реальные учётные данные PostgreSQL server_passwordтребует, чтобыserver_usernameбыл заданserver_usernameбезserver_passwordдопустим для аутентификации trust
server_password
Пароль открытым текстом для серверного пользователя PostgreSQL, указанного в server_username.
Когда server_password не задан и пользователь имеет право на passthrough (нет server_username или server_username равен username), PgDoorman использует passthrough authentication: криптографический материал из аутентификации клиента переиспользуется для бэкенд-соединения. Это убирает пароли открытым текстом из конфигурационных файлов.
server_password требует, чтобы server_username был задан.
pool_size
Максимальное число бэкенд-соединений с PostgreSQL для этого пользователя. В transaction mode соединения разделяются между клиентами, поэтому это значение обычно намного меньше числа клиентов. Аналог default_pool_size из PgBouncer, но настраивается per-user, а не глобально.
По умолчанию: 40.
min_pool_size
Минимальное число соединений, поддерживаемое в пуле для этого пользователя. Соединения прогреваются при старте (до первого цикла retain) и затем поддерживаются периодическим пополнением. Если задано, должно быть меньше или равно pool_size.
По умолчанию: None.
server_lifetime
Закрывать серверные соединения для этого пользователя, открытые дольше указанного значения, в миллисекундах. Применяется только к idle-соединениям. Если не задано, используется настройка server_lifetime пула.
По умолчанию: None (uses pool setting).
По умолчанию PgDoorman использует passthrough authentication: криптографическое доказательство клиента (MD5-хеш или SCRAM ClientKey) автоматически переиспользуется для аутентификации в PostgreSQL. Пароли открытым текстом в конфиге не нужны.
Задавайте server_username и server_password только когда бэкенд-пользователь PostgreSQL отличается от имени пользователя пула (например, переопределение имени пользователя или JWT-аутентификация):
users:
- username: "app_user" # имя для клиента
password: "md5..." # хеш для аутентификации клиента
server_username: "pg_app_user" # другой бэкенд-пользователь PostgreSQL
server_password: "plaintext_pwd" # пароль открытым текстом для этого пользователя
Настройки Prometheus
pg_doorman отдаёт Prometheus-метрики на HTTP-сервере из секции [web]. Включите /metrics через [web]; таблицы ниже показывают, какие состояния пулера отражают метрики.
Включение HTTP-сервера
/metrics и встроенная веб-консоль обслуживаются одним и тем же HTTP-сервером [web]. Старые ключи prometheus.* пока принимаются как алиасы web.*, но новые конфиги должны использовать [web].
web:
enabled: true # включить HTTP-сервер для /metrics
host: "0.0.0.0"
port: 9127
# Веб-консоль по умолчанию выключена; см. руководство по веб-консоли
ui: false
ui_anonymous: false
Опции конфигурации
Полное описание веб-консоли — в Веб-консоли. Минимум для /metrics:
| Опция | Описание | По умолчанию |
|---|---|---|
enabled | Включить или отключить HTTP-сервер с /metrics. | false |
host | Адрес, на котором HTTP-сервер принимает соединения. | "0.0.0.0" |
port | Порт HTTP-сервера. | 9127 |
Настройка Prometheus
Добавьте следующий job в конфигурацию Prometheus, чтобы собирать метрики с pg_doorman:
scrape_configs:
- job_name: 'pg_doorman'
static_configs:
- targets: ['<pg_doorman_host>:9127']
Замените <pg_doorman_host> на имя хоста или IP-адрес вашего инстанса pg_doorman.
Доступные метрики
pg_doorman экспортирует следующие метрики:
Системные метрики
| Метрика | Описание |
|---|---|
pg_doorman_total_memory | Общий объём памяти, выделенный процессу pg_doorman, в байтах. Позволяет отслеживать потребление памяти приложением. |
Метрики соединений
| Метрика | Описание |
|---|---|
pg_doorman_connections_total | Накопительный счётчик принятых клиентских соединений по типу: plain (без TLS), tls, cancel (запрос отмены), total (сумма). Для темпа подключений используйте rate(pg_doorman_connections_total[5m]). |
pg_doorman_connection_count | Устаревшая gauge-версия pg_doorman_connections_total; будет удалена в 3.10. Новые правила и панели должны использовать pg_doorman_connections_total. |
Метрики сокетов (только Linux)
| Метрика | Описание |
|---|---|
pg_doorman_sockets | Счётчик сокетов, используемых pg_doorman, по типу сокета. Типы: 'tcp' (IPv4 TCP-сокеты), 'tcp6' (IPv6 TCP-сокеты), 'unix' (Unix domain sockets), 'unknown' (сокеты нераспознанного типа). Доступно только в Linux. |
Метрики пула
| Метрика | Описание |
|---|---|
pg_doorman_pools_clients | Число клиентов в пулах соединений по статусу, пользователю и базе. Значения статуса: idle (подключён, но не выполняет запросы), waiting (ждёт серверного соединения), active (выполняет запросы). |
pg_doorman_pools_servers | Число серверов в пулах соединений по статусу, пользователю и базе. Значения статуса: active (обслуживает клиента) и idle (свободен для новых соединений). |
pg_doorman_pools_bytes_total | Накопительный счётчик байт, переданных через пулы соединений, по направлению (received/sent), пользователю и базе. Для пропускной способности используйте rate(pg_doorman_pools_bytes_total[5m]). |
pg_doorman_pools_bytes | Устаревшая gauge-версия pg_doorman_pools_bytes_total; будет удалена в 3.10. |
pg_doorman_pool_size | Сконфигурированный максимальный размер пула на пользователя и базу. Полезен для расчёта оставшейся ёмкости пула вместе с pg_doorman_pools_servers. |
pg_doorman_backend_startup_parameter_errors_total | Накопительный счётчик запусков бэкенда, которые PostgreSQL отклонил из-за startup_parameters. Лейблы: пул и SQLSTATE. Отклонённый параметр и имя пользователя пишутся в строку лога уровня warn, а не в лейблы метрики. |
pg_doorman_startup_parameters_dropped_total | Накопительный счётчик событий, когда pg_doorman отбросил startup_parameters до отправки StartupMessage. Лейблы: пул и причина (cascade_budget_exceeded, packet_cap_exceeded, auth_query_oversize, auth_query_overlay_oversize, auth_query_bad_type, auth_query_invalid_json, auth_query_invalid_shape, auth_query_invalid_entry, dedicated_mode). |
Метрики запросов и транзакций
| Метрика | Описание |
|---|---|
pg_doorman_pools_query_duration_seconds | Гистограмма времени выполнения запросов на стороне PostgreSQL по пулу, в секундах. Квантили считайте через histogram_quantile(q, sum by (le, user, database) (rate(pg_doorman_pools_query_duration_seconds_bucket[5m]))); QPS — через rate(..._count[5m]). |
pg_doorman_pools_transaction_duration_seconds | Гистограмма полного времени транзакций по пулу, в секундах. Агрегируется так же, как pg_doorman_pools_query_duration_seconds. |
pg_doorman_pools_wait_duration_seconds | Гистограмма времени ожидания выдачи backend-соединения клиенту, в секундах. Для p99 используйте histogram_quantile(0.99, ...). |
pg_doorman_pools_transactions_total | Накопительный счётчик транзакций по пулу. Для TPS используйте rate(pg_doorman_pools_transactions_total[5m]). |
pg_doorman_pools_queries_percentile | Устаревшая метрика; будет удалена в 3.10. Это заранее посчитанные перцентили, которые нельзя корректно суммировать между репликами. Используйте pg_doorman_pools_query_duration_seconds_bucket и histogram_quantile(). |
pg_doorman_pools_transactions_percentile | Устаревшая метрика; будет удалена в 3.10. Используйте pg_doorman_pools_transaction_duration_seconds. |
pg_doorman_pools_transactions_count | Устаревшая gauge-версия pg_doorman_pools_transactions_total; будет удалена в 3.10. |
pg_doorman_pools_transactions_total_time | Сумма времени выполнения транзакций в пулах соединений, по пользователю и базе. В миллисекундах. |
pg_doorman_pools_queries_total | Накопительный счётчик запросов по пулу. Для QPS используйте rate(pg_doorman_pools_queries_total[5m]). |
pg_doorman_pools_queries_count | Устаревшая gauge-версия pg_doorman_pools_queries_total; будет удалена в 3.10. |
pg_doorman_pools_queries_total_time | Сумма времени выполнения запросов в пулах соединений, по пользователю и базе. В миллисекундах. |
pg_doorman_pools_avg_wait_time | Устаревшая метрика; будет удалена в 3.10. Это среднее значение, которое сглаживает пики хвостовой задержки. Используйте pg_doorman_pools_wait_duration_seconds_bucket и histogram_quantile(). |
Метрики auth_query
Эти метрики доступны только когда auth_query сконфигурирован для одного или нескольких пулов.
| Метрика | Описание |
|---|---|
pg_doorman_auth_query_cache_total | Накопительные события кеша auth_query по типу (hits/misses/refetches/rate_limited) и базе. Текущее число записей остаётся в pg_doorman_auth_query_cache{type="entries"}. |
pg_doorman_auth_query_auth_total | Накопительный счётчик результатов auth_query-аутентификации по result (success/failure) и базе. |
pg_doorman_auth_query_executor_total | Накопительный счётчик событий исполнителя auth_query по типу (queries/errors) и базе. |
pg_doorman_auth_query_dynamic_pools_total | Накопительный счётчик событий жизненного цикла динамических пулов auth_query по типу (created/destroyed) и базе. Текущее число пулов остаётся в pg_doorman_auth_query_dynamic_pools{type="current"}. |
pg_doorman_auth_query_cache | Текущее число закешированных учётных данных (type="entries"). Накопительные значения в этой метрике устарели; используйте pg_doorman_auth_query_cache_total. |
pg_doorman_auth_query_auth | Устаревшая gauge-версия pg_doorman_auth_query_auth_total; будет удалена в 3.10. |
pg_doorman_auth_query_executor | Устаревшая gauge-версия pg_doorman_auth_query_executor_total; будет удалена в 3.10. |
pg_doorman_auth_query_dynamic_pools | Метрики жизненного цикла динамических пулов auth_query по типу и базе. Типы: current (сейчас активные динамические пулы), created (всего создано с момента старта), destroyed (всего удалено сборщиком или при RELOAD). Имеет смысл только в passthrough mode. |
Метрики серверов
| Метрика | Описание |
|---|---|
pg_doorman_servers_prepared_hits | Текущая сумма попаданий в кеш prepared statements по активным бэкендам пула, с лейблами user и database. Gauge может уменьшаться при ротации бэкендов; для rate() используйте pg_doorman_servers_prepared_hits_total. |
pg_doorman_servers_prepared_misses | Текущая сумма промахов prepared statements по активным бэкендам пула, с лейблами user и database. Gauge может уменьшаться при ротации бэкендов; для rate() используйте pg_doorman_servers_prepared_misses_total. |
pg_doorman_servers_prepared_hits_total | Накопительный счётчик попаданий в кеш prepared statements по всем бэкендам пула, с лейблами user и database. Используйте rate() для скорости попаданий. |
pg_doorman_servers_prepared_misses_total | Накопительный счётчик промахов prepared statements по всем бэкендам пула, с лейблами user и database. Устойчивая ненулевая скорость означает, что запросы часто готовятся заново или кеш server_prepared_statements_cache_size слишком мал. |
Метрики клиентского кеша prepared statements
Клиентский кеш prepared statements делится на неограниченную Named-таблицу и Anonymous LRU, ограниченный client_anonymous_prepared_cache_size. Если этот параметр не задан, используется итоговый prepared_statements_cache_size.
| Метрика | Описание |
|---|---|
pg_doorman_clients_prepared_named_entries | Gauge с лейблами user и database. Сумма Named-записей по кешам всех подключённых клиентов. Named-записи не имеют верхнего лимита и живут до отключения клиента или DEALLOCATE. Устойчивый рост часто означает, что драйвер или ORM создаёт новые имена statement на каждый запрос. |
pg_doorman_clients_prepared_anonymous_entries | Gauge с лейблами user и database. Сумма Anonymous-записей по кешам всех подключённых клиентов. Anonymous-часть каждого клиента ограничена client_anonymous_prepared_cache_size, поэтому значение приближается максимум к connected_clients * cache_size. |
pg_doorman_clients_prepared_anonymous_evictions_total | Накопительный счётчик вытеснений из Anonymous LRU, с лейблами user и database. Устойчивая ненулевая скорость означает, что client_anonymous_prepared_cache_size мал для нагрузки и LRU вытесняет записи быстрее, чем приложение успевает их повторно использовать. |
Метрики query interner
Query interner общий для процесса. У этих метрик нет лейблов пула, пользователя или базы; для привязки к нагрузке смотрите метрики prepared statements выше и WARN-строки о synthetic miss.
| Метрика | Описание |
|---|---|
pg_doorman_query_interner_entries | Gauge по kind (named или anonymous). Число интернированных текстов запросов. Обновляется один раз за проход GC. |
pg_doorman_query_interner_bytes | Gauge по kind (named или anonymous). Суммарный объём интернированных текстов запросов в байтах. Обновляется один раз за проход GC. |
pg_doorman_query_interner_evictions_total | Counter по kind и reason (gc_passive или ttl_expired). Named-записи удаляются, когда их больше не держит ни один кеш вне interner; anonymous-записи удаляются после idle TTL. |
pg_doorman_query_interner_synthetic_misses_total | Counter синтетических ответов SQLSTATE 26000 для anonymous prepared statements, состояние которых уже недоступно при последующем Bind или Describe. Перед увеличением query_interner_anon_idle_ttl_seconds проверьте вытеснения из клиентского Anonymous LRU, WARN-логи, RESET INTERNER и TTL-вытеснения. |
pg_doorman_query_interner_gc_duration_seconds | Гистограмма времени одного прохода GC interner (named и anonymous вместе), в секундах. Помогает увидеть, когда большой interner делает обход заметным. |
pg_doorman_pooler_check_query_backend_total | Counter пробов pooler_check_query, отправленных в PostgreSQL (промах кеша или повторная проба после RELOAD). После прогрева значение должно быть стабильным; постоянно растущий rate означает, что популовый кеш не удерживает запись. |
pg_doorman_pooler_check_query_cache_total | Counter пробов pooler_check_query, обслуженных из популового кеша ответа без обращения к бэкенду. Hit rate = cache_total / (cache_total + backend_total). |
Метрики серверного TLS
Активны, если включён TLS к PostgreSQL (server_tls_mode != "disable").
| Метрика | Тип | Описание |
|---|---|---|
pg_doorman_server_tls_connections | gauge по пулу | Число активных TLS-соединений к PostgreSQL. |
pg_doorman_server_tls_handshake_duration_seconds | histogram по пулу | Распределение длительности TLS handshake. |
pg_doorman_server_tls_handshake_errors_total | counter по пулу | Счётчик неуспешных TLS handshake. Алертить при ненулевой скорости. |
Подробнее — см. Клиентский и серверный TLS.
Дашборд Grafana
Базовый набор панелей для дашборда:
- Число соединений по типу
- Использование памяти во времени
- Число клиентов и серверов по пулам
- Перцентили запросов и транзакций
- Сетевой трафик по пулам
Примеры запросов
Несколько примеров запросов Prometheus, которые могут быть полезны:
Темп подключений
rate(pg_doorman_connections_total{type="total"}[5m])
Загрузка пула
sum by (database) (pg_doorman_pools_clients{status="active"}) / sum by (database) (pg_doorman_pools_servers{status="active"} + pg_doorman_pools_servers{status="idle"})
Медленные запросы (p99)
histogram_quantile(0.99, sum by (le, user, database) (rate(pg_doorman_pools_query_duration_seconds_bucket[5m])))
Время ожидания клиентов (p99)
histogram_quantile(0.99, sum by (le, user, database) (rate(pg_doorman_pools_wait_duration_seconds_bucket[5m])))
Hit rate кеша auth_query
rate(pg_doorman_auth_query_cache_total{type="hits"}[5m]) / clamp_min(rate(pg_doorman_auth_query_cache_total{type="hits"}[5m]) + rate(pg_doorman_auth_query_cache_total{type="misses"}[5m]), 0.001)
Темп ошибок auth_query
rate(pg_doorman_auth_query_auth_total{result="failure"}[5m])
Бенчмарки
Русская версия страницы с бенчмарками пока не ведётся. Чтобы не публиковать англоязычный текст под русской навигацией, здесь оставлена ссылка на оригинал.
Открыть бенчмарки на английском
История изменений
Русская версия changelog пока не ведётся. Чтобы не публиковать англоязычный текст под русской навигацией, здесь оставлена ссылка на оригинал.
Открыть changelog на английском
Вклад в PgDoorman
Спасибо за интерес к развитию PgDoorman! Это руководство поможет настроить окружение для разработки и разобраться в процессе вклада в проект.
С чего начать
Зависимости
Чтобы запускать интеграционные тесты, нужно только:
Установка 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!