Многоверсионность
Заморозка
13
Авторские права
© Postgres Professional, 2016–2022.
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
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_freeze_min_age = 050 000 000
vacuum_freeze_table_age = 150 000 000
autovacuum_freeze_max_age = 200 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 требует перезапуска сервера.
Параметры также можно устанавливать на уровне отдельных таблиц
с помощью параметров хранения. Это имеет смысл делать только
в особенных случаях, когда таблица действительно требует особого
обхождения. Обратите внимание, что имена параметров на уровне
таблиц немного отличаются от имен конфигурационных параметров.
В модуле «Блокировки» рассматриваются т. н. мультитранзакции и
дополнительные параметры настройки заморозки для них.
13
Заморозка вручную
VACUUM FREEZE
принудительная заморозка версий строк с xmin любого возраста
тот же эффект и при VACUUM FULL, CLUSTER
COPY … WITH FREEZE
принудительная заморозка сразу после загрузки
таблица должна быть создана или опустошена в той же транзакции
могут нарушиться правила изоляции транзакции
Иногда бывает удобно управлять заморозкой вручную, а не дожидаться
автоочистки.
Заморозку можно вызвать вручную командой VACUUM FREEZE —
при этом будут заморожены все версии строк, без оглядки на возраст
транзакций (как будто параметр autovacuum_freeze_min_age = 0).
При перестройке таблицы командами VACUUM FULL или CLUSTER все
строки также замораживаются.
Данные можно заморозить и при начальной загрузке с помощью
команды COPY, указав параметр FREEZE. Для этого таблица должна
быть создана (или опустошена командой TRUNCATE) в той же
транзакции, что и COPY. Поскольку для замороженных строк действуют
отдельные правила видимости, такие строки будут видны в снимках
данных других транзакций в нарушение обычных правил изоляции (это
касается транзакций с уровнем Repeatable Read или Serializable), но
обычно это не представляет проблемы. Подробнее такой случай
рассматривается в практике.
14
Итоги
Пространство номеров транзакций закольцовано
Достаточно старые версии строк замораживаются
процессом очистки
Для оптимизации используется карта заморозки
15
Практика
1. Проверьте с помощью расширения pageinspect, что при
использовании команды COPYWITH FREEZE версии
строк действительно замораживаются.
2. Убедитесь, что даже на уровне изоляции Repeatable Read
строки, загруженные командой COPYWITH FREEZE,
оказываются видны в снимке данных.
3. Уменьшив значение параметра autova cuum _freeze_max_age
и отключив автоочистку, воспроизведите ситуацию
принудительного срабатывания автоочистки, выполнив
соответствующее количество транзакций.
Учтите, что срабатывание произойдет не сразу, а при
выполнении ручной очистки какой-нибудь таблицы
(или при перезапуске сервера).
3. Чтобы транзакциям выделялись настоящие (не виртуальные)
номера, в транзакции нужно менять данные.
Можно организовать цикл в bash, в котором вызывать psql с командой
обновления:
psql -c 'UPDATE …'
Другой вариант – использовать для организации цикла PL/pgSQL.
Поскольку внутри серверного кода явно управлять транзакциями
нельзя, придется использовать блок с обработкой исключений. Тогда
при перехвате исключения транзакция будет откатываться к неявной
точке сохранения: фактически начнется новая вложенная транзакция
(см. тему «Страницы и версии строк»).
Третий вариант – использовать утилиту pgbench: