Многоверсионность
Заморозка
16
Авторские права
© Postgres Professional, 2016–2025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Проблема переполнения счетчика транзакций
Заморозка версий строк и правила видимости
Настройка автоочистки для выполнения заморозки
Заморозка вручную
3
Переполнение счетчика
меньшие номера — прошлое, бóльшие — будущее
разрядность счетчика — 32 бита, что делать при переполнении?
1
2
транзакция 2
началась позже
транзакции 1
прошлое будущее
Кроме освобождения места в страницах, очистка выполняет также
задачу по предотвращению проблем, связанных с переполнением
счетчика транзакций.
Под номер транзакции в PostgreSQL выделено 32 бита. Это довольно
большое число (около 4 млрд номеров), но при активной работе
сервера оно вполне может быть исчерпано. Например при нагрузке
1000 транзакций в секунду это произойдет всего через полтора месяца
непрерывной работы.
Но мы говорили о том, что механизм многоверсионности полагается на
последовательную нумерацию транзакций — из двух транзакций
транзакция с меньшим номером считается начавшейся раньше.
Понятно, что нельзя просто обнулить счетчик и продолжить нумерацию
заново.
Почему под номер транзакции не выделено 64 бита — ведь это
полностью исключило бы проблему? Дело в том, что (как
рассматривалось в теме «Страницы и версии строк») в заголовке
каждой версии строки хранятся два номера транзакций — xmin и xmax.
Заголовок и так достаточно большой, а увеличение разрядности
привело бы к его увеличению еще на 8 байт.
4
пространство номеров транзакций закольцовано
половина номеров — прошлое, половина — будущее
Нумерация по кругу
1
1
2
возраст транзакции
Поэтому вместо линейной схемы все номера транзакций закольцованы.
Для любой транзакции половина номеров «против часовой стрелки»
считается принадлежащей прошлому, а половина «по часовой
стрелке» — будущему.
Возрастом транзакции называется число транзакций, прошедших
с момента ее появления в системе (независимо от того, переходил ли
счетчик через ноль или нет).
5
Проблема видимости
1
1
2
2
3
3
4
транзакция 1
находится далеко
в прошлом
транзакция 1
внезапно оказалась
в будущем?
В такой закольцованной схеме возникает неприятная ситуация.
Транзакция, находившаяся в далеком прошлом (транзакция 1
на слайде), через некоторое время окажется в той половине круга,
которая относится к будущему. Это, конечно, нарушит правила
видимости и приведет к проблемам.
6
замороженные версии строк считаются «бесконечно старыми»
номер транзакции xmin может быть использован заново
Заморозка версий строк
2
3
3
4
Чтобы не допустить путешествий из прошлого в будущее, процесс
очистки выполняет еще одну задачу. Он находит достаточно старые и
«холодные» версии строк (которые видны во всех снимках и изменение
которых уже маловероятно) и специальным образом помечает —
«замораживает» — их. Замороженная версия строки считается старше
любых обычных данных и всегда видна во всех снимках данных. При
этом уже не требуется смотреть на номер транзакции xmin, и этот
номер может быть безопасно использован заново. Таким образом,
замороженные версии строк всегда остаются в прошлом.
7
указатель
Еще одна задача процесса очистки
если вовремя не заморозить версии строк, они окажутся в будущем
и сервер остановится для предотвращения ошибки
Заморозка версий строк
xmin committed
xmin aborted
xmax committed
xmax aborted
данные
100 42,FOO
(0,1) normal
0 t t
признак
заморозки
на номер
больше не
смотрим
xmaxxmin
Для того чтобы пометить версию строки как замороженную, для
транзакции xmin выставляются одновременно оба бита-подсказки —
бит фиксации и бит отмены.
Заметим, что транзакцию xmax замораживать не нужно. Ее наличие
означает, что данная версия строки больше не актуальна. После того,
как она перестанет быть видимой в снимках данных, такая версия
строки будет очищена.
Многие источники (включая документацию) упоминают специальный
номер FrozenTransactionId = 2, который записывается на место xmin
в замороженных версиях. Такая система действовала до версии 9.4,
но сейчас заменена на биты-подсказки — это позволяет сохранить
в версии строки исходный номер транзакции, что удобно для целей
поддержки и отладки. Однако транзакции с номером 2 еще могут
встретиться в старых системах, даже обновленных до последних
версий.
Важно, чтобы версии строк замораживались вовремя. Если возникнет
ситуация, при которой еще не замороженный номер транзакции рискует
попасть в будущее, PostgreSQL аварийно остановится. Это возможно
в двух случаях: либо транзакция не завершена и, следовательно, не
может быть заморожена, либо не сработала очистка.
При запуске сервера транзакция будет автоматически отменена;
дальше администратор должен вручную выполнить очистку, и после
этого система сможет продолжить работу.
8
vacuum_freeze_min_age
минимальный возраст,
с которого начинается заморозка
Настройка
vacuum_freeze_min_age
Заморозкой управляют четыре основных параметра.
Параметр vacuum_freeze_min_age определяет минимальный возраст
транзакции xmin, с которого начинается заморозка.
Чем меньше это значение, тем больше может быть накладных
расходов. Если строка «горячая» и активно меняется, заморозка ее
версий будет пропадать без пользы: уже замороженные версии будут
вычищаться, а новые версии придется снова замораживать.
Поэтому более молодые версии строк замораживаются только в тех
случаях, когда это точно не добавляет работы, например, если
в странице уже требуется заморозка других (более старых) строк
или при полной очистке таблицы.
Заметим, что очистка просматривает только страницы, не отмеченные
в карте видимости. Если на странице остались только актуальные
версии, то очистка не придет в такую страницу и не заморозит их.
В заголовке табличной страницы также имеется признак видимости
всех версий строк в ней; очистка использует его вместе
с соответствующей отметкой в карте видимости.
9
vacuum_freeze_table_age
при достижении такого возраста
замораживаются версии строк на всех страницах
(«агрессивная» заморозка)
для ускорения используется карта заморозки
Настройка
vacuum_freeze_min_age
vacuum_freeze_table_age
Параметр vacuum_freeze_table_age определяет возраст транзакции,
при котором пора выполнять заморозку версий строк на всех страницах
таблицы. Такая заморозка называется «агрессивной».
Для каждой таблицы хранится номер транзакции (pg_class.relfrozenxid),
для которого известно, что в версиях строк не осталось более старых
незамороженных номеров транзакций. Возраст этой транзакции
и сравнивается со значением параметра.
Чтобы не просматривать всю таблицу целиком, вместе с картой
видимости ведется карта заморозки. В ней отмечены страницы,
в которых заморожены все версии строк. Такие страницы при заморозке
можно пропускать.
Даже в агрессивном режиме все версии строк с транзакциями младше
vacuum_freeze_min_age не замораживаются, поэтому после заморозки
новый возраст транзакции relfrozenxid будет равен не нулю,
а vacuum_freeze_min_age. Таким образом, заморозка всех страниц
выполняется раз в (vacuum_freeze_table_agevacuum_freeze_min_age)
транзакций.
Мы уже говорили, что слишком маленькое значение параметра
vacuum_freeze_min_age увеличивает накладные расходы на очистку.
Но при больших значениях агрессивная заморозка будет выполняться
слишком часто, что тоже плохо. Установка этого параметра требует
компромисса.
10
autovacuum_freeze_max_age
при достижении такого возраста
заморозка запускается принудительно
определяет размер CLOG
VACUUM (index_cleanup off)
Настройка
vacuum_freeze_min_age
vacuum_freeze_table_age
autovacuum_freeze_max_age
Параметр autovacuum_freeze_max_age определяет возраст
транзакции, при котором заморозка будет выполняться принудительно.
Автоочистка для предотвращения последствий переполнения счетчика
транзакций запустится, даже если она отключена параметрами.
Этот параметр также определяет размер структуры CLOG: данные
о статусе более старых транзакций точно никогда не понадобятся,
поэтому часть файлов из PGDATA/pg_xact может быть удалена.
Если администратор понимает, что автоочистка не успеет заморозить
версии строк до переполнения счетчика транзакций, можно
воспользоваться ручной очисткой с параметром index_cleanup off.
В этом случае индексы не будут очищаться, но за счет этого версии
строк в таблицах будут заморожены быстрее.
11
vacuum_failsafe_age
при достижении такого возраста
очистка переходит в защитный режим работы
Настройка
vacuum_freeze_min_age
autovacuum_freeze_max_age
vacuum_failsafe_age
vacuum_freeze_table_age
Параметр vacuum_failsafe_age управляет включением защитного
режима работы очистки, который служит для ускорения заморозки
номеров транзакций.
В этом режиме задержки, задаваемые параметрами
autovacuum_vacuum_cost_delay и vacuum_cost_delay, будут отменены.
Также не будут выполняться некоторые необязательные работы
(например очистка индексов). Такие меры позволят очистке быстрее
заморозить старые транзакции и перейти в обычный режим работы.
12
Настройка
Конфигурационные параметры
vacuum_freeze_min_age = 0 50 000 000
vacuum_freeze_table_age = 150 000 000
autovacuum_freeze_max_age = 200 000 000
vacuum_failsafe_age = 1 600 000 000
Параметры хранения таблиц
autovacuum_freeze_min_age
toast.autovacuum_freeze_min_age
autovacuum_freeze_table_age
toast.autovacuum_freeze_table_age
autovacuum_freeze_max_age
toast.autovacuum_freeze_max_age
?
Значения по умолчанию довольно консервативны. Предел для
autovacuum_freeze_max_age – порядка 2 млрд транзакций,
а используется значение в 10 раз меньшее. Можно увеличить значения
vacuum_freeze_table_age и autovacuum_freeze_max_age для
уменьшения накладных расходов, но важно понимать, что если по
каким-то причинам (например, из-за активной транзакции) автоочистка
вовремя не справится с заморозкой, у администратора останется мало
времени для принятия мер. Заметьте, что изменение параметра
autovacuum_freeze_max_age требует перезапуска сервера.
Значение по умолчанию vacuum_failsafe_age значительно больше, чем
autovacuum_freeze_max_age, и если до исчерпания номеров останется
мало времени, защитный режим ускорит заморозку.
Отметим, что сервер может скорректировать установленные значения
параметров vacuum_freeze_min_age, vacuum_freeze_table_age и
vacuum_failsafe_age исходя из значения autovacuum_freeze_max_age.
Ряд параметров также можно устанавливать на уровне отдельных
таблиц с помощью параметров хранения. Это имеет смысл делать
только в особенных случаях, когда таблица действительно требует
особого обхождения. Заметьте, что имена параметров на уровне таблиц
немного отличаются от имен конфигурационных параметров.
В модуле «Блокировки» рассматриваются т. н. мультитранзакции
и дополнительные параметры настройки заморозки для них.
14
Заморозка вручную
VACUUM
заморозка версий строк по возрасту в соответствии с настройками
VACUUM FREEZE
принудительная заморозка версий строк с xmin любого возраста
тот же эффект и при VACUUM FULL, CLUSTER
COPY … WITH FREEZE
принудительная заморозка сразу после загрузки
таблица должна быть создана или опустошена в той же транзакции
могут нарушиться правила изоляции транзакции
Несмотря на то, что во время работы автоочистки при необходимости
выполняется и заморозка, иногда бывает удобно управлять заморозкой
вручную.
Команда VACUUM, как и автоочистка, выполнит заморозку
в соответствии с настройками.
Если выполнить команду VACUUM FREEZE, будут заморожены все
версии строк без оглядки на возраст транзакций (как будто параметры
vacuum_freeze_min_age и vacuum_freeze_table_age равны нулю).
При перестройке таблицы командами VACUUM FULL или CLUSTER
все строки также замораживаются.
Данные можно заморозить и при начальной загрузке с помощью
команды COPY, указав параметр FREEZE. Для этого таблица должна
быть создана (или опустошена командой TRUNCATE) в той же
транзакции, что и COPY. Поскольку для замороженных строк действуют
отдельные правила видимости, такие строки будут видны в снимках
данных других транзакций в нарушение обычных правил изоляции (для
транзакций с уровнем Repeatable Read или Serializable), но обычно это
не представляет проблемы. Подробнее такой случай рассматривается
в практике.
Текущая реализация такой заморозки для таблиц с TOAST строит карту
видимости только для основной части таблицы, но не для TOAST. Это
приводит к дополнительному проходу по всем страницам при
последующей очистке.
15
Итоги
Пространство номеров транзакций закольцовано
Достаточно старые версии строк замораживаются
процессом очистки
Для оптимизации используется карта заморозки
16
Практика
1. Проверьте с помощью расширения pageinspect, что при
использовании команды COPYWITH FREEZE версии
строк действительно замораживаются.
2. Убедитесь, что даже на уровне изоляции Repeatable Read
строки, загруженные командой COPYWITH FREEZE,
оказываются видны в снимке данных.
3. Уменьшив значение параметра autovacuum_freeze_max_age
и отключив автоочистку, воспроизведите ситуацию
принудительного срабатывания автоочистки, выполнив
соответствующее количество транзакций.
Учтите, что срабатывание произойдет не сразу, а при
выполнении ручной очистки какой-нибудь таблицы
(или при перезапуске сервера).
3. Чтобы транзакциям выделялись постоянные (не виртуальные)
номера, в транзакции нужно менять данные.
Можно организовать цикл в bash, в котором вызывать psql с командой
обновления:
psql -c 'UPDATE ...'
Другой вариант — использовать для организации цикла PL/pgSQL.
Для этого можно создать процедуру, выполняющую фиксацию
транзакции или использовать блок с обработкой исключений.
При перехвате исключения транзакция будет откатываться к неявной
точке сохранения: фактически начнется новая вложенная транзакция
(см. тему «Страницы и версии строк»).
Третий вариант — использовать утилиту pgbench: