lesson ── postgres-labs ── ~25 мин ── 5 шагов
Четыре клиентские сессии на один сервер. Ты соберёшь руками каскад, который в проде выглядит как «всё висит после деплоя»: забытая транзакция, за ней миграция, за ней обычные запросы. Потом найдёшь корень по PID и снимешь его.
Вкладки: psql-a - забытая транзакция, psql-b - миграция, psql-c
psql-ops - ты-дежурный, который разбирает завал.
В каждой вкладке сначала запусти psql.интерактивный sandbox
Поднимется контейнер postgreslab/postgres-base с PostgreSQL 17 и psql. В браузере откроется терминал, база lab уже настроена. Каждый шаг проверяется автоматически. Сеть air-gapped, наружу контейнер не ходит.
stack ── PostgreSQL 17 · psql · 1 GB RAM · air-gapped · самоуничтожается через 45 мин простоя
Открой psql во вкладке psql-a и начни транзакцию, не закрывая её:
BEGIN;
SELECT count(*) FROM flights;
Приглашение стало lab=*# - звёздочка значит «транзакция открыта».
Запрос отработал, но ACCESS SHARE на flights держится. Не вводи
COMMIT.
Звёздочка в приглашении psql = открытая транзакция. Это и есть idle in transaction.
✓ A висит в idle in transaction и держит ACCESS SHARE на flights.
Перейди во вкладку psql-b, запусти psql и выполни:
ALTER TABLE flights ADD COLUMN gate text;
Команда не вернёт приглашение - она повисла. ALTER TABLE хочет
ACCESS EXCLUSIVE, который конфликтует с ACCESS SHARE сессии A.
Предскажи: сколько невыданных (granted = f) блокировок по
flights сейчас в pg_locks?
Невыданная блокировка - это строка pg_locks с granted = f. Её ждёт B.
✓ B встал в очередь за ACCESS EXCLUSIVE - есть невыданная блокировка.
Вкладка psql-c, запусти psql, потом:
SELECT * FROM flights LIMIT 1;
C совместима с A (два читателя), но впереди в очереди стоит B с
ACCESS EXCLUSIVE, а очередь честная - перепрыгнуть ждущего нельзя.
Поэтому C тоже виснет. Предскажи, сколько сессий теперь ждут
блокировку (wait_event_type = 'Lock').
Ждут и B, и C. wait_event_type='Lock' - именно про ожидание тяжёлой блокировки.
✓ Две сессии в очереди. Один SELECT остановил всю таблицу.
Во вкладке psql-ops запусти psql и найди, кто кого блокирует:
SELECT w.pid AS waiting, b.pid AS blocking, b.state, b.query
FROM pg_stat_activity w
JOIN LATERAL unnest(pg_blocking_pids(w.pid)) AS bp(pid) ON true
JOIN pg_stat_activity b ON b.pid = bp.pid
WHERE cardinality(pg_blocking_pids(w.pid)) > 0;
Цепочка ведёт к сессии A в состоянии idle in transaction - это
корень. Запиши её PID (колонка blocking с state = idle in transaction).
pg_blocking_pids(pid) возвращает массив тех, кто мешает данному процессу.
✓ Видно две заблокированные сессии, корень - idle-in-transaction A.
Корень в idle in transaction, значит pg_cancel_backend не
поможет (нет активного запроса). Нужен pg_terminate_backend.
Во вкладке psql-ops найди PID сессии A и закрой её:
-- подставь PID сессии A из прошлого шага
SELECT pg_terminate_backend(<pid_A>);
Как только A закрыта, её ACCESS SHARE спадает: B применяет
миграцию, C получает результат. Проверим по факту - в flights
появилась колонка gate.
pg_terminate_backend закрывает соединение целиком: транзакция откатывается, блокировки спадают.
✓ Миграция прошла, колонка gate на месте. Каскад разобран.
Каскад блокировок - это забытая открытая транзакция, за которой в честной FIFO-очереди встал DDL с ACCESS EXCLUSIVE, а за ним - обычные запросы. Тяжёлая блокировка держится до конца транзакции, поэтому idle-in-transaction опасен. Диагностика идёт от ждущего к держащему через pg_blocking_pids, лечение - pg_terminate_backend.
команды
SELECT * FROM ... WHERE ... FOR UPDATE;явно заблокировать строкиSELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity;кто кого блокируетSELECT pg_terminate_backend(pid);закрыть сессию-блокировщикSELECT mode, granted FROM pg_locks WHERE relation='t'::regclass;блокировки на таблицеконцепции