Postgres Professional corporate blog
PostgreSQL
SQL
June 6

MVCC-7. Автоочистка

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

Затем мы рассмотрели внутристраничную очистку (и HOT-обновления), обычную очистку, ну а сегодня посмотрим на автоматическую очистку.

Автоочистка (autovacuum)


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

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

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

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

Автоматическая очистка — как раз тот самый механизм, который позволяет запускать очистку в зависимости от активности изменений в таблицах.

При включенной автоочистке (конфигурационный параметр autovacuum) в системе всегда присутствует процесс autovacuum launcher, который планирует работу, а реальной очисткой занимаются рабочие процессы autovacuum worker, несколько экземпляров которых могут работать параллельно.

Процесс autovacuum launcher составляет список баз данных, в которых есть какая-либо активность. Активность определяется по статистике, а чтобы она собиралась, должен быть установлен параметр track_counts. Никогда не выключайте autovacuum и track_counts, иначе автоочистка не будет работать.

Раз в autovacuum_naptime процесс autovacuum launcher запускает (с помощью процесса postmaster) рабочий процесс для каждой БД из списка. Иными словами, если в базе данных есть какая-то активность, то рабочие процессы будет приходить в нее с интервалом autovacuum_naptime. Для этого, если имеется несколько активных БД (N штук), то рабочие процессы запускаются в N раз чаще, чем autovacuum_naptime. Но при этом общее количество одновременно работающих рабочих процессов ограничено параметром autovacuum_max_workers.

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

  • всех таблиц, материализованных представлений и toast-таблиц, требующих очистки,
  • всех таблиц и материализованных представлений, требующих анализа (toast-таблицы не анализируются, потому что обращение к ним всегда происходит по индексу).

Дальше рабочий процесс по очереди очищает и/или анализирует отобранные объекты и по окончании очистки завершается.

Если процесс не успел выполнить всю намеченную работу за autovacuum_naptime, процесс autovacuum launcher пошлет в ту же базу данных еще один рабочий процесс, и они будут работать вместе. «Вместе» просто означает, что второй процесс построит свой список таблиц и пойдет по нему. Таким образом, параллельно будут обрабатываться разные таблицы, но на уровне одной таблицы параллелизма нет — если один из рабочих процессов уже занимается таблицей, другой пропустит ее и пойдет дальше.

Обсуждение необходимости параллельной обработки идет уже давно, но патч пока не принят.

Теперь разберемся детальнее, что значит «требует очистки» и «требует анализа».

Какие таблицы требуют очистки


Считается, что очистка необходима, если число «мертвых», то есть неактуальных, версий строк превышает установленное пороговое значение. Число мертвых версий постоянно собирается коллектором статистики и хранится в таблице pg_stat_all_tables. А порог задается двумя параметрами:

  • autovacuum_vacuum_threshold определяет абсолютное значение (в штуках),
  • autovacuum_vacuum_scale_factor определяет долю строк в таблице.

Итоговая формула такая: очистка требуется, если pg_stat_all_tables.n_dead_tup >= autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * pg_class.reltupes.

Настройки по умолчанию устанавливают autovacuum_vacuum_threshold = 50 и
autovacuum_vacuum_scale_factor = 0.2. Главный параметр здесь, конечно, autovacuum_vacuum_scale_factor — именно он важен для больших таблиц (а именно с ними связаны возможные проблемы). Значение 20% представляется сильно завышенным, скорее всего его потребуется существенно уменьшить.

Оптимальные значения параметров могут отличаться для разных таблиц в зависимости от их размера и характера изменений. Имеет смысл установить в целом адекватные значения, и — при необходимости — настроить специальным образом параметры на уровне некоторых таблиц с помощью параметров хранения:

  • autovacuum_vacuum_threshold и toast.autovacuum_vacuum_threshold,
  • autovacuum_vacuum_scale_factor и toast.autovacuum_vacuum_scale_factor.

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

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

  • autovacuum_enabled и toast.autovacuum_enabled.

Например, в прошлый раз мы создавали таблицу vac с отключенной автоочисткой, чтобы — для целей демонстрации — управлять очисткой вручную. Параметр хранения можно изменить следующим образом:

=> ALTER TABLE vac SET (autovacuum_enabled = off);

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

=> CREATE FUNCTION get_value(param text, reloptions text[], relkind "char")
RETURNS float
AS $$
  SELECT coalesce(
    -- если параметр хранения задан, то берем его
    (SELECT option_value
     FROM   pg_options_to_table(reloptions)
     WHERE  option_name = CASE
              -- для toast-таблиц имя параметра отличается
              WHEN relkind = 't' THEN 'toast.' ELSE ''
            END || param
    ),
    -- иначе берем значение конфигурационного параметра
    current_setting(param)
  )::float;
$$ LANGUAGE sql;

А вот и представление:

=> CREATE VIEW need_vacuum AS
  SELECT st.schemaname || '.' || st.relname tablename,
         st.n_dead_tup dead_tup,
         get_value('autovacuum_vacuum_threshold', c.reloptions, c.relkind) +
         get_value('autovacuum_vacuum_scale_factor', c.reloptions, c.relkind) * c.reltuples
         max_dead_tup,
         st.last_autovacuum
  FROM   pg_stat_all_tables st,
         pg_class c
  WHERE  c.oid = st.relid
  AND    c.relkind IN ('r','m','t');

Какие таблицы требуют анализа


С автоанализом дело обстоит примерно так же. Считается, что анализа требуют ту таблицы, у которых число измененных (с момента прошлого анализа) версий строк превышает пороговое значение, заданное двумя аналогичными параметрами: pg_stat_all_tables.n_mod_since_analyze >= autovacuum_analyze_threshold + autovacuum_analyze_scale_factor * pg_class.reltupes.

Умолчательные настройки автоанализа немного отличаются: autovacuum_analyze_threshold = 50 и autovacuum_analyze_scale_factor = 0.1. Их также можно определить на уровне параметров хранения отдельных таблиц:

  • autovacuum_analyze_threshold
  • autovacuum_analyze_scale_factor

Поскольку toast-таблицы не анализируются, соответствующих параметров для них нет.

Создадим представление и для анализа:

=> CREATE VIEW need_analyze AS
  SELECT st.schemaname || '.' || st.relname tablename,
         st.n_mod_since_analyze mod_tup,
         get_value('autovacuum_analyze_threshold', c.reloptions, c.relkind) +
         get_value('autovacuum_analyze_scale_factor', c.reloptions, c.relkind) * c.reltuples
         max_mod_tup,
         st.last_autoanalyze
  FROM   pg_stat_all_tables st,
         pg_class c
  WHERE  c.oid = st.relid
  AND    c.relkind IN ('r','m');

Пример


Для экспериментов установим такие значение параметров:

=> ALTER SYSTEM SET autovacuum_naptime = ‘1s’; -- чтобы долго не ждать
=> ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.03;  -- 3%
=> ALTER SYSTEM SET autovacuum_vacuum_threshold = 0;
=> ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.02; -- 2%
=> ALTER SYSTEM SET autovacuum_analyze_threshold = 0;
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

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

=> CREATE TABLE autovac(
  id serial,
  s char(100)
) WITH (autovacuum_enabled = off);
=> INSERT INTO autovac SELECT g.id,'A' FROM generate_series(1,1000) g(id);

Вот что покажет наше представление для очистки:

=> SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
   tablename    | dead_tup | max_dead_tup | last_autovacuum 
----------------+----------+--------------+-----------------
 public.autovac |        0 |            0 | 
(1 row)

Тут есть два момента, на которые стоит обратить внимание. Во-первых, max_dead_tup = 0, хотя 3% от 1000 строк составляет 30 строк. Дело в том, что у нас еще нет статистики по таблице, поскольку INSERT сам по себе ее не обновляет. Пока наша таблица не будет проанализирована, нули так и останутся, поскольку pg_class.reltuples = 0. Однако заглянем во второе представление для анализа:

=> SELECT * FROM need_analyze WHERE tablename = 'public.autovac';
   tablename    | mod_tup | max_mod_tup | last_autoanalyze 
----------------+---------+-------------+------------------
 public.autovac |    1000 |           0 | 
(1 row)

Поскольку в таблице изменилось (добавилось) 1000 строк, и это больше нуля, должен сработать автоанализ. Проверим это:

=> ALTER TABLE autovac SET (autovacuum_enabled = on);

После небольшой паузы видим, что таблица проанализирована и вместо нулей в max_mod_tup мы видим корректные 20 строк:

=> SELECT * FROM need_analyze WHERE tablename = 'public.autovac';
   tablename    | mod_tup | max_mod_tup |       last_autoanalyze        
----------------+---------+-------------+-------------------------------
 public.autovac |       0 |          20 | 2019-05-21 11:59:48.465987+03
(1 row)

=> SELECT reltuples, relpages FROM pg_class WHERE relname = 'autovac';
 reltuples | relpages 
-----------+----------
      1000 |       17
(1 row)

Вернемся к автоочистке:

=> SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
   tablename    | dead_tup | max_dead_tup | last_autovacuum
----------------+----------+--------------+-----------------
 public.autovac |        0 |           30 |
(1 row)

Max_dead_tup, как мы видим, уже исправился. Второй момент, на который надо обратить внимание — dead_tup = 0. Статистика показывает, что в таблице нет мертвых версий строк… и это правда. Очищать в нашей таблице пока нечего. Так и любая таблица, использующаяся только в режиме добавления данных (append-only), не будет очищаться и, стало быть, для нее не будет обновляться карта видимости. А это делает невозможным использование исключительно индексного сканирования (index-only scan).

(В следующий раз мы увидим, что очистка рано или поздно придет и в append-only-таблицу, но происходить это будет очень редко.)

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

Теперь снова отключим автоочистку и обновим 31 строку — на один больше, чем пороговое значение.

=> ALTER TABLE autovac SET (autovacuum_enabled = off);
=> UPDATE autovac SET s = 'B' WHERE id <= 31;
=> SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
   tablename    | dead_tup | max_dead_tup | last_autovacuum 
----------------+----------+--------------+-----------------
 public.autovac |       31 |           30 | 
(1 row)

Теперь условие срабатывания автоочистки выполняется. Включим автоочистку и после непродолжительной паузы увидим, что таблица обработана:

=> ALTER TABLE autovac SET (autovacuum_enabled = on);
=> SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
   tablename    | dead_tup | max_dead_tup |        last_autovacuum        
----------------+----------+--------------+-------------------------------
 public.autovac |        0 |           30 | 2019-05-21 11:59:52.554571+03
(1 row)

Регулирование нагрузки


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

Регулирование для обычной очистки


Чтобы иметь возможность управлять интенсивностью очистки и, следовательно, ее влиянием на систему, процесс чередует работу и ожидание. Очистка выполняет примерно vacuum_cost_limit условных единиц работы, а затем засыпает на vacuum_cost_delay мс.

Настройки по умолчанию устанавливают vacuum_cost_limit = 200, vacuum_cost_delay = 0. Последний нолик фактически означает, что (обычная) очистка не засыпает, так что конкретное значение vacuum_cost_limit не играет никакой роли. Это сделано из соображения, что если уж администратору пришлось запускать VACUUM вручную, то он, вероятно, хочет выполнить очистку как можно быстрее.

Тем не менее, если все-таки установить время сна, то указанный в vacuum_cost_limit объем работы будет складываться из стоимостей работы со страницами в буферном кэше. Каждое обращение к странице оценивается следующим образом:

  • если страница нашлась в буферном кэше, то vacuum_cost_page_hit = 1;
  • если не нашлась, то vacuum_cost_page_miss = 10;
  • если не нашлась, да еще пришлось вытеснять из буфера грязную страницу, то vacuum_cost_page_dirty = 20.

То есть с настройками vacuum_cost_limit по умолчанию, за один присест могут быть обработаны 200 страниц из кэша, или 20 страниц с диска, или 10 страниц с вытеснением. Понятно, что это довольно условные цифры, но подбирать их точнее нет смысла.

Регулирование для автоочистки


Регулирование нагрузки при автоматической очистке работает так же, как и для обычной. Но чтобы очистка, запускаемая вручную, и автоочистка могли работать с разной интенсивностью, для автоочистки сделаны собственные параметры: autovacuum_vacuum_cost_limit и autovacuum_vacuum_cost_delay. Если эти параметры принимают значение -1, то используется значение из vacuum_cost_limit и/или vacuum_cost_delay.

По умолчанию autovacuum_vacuum_cost_limit = -1 (то есть используется значение vacuum_cost_limit = 200) и autovacuum_vacuum_cost_delay = 20ms. На современной аппаратуре с этими цифрами автоочистка будет работать очень и очень медленно.

В версии 12 значение autovacuum_vacuum_cost_delay будет уменьшено до 2ms, что можно считать более подходящим первым приближением.

Кроме того следует учитывать, что предел, устанавливаемый этими параметрами, общий для всех рабочих процессов. Иными словами, при изменении числа одновременно работающих рабочих процессов общая нагрузка будет оставаться постоянной. Поэтому, если стоит задача увеличить производительность автоочистки, то при добавлении рабочих процессов стоит увеличить и autovacuum_vacuum_cost_limit.

Использование памяти и мониторинг


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

Автоочистка поступает абсолютно так же. Но одновременно работающих процессов может быть много, если установить autovacuum_max_workers в большое значение. К тому же вся память выделяется сразу и полностью, а не по необходимости. Поэтому для рабочего процесса автоочистки можно установить собственное ограничение с помощью параметра autovacuum_work_mem. По умолчанию этот параметр равен -1, то есть не используется.

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

Мы видели, что для мониторинга очистки можно использовать параметр VERBOSE (но его нельзя указать для автоочистки) или представление pg_stat_progress_vacuum (но оно показывает только текущую информацию). Поэтому основной способ мониторинга автоочистки — параметр log_autovacuum_min_duration, который выводит информацию в журнал сообщений сервера. По умолчанию он выключен (установлен в -1). Есть резон включить этот параметр (при значении 0 будет выводиться информация о всех запусках автоочистки) и наблюдать за цифрами.

Вот как выглядит выводимая информация:

=> ALTER SYSTEM SET log_autovacuum_min_duration = 0;
=> SELECT pg_reload_conf();
 pg_reload_conf 
----------------
 t
(1 row)

=> UPDATE autovac SET s = 'C' WHERE id <= 31;

student$ tail -n 7 /var/log/postgresql/postgresql-11-main.log
2019-05-21 11:59:55.675 MSK [9737] LOG:  automatic vacuum of table "test.public.autovac": index scans: 0
	pages: 0 removed, 18 remain, 0 skipped due to pins, 0 skipped frozen
	tuples: 31 removed, 1000 remain, 0 are dead but not yet removable, oldest xmin: 4040
	buffer usage: 78 hits, 0 misses, 0 dirtied
	avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
	system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
2019-05-21 11:59:55.676 MSK [9737] LOG:  automatic analyze of table "test.public.autovac" system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s

Все необходимые сведения здесь присутствуют.

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

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

Продолжение.

+15
2.6k 39
Comments 10
Top of the day