PostgreSQL
VACUUM: сборка мусора и autovacuum
Таблица orders занимает 200 GB, но в ней всего 50 GB реальных данных. 150 GB - мусор от миллионов UPDATE за последние месяцы. Запросы медленные, диск полный. VACUUM FULL решит проблему за 2 часа простоя. pg_repack решит за те же 2 часа без остановки сервиса.
- **Slack** (2019) обнаружил что таблица channels достигла 250 GB bloat из-за отставания autovacuum на волне роста COVID - пришлось экстренно запускать pg_repack в production; теперь dead_pct > 15% - триггер автоматического алерта
- **Shopify** настраивает autovacuum_vacuum_scale_factor = 0.01 для таблицы orders (10M+ строк) - при дефолте 0.2 autovacuum запускался бы только при 2M dead tuples, слишком поздно
- **Gitlab** мониторит txid wraparound риск через `age(datfrozenxid)` в pg_database: при приближении к 1.5B делают принудительный VACUUM FREEZE чтобы не допустить аварийного freeze всего кластера
Dead tuples - откуда берётся мусор
Каждый UPDATE в PG создаёт новую версию строки и помечает старую как мёртвую (dead tuple). DELETE тоже оставляет dead tuple - физически строка остаётся на месте, только xmax выставляется в txid удаляющей транзакции. Накопление dead tuples вызывает table bloat: файл таблицы растёт, seq scan медленнее, shared_buffers забивается мусором.
Долгоживущая транзакция (например зависший аналитический запрос или открытый курсор) удерживает xmin snapshot. VACUUM не может удалить dead tuples с xmax меньше этого xmin. Одна часовая транзакция способна накопить гигабайты мусора на активных таблицах.
Таблица имеет 1M live строк и 500K dead tuples. Почему Seq Scan медленнее чем на таблице с только 1M строк без dead tuples?
VACUUM vs VACUUM FULL - два разных инструмента
Обычный VACUUM помечает dead tuples как доступные для переиспользования, но не возвращает место OS. Физический размер файла не уменьшается. VACUUM FULL переписывает таблицу полностью в новый файл - возвращает место, но требует эксклюзивную блокировку (AccessExclusiveLock) на всё время работы. На таблице в 100 GB это может занять часы.
| Свойство | VACUUM | VACUUM FULL |
|---|---|---|
| Блокировка | ShareUpdateExclusiveLock (слабая) | AccessExclusiveLock (эксклюзивная) |
| Читатели/писатели | Параллельная работа разрешена | Все ждут |
| Возврат места OS | Нет (только reuse) | Да |
| Скорость | Быстро (инкрементально) | Медленно (полная перезапись) |
| Производство | Регулярно | Только при крайней необходимости |
Таблица занимает 50 GB, 80% - bloat. Запустили VACUUM. Сколько места вернётся OS?
Autovacuum tuning - правильные пороги
Autovacuum запускается когда `n_dead_tup > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tup`. По умолчанию: threshold=50, scale_factor=0.2. Для таблицы в 100M строк это 20M dead tuples - запуск слишком редко. На горячих таблицах нужно понижать scale_factor до 0.01 или даже 0.001.
autovacuum_vacuum_cost_delay контролирует throttling: vacuum засыпает на delay мс после накопления cost_limit очков работы (чтение страницы = 1 очко, запись = 20). Это позволяет vacuum работать без убийства I/O. На SSD-серверах можно снизить до 0-2 мс.
Таблица events имеет 10M live строк. С дефолтным scale_factor=0.2 autovacuum запустится при скольких dead tuples?
Table bloat - диагностика и измерение
Bloat - разница между физическим размером таблицы и фактическим объёмом данных. Возникает когда dead tuples накапливаются быстрее чем VACUUM их убирает, или когда VACUUM убрал мусор но место не вернулось OS. Bloat в индексах (index bloat) критичен - раздутый индекс медленнее читается и не влезает в cache.
Bloat в индексах: `SELECT * FROM pgstatindex('idx_orders_user_id')` показывает leaf_fragmentation и avg_leaf_density. Density < 50% говорит о сильном index bloat. REINDEX CONCURRENTLY перестраивает без блокировки (PG 12+).
pgstattuple показал dead_tuple_percent=5%, но pg_relation_size вдвое больше ожидаемого. Как это возможно?
pg_repack - дефрагментация без блокировок
pg_repack перестраивает таблицу в новый файл без эксклюзивной блокировки: создаёт копию, применяет изменения через trigger, затем атомарно переключает. Вся операция занимает AccessExclusiveLock только на финальной перестановке - миллисекунды. Это замена VACUUM FULL для продакшена.
pg_repack создаёт trigger на исходной таблице для захвата изменений во время копирования. Это значит таблица должна иметь PRIMARY KEY (без него pg_repack не работает). Во время работы немного увеличивается нагрузка на запись из-за trigger overhead.
VACUUM FULL - стандартный способ бороться с bloat в продакшене
VACUUM FULL блокирует таблицу полностью на часы - недопустимо для продакшена. Для борьбы с bloat без простоя используется pg_repack. VACUUM FULL допустим только при плановых окнах обслуживания.
VACUUM FULL держит AccessExclusiveLock: никакие SELECT, INSERT, UPDATE, DELETE не проходят. На таблице orders в 50 GB это 30-60 минут полного downtime для всего что работает с этой таблицей.
Зачем pg_repack требует PRIMARY KEY на таблице?
Ключевые идеи
- Dead tuples = физически живые строки с выставленным xmax; VACUUM помечает их как reusable но не возвращает место OS
- Autovacuum threshold = 50 + scale_factor * live_rows; для больших таблиц scale_factor нужно снижать до 0.01 per-table
- VACUUM FULL = полная блокировка на часы; pg_repack = перестройка без простоя через trigger-captured-changes
Связанные темы
VACUUM - часть экосистемы MVCC и мониторинга PG:
- MVCC и dead tuples — Dead tuples - прямое следствие MVCC: UPDATE не перезаписывает, а добавляет; без VACUUM heap растёт бесконечно
- Транзакции и долгие snapshot — Долгоживущие транзакции в REPEATABLE READ удерживают xmin - VACUUM не может удалить dead tuples старше этого xmin
- Индексы — Index bloat накапливается так же как table bloat; REINDEX CONCURRENTLY и pg_repack --only-indexes лечат без блокировки
Вопросы для размышления
- Если установить autovacuum_vacuum_scale_factor = 0 и autovacuum_vacuum_threshold = 100, что произойдёт с autovacuum поведением?
- Почему таблица после VACUUM FULL может быть быстрее чем после pg_repack? Подсказка: порядок строк на диске.
- txid wraparound - pg_database.datfrozenxid достигает 2^31. Что сделает PG если не запустить VACUUM FREEZE вовремя?