07 Занятие 07: Многоверсионность / Заморозка
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dba2_07_mvcc_freeze.html Archive: DBA2-handouts-16.zip Страниц PDF: 24
Page 1: Многоверсионность / Заморозка
Авторские права
© Postgres Professional, 2016–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Проблема переполнения счетчика транзакций
- Заморозка версий строк и правила видимости
- Настройка автоочистки для выполнения заморозки
- Заморозка вручную
Page 3: Переполнение счетчика
меньшие номера — прошлое, бóльшие — будущее разрядность счетчика — 32 бита, что делать при переполнении?
- транзакция 2
- началась позже
- транзакции 1
- 1
- 2
- прошлое
- будущее
Комментарии преподавателя
Кроме освобождения места в страницах, очистка выполняет также задачу по предотвращению проблем, связанных с переполнением счетчика транзакций.
Под номер транзакции в PostgreSQL выделено 32 бита. Это довольно большое число (около 4 млрд номеров), но при активной работе сервера оно вполне может быть исчерпано. Например при нагрузке 1000 транзакций в секунду это произойдет всего через полтора месяца непрерывной работы.
Но мы говорили о том, что механизм многоверсионности полагается на последовательную нумерацию транзакций — из двух транзакций транзакция с меньшим номером считается начавшейся раньше. Понятно, что нельзя просто обнулить счетчик и продолжить нумерацию заново.
Почему под номер транзакции не выделено 64 бита — ведь это полностью исключило бы проблему? Дело в том, что (как рассматривалось в теме «Страницы и версии строк») в заголовке каждой версии строки хранятся два номера транзакций — xmin и xmax. Заголовок и так достаточно большой, а увеличение разрядности привело бы к его увеличению еще на 8 байт.
Page 4: Нумерация по кругу
{#source-figure-004 caption=“Source slide page 4: Нумерация по кругу”}
пространство номеров транзакций закольцовано половина номеров — прошлое, половина — будущее
Комментарии преподавателя
Поэтому вместо линейной схемы все номера транзакций закольцованы. Для любой транзакции половина номеров «против часовой стрелки» считается принадлежащей прошлому, а половина «по часовой стрелке» — будущему.
Возрастом транзакции называется число транзакций, прошедших с момента ее появления в системе (независимо от того, переходил ли счетчик через ноль или нет).
Page 5: Проблема видимости
{#source-figure-005 caption=“Source slide page 5: Проблема видимости”}
Комментарии преподавателя
В такой закольцованной схеме возникает неприятная ситуация. Транзакция, находившаяся в далеком прошлом (транзакция 1 на слайде), через некоторое время окажется в той половине круга, которая относится к будущему. Это, конечно, нарушит правила видимости и приведет к проблемам.
Page 6: Заморозка версий строк
{#source-figure-006 caption=“Source slide page 6: Заморозка версий строк”}
замороженные версии строк считаются «бесконечно старыми» номер транзакции xmin может быть использован заново
Комментарии преподавателя
Чтобы не допустить путешествий из прошлого в будущее, процесс очистки выполняет еще одну задачу. Он находит достаточно старые и «холодные» версии строк (которые видны во всех снимках и изменение которых уже маловероятно) и специальным образом помечает — «замораживает» — их. Замороженная версия строки считается старше любых обычных данных и всегда видна во всех снимках данных. При этом уже не требуется смотреть на номер транзакции xmin, и этот номер может быть безопасно использован заново. Таким образом, замороженные версии строк всегда остаются в прошлом.
https://postgrespro.ru/docs/postgresql/16/routine-vacuuming#VACUUM-FOR -WRAPAROUND
Page 7: Заморозка версий строк
{#source-figure-007 caption=“Source slide page 7: Заморозка версий строк”}
Еще одна задача процесса очистки
- если вовремя не заморозить версии строк, они окажутся в будущем
- и сервер остановится для предотвращения ошибки
Комментарии преподавателя
Для того чтобы пометить версию строки как замороженную, для транзакции xmin выставляются одновременно оба бита-подсказки — бит фиксации и бит отмены.
Заметим, что транзакцию xmax замораживать не нужно. Ее наличие означает, что данная версия строки больше не актуальна. После того, как она перестанет быть видимой в снимках данных, такая версия строки будет очищена.
Многие источники (включая документацию) упоминают специальный номер FrozenTransactionId = 2, который записывается на место xmin в замороженных версиях. Такая система действовала до версии 9.4, но сейчас заменена на биты-подсказки — это позволяет сохранить в версии строки исходный номер транзакции, что удобно для целей поддержки и отладки. Однако транзакции с номером 2 еще могут встретиться в старых системах, даже обновленных до последних версий.
Важно, чтобы версии строк замораживались вовремя. Если возникнет ситуация, при которой еще не замороженный номер транзакции рискует попасть в будущее, PostgreSQL аварийно остановится. Это возможно в двух случаях: либо транзакция не завершена и, следовательно, не может быть заморожена, либо не сработала очистка.
При запуске сервера транзакция будет автоматически отменена; дальше администратор должен вручную выполнить очистку, и после этого система сможет продолжить работу.
Page 8: Настройка
{#source-figure-008 caption=“Source slide page 8: Настройка”}
vacuum\_freeze\_min\_age
- минимальный возраст, с которого начинается заморозка
Комментарии преподавателя
Заморозкой управляют четыре основных параметра.
Параметр vacuum_freeze_min_age определяет минимальный возраст транзакции xmin, с которого начинается заморозка.
Чем меньше это значение, тем больше может быть накладных расходов. Если строка «горячая» и активно меняется, заморозка ее версий будет пропадать без пользы: уже замороженные версии будут вычищаться, а новые версии придется снова замораживать.
Поэтому более молодые версии строк замораживаются только в тех случаях, когда это точно не добавляет работы, например, если в странице уже требуется заморозка других (более старых) строк или при полной очистке таблицы.
Заметим, что очистка просматривает только страницы, не отмеченные в карте видимости. Если на странице остались только актуальные версии, то очистка не придет в такую страницу и не заморозит их.
В заголовке табличной страницы также имеется признак видимости всех версий строк в ней; очистка использует его вместе с соответствующей отметкой в карте видимости.
Page 9: Настройка
{#source-figure-009 caption=“Source slide page 9: Настройка”}
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 увеличивает накладные расходы на очистку. Но при больших значениях агрессивная заморозка будет выполняться слишком часто, что тоже плохо. Установка этого параметра требует компромисса.
Page 10: Настройка
{#source-figure-010 caption=“Source slide page 10: Настройка”}
autovacuum\_freeze\_max\_age
- при достижении такого возраста заморозка запускается принудительно определяет размер CLOG
- VACUUM (index_cleanup off)
Комментарии преподавателя
Параметр autovacuum_freeze_max_age определяет возраст транзакции, при котором заморозка будет выполняться принудительно. Автоочистка для предотвращения последствий переполнения счетчика транзакций запустится, даже если она отключена параметрами.
Этот параметр также определяет размер структуры CLOG: данные о статусе более старых транзакций точно никогда не понадобятся, поэтому часть файлов из PGDATA/pg_xact может быть удалена.
Если администратор понимает, что автоочистка не успеет заморозить версии строк до переполнения счетчика транзакций, можно воспользоваться ручной очисткой с параметром index_cleanup off. В этом случае индексы не будут очищаться, но за счет этого версии строк в таблицах будут заморожены быстрее.
Page 11: Настройка
{#source-figure-011 caption=“Source slide page 11: Настройка”}
vacuum\_failsafe\_age
- при достижении такого возраста очистка переходит в защитный режим работы
Комментарии преподавателя
Параметр vacuum_failsafe_age управляет включением защитного режима работы очистки, который служит для ускорения заморозки номеров транзакций.
В этом режиме задержки, задаваемые параметрами autovacuum_vacuum_cost_delay и vacuum_cost_delay, будут отменены. Также не будут выполняться некоторые необязательные работы (например очистка индексов). Такие меры позволят очистке быстрее заморозить старые транзакции и перейти в обычный режим работы.
Page 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.
Ряд параметров также можно устанавливать на уровне отдельных таблиц с помощью параметров хранения. Это имеет смысл делать только в особенных случаях, когда таблица действительно требует особого обхождения. Заметьте, что имена параметров на уровне таблиц немного отличаются от имен конфигурационных параметров.
В модуле «Блокировки» рассматриваются т. н. мультитранзакции и дополнительные параметры настройки заморозки для них.
https://postgrespro.ru/docs/postgresql/16/runtime-config-client
https://postgrespro.ru/docs/postgresql/16/runtime-config-autovacuum
Page 13: Заморозка
Установим для демонстрации параметры заморозки. Небольшой возраст транзакции:
Возраст, после которого будет выполняться заморозка всех страниц:
И отключим автоматическую очистку, чтобы запускать ее вручную в нужный момент.
Создадим таблицу с данными. Установим минимальный fillfactor: на каждой странице будет всего две строки.
Создадим представление для наблюдения за битами-подсказками на первых двух страницах таблицы. Сейчас нас интересует только xmin и биты, которые относятся к нему, поскольку версии строк с ненулевым xmax будут очищены. Кроме того, выведем и возраст транзакции xmin.
Для того чтобы заглянуть в карту видимости и заморозки, воспользуемся еще одним расширением:
Page 14: Page 14
Вставляем данные. Сразу выполним очистку, чтобы построить карту видимости.
После очистки обе страницы отмечены в карте видимости (all_visible):
Каков возраст транзакции, создавшей строки?
Возраст равен 1; версии строк с такой транзакцией еще не будут заморожены. Обновим строку на нулевой странице. Новая версия попадет на ту же страницу благодаря небольшому значению fillfactor.
Сейчас нулевая страница уже будет обработана заморозкой: возраст транзакции превышает значение, установленное в vacuum_freeze_min_age; страница изменена и исключена из карты видимости.
Выполняем очистку.
Очистка обработала измененную страницу. У одной версии строки установлены оба бита — это признак заморозки. Другая версия строки слишком молода, однако тоже была заморожена при проходе страницы (это позволило отметить нулевую страницу в карте заморозки):
Page 15: Page 15
Теперь обе страницы отмечены в карте видимости (все версии строк на них актуальны). Очистка теперь не будет обрабатывать ни одну из этих страниц, и незамороженные версии строк на первой странице так и останутся незамороженными.
Именно для такого случая и требуется параметр vacuum_freeze_table_age, определяющий, в какой момент нужно просмотреть страницы, отмеченные в карте видимости, если они не отмечены в карте заморозки. Для каждой таблицы сохраняется наибольший номер транзакции, для которого все версии строк с меньшими номерами xmin гарантированно заморожены. Ее возраст и сравнивается со значением параметра.
Сымитируем выполнение еще одной транзакции, чтобы возраст relfrozenxid таблицы достиг значения параметра vacuum_freeze_table_age.
Теперь, поскольку гарантированно была проверена вся таблица, номер замороженной транзакции можно увеличить — мы уверены, что в страницах не осталось более старой незамороженной транзакции.
Вот что получилось в страницах:
Page 16: Page 16
Обе страницы теперь отмечены в карте заморозки.
Номер последней замороженной транзакции есть и на уровне всей БД:
Он устанавливается в минимальное значение из relfrozenxid всех таблиц этой БД. Если возраст datfrozenxid превысит значение параметра autovacuum_freeze_max_age, автоочистка будет запущена принудительно.
Page 17: Заморозка вручную
VACUUM
- заморозка версий строк по возрасту в соответствии с настройками
- VACUUM FREEZE
- принудительная заморозка версий строк с xmin любого возраста
- тот же эффект и при VACUUM FULL, CLUSTER
- COPY … WITH FREEZE
- принудительная заморозка сразу после загрузки
- таблица должна быть создана или опустошена в той же транзакции
- могут нарушиться правила изоляции транзакции
Комментарии преподавателя
Несмотря на то, что во время работы автоочистки при необходимости выполняется и заморозка, иногда бывает удобно управлять заморозкой вручную.
Команда VACUUM, как и автоочистка, выполнит заморозку в соответствии с настройками.
Если выполнить команду VACUUM FREEZE, будут заморожены все версии строк без оглядки на возраст транзакций (как будто параметры vacuum_freeze_min_age и vacuum_freeze_table_age равны нулю). При перестройке таблицы командами VACUUM FULL или CLUSTER все строки также замораживаются.
https://postgrespro.ru/docs/postgresql/16/sql-vacuum
Данные можно заморозить и при начальной загрузке с помощью команды COPY, указав параметр FREEZE. Для этого таблица должна быть создана (или опустошена командой TRUNCATE) в той же транзакции, что и COPY. Поскольку для замороженных строк действуют отдельные правила видимости, такие строки будут видны в снимках данных других транзакций в нарушение обычных правил изоляции (для транзакций с уровнем Repeatable Read или Serializable), но обычно это не представляет проблемы. Подробнее такой случай рассматривается в практике.
https://postgrespro.ru/docs/postgresql/16/sql-copy
Текущая реализация такой заморозки для таблиц с TOAST строит карту видимости только для основной части таблицы, но не для TOAST. Это приводит к дополнительному проходу по всем страницам при последующей очистке.
Page 18: Итоги
Пространство номеров транзакций закольцовано
- Достаточно старые версии строк замораживаются процессом очистки
- Для оптимизации используется карта заморозки
Page 19: Практика
- Проверьте с помощью расширения pageinspect, что при использовании команды COPY … WITH FREEZE версии строк действительно замораживаются.
- Убедитесь, что даже на уровне изоляции Repeatable Read строки, загруженные командой COPY … WITH FREEZE, оказываются видны в снимке данных.
- Уменьшив значение параметра autovacuum_freeze_max_age и отключив автоочистку, воспроизведите ситуацию принудительного срабатывания автоочистки, выполнив соответствующее количество транзакций. Учтите, что срабатывание произойдет не сразу, а при выполнении ручной очистки какой-нибудь таблицы (или при перезапуске сервера).
Комментарии преподавателя
- Чтобы транзакциям выделялись постоянные (не виртуальные) номера, в транзакции нужно менять данные.
Можно организовать цикл в bash, в котором вызывать psql с командой обновления: psql -c ‘UPDATE …’
Другой вариант — использовать для организации цикла PL/pgSQL. Для этого можно создать процедуру, выполняющую фиксацию транзакции или использовать блок с обработкой исключений. При перехвате исключения транзакция будет откатываться к неявной точке сохранения: фактически начнется новая вложенная транзакция (см. тему «Страницы и версии строк»).
Третий вариант — использовать утилиту pgbench:
https://postgrespro.ru/docs/postgresql/16/pgbench
Page 20: 1. Заморозка при COPY WITH FREEZE
Создаем таблицу и загружаем несколько строк в одной и той же транзакции:
Проверяем версии строк:
2. COPY WITH FREEZE и изоляция
В другом сеансе начнем транзакцию с уровнем изоляции Repeatable Read.
Page 21: Page 21
Обратите внимание, что эта транзакция не должна обращаться к таблице t. Теперь опустошим таблицу и загрузим в нее новые строки в одной транзакции. Если бы параллельная транзакция прочитала содержимое t, команда TRUNCATE ожидала бы ее завершения.
Теперь параллельная транзакция видит новые данные, хотя это и нарушает изоляцию:
3. Аварийное срабатывание автоочистки
Предварительно заморозим все транзакции во всех базах. Для этого удобно воспользоваться командой vacuumdb:
Максимальный возраст незамороженных транзакций по всем БД:
Отключаем автоочистку.
Page 22: Page 22
Уменьшаем значения параметров:
Требуется перезагрузка сервера.
Получить большое количество транзакций можно разными способами; например, можно воспользоваться утилитой pgbench. Попросим ее инициализировать свои таблицы и выполнить 100000 транзакций.
Выполнение ста тысяч транзакций может занять заметное время, ключ –protocol=prepared немного ускоряет работу за счет использования подготовленных операторов:
Page 23: Page 23
Видно, что возраст незамороженных транзакций превышает установленное пороговое значение (100000):
Теперь при выполнении команды VACUUM для любой таблицы будет запущен процесс автоочистки.
Среди процессов появился autovacuum worker:
И через некоторое время транзакции окажутся замороженными: