Многоверсионность
Заморозка
16
Авторские права
© Postgres Professional, 2016–2025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания 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_age vacuum_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 полноценно
обрабатывает только основную часть таблицы, а в карте видимости
признак видимости всех строк не проставляется. А это означает
дополнительный проход по всем страницам при последующей очистке.
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: