github ozontech/pg_doorman v3.10.5

4 hours ago

Binary upgrade when file descriptors run out

3.10.5 fixes a failure mode observed in 3.9.1 during SIGUSR2 binary
upgrade where pg_doorman could run out of available file descriptors, return
EMFILE from every subsequent accept(), and spin the accept loop
filling the log with "Too many open files" errors until restart.

The fix changes three parts of the upgrade path:

  • TCP and Unix accept loops treat EMFILE/ENFILE as local fd exhaustion:
    sleep for 10 ms and log at most once every 5 seconds. Other accept errors
    still log normally.
  • The migration channel is sized from the live fd budget at signal time:
    current RLIMIT_NOFILE, open fds from /proc/self/fd, and reserved headroom
    for handoff pipes/socketpairs and per-client fd work. If no safe headroom
    remains, pg_doorman starts the new process without client migration and logs
    the budget decision.
  • Client migration reserves a channel slot before dup() on the client fd, so
    a full queue applies backpressure before creating an extra fd.

/metrics scrape uses cached socket-state counts

/metrics no longer walks /proc/PID/net/tcp and /proc/PID/net/unix on the
request path. On hosts with thousands of sockets, the synchronous walk could
hold worker threads long enough for regular Prometheus scrapes to affect
client p99.

Socket-state counts are refreshed in the background and served from cache. The
/metrics handler, periodic print_all_stats output, and admin SHOW SOCKETS
command read that cached data. The Web UI sockets endpoint still refreshes
socket details on demand for operator use.

On the affected production workload, after the same patch from PR #263,
client p99 under continuous scraping returned from about 90 ms to about 45 ms.

Configurable kernel TCP socket buffer size

New general.tcp_socket_buffer_size (ByteSize, default 0). When set to a
non-zero value, pg_doorman calls setsockopt(SO_RCVBUF/SO_SNDBUF) on every
accepted client TCP socket and outbound backend TCP socket. This sets fixed
send/receive buffer limits and disables Linux TCP autotuning for those sockets.

The default 0 keeps the previous behavior. Operators who see memory return
after a pg_doorman restart with many long-lived idle clients may be seeing
kernel TCP buffer accumulation rather than process RSS. Depending on kernel and
cgroup mode, that memory can show up as socket memory, for example sock in
cgroup v2 memory.stat.

Config reloads do not resize already-open sockets. During SIGUSR2 binary
upgrade, migrated client sockets are reconfigured in the new process; backend
sockets pick up the value only when opened or reconnected.

Equivalent of PgBouncer's tcp_socket_buffer parameter. Odyssey and PgCat have
no analogue.

Prepared statements and startup-time planner parameters

sync_server_parameters now replays safe parameters sent by the client in
StartupMessage, not only PostgreSQL-reported ParameterStatus values. This
preserves startup-time session state such as search_path,
default_transaction_isolation, default_transaction_read_only,
default_text_search_config, and role when a transaction-mode client lands
on a different backend connection. Configured startup_parameters still win
over client-supplied values.

The prepared-statement cache key now includes a digest of those startup-time
planner parameters. Two clients that prepare the same query under different
search_path values no longer share one server-side prepared statement.

pg_doorman also rolls back optimistic per-backend prepared-statement LRU entries
when PostgreSQL rejects Parse. Reusing the same client statement name after a
failed Parse now forces a fresh Parse instead of hitting a stale DOORMAN_<N>
entry and surfacing SQLSTATE 26000.

pooler_check_query now uses a real backend probe

Behavior change. Before 3.10.0, pg_doorman answered any pooler_check_query
match locally with a hardcoded empty result. JDBC keepalives such as select 1
did not receive the row a real PostgreSQL would return, and pg_doorman could
report the pool as healthy even when PostgreSQL was down.

The first matching probe per pool now does one PostgreSQL round-trip and caches
the real response. Later matching probes are served from the per-pool cache.
The cache is keyed by query string and is invalidated on RELOAD when the
configured value changes.

pooler_check_query must be deterministic and side-effect free. Values such as
select now(), select pg_is_in_recovery(), select count(*) from <table>,
UPDATE, INSERT, DELETE, CALL, and DO will have their first response
cached.

New metrics:

  • pg_doorman_pooler_check_query_backend_total
  • pg_doorman_pooler_check_query_cache_total

Other operator-visible changes

  • Per-eviction TRACE log lines for the query interner and per-client
    anonymous prepared-statement LRU; one aggregate DEBUG line per GC sweep
    that evicted anything.
  • Web UI lifecycle events now distinguish real process restarts from routine
    RELOAD; shutdown, migration, and unresolved config validation errors use a
    persistent banner instead of a transient toast.
  • Dynamic-pool GC no longer races freshly created pools; the pool lifecycle now
    uses an RAII guard.
  • Binary-upgrade readiness wait uses poll(2) instead of select(2), avoiding
    the old panic when the readiness fd landed above 1024.
  • NO_COLOR is honored by the logger.
  • EL7/EL8 RPM packaging is back.
  • Documentation site, install guide, systemd guide, and binary-upgrade tutorial
    were updated.

Upgrade notes

No config changes are required. tcp_socket_buffer_size defaults to
0 and leaves Linux TCP autotuning enabled unless explicitly set.

Full changelog: https://ozontech.github.io/pg_doorman/changelog.html


Binary upgrade при нехватке файловых дескрипторов

3.10.5 исправляет отказ, наблюдавшийся в 3.9.1 при SIGUSR2-апгрейде:
pg_doorman мог упереться в лимит открытых файловых дескрипторов, возвращать
EMFILE из каждого последующего accept() и крутить listener, забивая лог
ошибками «Too many open files» до рестарта.

Фикс меняет три части пути апгрейда:

  • TCP- и Unix-accept loops считают EMFILE/ENFILE локальной нехваткой
    файловых дескрипторов: спят 10 мс и пишут не больше одной строки в 5 секунд.
    Остальные ошибки accept() логируются как раньше.
  • Размер migration channel считается на момент сигнала по живому fd-бюджету:
    текущий RLIMIT_NOFILE, открытые fd из /proc/self/fd и запас под handoff
    pipes/socketpairs и работу с клиентскими fd. Если безопасного запаса нет,
    pg_doorman запускает новый процесс без миграции клиентов и пишет решение в
    лог.
  • Миграция клиента сначала резервирует слот в канале и только потом вызывает
    dup() на клиентском fd. Полная очередь теперь создаёт backpressure до
    появления лишнего fd.

/metrics использует кэш счётчиков socket-state

/metrics больше не читает /proc/PID/net/tcp и /proc/PID/net/unix на пути
запроса. На хостах с тысячами сокетов такой синхронный обход мог надолго
занимать рабочие потоки, поэтому регулярные Prometheus scrape-ы влияли на
клиентский p99.

Счётчики состояний сокетов обновляются в фоне и отдаются из кэша. /metrics,
периодическая строка print_all_stats и команда SHOW SOCKETS читают
кэшированные данные. Web UI со списком сокетов по-прежнему обновляет детали по
запросу оператора.

На затронутой production-нагрузке после выката того же патча из PR #263
клиентский p99 при постоянном scrape-е вернулся примерно с 90 мс к 45 мс.

Настраиваемый размер kernel TCP socket buffer

Новый параметр general.tcp_socket_buffer_size (ByteSize, по умолчанию 0).
Если значение не ноль, pg_doorman вызывает setsockopt(SO_RCVBUF/SO_SNDBUF)
для каждого принятого клиентского TCP-сокета и каждого исходящего TCP-сокета к
PostgreSQL. Это задаёт фиксированные лимиты send/receive buffer и отключает
Linux TCP autotuning для этих сокетов.

Значение 0 сохраняет прежнее поведение. Если после рестарта pg_doorman с
большим числом долгоживущих idle-клиентов память заметно возвращается,
причиной может быть накопление kernel TCP buffers, а не RSS процесса. В
зависимости от ядра и режима cgroup эта память может быть видна как socket
memory, например sock в cgroup v2 memory.stat.

RELOAD не меняет размер уже открытых сокетов. При SIGUSR2 binary upgrade
мигрированные клиентские сокеты перенастраиваются в новом процессе; сокеты к
PostgreSQL получают значение только при открытии или reconnect.

Это аналог параметра PgBouncer tcp_socket_buffer. У Odyssey и PgCat аналога
нет.

Prepared statements и startup-time planner parameters

sync_server_parameters теперь повторяет безопасные параметры, которые клиент
прислал в StartupMessage, а не только значения из PostgreSQL
ParameterStatus. Это сохраняет startup-time состояние сессии, например
search_path, default_transaction_isolation,
default_transaction_read_only, default_text_search_config и role, когда
клиент в transaction mode попадает на другое backend-соединение.
Сконфигурированные startup_parameters по-прежнему сильнее клиентских
значений.

Ключ prepared-statement cache теперь включает digest этих startup-time planner
parameters. Два клиента, которые готовят один и тот же запрос при разных
значениях search_path, больше не делят один prepared statement на стороне
PostgreSQL.

pg_doorman также откатывает оптимистичные записи в per-backend
prepared-statement LRU, если PostgreSQL отклоняет Parse. Повторное
использование того же клиентского statement name после неудачного Parse
теперь запускает новый Parse, а не попадает в устаревший DOORMAN_<N> и
SQLSTATE 26000.

pooler_check_query теперь делает реальную проверку PostgreSQL

Изменение поведения. До 3.10.0 pg_doorman отвечал на любой
pooler_check_query локально, жёстко заданным пустым результатом. JDBC
keepalive вроде select 1 не получал строку, которую вернул бы настоящий
PostgreSQL, а pg_doorman мог считать пул здоровым даже при недоступном
PostgreSQL.

Первый подходящий запрос в каждом пуле теперь делает один round-trip в
PostgreSQL и кэширует реальный ответ. Следующие такие же проверки обслуживаются
из кэша пула. Кэш ключуется строкой запроса и инвалидируется на RELOAD, если
настроенное значение изменилось.

pooler_check_query должен быть детерминированным и без побочных эффектов.
Значения вроде select now(), select pg_is_in_recovery(),
select count(*) from <table>, UPDATE, INSERT, DELETE, CALL и DO
закэшируют первый успешный ответ.

Новые метрики:

  • pg_doorman_pooler_check_query_backend_total
  • pg_doorman_pooler_check_query_cache_total

Другие изменения, заметные оператору

  • TRACE-лог на каждое вытеснение для query interner и клиентского Anonymous LRU;
    один агрегированный DEBUG-лог на GC sweep, если sweep что-то вытеснил.
  • Web UI lifecycle events теперь отличают реальный рестарт процесса от обычного
    RELOAD; shutdown, migration и неразрешённые ошибки валидации конфига
    показываются постоянным баннером вместо временного уведомления.
  • Dynamic-pool GC больше не гоняется с только что созданными пулами; жизненный
    цикл пула теперь защищён RAII guard.
  • Binary-upgrade readiness wait использует poll(2) вместо select(2), чтобы
    не падать, когда readiness fd оказался выше 1024.
  • Логгер учитывает NO_COLOR.
  • Вернулась сборка RPM для EL7/EL8.
  • Обновлены documentation site, install guide, systemd guide и tutorial по
    binary upgrade.

Замечания по апгрейду

Менять конфиг не нужно. tcp_socket_buffer_size по умолчанию равен
0 и оставляет Linux TCP autotuning включённым, пока параметр явно
не задан.

Полный changelog: https://ozontech.github.io/pg_doorman/changelog.html

Don't miss a new pg_doorman release

NewReleases is sending notifications on new releases.