linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть III — Очистка: vacuum, freeze, wraparound

$ глава 15 · 60 минут

Заморозка и переполнение счётчика (wraparound)

У PostgreSQL есть встроенная бомба замедленного действия, и любой, кто держит базу под нагрузкой годами, рано или поздно про неё узнаёт. Идентификатор транзакции - xid - это всего лишь 32-битное число. Значит, оно не бесконечно: примерно через четыре миллиарда транзакций счётчик переполнится и пойдёт по кругу.

А вся видимость в PostgreSQL построена на сравнении: «эта версия старше моего снимка или новее?». Если счётчик пошёл по кругу, сравнения ломаются: старые транзакции вдруг начинают выглядеть как будущие, и данные, которые годами были видны, могут исчезнуть. Это называется wraparound, и это один из немногих способов в PostgreSQL потерять данные молча.

Защита от него - заморозка (freeze). В этой главе разберём, как 32 бита превращаются в круг, что значит «заморозить» версию строки, как читать age(relfrozenxid) и какая лестница предупреждений ведёт от спокойной работы до аварийного однопользовательского режима.

15.1 32 бита и круг вместо линии

Каждой транзакции, которая меняет данные, PostgreSQL выдаёт xid - следующее число счётчика. Видимость версии решается сравнением её xmin/xmax с номерами транзакций в снимке: меньше - в прошлом, больше - в будущем.

Проблема в том, что 32-битный счётчик вмещает чуть больше четырёх миллиардов значений (2³²). На активной базе это не «никогда» - это месяцы или даже недели. Когда счётчик доходит до предела, он обнуляется и начинает заново.

Чтобы сравнения продолжали работать, PostgreSQL считает их не на прямой, а на круге. Для любого xid половина значений (примерно два миллиарда) считается «прошлым», вторая половина - «будущим».

Вот где ловушка: если версия строки осталась с xmin, который старше текущего более чем на два миллиарда, круг «перевернёт» её - она начнёт выглядеть как версия из будущего, ещё не наступившая. А значит, перестанет быть видимой. Данные на месте, но СУБД считает их несуществующими.

15.2 Заморозка: вынуть версию из круга

Решение - не дать ни одной версии состариться больше чем на два миллиарда транзакций. Для этого её xmin нужно «заморозить»: пометить как «эта версия старше всего на свете, видна всем безусловно, в сравнениях по кругу не участвует».

Исторически заморозка переписывала xmin в специальное значение FrozenTransactionId (число 2). Современный PostgreSQL не трогает само xmin, а выставляет в заголовке кортежа бит-подсказку HEAP_XMIN_FROZEN: версия считается замороженной, а исходный xid остаётся виден для отладки. Результат тот же - замороженная версия выпадает из круга и больше никогда не «перевернётся».

Замораживает версии VACUUM. Обычный проход замораживает те, что уже достаточно стары (старше vacuum_freeze_min_age, по умолчанию 50 миллионов). А когда таблица в целом стареет опасно - запускается агрессивный проход, который проходит даже те страницы, что карта видимости считает «всё видно всем», и замораживает всё подряд.

15.3 relfrozenxid: возраст таблицы

Как PostgreSQL понимает, что таблица стареет? У каждой таблицы есть relfrozenxid - граница, гарантирующая: все версии старше неё уже заморожены. Хранится в pg_class.

Полезнее смотреть не на само число, а на его возраст - на сколько транзакций оно отстало от текущего счётчика:

sql
SELECT relname, age(relfrozenxid) AS xid_age
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;

age(relfrozenxid) растёт с каждой новой транзакцией в базе и сбрасывается вниз, когда VACUUM проходит таблицу и продвигает relfrozenxid вперёд. Если возраст какой-то таблицы упорно растёт и приближается к 200 миллионам - её autovacuum по какой-то причине не обрабатывает, и пора разбираться.

На уровне всей базы то же самое показывает datfrozenxid в pg_database - это минимум по всем таблицам базы. Именно он определяет, насколько база близка к опасной черте. Подробности счётчиков - в freeze.

15.4 Лестница защиты от wraparound

PostgreSQL не ждёт катастрофы пассивно. По мере роста возраста включаются всё более жёсткие меры - лестница из четырёх ступеней:

Разберём ступени:

  • 200 млн (autovacuum_freeze_max_age) - запускается анти-wraparound autovacuum. Он стартует даже на таблице, которую autovacuum обычно не трогает, и даже если autovacuum выключен целиком. Это первая и обычно достаточная линия обороны;
  • 1.6 млрд (vacuum_failsafe_age) - failsafe. VACUUM понимает, что времени мало, и переходит в режим выживания: пропускает очистку индексов и задержки, всё ради скорейшей заморозки;
  • меньше 40 млн до края (примерно возраст 2.1 млрд) - сервер начинает писать в лог настойчивые WARNING с обратным отсчётом;
  • меньше 3 млн до края - сервер отказывается выдавать новые xid. База перестаёт принимать запросы, меняющие данные. Выход один: остановить сервер и запустить VACUUM в однопользовательском режиме.

15.5 Однопользовательский режим: дно лестницы

Если все предохранители проигнорированы и база встала, спасение выглядит так:

bash
# сервер остановлен; запускаем его в single-user режиме
postgres --single -D /var/lib/postgresql/data <имя_базы>
sql
backend> VACUUM;

В этом режиме к базе подключён ровно один процесс, никаких клиентов. VACUUM проходит и замораживает старые версии, datfrozenxid продвигается, и после рестарта база снова принимает запросы.

Это аварийный сценарий, и попадать в него не нужно. Вся лестница выше существует именно для того, чтобы анти-wraparound autovacuum разобрался с заморозкой задолго до дна. До single-user доходят обычно те, кто намеренно отключил autovacuum «ради производительности» - классическая дорогая ошибка. Подробнее про сам механизм круга - в wraparound.

15.6 Multixact: второй счётчик, который тоже переполняется

У wraparound есть младший брат, про которого часто забывают. Когда одну строку одновременно блокируют несколько транзакций (например, SELECT ... FOR SHARE или проверки внешних ключей), PostgreSQL не может записать в xmax один xid - блокировщиков несколько. Вместо этого он заводит multixact: отдельный идентификатор, за которым стоит список транзакций-участников.

Multixact-идентификаторы - тоже 32-битный счётчик со своим кругом и своим переполнением. У таблицы для него есть relminmxid (аналог relfrozenxid), а пороги называются autovacuum_multixact_freeze_max_age (по умолчанию 400 миллионов). VACUUM замораживает и multixact тоже.

Коварство в том, что мониторинг часто следит только за обычным age(relfrozenxid) и упускает age(relminmxid). На нагрузке с большим количеством блокировок строк или внешних ключей второй счётчик может побежать к краю быстрее первого. Смотреть надо за обоими. Разбор - в multixact.

Копнуть глубже: заморозка в PostgreSQL 17 стала дешевле. VACUUM хранит список «мёртвых» указателей в памяти, и в прежних версиях этот буфер был ограничен одним гигабайтом, из-за чего большим таблицам приходилось делать несколько проходов по индексам. В 17-й версии структуру заменили на компактное дерево, которое вмещает на порядки больше указателей в той же памяти. На больших таблицах это заметно ускоряет и заморозку, и обычную уборку.

Уроки в sandbox

lab-15.1. Старение и freeze

Понаблюдаем за возрастом таблицы. Посмотрим age(relfrozenxid), искусственно состарим базу (нагенерим транзакций), увидим рост возраста, потом прогоним VACUUM FREEZE и убедимся, что возраст упал почти до нуля. Сначала предскажи, как изменится age() после каждого шага.

  1. Посмотри текущий age(relfrozenxid) для таблицы flights через запрос к pg_class.

  2. Сгенерируй много коротких транзакций (например, цикл с SELECT txid_current();) и снова сними age() - предскажи, вырастет ли он.

  3. Запусти VACUUM FREEZE flights; и сними age(relfrozenxid) ещё раз - он должен резко упасть.

  4. Сравни age(relminmxid) до и после - проверь, что multixact-граница тоже на месте.

  5. Найди таблицу с самым большим возрастом в базе через ORDER BY age(relfrozenxid) DESC.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • `xid` - 32-битное число; счётчик транзакций переполняется примерно через 4 млрд транзакций, поэтому сравнения видимости идут по кругу, а не по прямой.
  • Версия старше текущего `xid` более чем на ~2 млрд «переворачивается» в будущее и становится невидимой - это и есть wraparound и молчаливая потеря данных.
  • Заморозка (freeze) помечает версию как видимую всем безусловно (бит `HEAP_XMIN_FROZEN`), выводя её из круга навсегда; делает это VACUUM.
  • `age(relfrozenxid)` - возраст таблицы в транзакциях; растёт со временем, падает после прохода VACUUM; на уровне базы то же показывает `datfrozenxid`.
  • Лестница защиты: 200 млн → анти-wraparound autovacuum; 1.6 млрд → failsafe; <40 млн до края → WARNING; <3 млн → база встаёт.
  • Из остановившейся базы выходят через VACUUM в однопользовательском режиме; туда обычно попадают те, кто отключил autovacuum.
  • Multixact - второй 32-битный счётчик (блокировки строк, внешние ключи) со своим `relminmxid`; за `age(relminmxid)` нужно следить отдельно.

Контрольные вопросы

  1. Почему сравнение транзакций в PostgreSQL идёт по кругу, а не по прямой, и чем это опасно?

    Показать ответ

    Потому что xid - 32-битное число и счётчик неизбежно переполняется примерно через 4 млрд транзакций. Чтобы сравнения «старше/новее» продолжали работать после обнуления, PostgreSQL делит круг пополам: ~2 млрд значений назад считаются прошлым, ~2 млрд вперёд - будущим. Опасность: версия строки, чей xid отстал больше чем на 2 млрд, «перепрыгивает» в будущее и становится невидимой. Данные физически на месте, но СУБД их не показывает - это и есть wraparound.

  2. Что значит «заморозить» версию строки и кто это делает?

    Показать ответ

    Заморозить - значит пометить версию как старше всего на свете и видимую всем безусловно, чтобы она больше не участвовала в круговых сравнениях и не могла «перевернуться» в будущее. Современный PostgreSQL выставляет для этого бит-подсказку HEAP_XMIN_FROZEN в заголовке кортежа, не переписывая сам xmin. Делает заморозку VACUUM: обычный проход морозит версии старше vacuum_freeze_min_age, а агрессивный/анти-wraparound проход - всё подряд, включая страницы, помеченные в карте видимости как «всё видно».

  3. На что смотреть, чтобы вовремя заметить приближение wraparound?

    Показать ответ

    На age(relfrozenxid) по таблицам (через pg_class) и на age(datfrozenxid) по базам (через pg_database). Возраст - это на сколько транзакций граница заморозки отстала от текущего счётчика. Он должен держаться заметно ниже 200 млн (autovacuum_freeze_max_age). Если возраст какой-то таблицы упорно растёт к этому порогу и выше - autovacuum её не успевает или не трогает. Отдельно нужно следить за age(relminmxid) - возрастом multixact-счётчика, про который часто забывают.

  4. Опишите лестницу защиты от wraparound по ступеням.

    Показать ответ

    Первая ступень - возраст 200 млн (autovacuum_freeze_max_age): запускается анти-wraparound autovacuum, причём даже если autovacuum отключён. Вторая - 1.6 млрд (vacuum_failsafe_age): failsafe-режим, VACUUM пропускает очистку индексов и паузы, гонит только заморозку. Третья - меньше 40 млн транзакций до края: сервер пишет в лог настойчивые WARNING с обратным отсчётом. Четвёртая - меньше 3 млн до края: сервер перестаёт выдавать новые xid, база встаёт, и спасает только VACUUM в однопользовательском режиме.

  5. Чем multixact-wraparound отличается от обычного и почему его легко проглядеть?

    Показать ответ

    Обычный wraparound - про счётчик xid, который тратится на каждую изменяющую транзакцию. Multixact - отдельный 32-битный счётчик, который тратится, когда одну строку одновременно блокируют несколько транзакций (FOR SHARE, проверки внешних ключей). У него свой круг, своя граница relminmxid и свой порог autovacuum_multixact_freeze_max_age (400 млн). Проглядеть легко, потому что типовой мониторинг следит только за age(relfrozenxid). На нагрузке с обилием блокировок строк multixact может добежать до края раньше обычного счётчика.

← Предыдущая14-vacuum-horizonСледующая →16-autovacuum-bloat
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки