Приложение заняло все подключения 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_connections | 100 | всегда | общий потолок фоновых процессов-соединений кластера | требует перезапуска (PGC_POSTMASTER) |
superuser_reserved_connections | 3 | всегда | слоты только для ролей с атрибутом SUPERUSER | требует перезапуска |
reserved_connections | 0 | 16 | слоты для ролей с правами 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 предлагает набор предопределённых ролей специально под эту задачу, и они официально описаны в документации как предназначенные для настройки учётной записи мониторинга.
- pg_monitor — сборная роль, которая сама является членом трёх ниже перечисленных. Даёт доступ к
pg_stat_activity(все сессии, а не только свои),pg_stat_replication,pg_stat_database, функциям видаpg_stat_get_*и текущим значениям конфигурации. - pg_read_all_settings — чтение всех параметров конфигурации, включая те, что обычно видны только суперпользователю (пути к данным, объём shared_buffers и т.п.) — удобно для дашборда «конфигурация сервера» без прав его менять.
- pg_read_all_stats — чтение всех
pg_stat_*представлений и статистических функций расширений (например,pg_stat_statements), в том числе информации по чужим сессиям, которая иначе видна только суперпользователю или владельцу сессии. - pg_stat_scan_tables — выполнение диагностических функций, которые берут
ACCESS SHAREблокировку на таблицы (например,pgrowlocks) — на практике нужна редко, но входит в pg_monitor по умолчанию.
Для типового 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 = 3max_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 РМ (это оценка по нашей практике эксплуатации, а не официальная рекомендация вендора, и её нужно поджимать по факту наблюдения за конкретной нагрузкой):
| Метрика (ключ шаблона) | Что показывает | Warning | High/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 дольше одного цикла опроса — сигнал о том, что мониторинг сам не может подключиться, и это надо ловить отдельно от бизнес-метрик, иначе тревога «база недоступна» потонет среди тревог о медленных запросах.
Наш пошаговый сценарий внедрения у клиента
- Снимаем недельный профиль по
pg_stat_activity(пиковое число сессий по базам и ролям, доляidle in transaction) — без этого шага любые пороги и лимиты будут гаданием, а не инженерным решением. - Фиксируем версию кластера. На PostgreSQL 16-18 идём по полному сценарию с
reserved_connectionsиpg_use_reserved_connections. На версиях 12-15 фиксируем это как технический долг и включаем обновление кластера в план работ — иначе честного решения для мониторинга без SUPERUSER просто нет. - Выставляем
reserved_connections(у нас практика — 3-5 слотов на стенд до 50 РМ) в конфиге, планируем окно на перезапуск службыpostgresql. - Создаём роль
zbx_monitorсCONNECTION LIMIT, выдаёмpg_use_reserved_connectionsиpg_monitor, прописываем допуск вpg_hba.confпо паролюscram-sha-256, пароль — в Bitwarden. - Ограничиваем основного прикладного потребителя через
ALTER ROLE ... CONNECTION LIMITиALTER DATABASE ... CONNECTION LIMIT, оставляя зазор под отчётность и разовые административные подключения. - Включаем
idle_in_transaction_session_timeoutиidle_session_timeout, сверяем значение с реальной длительностью легитимных длинных операций (закрытие месяца в 1С может идти дольше стандартных 5 минут — таймаут подбираем по факту, а не по шаблону). - Там, где приложение открывает соединения напрямую и часто — разворачиваем PgBouncer в режиме
transaction, оставляя мониторинг подключённым мимо пулера, напрямую на 5432. - Разворачиваем шаблон «PostgreSQL by Zabbix agent 2», калибруем пороги по данным из первого шага, заводим отдельный триггер на потерю самого мониторинга.
- Проверяем сценарий вживую: искусственно забиваем
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-метрики) — это почти всегда именно исчерпание пула, а не блокировка.