10. VACUUM, ANALYZE
В лекции про многоверсионность 3. MVCC в PostgreSQL мы столкнулись с фундаментальным архитектурным фактом: движок PostgreSQL никогда не модифицирует существующие данные «на месте» (in-place) и физически не стирает строки при вызове DELETE. Каждая операция обновления или удаления оставляет за собой «мертвый» кортеж (dead tuple), помеченный системным идентификатором транзакции t_xmax.
Если бы в архитектуре не было встроенного механизма непрерывной санитарной очистки, любая высоконагруженная база данных за считанные недели превратилась бы в гигантский мусорный полигон: полезные данные утонули бы в терабайтах мертвых версий, дисковое пространство исчерпалось, а эффективность кэша shared_buffers рухнула до нуля.
За поддержание физической чистоты страниц и математической зоркости планировщика отвечают две фундаментальные системные операции: VACUUM и ANALYZE. Для Go-разработчика понимание их внутренней механики — это граница, отделяющая стабильно работающий бэкенд от внезапного паралича продакшена посреди ночи.
1. VACUUM: Мусорщик в мире MVCC
VACUUM — это процесс, который сканирует таблицы и помечает место, занятое мертвыми строками, как свободное.
Важно понимать: обычный VACUUM не возвращает место операционной системе. Он просто говорит PostgreSQL: "Эти байты внутри 8-килобайтной страницы больше не нужны, сюда можно записать новый INSERT".
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph PageBefore ["1. Страница Heap до очистки (8 KB)"]
direction TB
T1["Живой кортеж A (t_xmax = 0)"]
T2["Мертвый кортеж B (t_xmax = 100, COMMIT в прошлом)"]
T3["Живой кортеж C (t_xmax = 0)"]
end
subgraph VacuumAction ["2. Проход VACUUM"]
VProc["VACUUM Worker: <br> • Удаление ссылок из B-Tree индексов <br> • Пометка слота B как свободного <br> • Обновление Free Space Map (FSM) <br> • Выставление бита all_visible в Visibility Map"]
end
subgraph PageAfter ["3. Страница после VACUUM (8 KB)"]
direction TB
CT1["Живой кортеж A"]:::process
CFS["[ Свободное окно для записи (Free Space) ]"]
CT3["Живой кортеж C"]:::process
end
PageBefore --> VacuumAction
VacuumAction --> PageAfter
CFS -.->|"Новый INSERT из Go"| NewInsert["Новый кортеж D без расширения файла!"]
Как физически работает конвейер VACUUM (пошагово)
- Сканирование Heap: Воркер вакуума считывает страницы таблицы из
shared_buffersили поднимает их с диска. - Проверка горизонта транзакций: Сверяясь с глобальным снимком активных транзакций, процесс выявляет мертвые кортежи, чей
t_xmaxгарантированно завершен и невидим ни для одной текущей или потенциальной сессии. - Двухфазная очистка индексов: Это критически важный шаг! Сначала вакуум собирает список адресов
CTIDмертвых строк в буферmaintenance_work_mem, обходит все до единого индексы таблицы (B-Tree, GIN, GiST) и вычищает из их листовых страниц указатели на эти мертвые кортежи. - Очистка страниц Heap: Только после того, как все ссылки из индексов гарантированно удалены, вакуум возвращается к страницам Heap и переводит слоты мертвых кортежей в статус свободного пространства.
- Обновление Free Space Map (FSM): В специальный вспомогательный файл (
_fsm) записывается точный объем свободного места на каждой странице, чтобы последующие операцииINSERTмгновенно находили свободные блоки без линейного сканирования файлов.
VACUUM FULL: Когда всё очень плохо
Существует специальная агрессивная форма очистки — команда VACUUM FULL. В отличие от обычного вакуума, она физически возвращает неиспользуемые байты операционной системе:
- Создает абсолютно новый файл таблицы на диске.
- Последовательно переносит в новый файл исключительно живые кортежи, упаковывая их максимально плотно без малейших зазоров.
- С нуля перестраивает все индексы таблицы.
- Физически удаляет старый раздутый файл, возвращая гигабайты пространства ОС.
2. Autovacuum: Твой невидимый помощник
В современных версиях PostgreSQL администраторам редко приходится инициировать очистку вручную: за чистотой следит системный демон — Autovacuum Launcher и пул его дочерних воркеров.
Демон пробуждается раз в период времени, заданный параметром autovacuum_naptime (по умолчанию раз в 1 минуту), опрашивает статистику подсистемы сбора метрик и вычисляет, не пора ли запустить вакуум для конкретной таблицы.
Mechanical Sympathy для Highload
Представьте таблицу на 100 миллионов строк. При стандартном коэффициенте scale_factor = 0.2 Autovacuum запустится только тогда, когда в таблице накопится:
$$50 + (0.2 \times 100\,000\,000) \approx 20\,000\,000 \text{ мертвых строк!}$$
Накопление 20 миллионов мертвых строк означает чудовищное раздувание таблицы (Table Bloat), деградацию дискового ввода-вывода и засорение shared_buffers.
В высоконагруженных проектах для тяжелых таблиц значение scale_factor принудительно снижают до 0.01 (1%) или даже 0.005 (0.5%), обеспечивая непрерывную, мягкую и незаметную для пользователей очистку.
3. ANALYZE: Глаза и математический компас планировщика
Если задача VACUUM — физическое освобождение дискового пространства внутри страниц, то миссия команды ANALYZE — обеспечение интеллектуальной работы планировщика запросов.
Как мы разбирали в 9. Оптимизация PostgreSQL, стоимостной оптимизатор принимает решение о выборе алгоритма (Index Scan, Bitmap Scan, Seq Scan, Hash Join, Merge Join) исключительно на базе математической статистики.
Процесс ANALYZE инспектирует репрезентативную случайную выборку строк таблицы и рассчитывает:
- Общее число физических страниц и строк в таблице.
- Selection Cardinality (степень уникальности значений в каждом столбце —
n_distinct). - Список наиболее частых значений (Most Common Values, MCV) и их процентную частоту.
- Гистограммы квантилей распределения значений (Histogram Bounds).
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Table["Таблица orders <br> (10 млн строк)"]:::process --> Sampler["ANALYZE: <br> Случайная выборка ~30 000 строк"]
Sampler --> StatsCalc["Расчет метрик: <br> • n_distinct <br> • Гистограммы MCV"]
StatsCalc --> Catalog["Системный каталог pg_statistic <br> (Используется Planner при каждом EXPLAIN)"]
Почему актуальность статистики критична для Go-приложения?
Представьте таблицу задач tasks со столбцом status:
- 99% строк имеют статус
'COMPLETED'. - 1% строк имеют статус
'IN_PROGRESS'.
Если статистика актуальна, то при выполнении запроса SELECT * FROM tasks WHERE status = 'IN_PROGRESS' планировщик безошибочно выберет точечный Index Scan, прочитав пару индексных страниц за 1 миллисекунду.
Если же статистика устарела (например, база считает, что статусы распределены 50/50), оптимизатор решит, что индекс неэффективен, и запустит Sequential Scan по всей многогигабайтной таблице, превратив быстрый микросервисный запрос в катастрофическую блокировку ввода-вывода!
4. Защита целостности: Visibility Map и предотвращение Wraparound
Помимо базовой сборки мусора, Autovacuum выполняет две жизненно важные архитектурные функции, без которых надежное функционирование СУБД невозможно:
1. Актуализация карты видимости (Visibility Map)
Каждый раз, когда VACUUM обрабатывает страницу и убеждается, что на ней больше нет ни одного мертвого кортежа и незакоммиченных транзакций, он выставляет бит all_visible в файле Visibility Map.
Как мы выяснили в 4. Индексы в PostgreSQL, именно этот бит позволяет планировщику применять сверхбыстрый Index-Only Scan, извлекая данные непосредственно из B-Tree индекса без единого обращения к диску за страницами Heap.
2. Заморозка транзакций (Freezing) и угроза Wraparound
Идентификаторы транзакций xmin/xmax имеют размер 32 бита (лимит около 4 миллиардов значений, закольцованных по модулю $2^{32}$). Если счетчик сделает полный оборот, старые транзакции окажутся «в будущем», и данные базы мгновенно исчезнут для всех правил видимости!
Чтобы исключить эту катастрофу, вакуум регулярно выполняет процедуру Freezing (Заморозку): он инспектирует старые строки и заменяет их xmin на фиксированный служебный маркер FrozenTransactionId (равный 2), который навсегда закрепляет строку как видимую в абсолютном прошлом. Если Autovacuum не успевает за темпом генерации xid, PostgreSQL принудительно аварийно останавливается и переходит в режим read-only для спасения данных от переполнения.
5. Практические советы по тюнингу и конфигурированию
В высоконагруженных системах стандартная конфигурация Autovacuum требует осознанной калибровки:
maintenance_work_mem: Объем оперативной памяти, выделяемый воркеру вакуума для удержания буфераCTIDмертвых строк при обходе индексов. Если памяти недостаточно, процессу придется совершать множественные проходы по тяжелым B-Tree деревьям. В промышленных серверах устанавливайте от 512 МБ до 2 ГБ.autovacuum_max_workers: По умолчанию равен3. Если в вашем кластере сотни активных таблиц с постоянными вставками и обновлениями, три воркера не справятся с нагрузкой. Увеличьте пул до 5–8 воркеров (при наличии свободных ядер CPU).autovacuum_vacuum_cost_limit: Ограничитель бюджета дискового ввода-вывода. По умолчанию вакуум делает паузы, чтобы не мешать пользовательским запросам. Если вакуум отстает от потока операцийUPDATEиз вашего Go-сервиса, увеличьте этот лимит (например, с 200 до 1000–2000 единиц) или снизьте задержкуautovacuum_vacuum_cost_delay.
Резюме для инженера
- VACUUM освобождает страницы изнутри: Он не уменьшает размер файлов ОС, но открывает слоты для новых
INSERTиUPDATE. - ANALYZE питает планировщик: Без актуальной статистики кардинальности и гистограмм распределения база неизбежно начнет деградировать на неоптимальных планах.
- Агрессивный Autovacuum: Снижайте
scale_factorна больших таблицах, чтобы не накапливать миллионы мертвых строк. - Visibility Map и Freeze: Регулярный вакуум делает возможным Index-Only Scan и предотвращает катастрофу Transaction ID Wraparound.
- Транзакционная гигиена в Go: Строго контролируйте таймауты контекста и не допускайте долгоживущих открытых транзакций.
Мы завершили детальный разбор внутреннего устройства одиночного сервера PostgreSQL: от физического слоя страниц и MVCC до тонкостей планировщика и фоновой сборки мусора. Однако надежность критически важных информационных систем никогда не доверяется одной машине. В заключительной лекции раздела мы разберем, как объединять серверы в отказоустойчивые кластеры: 11. Репликация в PostgreSQL.