Перейти к основному содержимому

Команда VACUUM и утилита vacuumdb

Команда SQL VACUUM – процесс обслуживания СУБД, который решает две основные задачи:

  1. Удаление полностью устаревших версий записей (dead tuples).
  2. «Заморозка» (перенос в архив и высвобождение) идентификаторов транзакций.

На эту же команду возложены и связанные вспомогательные задачи по обслуживанию системных объектов.

Общий алгоритм выглядит следующим образом:

  1. В цикле по каждой из таблиц получить блокировку ShareUpdateExclusiveLock:

    • Просмотреть все страницы, не отмеченные в карте видимости как «полностью видимые».
    • Получить список устаревших версий и одновременно заморозьте версии, относящиеся к «древней истории» транзакций.
    • Удалить из индексов ссылки, соответствующие устаревшим версиям записей.
    • В цикле по страницам таблицы удалить устаревшие версии и плотно перераспределите оставшиеся действительные записи.
    • Обновить карту свободного пространства и карту видимости.
    • Завершить цикл по страницам.
    • Очистить индексы.
    • Уменьшить «хвост» таблицы на последнюю занятую страницу, если это возможно.
    • Обновить статистику и описания системного каталога по таблице.
  2. Освободить блокировку ShareUpdateExclusiveLock.

  3. Завершить цикл по таблицам.

  4. Обновить статистику и описания системного каталога - pg_stat_all_tables.n_live_tup, n_dead_tup, last_vacuum, vacuum_count и т.д.

  5. Если возможно, то удалить старые файлы и страницы clog.

Подробнее по блокам:

  1. Выполнение «заморозки» и удаление ссылок на устаревшие записи из индексов.
    Сессия для начала отводит буфер размером maintenance_work_mem в локальной памяти процесса и просматривает записи. Если записи подходят для «заморозки», то они замораживаются. В буфер постепенно набирается список полностью устаревших записей.
    После набора полного списка устаревших записей (или заполнения maintenance_work_mem, что раньше) попавшие в список записи удаляются из индексов (точнее, из индексов удаляются ссылки на устаревшие версии записей).
    Если в первом проходе maintenance_work_mem была переполнена, то будет набрана следующая порция списка, те же шаги повторятся для нее.

  2. Удаление самих устаревших записей и обновление карт свободного пространства/видимости по каждой из страниц.
    Здесь алгоритм изменяет блок данных, плотно упорядочивая действительные записи и пропуская устаревшие. Порядок таблицы указателей на записи в заголовке блока не изменяется, чтобы не аннулировать ссылки на эти указатели из индексов (ctid).
    Изменения отмечаются в картах свободного пространства и видимости отдельно по каждому из блоков.

  3. Очистка индексов после массового удаления ссылок.
    Здесь выполняется обновление статистики и других описаний объектов в системных каталогах по результатам обработки.
    Если «хвостовая часть» таблицы более не содержит данных, здесь же файл таблицы будет усечен, а дисковое пространство - возвращено операционной системе.

  4. Постобработка.

    Когда завершен процесс по всем таблицам, сервер обновляет некоторое количество статистики и системных каталогов по результатам операции.

    Здесь же отсекаются устаревшие части массива статусов транзакций clog (часть буферов в памяти и старые файлы).

VACUUM не использует транзакции, но сам не может быть выполнен в рамках транзакции.

С версии 5.1.0 Pangolin (ядро PostgreSQL 13) VACUUM может параллельно выполнять операции над несколькими индексами (ключ PARALLEL). В варианте автоочистки эта возможность еще недоступна.

Эффективные права VACUUM на все время обработки таблицы совпадают с правами владельца таблицы.

«Настоящий» VACUUM по всей базе (включающий очистку таблиц системного каталога) – может быть выполнен только суперпользователем. Пользовательский VACUUM таблицы каталога не обрабатывает, а пропускает без ошибок и предупреждений.

Дополнительно, VACUUM не обрабатывает следующие таблицы:

  • временные таблицы других сессий, поскольку нет смысла искать их содержимое на диске;
  • секционированные таблицы, поскольку соответствующие действия выполняются на секциях поодиночке.

Обработка пропускается без предупреждений и ошибок, чтобы не осложнять администраторам полную обработку базы.

VACUUM использует для просмотра таблицы отдельный временной кольцевой буфер в частной памяти выполняющей сессии, чтобы избежать вымывания общего кеша в разделяемой памяти.

Параметр maintenance_work_mem крайне важен для VACUUM, потому что от объема буфера прямо зависит количество повторений цикла очистки при объемном списке устаревших версий. При инсталляции Pangolin параметр будет установлен в 1/24 от объема физической памяти сервера.

Утилита vacuumdb - оболочка команды VACUUM для внешних скриптов ОС.

Подходит для:

  • очистки и обновления статистики сразу после масштабной трансформации данных - лучше выполнять сразу с ANALYZE;
  • предотвращения оборота счетчика транзакций (wraparound); когда найдены проблемные таблицы по relfrozenxid – рекомендуется сразу же выполнить по ним VACUUM, можно с агрессивной заморозкой (FREEZE).

Не подходит для:

  • очистки таблицы после постоянного изменения ее назначения в архитектуре приложения, поскольку не освободит дисковое пространство и не уплотнит данные по блокам,
  • очистки таблицы, раздутой в результате пика аномальной нагрузки или отставания автоочистки по той же причине;
  • полной дефрагментации таблицы;
  • применения новых параметров хранения таблицы;
  • постоянной фоновой очистки, периодического обслуживания.

Заморозка (перенос в историю) транзакций

Выше упоминалось, что внутренний счетчик идентификаторов транзакций сервера, XID, напоминает последовательность с атрибутом CYCLE - допускает повторное использование значений. Размерность значений этого счетчика - всего 32 бита. Для транзакций доступно 232 - 3 идентификатора (xid 0, 1 и 2 зарезервированы). Каждая из транзакций со своей точки зрения отличает XID прошлых транзакций от будущих. К примеру, транзакция номер 100, учитывая разрядность счетчика, видит примерно:

  • 2^31 транзакций как прошлые (большинство из них видимы): (2^31 + 101, 2^31 + 102, 2^31 + (2^31 - 1), 3, 4, 5, .., 99);
  • 2^31 транзакций как будущие (невидимы): (101, 102, .., 2^31 + 100).

Здесь удобнее представлять пространство xid не в виде линейки, а в виде кольца, похожего на циферблат часов:

Если часовая стрелка указывает на известную транзакцию, то по часовой стрелке расположатся будущие транзакции, а против - прошлые.

Правило видимости ограничивает количество одновременно отслеживаемых транзакций сервера (по всем базам) числом не 2^32, а 2^31, порядка 2.1 млрд. Система со средней скоростью 10 000 TPS израсходует это количество XID за двое с половиной суток. На первый взгляд, при исчерпании пространства номеров можно просто проверить, что активных транзакций старше (например) суток больше нет, освободить более не активные идентификаторы, передать их новым транзакциям. Однако, при более внимательном взгляде видна следующая проблема.

Дело в том, что в MVCC Pangolin номера транзакций сохранены среди пользовательских данных вместе с версиями записей, именно они указывают срок действия каждой версии.

Вспомним о транзакции номер 100 и перенесемся от ее старта в будущее на двое с половиной суток (для 10К TPS):

Пусть транзакция № 100 примерно 2.5 суток назад добавила запись. Срок действия этой записи (ее первой версии) стартует от xmin = 100. За 2.5 суток запись никто не менял (первую версию не закрывал, новые не добавлял). Сейчас активна транзакция № 2^31+100. Она видит запись, потому что транзакция 100 для нее - в прошлом. Но, если открыть следующую транзакцию № 2^31+101, то она не увидит эту запись, поскольку идентификатор 100 для нее соответствует далекому будущему!

Ситуация на иллюстрации называется transaction wraparound. По-русски – оборот или зацикливание счетчика транзакций.

Проблему же называют transaction wraparound problem.

Сервер при приближении transaction wraparound:

  • за 10 миллионов транзакций до 2^31 выдает в протоколе множество предупреждений вида:

    WARNING: database "mydb" must be vacuumed within 177009986 transactions
    HINT: To avoid a database shutdown, execute a database-wide VACUUM in "mydb".
  • за 1 миллион транзакций до 2^31 переходит в режим «только для чтения» по всем обслуживаемым базам (последний миллион транзакций резервируется для операций по выводу СУБД в обычное состояние).

Чтобы выйти из этого состояния, придется не только подтвердить, что транзакция 100 более не активна. Необходимо отказаться от использования идентификатора 100 в действительных версиях записей всех таблиц. В том числе, пометить упомянутую запись с xmin = 100 как «архивную» или «историческую» – одинаково видимую всем активным сейчас транзакциям. Вместе со всеми записями, которые не менялись с тех пор, как транзакция 100 добавила или обновила их. По всем базам и таблицам.

В остальных транзакциях при просмотре версии по одному признаку «архивная» ясно, что запись относится к «древней истории» базы. Признака достаточно, чтобы сразу же считать запись видимой. Поэтому фактическое начало «срока годности» более не важно, проверять xmin нет смысла.

Если пометить все действительные записи с xmin = 100 как «архивные», то можно:

  • добавить транзакцию 2^31+101 без нарушения правил видимости – теперь она увидит неизменные записи транзакции № 100 по одному признаку «архивная»;
  • считать номер 100 свободным для следующих транзакций.

Признаком архивной версии в Pangolin служат одновременно установленные биты committed и aborted в маске битовых флагов записи t_infomask.

Процесс «переноса в исторический архив» - выставления архивных признаков версий записей – называется заморозкой кортежей (tuple freezing).

Поскольку отдельного процесса обслуживания структур данных (подобия Oracle SMON) нет в Pangolin, обязанность выполнять заморозку возложена на средства очистки - VACUUM и процессы автоочистки. К примеру, заморозка входит в первый из блоков алгоритма VACUUM.

Важно:

  • замораживаемые версии записей не очищаются (это означало бы удаление 99% БД, всей истории данных);
  • подлежащие очистке (xmax > 0, срок годности завершен) - не замораживаются. Поскольку они будут когда-то очищены безопасным для соседних операций образом, их заморозку посчитали бессмысленной.

Подчиняются алгоритмы заморозки четырем параметрам, которые выражены в единицах счетчиков транзакций/мультитранзакций.

ПараметрНазначениеПо умолчанию
vacuum_freeze_min_ageМинимальный возраст транзакции xmin, с которого версию строки можно замораживать. Версии младше этого возраста VACUUM без ключа FREEZE не замораживает. Внутренний порог - до половины autovacuum_freeze_max_age.50 миллионов транзакций
vacuum_freeze_table_ageВозраст транзакции - порога заморозки таблицы pg_class.relfrozenxid, при котором очистка игнорирует карту видимости и проводит заморозку по каждому блоку таблицы (агрессивный режим). Фактический максимум для параметра равен 0.95 * autovacuum_freeze_max_age.150 миллионов транзакций
vacuum_multixact_freeze_min_ageМинимальный возраст мультитранзакции, с которого идентификаторы мультитранзакций будут заменяться новыми идентификаторами транзакций или мультитранзакций. Внутренний порог - до половины autovacuum_multixact_freeze_max_age.5 миллионов мультитранзакций
vacuum_multixact_freeze_table_ageВозраст транзакции - порога заморозки мультитранзакций таблицы pg_class.relminmxid, при котором игнорируется карта видимости и проводится полная (агрессивная) заморозка. Фактический максимум: 0.95% * autovacuum_multixact_freeze_max_age.150 миллионов мультитранзакций

Третья пара параметров - пороги автоматической агрессивной заморозки таблицы autovacuum_freeze_max_age и autovacuum_multixact_freeze_max_age - относится к автоочистке для предотвращения зацикливания счетчиков транзакций и мультитранзакций. Подробнее о них – в разделе Автоочистка для предотвращения зацикливания счетчиков.

Внутренние пороги и максимумы параметров введены для того, чтобы дать шанс автоочистке или командам VACUUM исправить ситуацию по «устаревающей» таблице до того, как ей потребуется принудительная автоочистка.

В Pangolin различаются два режима заморозки, второй из которых существует для коррекции недостатка первого.

«Ленивый» режим – во время очистки

Алгоритм в общем виде:

  1. Выберите старейшую активную транзакцию в системе, OldestXmin. Например, если в системе активны транзакции 51 000 000, 51 000 001, 51 000 002 - выберите OldestXmin = 51 000 000.

  2. Отступите от нее на vacuum_freeze_min_age транзакций в прошлое и получите порог заморозки: freezeLimit_txid = (OldestXmin − vacuum_freeze_min_age).

    Например, для старейшей активной транзакции 51 000 000 и отступа по умолчанию vacuum_freeze_min_age = 50 000 000 транзакций порог такой: freezeLimit_txid = (51 000 000 - 50 000 000) = 1 000 000.

  3. Если блок не помечен в карте видимости как полностью замороженный, то в цикле по записям блока, если xmin < freezeLimit_txid, установите признак заморозки.

  4. Завершите цикл по записям.

  5. Если заморожены все записи в блоке, то установите в карте видимости признак «полностью замороженной» страницы.

Ленивая заморозка выполняется во время обычной очистки, которая пропускает полностью видимые блоки согласно карте видимости. Целые страницы полностью видимых данных постоянно остаются без ее внимания, если не содержат устаревшие версии (dead tuples).

Второй режим заморозки периодически компенсирует этот недостаток.

«Агрессивный» режим – полная заморозка таблицы

В этом режиме выполняется полная заморозка всей таблицы, кроме тех страниц, которые уже полностью заморожены (второй бит в Карте видимости).

После полной заморозки обновляется порог заморозки таблицы - pg_class.relfrozenxid. Сюда вносится новое значение xid самой старой замороженной транзакции таблицы. Обновление поля происходит только в этом режиме (при полной заморозке), поскольку в ленивом режиме неизвестно, какие номера могли бы использоваться в пропущенных страницах.

Во время полных заморозок таблиц постоянно обновляется и порог заморозки содержащей их базы, pg_database.datfrozenxid. Представляет из себя старейший из порогов заморозки таблиц по базе.

Критерий принятия решения о полной заморозке таблицы - слишком большое «отставание» порога relfrozenxid по таблице от последней транзакции.

Примерный алгоритм:

  1. Выберите старейшую активную транзакцию в системе, OldestXmin.
  2. Отступите от нее на vacuum_freeze_min_age транзакций назад и получите целевой порог заморозки таблицы: freezeLimit_txid = (OldestXminvacuum_freeze_min_age)
  3. Сравните фактический порог заморозки таблицы, pg_class.relfrozenxid, с целью: не старше vacuum_freeze_table_age от текущей транзакции. Если pg_class.relfrozenxid < (OldestXminvacuum_freeze_table_age), то в цикле по страницам, не отмеченным в карте видимости как «полностью замороженные» и цикле по версиям записей, если xmin < freezeLimit_txid, то установите признак заморозки.
  4. Завершите цикл по записям.
  5. Если удалось заморозить все записи блока, то установите в карте видимости признак «полностью заморожен».
  6. Завершите цикл по страницам.
  7. Обновите pg_class.relfrozenxid – поместите сюда номер старейшего из замороженных идентификаторов (скорее всего, freezeLimit_txid).
  8. Если минимальный из pg_class.relfrozenxid в результате изменился (сейчас удалось заморозить старейшую таблицу) - обновите pg_database.datfrozenxid.
  9. По возможности, удалите из clog страницы и файлы старше тех, которые содержат pg_database.datfrozenxid.

Поскольку в Ленивом режиме пороги заморозки таблиц и базы не обновляются, время агрессивной заморозки наступает примерно раз в (vacuum_freeze_table_age - vacuum_freeze_min_age) транзакций.

По умолчанию – примерно раз в 100 млн транзакций.

Заморозку можно принудительно выполнить в агрессивном режиме: VACUUM FREEZE. На время этой операции пороги vacuum_freeze_min_age и vacuum_freeze_table_age будут установлены в 0, а в Карте видимости таблицы не будут учитываться признаки видимости страниц – только признаки заморозки.

Использование карты видимости можно отключить и полностью: VACUUM (DISABLE_PAGE_SKIPPING) t. Такой режим полезен для пересоздания разрушенной карты.

Внимание!

В любом из режимов заморозки нельзя заморозить версию записи, открытую любой из активных транзакций по всему кластеру баз Pangolin (серверу).

Примечание

Полностью алгоритмы можно изучить в исходных кодах сервера:

«Вежливый» режим VACUUM

VACUUM по крупной, значительно измененной таблице - длительная операция с плотным использованием дисковых и процессорных ресурсов. Есть возможность запустить очистку так, чтобы она периодически приостанавливалась, пропуская к ресурсам пользовательские сессии.

Этот режим включается установкой параметра vacuum_cost_delay в ненулевое значение. Параметр задает продолжительность паузы после обработки очередной порции данных. Размерность - астрономическое время. Если единица измерения не указана, то понимается как миллисекунды. По умолчанию - 0 (паузы выключены). Теоретически, здесь можно указывать и доли миллисекунд, практически - не все платформы обеспечивают точный микросекундный таймер.

Когда паузы включены, порция данных описывается четырьмя параметрами:

ПараметрНазначениеПо умолчанию
vacuum_cost_page_hitСтоимость просмотра 1 блока, обнаруженного в кеше shared_buffers1
vacuum_cost_page_missСтоимость просмотра 1 блока при чтении его с диска10
vacuum_cost_page_dirtyСтоимость изменения 1 блока, включая вытеснение его из кеша на диск20
vacuum_cost_limitНакопленная стоимость операций, после которой выдерживается пауза200

Для примера рассчитаем максимальную скорость VACUUM в настройках по умолчанию при vacuum_cost_delay = 2ms.

Максимальный размер порции будет достигнут при просмотре таблицы в кеше и составит (200 баллов/1 балл/блок ) = 200 блоков по 8 КБ = 1600 КБ. Пусть обработка порции занимает пренебрежимо малое время, тогда при паузе в 2 мс за секунду будет обработано (1000 мс/2 мс) = 500 порций, что составит 1600 КБ * 500 = 800000 КБ, примерно 781 МБ.

Если все блоки придется прочитать с диска, то порция сократится до 160 КБ (200 баллов/10 баллов за блок), а максимальная скорость снизится до 78.1 МБ в секунду.

Изменение тех блоков, которые уже считаны с диска в память, будет проходить со скоростью до 39.05 МБ/с.

Ручные команды VACUUM очень редко выполняются в «вежливом» режиме, а автоматическая очистка работает в нем почти всегда.

Блокировки во время VACUUM

На уровне таблицы VACUUM без ключа FULL ограничивается блокировкой уровня SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock). В этом режиме таблица защищена от параллельного изменения схемы и запуска любого другого процесса VACUUM. Не допускаются параллельные CREATE INDEXCONCURRENTLY и без), ANALYZE, CREATE STATISTICS, COMMENT, REINDEX CONCURRENTLY, CREATE TRIGGER, большинство ALTER TABLE, REFRESH MATERIALIZED VIEWCONCURRENTLY и без), DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL.

Подробнее - в документации, раздел «13.3. Явные блокировки».

Сессия, которая пытается выполнить DDL по таблице во время VACUUM, будет заблокирована на время выполнения. За блокированным DDL постепенно будут вставать в очередь (блокироваться) и остальные сессии, пытающиеся выполнить по той же таблице DML. Инцидент может развиваться вплоть до переполнения пулов соединений и приостановки приложения. В отличие от ручного VACUUM, автоочистка избегает подобных инцидентов автоматически.

Дополнительно, каждая страница (блок объекта в памяти) на время очистки и заморозки закрывается на легковесную блокировку (LWLock) типа content_lock в исключительном режиме. Эта блокировка гарантирует согласованность служебных структур внутри блока: xmin, xmax и флагов infomask записей (включая признак заморозки), порядка записей, списка указателей на них. Поэтому VACUUM (и очистка, и заморозка) конфликтует на уровне страницы с UPDATE (включая внутриблоковые обновления HOT) и INSERT, запрещая параллельные изменения страницы до завершения ее обработки. Это обычно не вызывает видимых проблем.