· 16 мин чтения

VACUUM и autovacuum в PostgreSQL: боремся с bloat и wraparound грамотно

Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За 15+ лет я повидал в корпоративных базах PostgreSQL все возможные способы себя покалечить — от отключённого autovacuum до ручного VACUUM FULL в пиковые часы. И каждый раз результат один: база распухает, запросы тормозят, а админы удивляются. В этом материале — как работает VACUUM, чем отличается от VACUUM FULL, как тонко настроить autovacuum под OLTP-нагрузку и не влететь в transaction ID wraparound, от которого приходит в себя только отдельный техник с бутылкой валерьянки.

MVCC и откуда берётся bloat

PostgreSQL использует Multi-Version Concurrency Control. Когда вы делаете UPDATE строки, сервер не правит её на месте — он создаёт новую версию, а старая остаётся в файле и помечается как «умершая» (dead tuple) после завершения всех транзакций, которые могли её видеть. DELETE работает похоже: строка не удаляется, а только помечается.

Без очистки этих dead tuples таблица растёт. Это и есть bloat — раздутие. Реальных данных 5 ГБ, а файл таблицы — 12 ГБ, потому что там ещё 7 ГБ «трупов» старых версий. Запросы тормозят: планировщик вынужден читать больше блоков, индексы раздуваются, кэш забит мусором.

VACUUM — это сборщик мусора PostgreSQL. Он проходит по таблице, находит dead tuples и помечает их пространство как свободное для переиспользования. Но файл при этом не уменьшается: освобождённое место ждёт новых INSERT/UPDATE, а ОС про него не узнаёт.

VACUUM vs VACUUM FULL vs ANALYZE

КомандаЧто делаетБлокировкаВозвращает место ОС
VACUUMПомечает dead tuples, обновляет visibility mapShareUpdateExclusive (не блокирует чтение/запись)Только если в конце есть пустые блоки
VACUUM FULLПереписывает таблицу в новый файл без мусораAccessExclusive (полная блокировка)Да, полностью
ANALYZEОбновляет статистику для планировщикаShareUpdateExclusiveНет
VACUUM FREEZEЗамораживает XID, защищает от wraparoundShareUpdateExclusiveНет
pg_repackАналог VACUUM FULL, но без эксклюзивной блокировкиКороткая ACCESS EXCLUSIVE в начале и концеДа

У нас на практике VACUUM FULL на продакшене мы запускаем только ночью и только если bloat перевалил за 40%. Для горячих таблиц всегда используем pg_repack — он делает копию, накатывает на неё изменения через триггер и переключает одним ALTER.

Как работает autovacuum

Фоновый процесс autovacuum launcher раз в autovacuum_naptime (по умолчанию 1 минута) проверяет все базы и решает, на каких таблицах нужна работа. Критерий срабатывания:

dead_tuples > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tup

По умолчанию scale_factor = 0.2, threshold = 50. То есть таблица на 1 млн строк триггерит autovacuum только когда накопилось 200 050 мёртвых кортежей. Для горячих таблиц с тысячами UPDATE в минуту это слишком высокий порог — bloat успевает вырасти до 30% до того, как autovacuum очнётся.

Для ANALYZE своё правило: autovacuum_analyze_scale_factor * n_live_tup (по умолчанию 0.1). Тоже часто нужно уменьшать.

Тюнинг глобальных параметров

Мой типовой набор параметров в postgresql.conf для корпоративной OLTP-нагрузки с базой 200 ГБ и сервером на 64 ГБ RAM:

autovacuum = on                               # НИКОГДА не выключайте
autovacuum_max_workers = 6                    # По умолчанию 3 — мало
autovacuum_naptime = 30s                      # Проверять чаще, чем раз в минуту
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.05         # По умолчанию 0.2 — слишком лениво
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.02        # По умолчанию 0.1
autovacuum_vacuum_cost_delay = 2ms            # По умолчанию 2ms уже в PG12+
autovacuum_vacuum_cost_limit = 2000           # По умолчанию 200 — мало для SSD
autovacuum_freeze_max_age = 200000000         # За 200 млн транзакций — FREEZE
autovacuum_multixact_freeze_max_age = 400000000

# Параллельные workers
maintenance_work_mem = 2GB                    # autovacuum берёт отсюда
max_parallel_maintenance_workers = 4

Ключевое: cost_limit определяет, сколько «стоимости ввода-вывода» worker может потратить, прежде чем заснуть на cost_delay. На NVMe с дисковой пропускной 3 ГБ/с нет смысла ограничивать autovacuum в 200 единиц — он будет работать часами. 2000 — честный компромисс: autovacuum быстр, но не душит продакшен.

Потабличная настройка autovacuum

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

-- Таблица с частыми UPDATE — более агрессивный autovacuum
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold = 1000,
  autovacuum_analyze_scale_factor = 0.01,
  autovacuum_vacuum_cost_delay = 1,
  autovacuum_vacuum_cost_limit = 5000
);

-- Append-only таблица логов — почти не нужен VACUUM, но FREEZE обязателен
ALTER TABLE audit_log SET (
  autovacuum_vacuum_scale_factor = 0.5,
  autovacuum_freeze_min_age = 0,
  autovacuum_freeze_max_age = 100000000
);

-- Посмотреть текущие параметры
SELECT relname, reloptions FROM pg_class WHERE reloptions IS NOT NULL;

Я всегда выделяю топ-10 самых активных таблиц и настраиваю их потабличные опции отдельно. На средней базе это даёт 2–3 таблицы с очень частыми UPDATE, 5–7 — умеренными, остальные — append-only или почти статичные.

Transaction ID wraparound — самая страшная проблема

Каждая транзакция получает 32-битный идентификатор XID. Максимум — 2^31 = 2,147,483,648. Чтобы не переполниться, PostgreSQL использует циклическую арифметику, а VACUUM FREEZE «замораживает» старые строки, помечая их как видимые для всех будущих транзакций. Если не замораживать — база при достижении 2 млрд переходит в режим single-user, перестаёт принимать записи и просит запустить VACUUM вручную.

На базе с 5000 TPS 2 млрд транзакций — это 4,5 дня. Если ваш autovacuum_freeze_max_age = 200 млн и autovacuum регулярно заморозил старые строки, wraparound вам не грозит. Но если autovacuum отстаёт (или отключён!) — беда. Мониторим возраст:

SELECT datname, age(datfrozenxid) AS xid_age,
       round(100 * age(datfrozenxid)::numeric / 2000000000, 2) AS pct_to_wraparound
FROM pg_database ORDER BY xid_age DESC;

Если процент перевалил за 50 — пора разбираться, почему autovacuum не справляется. При 90% — срочная ручная VACUUM FREEZE на проблемные таблицы.

Мини-кейс: распухшая база на 1С

В июле 2025 позвали разобраться с базой PostgreSQL под 1С:УТ. База официально 85 ГБ по данным, фактический размер на диске — 310 ГБ. Дисковый раздел на 80% заполнен, 1С тормозит, ночное закрытие — два часа. Сервер Dell с Xeon Platinum 8280, 128 ГБ RAM, но autovacuum отключён «чтобы не тормозил днём» — классическая ошибка.

Что сделали:

Результат: база ужалась до 112 ГБ (-64% места на диске), ночное закрытие — 38 минут (было 2 часа), транзакции на 30% быстрее за счёт меньшего числа страниц в кэше. Стоимость работ — 45 тыс. руб. плюс 15 тыс. за еженедельный мониторинг. Сервер дожил до плановой миграции на новое железо через 14 месяцев без единого инцидента.

Мониторинг и алерты

Без мониторинга вы узнаете о проблеме, когда диск забьётся или база встанет. Минимальный набор алертов в Prometheus/Grafana:

-- Состояние autovacuum сейчас
SELECT pid, datname, relid::regclass, phase,
       heap_blks_total, heap_blks_scanned,
       round(100.0 * heap_blks_scanned / heap_blks_total, 1) AS pct
FROM pg_stat_progress_vacuum;

-- Топ-10 раздутых таблиц
SELECT schemaname, relname,
       pg_size_pretty(pg_relation_size(relid)) AS size,
       n_dead_tup, n_live_tup,
       round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup,0), 1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;

Чек-лист здоровой базы

Вешу на стену в серверной и раздаю младшим админам:

Разберёмся с bloat и тормозами PostgreSQL

Проведу аудит вашей базы, найду проблемные таблицы, настрою autovacuum потабличный, подниму мониторинг и избавлю от ночных окон VACUUM FULL. Работаю с базами от 20 ГБ до нескольких ТБ, 1С-совместимые сборки — в том числе. Дата-центр МТС на связке Mellanox 40G для standby — из наших стандартных решений.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — частые вопросы по VACUUM

В чём разница между VACUUM и VACUUM FULL?
VACUUM помечает мёртвые кортежи как переиспользуемые, таблица остаётся доступной для записи. VACUUM FULL полностью переписывает таблицу в новый файл и отдаёт место ОС, но блокирует таблицу эксклюзивной блокировкой на всё время операции.
Почему autovacuum не успевает?
Три типичные причины: autovacuum_max_workers меньше количества горячих таблиц, слишком большой autovacuum_vacuum_cost_delay, нагрузка на диск загибает процесс. Обычно помогает увеличение max_workers до 6–10 и уменьшение cost_delay до 2 мс.
Что такое transaction ID wraparound?
PostgreSQL использует 32-битные идентификаторы транзакций. Если не чистить старые XID через VACUUM FREEZE, через 2 миллиарда транзакций база перестаёт принимать записи и уходит в режим single-user. Это катастрофа — избегайте любой ценой.
Нужно ли запускать VACUUM FULL вручную?
Только когда bloat превысил 30–40% от размера таблицы и вы подтвердили это через pgstattuple. В остальных случаях обычного autovacuum хватает. Вместо VACUUM FULL часто лучше использовать pg_repack — он делает то же, но без блокировки.
Как мониторить bloat?
Через расширение pgstattuple или запрос к pg_stat_user_tables. Я всегда ставлю экспортёр Prometheus с метрикой pg_bloat_size и алертом при >30%. Для точных замеров — pgstattuple_approx на больших таблицах.

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.