Выберите продукт

PostgreSQL idle in transaction: диагностика, причины и безопасное лечение

Сессии PostgreSQL в состоянии idle in transaction выглядят безобидно, но удерживают старый snapshot, тормозят vacuum и раздувают таблицы. Разберём, как найти источник через pg_stat_activity, оценить ущерб, при необходимости завершить сессию и безопасно включить idle_in_transaction_session_timeout.
PostgreSQL idle in transaction: диагностика, причины и безопасное лечение

Состояние idle in transaction — частая причина «внезапных» деградаций PostgreSQL: растёт задержка запросов, autovacuum перестаёт нормально убирать мусор, накапливается bloat, а иногда всё упирается в блокировки и цепочки ожидания. Снаружи при этом кажется, что нагрузки нет: сессия просто «сидит» и ничего не делает.

Почти всегда это означает одно: клиент открыл транзакцию (явно или неявно), выполнил одну или несколько команд и перестал отправлять запросы, не завершив транзакцию COMMIT/ROLLBACK. PostgreSQL обязан сохранять согласованность так, как будто транзакция ещё может продолжиться.

Что значит idle in transaction и почему это опасно

Пока транзакция не завершена, сервер вынужден удерживать контекст, который влияет на систему целиком. В частности:

  • держит снимок видимости данных (snapshot) для MVCC;
  • может удерживать блокировки на затронутых объектах (в зависимости от выполненных команд);
  • мешает vacuum удалять версии строк, которые потенциально видимы этой транзакции;
  • иногда удерживает ресурсы вроде курсоров, временных объектов и т.д.

Самая токсичная часть — старый snapshot. Даже если тяжёлых блокировок нет, autovacuum не сможет вычистить «мёртвые» версии строк, пока существует транзакция, которая теоретически может их увидеть. На практике это приводит к цепочке последствий:

  • растёт bloat таблиц и индексов;
  • ухудшается производительность запросов из-за лишних чтений и разрастания индексов;
  • повышается риск проблем с заморозкой XID (wraparound), если долго не удаётся продвинуть «горизонт»;
  • на репликах (hot standby) возможны конфликты из-за старых снимков.

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

Как найти idle in transaction через pg_stat_activity

Основной «фонарик» — pg_stat_activity. Он показывает текущие состояния сессий, возраст транзакций и последний выполненный запрос. Начните с прямого списка «простои в транзакции» и сортировки по возрасту.

Быстрый список «подозрительных» сессий

SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  now() - xact_start AS xact_age,
  now() - state_change AS state_age,
  wait_event_type,
  wait_event,
  left(query, 200) AS query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_age DESC;

Что смотреть в первую очередь:

  • xact_age — возраст транзакции. Чем он больше, тем выше шанс проблем с MVCC/vacuum.
  • state_age — как давно сессия именно «простаивает». Иногда транзакция старая, но работа в ней активная; нас интересует именно простой.
  • application_name — если вы задаёте его в приложении/пуле, поиск источника занимает секунды.
  • client_addr — помогает отличить сервисы от «ручных» подключений (IDE, psql, админские машины).
  • query — последняя команда. Часто это BEGIN или запрос перед тем, как код «ушёл ждать».

Long transactions: проблемы не только у idle

Отдельный класс — просто очень долгие транзакции, которые не обязательно находятся в idle in transaction. Например: длинные отчёты на REPEATABLE READ или массовые изменения без батчинга. Для общего обзора полезно посмотреть топ по возрасту транзакций:

SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  now() - xact_start AS xact_age,
  left(query, 200) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_age DESC
LIMIT 30;

Если вверху списка в основном active, это уже история про оптимизацию запросов, батчинг, уровни изоляции и сопутствующие таймауты.

Если вы держите PostgreSQL на отдельном сервере, удобнее и предсказуемее управлять CPU/RAM и дисковой подсистемой на VDS, чем в общем окружении.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Как понять, мешает ли idle in transaction прямо сейчас

Вопрос «Ну висит сессия, и что?» решается быстрой оценкой по трём направлениям: блокировки, влияние на vacuum и признаки накопления мусора. Не обязательно сразу строить сложные отчёты — на инциденте важна скорость.

Проверяем признаки ожиданий и блокировок

Если транзакция удерживает блокировки, другие сессии будут ждать. Быстрый способ увидеть «кто вообще ждёт чего-то» — посмотреть активные ожидания:

SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  now() - xact_start AS xact_age,
  now() - state_change AS state_age,
  wait_event_type,
  wait_event,
  left(query, 200) AS query
FROM pg_stat_activity
WHERE wait_event_type IS NOT NULL
ORDER BY xact_age DESC;

Если много сессий ждут, а среди «старых» транзакций есть idle in transaction, это частый кандидат на роль пробки. Для полноценной связки «кто кого блокирует» обычно подключают pg_locks, но даже этот запрос часто даёт направление.

Проверяем влияние на vacuum косвенно: dead tuples

Удержание snapshot напрямую бьёт по уборке мусора. Косвенно это видно по росту n_dead_tup и стагнации показателей vacuum. Быстро посмотрите топ таблиц по «мёртвым» строкам:

SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  last_vacuum,
  vacuum_count,
  autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

Если n_dead_tup заметно растёт, а autovacuum «ходит», но эффекта нет, очень часто причина — именно старая транзакция со snapshot (в том числе idle in transaction).

Если тема autovacuum в целом болит (параметры, пороги, freeze, индексы), держите под рукой разбор: настройка autovacuum и борьба с bloat в PostgreSQL.

Результат запроса pg_stat_activity со списком сессий и возрастом транзакций

Почему это происходит: типовые причины

В проде причины повторяются, и почти всегда это дисциплина транзакций на стороне клиента.

  • Пул соединений + неправильный жизненный цикл транзакции. Взяли коннект, сделали BEGIN, выполнили запрос и ушли ждать внешний I/O (HTTP, очередь, таймер), не закрыв транзакцию.
  • «Ручной» клиент (psql, DBeaver, DataGrip): человек открыл транзакцию, посмотрел данные, отвлёкся, ноутбук уснул.
  • ORM и автотранзакции. Транзакция живёт «на запрос» (request-scope), а обработчик завис или делает долгий I/O.
  • Отключён autocommit и «неочевидные» BEGIN появляются без явного намерения.
  • Курсоры и долго живущие сессии, которые держат контекст дольше, чем ожидалось.

Админская мораль простая: транзакция должна быть короткой либо контролируемой и наблюдаемой. «Пусть повисит» для OLTP обычно заканчивается деградацией всей базы.

Как лечить: от мягких мер к жёстким

Лечение стройте по принципу минимального вмешательства, но с понятным дедлайном. Сессию всегда можно завершить, но лучше сначала понять, что вы прерываете и как это отразится на приложении.

Шаг 1. Найти владельца и контекст

Сопоставьте application_name, usename, client_addr. Если это «человек» — часто достаточно попросить завершить транзакцию (commit/rollback) или просто закрыть клиент. Если это сервис — фиксируйте точное имя воркера/пула и время, чтобы разработчики могли найти место в логах.

Шаг 2. Аккуратно прервать сессию

Если сессия простаивает, активного запроса нет, поэтому pg_cancel_backend обычно не поможет: отменять нечего. В таком случае применяется завершение бэкенда:

SELECT pg_terminate_backend(12345);

Эффект: соединение разорвётся, транзакция откатится. Для приложения это выглядит как ошибка соединения/транзакции, и корректный код должен уметь повторить операцию (учитывая идемпотентность и бизнес-логику).

Не делайте terminate «первой реакцией». Но и не тяните бесконечно: одна зависшая транзакция может стоить дороже, чем один откат и ретрай.

Шаг 3. Включить idle_in_transaction_session_timeout

Главный предохранитель — idle_in_transaction_session_timeout. Он завершает сессию, которая слишком долго находится в состоянии idle in transaction. Это лучшее средство профилактики повторений из-за человеческого фактора или редких багов.

Проверить текущее значение:

SHOW idle_in_transaction_session_timeout;

Задать на уровне базы (пример — 2 минуты):

ALTER DATABASE yourdb SET idle_in_transaction_session_timeout = '2min';

Задать на уровне роли (часто самый безопасный старт — только роль приложения):

ALTER ROLE appuser SET idle_in_transaction_session_timeout = '2min';

Или глобально через ALTER SYSTEM (делайте так, только если понимаете последствия для всех ролей):

ALTER SYSTEM SET idle_in_transaction_session_timeout = '2min';
SELECT pg_reload_conf();

Как выбрать значение:

  • Для типичного веб-OLTP часто достаточно 30–120 секунд.
  • Если есть легаси-участки, где транзакции «думают», начните с 5–10 минут и снижайте постепенно.
  • Разным ролям можно задать разные лимиты: приложению меньше, админам/аналитике больше.

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

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Как не сломать приложение таймаутом: практические рекомендации

Главная ошибка — включить idle_in_transaction_session_timeout «везде и сразу», а потом ловить непонятные обрывы транзакций в пике. Внедряйте управляемо.

Включайте по ролям и наблюдайте

Начните с роли приложения и смотрите, где всплывают ошибки. Если у вас несколько сервисов ходят в одну базу — разделяйте роли и выставляйте разные значения. Так вы локализуете последствия и не сломаете админские операции.

Убедитесь, что ретраи безопасны

После разрыва соединения приложение получит ошибку и должно корректно повторить транзакцию. Но ретраи опасны для неидемпотентных операций (условно «списать деньги»). Админу полезно проговорить это с разработчиками и предложить ключи идемпотентности там, где это критично.

Не держите транзакцию во время внешнего I/O

Классика: в транзакции сделали SELECT ... FOR UPDATE, затем ушли во внешний API, а потом вернулись обновлять запись. На время внешнего вызова транзакция может стать idle in transaction, а блокировки и snapshot будут удерживаться. Правильнее: минимально фиксировать состояние в короткой транзакции, внешний I/O делать вне транзакции, затем выполнять вторую короткую транзакцию с повторной проверкой состояния.

Если причина в неверной конфигурации пула, часто помогает правильная настройка PgBouncer и режимов транзакций/сессий: практический гайд по PgBouncer для PostgreSQL.

Мониторинг производительности базы данных с метриками задержек и vacuum

Полезные дополнительные таймауты вокруг темы

idle_in_transaction_session_timeout закрывает именно простои внутри транзакции. Рядом часто уместно включать и другие ограничители, чтобы «края» были предсказуемыми:

  • statement_timeout — ограничивает время выполнения одного SQL-оператора (борется с «вечными» запросами).
  • lock_timeout — ограничивает ожидание блокировки (уменьшает залипание в очередях ожидания).
  • transaction_timeout — ограничивает общую длительность транзакции, даже если она активна (доступность зависит от версии PostgreSQL).

Практика: для API и OLTP — умеренные таймауты, для аналитики — более мягкие, но лучше в отдельном контуре (отдельные роли/пулы, а иногда и отдельная реплика для чтения).

Оперативный чек-лист для дежурного админа

  1. Найдите idle in transaction с максимальным xact_age в pg_stat_activity.
  2. Определите источник по application_name/client_addr и решите, можно ли дать владельцу время завершить транзакцию.
  3. Проверьте наличие ожиданий/блокировок по wait_event_type и общей картине ожиданий.
  4. Посмотрите топ таблиц по n_dead_tup в pg_stat_user_tables.
  5. Если риск для продакшена высокий — завершайте сессию через pg_terminate_backend.
  6. После инцидента включите idle_in_transaction_session_timeout (начните с роли приложения) и зафиксируйте правила: никаких внешних I/O внутри транзакции, никаких «думающих» транзакций в OLTP.

Итоги

idle in transaction — это не «косметика» в мониторинге, а реальный источник долгосрочных проблем: bloat, деградация запросов, конфликты vacuum и блокировки. Диагностика обычно быстрая: pg_stat_activity показывает виновника по возрасту транзакции и атрибутам сессии. Лучшее лечение — профилактика: разумный idle_in_transaction_session_timeout плюс дисциплина транзакций в приложении.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND

Если локальный DNS в Debian или Ubuntu не стартует с ошибкой address already in use, причина часто в systemd-resolved и DNSStubLis ...
Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting

Ошибка mount.nfs: access denied by server while mounting в Debian и Ubuntu обычно указывает на проблему на стороне NFS-сервера: не ...
Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home

Если в Debian или Ubuntu DNS-сервер не стартует из-за ошибки port 53 busy, часто причина в systemd-resolved с локальным слушателем ...