65 лет полету человека в космос! Хостинг и домены со скидкой
до 22.04.2026 Подробнее
Выберите продукт

PostgreSQL: диагностика блокировок с pg_stat_activity и pg_locks

Практический разбор блокировок в PostgreSQL: как по pg_stat_activity и pg_locks быстро найти ждущие запросы, определить настоящего блокера, увидеть цепочки ожиданий и отличить deadlock от долгого lock wait.
PostgreSQL: диагностика блокировок с pg_stat_activity и pg_locks

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

Для оперативной диагностики почти всегда хватает двух системных представлений:

  • pg_stat_activity — кто подключён, что делает, какие состояния, сколько длится транзакция и запрос.
  • pg_locks — какие блокировки удерживаются и какие ожидаются (через granted).

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

Быстрый чек-лист: действительно ли это блокировки

Перед тем как глубоко копать pg_locks, убедитесь, что симптомы похожи именно на lock wait, а не на I/O, CPU или сеть:

  • Много сессий «как будто работают», но прогресса нет, а CPU не загружен.
  • В pg_stat_activity.wait_event_type часто видно Lock.
  • Растут таймауты в приложении, увеличивается число активных/висящих транзакций.

Начинать почти всегда стоит с обзора активности: это быстрее и проще интерпретировать, чем сырые строки блокировок.

pg_stat_activity: кого смотреть в первую очередь

Базовый обзор текущих сессий. Сортировка ниже поднимает вверх тех, кто ждёт блокировки, и тех, у кого самые «старые» транзакции/запросы:

SELECT
  now() AS ts,
  pid,
  usename,
  datname,
  application_name,
  client_addr,
  state,
  wait_event_type,
  wait_event,
  xact_start,
  query_start,
  now() - xact_start AS xact_age,
  now() - query_start AS query_age,
  left(query, 200) AS query_short
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY
  (wait_event_type = 'Lock') DESC,
  (now() - xact_start) DESC NULLS LAST,
  (now() - query_start) DESC NULLS LAST;

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

  • state: active, idle, idle in transaction (обычно самая опасная история — транзакция открыта, а сессия «ничего не делает»).
  • wait_event_type/wait_event: если wait_event_type = Lock, это кандидат в blocked query.
  • xact_start и возраст транзакции: длинные транзакции часто являются корневой причиной очередей и проблем с VACUUM.
  • query_start и возраст запроса: помогает быстро отделить «обычно долгий» отчёт от внезапной деградации.

Чаще всего виновник — не тот, кто ждёт (blocked), а тот, кто давно держит транзакцию и не завершает её. Поэтому всегда смотрите и ожидающих, и самых «старых» по xact_age.

Быстро найти запросы, которые прямо сейчас ждут lock

SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  wait_event_type,
  wait_event,
  now() - query_start AS waiting_for,
  left(query, 300) AS query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY waiting_for DESC;

Это ваши blocked queries. Дальше задача — найти того, кто блокирует, а не пытаться лечить последствия.

Таблица активности PostgreSQL: сессии и ожидания Lock в pg_stat_activity

pg_locks: что именно заблокировано и кто держит

pg_locks показывает блокировки по каждому ресурсу и режиму. Ключевое поле — granted:

  • granted = true — блокировка уже удерживается.
  • granted = false — сессия ждёт блокировку.

Сами ресурсы бывают разными (таблицы, строки, транзакции, advisory locks), поэтому для диагностики почти всегда нужно соединять pg_locks с pg_stat_activity.

Кто кого блокирует: самый практичный запрос (pg_blocking_pids)

Если ваша версия PostgreSQL поддерживает pg_blocking_pids(pid), это самый короткий путь увидеть пары blocked → blocker.

SELECT
  a.pid AS blocked_pid,
  a.usename AS blocked_user,
  a.application_name AS blocked_app,
  a.client_addr AS blocked_client,
  now() - a.query_start AS blocked_for,
  left(a.query, 200) AS blocked_query,
  b.pid AS blocker_pid,
  b.usename AS blocker_user,
  b.application_name AS blocker_app,
  b.client_addr AS blocker_client,
  now() - b.xact_start AS blocker_xact_age,
  b.state AS blocker_state,
  left(b.query, 200) AS blocker_query
FROM pg_stat_activity a
JOIN LATERAL unnest(pg_blocking_pids(a.pid)) AS bp(blocker_pid) ON true
JOIN pg_stat_activity b ON b.pid = bp.blocker_pid
WHERE a.wait_event_type = 'Lock'
ORDER BY blocked_for DESC, blocker_xact_age DESC NULLS LAST;

Дальше обычно один из трёх сценариев:

  • ждём, если блокировка штатная и краткоживущая;
  • просим корректно завершить транзакцию (commit/rollback) на стороне приложения/пользователя;
  • аккуратно отменяем запрос или завершаем сессию, если блокировка держится слишком долго и ломает критичный контур.
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Если нужна детализация по объектам: relation locks (таблицы)

Когда подозрение на DDL против DML (например, ALTER TABLE «держит» таблицу), полезно посмотреть блокировки уровня relation и привязать их к схеме/таблице:

SELECT
  a.pid,
  a.usename,
  a.application_name,
  a.client_addr,
  l.locktype,
  l.mode,
  l.granted,
  n.nspname AS schema,
  c.relname AS relation,
  now() - a.query_start AS age,
  left(a.query, 200) AS query
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
LEFT JOIN pg_class c ON c.oid = l.relation
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE a.datname = current_database()
  AND l.locktype = 'relation'
ORDER BY
  (l.granted = false) DESC,
  age DESC;

Так вы быстро увидите, какая таблица «под замком», и кто удерживает конфликтный режим.

Цепочки ожиданий: когда blocked блокируют других blocked

Частый реальный кейс: есть не один blocker, а цепочка. Например, сессия A держит блокировку, сессия B ждёт A, а сессия C ждёт B. Внешне кажется, что «падает всё», хотя корень один.

Практический алгоритм:

  • найти «топ блокеров» по количеству ожидающих;
  • проверить возраст транзакции блокера и его state;
  • если blocker — idle in transaction, это почти всегда проблема границ транзакций в приложении.

Топ «виновников»: кто блокирует больше всего сессий

WITH blocked AS (
  SELECT
    a.pid AS blocked_pid,
    unnest(pg_blocking_pids(a.pid)) AS blocker_pid
  FROM pg_stat_activity a
  WHERE a.wait_event_type = 'Lock'
)
SELECT
  b.blocker_pid,
  sa.usename,
  sa.application_name,
  sa.client_addr,
  sa.state,
  now() - sa.xact_start AS xact_age,
  count(*) AS blocked_sessions,
  left(sa.query, 200) AS blocker_query
FROM blocked b
JOIN pg_stat_activity sa ON sa.pid = b.blocker_pid
GROUP BY
  b.blocker_pid,
  sa.usename,
  sa.application_name,
  sa.client_addr,
  sa.state,
  sa.xact_start,
  sa.query
ORDER BY blocked_sessions DESC, xact_age DESC NULLS LAST;

Во время инцидента это особенно удобно: вместо десятков «ждущих» вы видите 1–3 сессии, которые реально держат систему.

Если параллельно у вас ещё и фронт «ложится» из‑за очередей, может помочь кэширование/ограничение конкуренции на уровне веб-слоя; идеи по устойчивости очередей есть в заметке про настройку proxy_cache со stale и lock в Nginx.

Схема цепочки ожиданий блокировок в PostgreSQL: blocked и blocker

Deadlock: чем отличается от «просто долго ждём»

Deadlock — взаимная блокировка, когда две (или больше) транзакции ждут друг друга, и прогресс невозможен. PostgreSQL умеет находить deadlock и разрывает одну из транзакций, поэтому обычно это проявляется как ошибка запроса (а не как бесконечное ожидание).

Важно для диагностики:

  • Долгое ожидание блокировки само по себе не означает deadlock: часто виноваты длинные транзакции, DDL, массовые апдейты.
  • Deadlock почти всегда лечится в приложении: единый порядок захвата ресурсов, меньше широких транзакций, аккуратнее с конкурентными обновлениями.
  • Для расследования нужен контекст из логов.

Логи для расследования deadlock и долгих ожиданий

Минимальный набор параметров, который стоит проверить (значения подбирайте под свою нагрузку и политику логирования):

  • log_lock_waits — логировать ожидания блокировок дольше порога;
  • deadlock_timeout — через какое время начинается проверка на deadlock;
  • log_line_prefix — чтобы в логах удобно коррелировать события по PID/пользователю/базе.

Когда включён log_lock_waits, расследование перестаёт быть «угадайкой»: в логах появляется, кто кого ждёт и что именно происходит.

Таймауты как страховка: lock_timeout и statement_timeout

Таймауты не лечат первопричину, но защищают систему от «вечных» ожиданий и разрастания очередей. Два ключевых механизма:

  • lock_timeout — сколько ждать блокировку, прежде чем запрос завершится ошибкой;
  • statement_timeout — максимальное время выполнения оператора, включая ожидание блокировки.

Часто удобно задавать их на уровне роли/приложения: для веб-запросов один профиль, для миграций и фоновых задач — другой.

Поставить таймауты на сессию (быстро для диагностики)

SET lock_timeout = '3s';
SET statement_timeout = '15s';
FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Cancel или terminate: что безопаснее во время инцидента

Когда blocker найден, рука тянется к «terminate backend». Но отмена запроса и убийство сессии — разные по последствиям действия:

  • pg_cancel_backend(pid) — пытается отменить текущий запрос, соединение остаётся. Если сессия idle in transaction, отменять может быть нечего.
  • pg_terminate_backend(pid) — завершает сессию принудительно. Транзакция откатывается, блокировки снимаются, но приложение получит ошибку соединения и может начать ретраи.

Если сомневаетесь, начните с pg_cancel_backend. Переходите к pg_terminate_backend, когда сессия держит транзакцию слишком долго, блокирует критичный путь и «по-хорошему» не отпускает.

Аккуратно отменить запрос

SELECT pg_cancel_backend(12345);

Принудительно завершить сессию

SELECT pg_terminate_backend(12345);

Проверки перед pg_terminate_backend

  • Это точно blocker, а не «ждущий» (проверьте через pg_blocking_pids).
  • Посмотрите xact_age и state; idle in transaction с большим возрастом обычно красный флаг.
  • Убедитесь, что это не критичный DDL/миграция, откат которой будет очень дорогим.
  • Подумайте о ретраях: приложение может мгновенно переподключиться и повторить тот же конфликтующий сценарий.

Частые причины блокировок и как их узнавать

Idle in transaction

Самая частая причина «непонятных» блокировок: приложение открыло транзакцию и ушло в idle, забыв commit/rollback. Такая сессия может удерживать блокировки и параллельно ухудшать ситуацию с автоочисткой.

Диагностика: state = 'idle in transaction' плюс большой xact_age. Лечение: корректные границы транзакций в коде, ограничение времени «жизни» транзакции, дисциплина на уровне приложения.

DDL против DML

Некоторые операции ALTER TABLE, создание индексов без concurrently, изменение типов — берут конфликтные блокировки. В итоге даже обычные SELECT/UPDATE выстраиваются в очередь.

Лечение: планировать миграции, по возможности использовать менее блокирующие варианты и обязательно контролировать время выполнения.

Массовые UPDATE и «горячие» строки

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

Лечение: дробить батчи, менять порядок обновлений, убирать лишний параллелизм, следить за планами и индексами.

Runbook: минимальный набор запросов для дежурного

  1. Кто ждёт lock прямо сейчас:

    SELECT pid, usename, application_name, client_addr, now() - query_start AS waiting_for, left(query, 200)
    FROM pg_stat_activity
    WHERE wait_event_type = 'Lock'
    ORDER BY waiting_for DESC;
  2. Blocked → Blocker с контекстом:

    SELECT
      a.pid AS blocked_pid,
      now() - a.query_start AS blocked_for,
      left(a.query, 200) AS blocked_query,
      b.pid AS blocker_pid,
      now() - b.xact_start AS blocker_xact_age,
      b.state AS blocker_state,
      left(b.query, 200) AS blocker_query
    FROM pg_stat_activity a
    JOIN LATERAL unnest(pg_blocking_pids(a.pid)) AS bp(blocker_pid) ON true
    JOIN pg_stat_activity b ON b.pid = bp.blocker_pid
    WHERE a.wait_event_type = 'Lock'
    ORDER BY blocked_for DESC;
  3. Топ блокеров по числу заблокированных сессий:

    WITH blocked AS (
      SELECT a.pid AS blocked_pid, unnest(pg_blocking_pids(a.pid)) AS blocker_pid
      FROM pg_stat_activity a
      WHERE a.wait_event_type = 'Lock'
    )
    SELECT blocker_pid, count(*) AS blocked_sessions
    FROM blocked
    GROUP BY blocker_pid
    ORDER BY blocked_sessions DESC;

Что улучшить, чтобы реже ловить блокировки

  • Зафиксировать таймауты. Для веб-контуров часто выгоднее ограничить ожидание через lock_timeout и statement_timeout, чем копить очередь.

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

  • Разносить тяжёлые миграции. DDL в часы пик — частый источник массовых очередей.

  • Включить диагностическое логирование ожиданий. Это резко упрощает разбор инцидентов.

  • Хранить runbook рядом. Чтобы в стрессе не вспоминать запросы и не действовать наугад.

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

Итог

pg_stat_activity отвечает на вопрос «кто сейчас что делает и сколько это длится», а pg_locks — «кто что держит и кто чего ждёт». Вместе они позволяют быстро разобрать почти любой инцидент с блокировками: найти blocked queries, определить настоящего blocker’а, увидеть очереди/цепочки и принять решение — ждать, отменять запрос или завершать проблемную сессию.

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

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

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...