11. VACUUM и garbage collection
Введение: Механизм уборки мусора в базах данных
В системах с многоверсионным управлением конкурентным доступом (MVCC) ни один оператор UPDATE или DELETE не стирает физические байты с диска немедленно. Вместо этого СУБД помечает старую версию строки («мертвый кортеж» — dead tuple) как неактивную, сохраняя ее в страницах таблицы до тех пор, пока хотя бы одна активная транзакция теоретически может к ней обратиться.
Без специального автоматического механизма уборки база данных за считанные недели превратилась бы в гигантскую свалку отживших свой век версий: таблицы раздувались бы до сотен гигабайт, сканирования индексов деградировали бы в десятки раз, а свободное место на дисках стремительно испарялось.
Процесс очистки мусора в реляционных базах данных во многом подобен сборщику мусора (Garbage Collector) в рантайме Go, но оперирует не байтами оперативной памяти, а 8-килобайтными дисковыми страницами и индексами. Для инженера уровня Senior/Lead понимание этого механизма критично, потому что:
- «Раздутые» (bloated) таблицы и индексы — главная скрытая причина деградации задержек (
latency) под нагрузкой. - Неправильная конфигурация
autovacuumприводит к остановке кластера из-за переполнения 32-битного счетчика транзакций (Transaction ID Wraparound) или экспоненциальному росту дискового пространства. - Долгоживущие транзакции в Go-приложении замораживают горизонт очистки, порождая каскадные аварии по всей инфраструктуре.
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
A["UPDATE / DELETE создают мёртвые кортежи"]:::process --> B["Ожидание завершения всех активных транзакций"]
B --> C{"Достигнут xmin_horizon?"}
C -->|"Нет"| D["Продолжение работы приложения"]
C -->|"Да"| E["Запуск VACUUM или Purge Thread"]
E --> F["Сканирование страниц данных"]
F --> G["Обновление Visibility Map и FSM"]
G --> H["Пометка свободного места для повторного использования"]
H --> I["Запрос к ОС на освобождение страниц при возможности"]
Архитектура MVCC и возникновение мёртвых данных
При изменении строки СУБД создаёт новую версию с новым xmin, а у старой версии проставляется xmax. Пока существует хотя бы одна транзакция, снимок которой (snapshot) включает xmin старой версии, эта версия остаётся «живой» для неё.
Момент, с которого все мёртвые строки гарантированно не нужны ни одной транзакции, называется горизонтом очистки (cleanup horizon). В PostgreSQL это значение xmin минус количество активных транзакций на момент запуска VACUUM. Всё, что старше этого горизонта (xmin_horizon), можно безопасно удалять или помечать как свободное.
Реализация под капотом: Два архитектурных лагеря
Подходы к сборке мусора в современных СУБД радикально различаются по внутренней механике.
1. PostgreSQL: Inline VACUUM
Процесс autovacuum (или ручной VACUUM) работает непосредственно с файлами данных таблицы:
- Сканирование: Фоновый процесс последовательно читает страницы данных.
- Удаление кортежей: Находит строки с
xmax < horizon, стирает их заголовки. - Обновление структур: Заполняет
Free Space Map (FSM)информацией о свободных слотах на страницах. ОбновляетVisibility Map (VM), помечая страницы, где все строки видны всем, что ускоряетIndex Only Scan. - Важно: Пространство не возвращается операционной системе. Файл
.mdостаётся того же размера, но внутри появляются «дыры», которые переиспользуются для новыхINSERT/UPDATE.
2. MySQL/InnoDB: Background Purge
InnoDB не делает VACUUM в классическом понимании. Вместо этого работает purge thread:
- Undo Log: При
UPDATE/DELETEстарая версия копируется в сегменты отката (undo tablespace). - Purge: Фоновый поток сканирует историю отката, удаляет старые версии из undo-логов и сбрасывает информацию о «призраках» из кластерного и вторичных индексов.
- Page Freeing: Страницы, ставшие полностью пустыми, возвращаются в
Free Listтабличного пространства. - Особенность: InnoDB не выполняет компактизацию «на лету» внутри одной страницы так агрессивно, как PG. Дефрагментация требует
ALTER TABLE ... FORCE.
Механическая симпатия: Цена уборки для железа и ОС
Запуск VACUUM — это не бесплатная фоновая задача. Он создаёт серьёзную нагрузку на дисковую подсистему и память.
Дисковый I/O и кэш страниц ОС
VACUUM выполняет последовательное чтение файлов данных. Это эффективно заполняет кэш страниц ОС (page cache). Однако при обновлении FSM и VM происходят случайные записи небольших блоков. Ядро ОС переводит эти записи в dirty pages, которые позже сбрасываются через writeback threads.
- Влияние на latency: Если
autovacuumнастроен слишком агрессивно, он конкурирует с пользовательскими запросами заI/O queue. В облачных средах с ограниченнымburst IOPSэто приводит к таймаутамQueryContextв Go. - syscalls: Активно используются
posix_fadvise(POSIX_FADV_DONTNEED)для очистки кэша ОС от страниц, которые уже обработаны, чтобы не вытеснять «горячие» данные рабочих процессов.
CPU и предсказание ветвлений
Сканирование страниц на наличие мёртвых кортежей — это линейный обход памяти с проверкой флагов в заголовке каждой строки (HeapTupleHeader). Современные autovacuum реализации используют векторные инструкции и оптимизированные циклы, но высокая фрагментация таблиц приводит к промахам кэша L3. Проход по разряженным страницам увеличивает IPC (Instructions Per Cycle) и время ожидания памяти.
VACUUM FULL и возврат пространства операционной системе
Обычный VACUUM не уменьшает размер файла на диске. Для возврата места ОС требуется VACUUM FULL (PostgreSQL) или OPTIMIZE TABLE / ALTER TABLE ... FORCE (MySQL).
Этот процесс:
- Создаёт копию таблицы в новом файле, переписывая только живые строки.
- Реорганизует индексы с нуля.
- Заменяет оригинальный файл новым.
- Вызывает
fallocateилиtruncate, возвращая место ОС.
Практика в Go: Управление долгоживущими транзакциями
Ваша задача — не мешать autovacuum и отслеживать его прогресс.
Паттерн: Мониторинг возраста транзакций
В PostgreSQL переполнение счётчика txid (32-бит) приводит к потере данных. VACUUM должен сбрасывать frozenxid. Отслеживайте age(datfrozenxid) в метриках.
func GetDatabaseAgeMetrics(ctx context.Context, db *sql.DB) (map[string]int64, error) {
rows, err := db.QueryContext(ctx, `
SELECT datname, age(datfrozenxid)
FROM pg_database
WHERE datallowconn = true
`)
if err != nil {
return nil, fmt.Errorf("query db ages: %w", err)
}
defer rows.Close()
ages := make(map[string]int64)
for rows.Next() {
var name string
var age int64
if err := rows.Scan(&name, &age); err != nil {
continue
}
ages[name] = age
}
return ages, nil
}
// Если age > 1.5 млрд, срочно запускайте ручное VACUUM, иначе база остановится!
Паттерн: Избегание блокировок горизонта
Вместо удержания открытой транзакции для чтения данных, используйте моментальные снимки на уровне приложения или короткие транзакции с REPEATABLE READ.
// ПЛОХО: Транзакция держится пока обрабатывается внешняя логика
func ProcessExternalData(ctx context.Context, db *sql.DB, data []byte) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
// ... тяжёлые вычисления, HTTP вызовы, sleep ...
// Горизонт очистки заблокирован на это время!
_, err = tx.ExecContext(ctx, "UPDATE status SET processed = true WHERE id = $1", id)
return tx.Commit()
}
// ХОРОШО: Чтение в одной транзакции, обработка вне, запись в другой
func ProcessDataSafe(ctx context.Context, db *sql.DB, data []byte) error {
var record MyRecord
err := db.QueryRowContext(ctx, "SELECT id, payload FROM tasks WHERE id = $1 FOR UPDATE SKIP LOCKED", id).Scan(&record.ID, &record.Payload)
if err != nil {
return err
}
// Обработка вне транзакции БД
result, err := heavyComputation(record.Payload)
if err != nil {
return err
}
_, err = db.ExecContext(ctx, "UPDATE tasks SET result = $1, status = 'done' WHERE id = $2", result, record.ID)
return err
}
Параллель с Garbage Collector в Go
Понимание VACUUM через призму runtime.GC Go помогает лучше усвоить оба механизма:
| Параметр | Go GC (runtime) | DB VACUUM / Purge |
|---|---|---|
| Объекты | Структуры в куче (heap) | Строки (tuples) в файлах данных |
| Триггер | Превышение порога выделения / таймер | Превышение % мёртвых строк / таймер / txid age |
| Остановка мира | Да (STW фазы, но короткая) | Нет (работает асинхронно), кроме VACUUM FULL |
| Возврат памяти | madvise(MADV_DONTNEED) ОС |
Обычно нет (только VACUUM FULL возвращает ФС) |
| Проблема «пиннинга» | Живые корни удерживают граф | Открытая транзакция удерживает горизонт |
| Фрагментация | Вызывает аллокации новых объектов | Вызывает bloat страниц и замедление seq scan |
Обе системы жертвуют мгновенным возвратом ресурсов ради гарантии согласованности и минимизации влияния на основную работу. Главный враг и в Go, и в БД — долгоживущие ссылки на старые объекты.
Итог
- Назначение:
VACUUMиPurgeудаляют мёртвые версии строк, обновляют карты видимости и освобождают место для переиспользования. - Архитектура: PostgreSQL обновляет
FSM/VMвнутри файлов, InnoDB очищаетundo logsи индексы фоновым потоком. - Железо: Последовательное чтение страниц эффективно, но случайные записи в метаданные конкурируют за
I/O queueи кэш ОС. - В Go: Самая большая угроза — открытые транзакции во время тяжёлой обработки. Закрывайте
*sql.Txмгновенно, мониторьтеage(datfrozenxid), никогда не запускайтеVACUUM FULLиз кода. - Сравнение с Go GC: Оба механизма асинхронны и страдают от «задержки уборки» при наличии активных корней/транзакций.
Понимание того, как база данных управляет версиями строк и очищает их, подводит нас к следующему уровню изоляции, который обеспечивает абсолютную консистентность снимков без блокировок. В следующей статье мы детально разберём, как СУБД создаёт и использует эти снимки: 12. Snapshot isolation.