Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Дедлоки и блокировки в PostgreSQL: диагностика

Postgres завис: блокировки, дедлоки и живые запросы — диагностика

Почему Postgres «висит»: pg_stat_activity и pg_locks, дедлоки и их автоматическое разрешение, idle in transaction и пошаговый разбор инцидента.

Прямой ответ

«Postgres висит» — почти всегда блокировки (запрос ждёт ресурс, занятый другой транзакцией — не медленно, а именно ждёт — wait_event_type='Lock' в pg_stat_activity — первый же запрос диагностики), а настоящий дедлок (две транзакции ждут друг друга по кругу) — редкость и Postgres разруливает его сам (deadlock_timeout=1с — детектор находит цикл — одну транзакцию откатывает с ошибкой deadlock detected — если видите эту ошибку — дедлок был и уже разрешён — разбираться надо с причиной, не с «почему висит»); диагностический минимум (три запроса): (1) SELECT pid, state, wait_event_type, wait_event, now()-xact_start AS xact_age, left(query,80) FROM pg_stat_activity WHERE state <> 'idle' ORDER BY xact_age DESC — кто долго в транзакции и что ждёт; (2) SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid, blocked.query AS blocked_query FROM pg_stat_activity blocked JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS bp(pid) ON true JOIN pg_stat_activity blocking ON blocking.pid = bp.pid WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0 — кто кого держит (pg_blocking_pids — встроенная функция — цепочки видны сразу); (3) pg_locks (детали: какие объекты/granted=false — «ждёт row lock на таблице orders» — транзакционная механика); лечебные действия: SELECT pg_cancel_backend(pid) (мягко — отменить запрос — транзакция жива) → pg_terminate_backend(pid) (жёстко — убить сессию — откат — «неотменяемый» idle in transaction); главные виновники (по частоте): idle in transaction (приложение открыло транзакцию и «задумалось» — HTTP-вызов внутри транзакции/сессия ORM не закрыта — держит блокировки, не делая ничего — лечение: idle_in_transaction_session_timeout (5 мин — сервер убьёт — защита по умолчанию должна быть), пул/сессии — close вовремя — пулинг); DDL против DML (ALTER TABLE берёт ACCESS EXCLUSIVE — один незакоммиченный SELECT держит ACCESS SHARE — миграция встаёт в очередь, за ней — все новые запросы к таблице — «миграция на проде повесила сервис» — Alembic-практика — лечение: lock_timeout=3s на сессии миграции (не дождалась — отвалилась — повторить в тихое окно — лучше откат миграции, чем очередь к таблице); горячие строки (счётчик/баланс — UPDATE одной строки из 500 потоков — очередь на row lock — не дедлок, но «висит» — лечение: шардинг счётчика/асинхронная агрегация — паттерны); порядок UPDATE в двух транзакциях (A обновляет строки 1,2 — B строки 2,1 — классический дедлок — лечение: единый порядок обхода (ORDER BY id) — код-ревью — батч-UPDATE всегда сортируем); длинные транзакции вообще (не блокируют SELECT'ы — MVCC — но держат row-блокировки и мешают vacuum — «таблица раздулась» — следствие тех же долгих транзакций — xact_age-мониторинг закрывает оба класса).

Разбор: от симптома к причине за 10 минут

Алгоритм инцидента (проверенный на реальных «висит»): (1) симптом — приложение: таймауты запросов к таблице orders — метрики: p99 в потолок, errors растут; БД CPU низкий (ключевой сигнал — «медленный запрос» ест CPU — «блокировка» — ждёт в нулях — планы/производительность — разные болезни); (2) pg_stat_activity: 47 сессий на orders — wait_event_type=Lock, xact_age до 4 минут; источник — один pid 8231 — idle in transaction 4:12 — «вот ты какой»; (3) что за сессия: application_name/client_addr — сервис биллинга — логи сервиса в это время — HTTP-вызов платёжки на 240 секунд (таймаут не выставлен — таймауты — транзакция держалась всё время внешнего вызова — антипаттерн «внешний вызов внутри транзакции» в чистом виде); (4) тушение: pg_terminate_backend(8231) — очередь рассосалась за 10 секунд (p99 вернулся — Grafana — инцидент 6 минут); (5) лечение (три слоя): код — платёжный вызов вынесен за транзакцию (паттерн — outbox — «сначала фиксируем намерение, потом внешний вызов»); БД — ALTER SYSTEM SET idle_in_transaction_session_timeout='300s' + lock_timeout для миграционных сессий; мониторинг — алерт «idle in transaction > 60с» и «xact_age > 5 мин» (Alertmanager — следующий раз — до пользовательских таймаутов); (6) постмортем в runbook: «висит orders → pg_blocking_pids → idle in transaction → terminate → смотреть внешний вызов в транзакции» — следующий дежурный уложился бы в 3 минуты. Вариант с дедлоком (для полноты): ошибка deadlock detected в логах биллинга — log_lock_waits=on + deadlock_timeout — в логе Postgres — детали цикла («process X waits ShareLock on transaction Y; blocked by process Z...» — читается как приговор) — причина: два батча обновляли одни заказы в разном порядке — фикс: ORDER BY id в обоих — дедлоки исчезли; «ловим дедлоки в проде» — не стратегия: порядок обхода — на этапе ревью батч-кода.

FAQ

Чем pg_cancel_backend отличается от pg_terminate_backend? cancel — отменяет текущий запрос (сигнал отмены — транзакция остаётся открытой — клиент получает ошибку — «висит один запрос»); terminate — убивает всю сессию (процесс падает — транзакция откатывается — клиент теряет соединение — «idle in transaction не отменяется cancel'ом» — нечего отменять — запроса нет — только terminate); порядок: сначала cancel (мягче — graceful-культура), не помогло/не к чему — terminate; права — роль pg_signal_backend (не суперпользователь — выдаём дежурным — доступы); риск terminate — откат длинной транзакции может занять время (минуты на больших изменениях — блокировки держатся до конца отката — «убили, а легче не стало» — ждём откат — pg_stat_activity показывает); автоматизация «убивать idle in transaction > N» — лучше таймаутом сервера, не скриптом-камикадзе.

Postgres сам разруливает дедлоки — зачем тогда что-то делать? Разруливает — ценой отката одной транзакции (её работа потеряна — приложение должно уметь повторить — ретраи на deadlock-ошибку — обязательны для батчей — ошибка 40P01 — идемпотентный повтор — идемпотентность); цена детекции: deadlock_timeout=1с — каждый потенциальный дедлок «висит» секунду до разбора (под нагрузкой тысячи ожиданий — латентность пилит — «дедлоков нет, но всё тормозит» — log_lock_waits покажет частые ожидания); частые дедлоки — симптом дизайна (конкурирующие батчи без порядка/длинные транзакции — лечится причиной); мониторинг: pg_stat_database.deadlocks — счётчик растёт — разбираем — не «само рассосётся».

Почему SELECT не блокируется, а UPDATE ждёт — как это работает? MVCC: читатели не блокируют писателей и наоборот (SELECT видит снапшот — старая версия строки — изоляции — «висит» на SELECT'ах — почти всегда не блокировки, а ресурсы/CPU/IO); UPDATE/DELETE берут row-level блокировку (две транзакции на одну строку — вторая ждёт — до коммита первой — дальше «увидела изменение — перечитала» или «пропустила» по логике); DDL — табличные блокировки (ACCESS EXCLUSIVE против всего — «безобидный ALTER» останавливает и SELECT'ы — миграции — lock_timeout обязателен); SELECT ... FOR UPDATE — явная row-блокировка («читаю и намерен менять» — очередь как у UPDATE — осознанно); практический вывод: «висят только пишущие запросы» — ищите держателя row/табличной блокировки (pg_blocking_pids), «висят все» — ресурсы (CPU/IO/соединения — ёмкость) — два разных дерева диагностики.

Какие настройки Postgres защищают от блокировочных инцидентов? Минимум на прод: idle_in_transaction_session_timeout=300s (главный убийца «задумчивых» транзакций — приложение должно получить ошибку и переподключиться — лучше явная ошибка, чем тихая блокировка сервиса); lock_timeout — сессионно для миграций/DDL (3–5 с — «не взял блокировку — отложим» — глобально не ставим — обычные транзакции имеют право ждать); statement_timeout — по классам нагрузки (пулинг-контекст — «запрос на час в веб-транзакции» — не норма — аналитика — отдельное подключение/реплика); log_lock_waits=on + deadlock_timeout=1s (ожидания >1с — в лог — материал для разбора — логи); алерты: idle in transaction >60 с, xact_age >5 мин, deadlocks-дельта, wait_event Lock-всплески (метрики); связка с vacuum (долгие транзакции — bloat — autovacuum — та же первопричина — «два инцидента одной природы» — таймауты транзакций закрывают оба); правило команды: внешние вызовы (HTTP/очереди) — вне транзакций — структура — ревью-пункт №1 для всего, что держит сессию БД.

Источники: документация PostgreSQL (explicit locking, pg_locks, lock-конфигурация) — ссылки в шапке.