· 13 мин чтения

Приложение заняло все подключения PostgreSQL — как оставить мониторингу резерв без SUPERUSER

Классическая картина у клиента на 30-50 рабочих мест: 1С-кластер плюс веб-сервис плюс пара интеграций держат PostgreSQL за горло, <code>max_connections</code> подходит к пределу, и в этот самый момент падает не приложение — падает мониторинг. Zabbix-агент пытается подключиться к базе, чтобы снять метрики и поднять тревогу, но получает <code>FATAL</code> на пустом месте, потому что свободных слотов уже нет. Стандартный «быстрый» ответ — выдать учётной записи мониторинга <code>SUPERUSER</code> — решает симптом и создаёт новую дыру в безопасности. У PostgreSQL есть штатный механизм для этой задачи начиная с 16-й версии, и мы в ITfresh обкатали его на боевых стендах на PostgreSQL 16-18. Ниже — как это устроено на уровне параметров сервера, ролей и пулера, без единого лишнего привилегированного бита.

Симптом: почему мониторинг умирает именно тогда, когда он нужнее всего

Ситуация повторяется у разных клиентов почти одинаково. Прикладной пул (1С-кластер, ORM веб-приложения, реже — прямые JDBC/ODBC-соединения отчётных форм) в момент пиковой нагрузки открывает соединения быстрее, чем закрывает. max_connections в конфигурации по умолчанию — 100, и для инсталляции с несколькими рабочими процессами 1С (rphost) плюс веб-бэкендом этот потолок выбирается за минуты при регламентном закрытии месяца или при зависшей транзакции, которая держит соединение в статусе idle in transaction.

В этот момент система мониторинга (у нас — Zabbix agent 2 с шаблоном «PostgreSQL by Zabbix agent 2») пытается открыть штатное соединение под учётной записью zbx_monitor и получает отказ. До PostgreSQL 16 текст ошибки был один: FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute. Начиная с 16-й версии добавилась вторая причина отказа — FATAL: remaining connection slots are reserved for roles with privileges of the pg_use_reserved_connections role. Смысл в обоих случаях один: обычный пул кончился, а мониторинг обычной ролью и есть — своего резерва у него исторически не было.

Результат предсказуем: график в Zabbix обрывается ровно в момент инцидента, дежурный инженер не получает алерт вовремя, а расследование постфактум идёт по логам Postgres, а не по дашборду. Это худший момент для потери наблюдаемости — именно тогда, когда решение нужно принимать быстро.

На PostgreSQL 18, который мы уже разворачиваем на новых стендах клиентов, механика та же, что и в 16-17: сам факт выхода новой мажорной версии не меняет модель резервирования слотов, она стабилизировалась именно в 16-й ветке. Поэтому всё, что описано ниже про reserved_connections и pg_use_reserved_connections, одинаково применимо к 16, 17 и 18 — мы проверяли сценарий на всех трёх при миграции клиентов с legacy 12-13 в 2026 году.

Почему выдать мониторингу SUPERUSER — неправильный, хоть и работающий, обходной путь

Атрибут SUPERUSER в PostgreSQL — это не «побольше прав на чтение», это полный обход системы разграничения доступа: игнорируются политики ROW LEVEL SECURITY, доступны системные каталоги вроде pg_authid с хэшами паролей ролей, доступна запись в любую таблицу любой базы кластера, доступны функции уровня администрирования (pg_terminate_backend на любой процесс, изменение конфигурации на лету и т.д.). Учётную запись с такими правами обязана мониторить сама служба безопасности — а на практике она просто прописана в конфиге Zabbix-агента открытым паролем на диске сервера БД.

Второй практический аргумент: суперпользовательский резерв (superuser_reserved_connections, по умолчанию 3 слота) считается общим для ролей с атрибутом SUPERUSER. Если вы посадили туда мониторинг, то в момент, когда админу реально нужно зайти под суперпользователем и разобрать завал руками, свободного слота может не остаться — мониторинг его уже занял. Резерв для дежурного администратора и резерв для автоматической системы наблюдения — это две разные задачи, и смешивать их в одном пуле неверно даже с чисто эксплуатационной точки зрения, не говоря об аудите.

Резерв до версии 16: что умел superuser_reserved_connections и почему этого мало

До PostgreSQL 16 в сервере было только одно понятие резерва — под суперпользователей. Формула доступных слотов для обычных ролей была фиксированной:

ПараметрЗначение по умолчаниюС какой версииЧто резервируетКогда применяется
max_connections100всегдаобщий потолок фоновых процессов-соединений кластератребует перезапуска (PGC_POSTMASTER)
superuser_reserved_connections3всегдаслоты только для ролей с атрибутом SUPERUSERтребует перезапуска
reserved_connections016слоты для ролей с правами pg_use_reserved_connectionsтребует перезапуска

Обратите внимание: оба параметра резерва имеют контекст PGC_POSTMASTER — это значит, что изменение значения через ALTER SYSTEM SET без последующего полного перезапуска кластера (не pg_reload_conf(), а именно restart службы postgresql) не подействует. Это стоит планировать заранее, в окне обслуживания, а не «на живую» в момент, когда мониторинг уже упал.

На версиях 12-15, которые ещё держатся у части клиентов на legacy-стендах, единственный штатный резерв — суперпользовательский, и обойти дилемму «либо мониторинг без резерва, либо мониторинг с SUPERUSER» без сторонних решений (отдельный unix-socket с урезанным max_connections для конкретного приложения, вынесенный PgBouncer с собственным пулом) не получится. Наша практическая рекомендация клиентам на старых версиях — это один из весомых аргументов для планового обновления кластера до 16 и выше, наравне с логической репликацией и улучшениями планировщика.

PostgreSQL 16-18: своя роль pg_use_reserved_connections для мониторинга

Начиная с PostgreSQL 16 появился отдельный параметр reserved_connections и предопределённая роль pg_use_reserved_connections. Идея ровно та, которой не хватало: резерв не привязан к суперпользовательскому статусу, а выдаётся по членству в конкретной роли. Мониторинг получает гарантированный слот, но остаётся обычной ролью без права записи в чужие данные и без доступа к системным секретам.

Настройка на стороне сервера (требует restart, задаётся в postgresql.conf или через ALTER SYSTEM):

ALTER SYSTEM SET reserved_connections = 5; -- изменение вступит в силу только после перезапуска службы postgresql, -- pg_reload_conf() здесь не сработает: параметр PGC_POSTMASTER

Дальше создаём отдельную роль для мониторинга и выдаём ей ровно два права — доступ к резерву соединений и доступ к статистике (о нём в следующем разделе):

CREATE ROLE zbx_monitor LOGIN PASSWORD 'см. Bitwarden' CONNECTION LIMIT 3; GRANT pg_use_reserved_connections TO zbx_monitor; GRANT pg_monitor TO zbx_monitor;

После перезапуска кластера общая арифметика слотов выглядит так: из 100 max_connections три зарезервированы под фактических суперпользователей, пять — под роль pg_use_reserved_connections, оставшиеся 92 доступны всем прочим клиентам. Когда прикладной пул выедает все 92, обычные клиенты получают FATAL: remaining connection slots are reserved..., а zbx_monitor продолжает подключаться штатно — он физически сидит в другом пуле слотов. CONNECTION LIMIT 3 на самой роли — это защита от обратной ситуации: если в скрипте мониторинга случится утечка соединений, она не сможет забить весь резерв целиком и лишить резерва другие сервисные роли, которым он тоже может понадобиться (например, роль для аварийного доступа поддержки).

pg_monitor и pg_read_all_stats: ровно те права, что нужны, и ни битом больше

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

Для типового Zabbix-мониторинга достаточно одной строки:

GRANT pg_monitor TO zbx_monitor;

Ни одна из этих ролей не даёт права INSERT/UPDATE/DELETE на прикладные таблицы, не даёт CREATEDB/CREATEROLE и не позволяет менять конфигурацию — только читать. Если политика безопасности клиента требует более узкого разреза (например, не давать даже безобидный pg_read_all_settings), можно выдать только pg_read_all_stats отдельно — статистику по сессиям и блокировкам это уже покрывает, чего в 90% дашбордов достаточно.

Отдельно стоит держать в голове: pg_read_all_stats открывает текст выполняемых запросов в pg_stat_activity.query для всех сессий в кластере, включая чужие. Если в приложении встречаются запросы с параметрами вроде паролей в открытом виде (плохая практика, но встречается в legacy 1С-обработках), эта роль их покажет мониторингу — довод в пользу того, чтобы вычистить такие места в коде, а не держать секреты подальше от глаз через отказ от мониторинга.

Ещё один довод в пользу узкого набора прав: если через год аудитор безопасности или регулятор задаст вопрос «кто и зачем имеет доступ к чтению чужих данных на продуктивном сервере БД», ответ «сервисная учётка мониторинга, роль pg_monitor, только чтение статистики» закрывает вопрос за одну фразу. Ответ «эта учётка суперпользователь, потому что иначе Zabbix не подключался при высокой нагрузке» такого закрытия не даёт и обычно тянет за собой отдельный пункт в отчёте аудита.

Вторая линия защиты: не позволить приложению самому съесть весь пул

Резерв для мониторинга снимает симптом «мониторинг ослеп», но не устраняет причину — а причина почти всегда в том, что одно приложение (чаще всего кластер 1С или ORM-пул веб-сервиса с неправильно настроенным max pool size) способно физически дотянуться до самого потолка max_connections. Здесь работают три независимых рубежа.

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

ALTER DATABASE erp_prod CONNECTION LIMIT 80; ALTER ROLE app_1c CONNECTION LIMIT 70;

Арифметика простая и её стоит держать под рукой в виде таблички для каждого клиента: если max_connections = 100, superuser_reserved_connections = 3, reserved_connections = 5, то на всех «обычных» клиентов остаётся 92 слота. Выставляя основному прикладному потребителю CONNECTION LIMIT 70, вы физически не даёте ему выесть оставшиеся 22 слота, которые нужны отчётным формам, регламентным заданиям и разовым административным подключениям — резерв мониторинга при этом вообще не участвует в этой борьбе, он в отдельном пуле.

Таймауты на зависшие сессии. Чаще всего слоты не столько «заканчиваются от нагрузки», сколько забиваются сессиями в статусе idle in transaction — открытая транзакция, которую забыли закрыть (обрыв связи с клиентом, исключение в коде 1С без ОтменитьТранзакцию(), зависший отчёт). Два параметра сервера закрывают эту дыру:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min'; ALTER SYSTEM SET idle_session_timeout = '30min'; SELECT pg_reload_conf();

idle_in_transaction_session_timeout обрывает сессию, которая держит открытую транзакцию дольше указанного времени, ничего не делая. idle_session_timeout (появился в PostgreSQL 14) обрывает вообще любую сессию, простаивающую без активной транзакции дольше лимита — полезно для клиентов, которые открыли соединение и забыли закрыть, не начав транзакцию вовсе. Оба параметра меняются через pg_reload_conf() без перезапуска — в отличие от параметров резерва, они не PGC_POSTMASTER.

Быстрая диагностика текущей картины по слотам — рабочий запрос, который мы держим в каждом дежурном runbook:

SELECT datname, usename, state, count(*) FROM pg_stat_activity GROUP BY 1, 2, 3 ORDER BY 4 DESC;

PgBouncer перед PostgreSQL: амортизатор, а не замена резерву

В нашей практике корневая причина исчерпания max_connections в половине случаев — не рост числа реальных пользователей, а отсутствие пулера между приложением и базой: 1С-кластер или веб-бэкенд открывает новое физическое соединение на каждый запрос или на каждый рабочий процесс, вместо того чтобы держать компактный пул и раздавать соединения по очереди. PgBouncer в режиме transaction решает это на уровне архитектуры, а не тюнинга лимитов.

[databases] erp_prod = host=127.0.0.1 port=5432 dbname=erp_prod [pgbouncer] listen_port = 6432 pool_mode = transaction max_client_conn = 2000 default_pool_size = 40 reserve_pool_size = 10 reserve_pool_timeout = 3

max_client_conn — это дешёвые клиентские соединения к самому PgBouncer, их можно держать тысячами, они не занимают слоты PostgreSQL. default_pool_size — это уже настоящие физические соединения к базе на пару пользователь/база, и именно это число нужно сверять с тем, что вы оставили приложению через ALTER ROLE ... CONNECTION LIMIT. reserve_pool_size — дополнительные соединения, которые пулер выдаёт из своего резерва, если клиент ждёт свободного слота дольше reserve_pool_timeout секунд — это внутренний «аварийный клапан» PgBouncer, концептуально похожий на reserved_connections в самом Postgres, но для обычных прикладных клиентов, а не для мониторинга.

Важный нюанс, о котором часто забывают: подключение мониторинга нужно оставить мимо пулера, напрямую на порт 5432. Если увести его через PgBouncer вместе с приложением, вы вернётесь к исходной проблеме на другом уровне — при исчерпании пула самого PgBouncer мониторинг снова останется без ответа, а резерв reserved_connections на стороне PostgreSQL в этой ситуации ничем не поможет, потому что запрос от Zabbix-агента вообще не дойдёт до сервера БД.

Шаблон «PostgreSQL by Zabbix agent 2»: подключение zbx_monitor и пороги тревог

С версий Zabbix 6.0.10 / 6.2.4 / 6.4 сбор метрик PostgreSQL вынесен в отдельный загружаемый плагин агента (Plugins.PostgreSQL), который идёт в комплекте с Zabbix agent 2 и не требует установки Zabbix-сервера на саму машину БД. Настройка стороны PostgreSQL — та же роль, что мы создали выше, плюс строка допуска в pg_hba.conf:

host all zbx_monitor 127.0.0.1/32 scram-sha-256

В шаблоне за макросами {$PG.URI}, {$PG.USER}, {$PG.PASSWORD} и {$PG.LLD.FILTER.DB.MATCHES} задаётся строка подключения, логин и пароль учётной записи zbx_monitor — пароль у нас хранится в Bitwarden и подставляется через макрос уровня узла сети, а не в открытом виде в конфиге. Discovery-правило шаблона само находит все базы в кластере и строит по каждой отдельный набор элементов данных.

Пороги тревог мы держим не «из коробки», а калибруем под конкретный стенд — ниже диапазон, с которым обычно стартуем у клиента до 50 РМ (это оценка по нашей практике эксплуатации, а не официальная рекомендация вендора, и её нужно поджимать по факту наблюдения за конкретной нагрузкой):

Метрика (ключ шаблона)Что показываетWarningHigh/Critical
pgsql.connections.total к max_connectionsзанятость общего пула соединенийот 75%от 90%
pgsql.connections.idle_in_transactionчисло сессий, зависших в открытой транзакции>5 дольше 5 минут>20 или дольше idle_in_transaction_session_timeout
pgsql.connections.waitingклиенты, ожидающие свободный слот/блокировку>0 дольше 2 минут>10
pgsql.replication.lag (если есть реплика)отставание физической/логической репликации>30 сек>5 мин

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

Наш пошаговый сценарий внедрения у клиента

  1. Снимаем недельный профиль по pg_stat_activity (пиковое число сессий по базам и ролям, доля idle in transaction) — без этого шага любые пороги и лимиты будут гаданием, а не инженерным решением.
  2. Фиксируем версию кластера. На PostgreSQL 16-18 идём по полному сценарию с reserved_connections и pg_use_reserved_connections. На версиях 12-15 фиксируем это как технический долг и включаем обновление кластера в план работ — иначе честного решения для мониторинга без SUPERUSER просто нет.
  3. Выставляем reserved_connections (у нас практика — 3-5 слотов на стенд до 50 РМ) в конфиге, планируем окно на перезапуск службы postgresql.
  4. Создаём роль zbx_monitor с CONNECTION LIMIT, выдаём pg_use_reserved_connections и pg_monitor, прописываем допуск в pg_hba.conf по паролю scram-sha-256, пароль — в Bitwarden.
  5. Ограничиваем основного прикладного потребителя через ALTER ROLE ... CONNECTION LIMIT и ALTER DATABASE ... CONNECTION LIMIT, оставляя зазор под отчётность и разовые административные подключения.
  6. Включаем idle_in_transaction_session_timeout и idle_session_timeout, сверяем значение с реальной длительностью легитимных длинных операций (закрытие месяца в 1С может идти дольше стандартных 5 минут — таймаут подбираем по факту, а не по шаблону).
  7. Там, где приложение открывает соединения напрямую и часто — разворачиваем PgBouncer в режиме transaction, оставляя мониторинг подключённым мимо пулера, напрямую на 5432.
  8. Разворачиваем шаблон «PostgreSQL by Zabbix agent 2», калибруем пороги по данным из первого шага, заводим отдельный триггер на потерю самого мониторинга.
  9. Проверяем сценарий вживую: искусственно забиваем max_connections тестовыми подключениями до отказа обычным ролям и убеждаемся, что zbx_monitor продолжает подключаться и алерт по занятости пула приходит раньше, чем база откажет реальным пользователям.

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

Частые вопросы

Нужно ли перезапускать PostgreSQL после изменения reserved_connections?
Да. И reserved_connections, и superuser_reserved_connections имеют контекст PGC_POSTMASTER — они читаются только при старте процесса postmaster. Команда pg_reload_conf() (аналог SIGHUP) их не подхватит, нужен полноценный restart службы postgresql в окне обслуживания.
У нас PostgreSQL 13, обновление не планируется в ближайшие месяцы — что делать с мониторингом сейчас?
До версии 16 отдельного резерва под непривилегированные роли в PostgreSQL нет. Рабочий компромисс на переходный период — не расширять superuser_reserved_connections под мониторинг, а вместо этого жёстко ограничить прикладные роли через ALTER ROLE ... CONNECTION LIMIT так, чтобы они физически не доходили до max_connections, оставляя постоянный зазор для служебных подключений, и включить idle_in_transaction_session_timeout, чтобы зависшие транзакции не съедали этот зазор. Это снижает риск, но не даёт гарантии уровня PostgreSQL 16+ и является поводом зафиксировать обновление в план работ.
pg_monitor открывает мониторингу тексты чужих SQL-запросов — это не угроза безопасности?
pg_read_all_stats (входит в pg_monitor) действительно показывает pg_stat_activity.query для всех сессий, включая чужие. Если в коде приложения встречаются запросы с параметрами вроде паролей в открытом виде — это стоит исправить в самом приложении (использовать подготовленные выражения с параметрами, а не подстановку строк), а не решать вопрос отказом от мониторинга активности сессий.
Можно ли выдать pg_use_reserved_connections не только мониторингу, а ещё одному сервисному аккаунту?
Технически да, роль можно выдавать нескольким учётным записям. Но каждая дополнительная роль в этом резерве уменьшает гарантию для остальных — резерв общий на всех участников pg_use_reserved_connections. Мы рекомендуем держать в этой роли только по-настоящему критичные для восстановления сервиса аккаунты: мониторинг и, при необходимости, отдельную учётку аварийного доступа поддержки — и не смешивать с обычными интеграциями.
Как отличить в логах именно исчерпание слотов от блокировок или сетевых проблем?
Исчерпание слотов даёт характерную запись FATAL в журнале PostgreSQL с текстом about remaining connection slots are reserved и обрыв на этапе установления соединения, до выполнения любого запроса. Блокировки, в отличие от этого, видны в pg_stat_activity как сессии в состоянии active с непустым wait_event_type (Lock) при успешно установленном соединении. Если мониторинг вообще не может подключиться (не показывает даже wait-метрики) — это почти всегда именно исчерпание пула, а не блокировка.
📄
Скачайте подробный разбор в PDF Кейсы, статистика, типовые ошибки и чек-лист самопроверки — 12 страниц
Скачать PDF

Подпишитесь на разборы ITfresh

Раз в неделю — практичные материалы по ИТ для бизнеса: без спама, только польза.

Письмо придёт в течение минутыНе нашли его во «Входящих» — загляните в папку «Спам» или «Промоакции» и нажмите «Не спам». Так все следующие выпуски будут приходить прямо в основную почту.