lesson ── postgres-labs ── ~25 мин ── 6 шагов
Перед тобой две psql-сессии: вкладка psql-a и вкладка psql-b,
обе подключены к одной базе lab. В psql-a мы откроем долгую транзакцию
и забудем её закрыть. В psql-b будем плодить мёртвые версии и запускать
VACUUM - и увидим, что он бессилен, пока psql-a держит горизонт.
Это главный сценарий раздувания в реальных базах. Перед шагами с VACUUM сначала предскажи: упадёт ли число мёртвых версий?
интерактивный sandbox
Поднимется контейнер postgreslab/postgres-base с PostgreSQL 17 и psql. В браузере откроется терминал, база lab уже настроена. Каждый шаг проверяется автоматически. Сеть air-gapped, наружу контейнер не ходит.
stack ── PostgreSQL 17 · psql · 1 GB RAM · air-gapped · самоуничтожается через 45 мин простоя
Переключись на вкладку psql-a и зафиксируй снимок:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT 1; -- здесь берётся снимок, горизонт встаёт на эту транзакцию
Не коммить и не закрывай эту вкладку. Транзакция перешла в состояние
idle in transaction - она ничего не делает, но держит горизонт.
✓ psql-a висит в idle in transaction — горизонт зафиксирован.
Переключись на psql-b и обнови раздутую демо-таблицу:
UPDATE bloat_demo SET payload = repeat('z', 200);SELECT dead_tuple_count, dead_tuple_percent
FROM pgstattuple('bloat_demo');Каждая строка получила новую версию, старые стали мёртвыми. pgstattuple
считает их живым сканом, без задержки статистики.
✓ Мёртвые версии появились — есть что убирать.
VACUUM bloat_demo;
SELECT dead_tuple_count FROM pgstattuple('bloat_demo');Предскажи до запуска: упадёт ли dead_tuple_count до нуля? VACUUM
отработает честно, но горизонт держит psql-a - её снимок ещё может
потребовать эти версии.
Версия удаляема, только если её xmax старше горизонта. Горизонт стоит на psql-a.
✓ VACUUM прошёл, но мёртвые версии на месте — горизонт не пустил.
SELECT pid, state, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;
Вот он - бэкенд psql-a, висящий в idle in transaction. В проде
именно так ищут виновника раздувания, а защищаются
idle_in_transaction_session_timeout.
✓ Виновник найден — долгая транзакция psql-a.
Вернись на вкладку psql-a и заверши транзакцию:
COMMIT;
Горизонт освобождён - больше ни один снимок не держит старые версии.
✓ Транзакция закрыта — горизонт сдвинулся вперёд.
VACUUM bloat_demo;
SELECT dead_tuple_count FROM pgstattuple('bloat_demo');Тот же VACUUM, та же таблица - но горизонт ушёл вперёд, и мёртвые
версии стали удаляемыми. Предскажи dead_tuple_count до запуска.
✓ Мусор убран. Одна транзакция держала его всё это время.
Горизонт транзакции - минимум по активным снимкам. VACUUM удаляет только версии старше горизонта. Долгая или idle-in-transaction транзакция держит горизонт и запрещает уборку мусора во всей базе.
команды
SELECT * FROM pg_stat_activity WHERE state='idle in transaction';найти держателя горизонтаSELECT dead_tuple_count FROM pgstattuple('bloat_demo');живой замер мёртвых версийSET idle_in_transaction_session_timeout='5min';защита от забытых транзакцийконцепции