PostgreSQL

Transaction ID Wraparound: тихий убийца

2019 год. Production PostgreSQL в крупном банке останавливается в 3:47 утра. Не из-за атаки, не из-за hardware failure - из-за 32-битного счётчика, который тихо считал транзакции годами и наконец досчитал до предела. Вся база ушла в read-only. Это называется txid wraparound - и это единственная проблема PostgreSQL, которая убивает базу без предупреждения, если за ней не следить.

  • **Basecamp (2020):** таблица users перестала читаться после wraparound в production. Команда обнаружила проблему когда пользователи начали получать ошибки входа. Исправление заняло 3 часа с downtime.
  • **GitLab (2017):** команда добавила специальный rake task для мониторинга txid age после анализа того, как другие компании потеряли данные из-за wraparound. Сейчас это часть их SRE runbook.
  • **Amazon RDS (регулярно):** AWS автоматически запускает 'emergency vacuum' на RDS инстансах когда age приближается к 1.6B, и уведомляет пользователей через CloudWatch alarm - именно потому что wraparound - это реальный риск.

32-битный счётчик транзакций

PostgreSQL нумерует каждую транзакцию 32-битным целым числом - transaction ID (txid). Максимальное значение: 2^32 = ~4.3 миллиарда. На высоконагруженных системах это число заканчивается быстрее, чем ожидают инженеры.

MVCC использует txid для определения видимости строк: tuple visible если xmin < current_txid и xmax = 0 (или xmax > current_txid). Если счётчик переполнится и начнётся с нуля - PostgreSQL увидит все старые строки как 'из будущего' и скроет их. База станет недоступна для чтения.

В 2019 году Сбербанк России столкнулся с полным останов производственной базы PostgreSQL из-за wraparound. Восстановление заняло несколько часов. Аналогичный инцидент был у команды Basecamp в 2020-м - таблица users перестала читаться.

Что произойдёт с строками в таблице, если txid счётчик переполнится и вернётся к нулю?

Freeze: заморозка старых транзакций

Чтобы избежать wraparound-катастрофы, PostgreSQL использует механизм freeze: старые xmin заменяются специальным значением FrozenTransactionId (=2). Frozen-строки видимы для всех транзакций вне зависимости от txid счётчика.

VACUUM выполняет freeze когда xmin строки старше vacuum_freeze_min_age (по умолчанию 50M транзакций). Страница целиком замораживается если все её строки старше vacuum_freeze_table_age (150M). Frozen-страницы пропускаются при следующих VACUUM - это экономит I/O.

Что означает 'замороженная' (frozen) строка в PostgreSQL?

Aggressive Vacuum: когда всё серьёзно

Обычный VACUUM freeze'ит только страницы, возраст которых превысил vacuum_freeze_table_age. Aggressive vacuum (включается автоматически когда age > autovacuum_freeze_max_age = 200M) сканирует всю таблицу целиком, не пропуская даже чистые страницы.

Aggressive vacuum создаёт серьёзную I/O нагрузку: он читает каждую страницу таблицы, даже пустые. На таблице orders GitLab (>500GB) одна итерация aggressive vacuum занимала 4-6 часов и поднимала disk I/O с 10 MB/s до 300 MB/s.

Чем aggressive vacuum отличается от обычного VACUUM?

Мониторинг: не пропустить момент

Единственный способ не попасть в wraparound-катастрофу - непрерывный мониторинг age транзакций. Нужно отслеживать как database-level age, так и table-level age, поскольку одна старая таблица может задержать freeze всей базы.

Shopify добавили alerting на txid age в свой мониторинг PostgreSQL в 2018 году после анализа инцидентов в индустрии. Правило: WARNING при age > 1.5B, CRITICAL при age > 1.8B, PAGE ON-CALL при age > 1.9B.

Какое из значений age(datfrozenxid) требует немедленного вмешательства?

Предотвращение: настройки и практики

Wraparound - решаемая проблема. Правильная настройка autovacuum и мониторинг age - достаточные условия для безопасной работы. Критически важно не отключать autovacuum и не допускать долгоживущих транзакций.

  • Никогда не отключать autovacuum (autovacuum = off) на продакшн-таблицах
  • Мониторить age(datfrozenxid) в Prometheus/Grafana с alerting
  • Избегать долгоживущих транзакций - они блокируют freeze xmin
  • Для таблиц с HIGH UPDATE-нагрузкой снижать autovacuum_freeze_max_age до 100M
  • В случае аварии: VACUUM FREEZE <table> - лечит конкретную таблицу без downtime

Долгоживущая транзакция (например, pg_dump без --no-synchronized-snapshots) блокирует freeze: PostgreSQL не может заморозить строки с xmin >= oldest_xmin. Одна транзакция на 24+ часа может исчерпать весь 2B-запас.

VACUUM FULL решает проблему wraparound лучше обычного VACUUM FREEZE

VACUUM FULL и VACUUM FREEZE решают разные проблемы. VACUUM FULL перезаписывает таблицу компактно (убирает bloat) но требует ExclusiveLock. VACUUM FREEZE обновляет xmin на FrozenTransactionId без блокировки. Для предотвращения wraparound нужен VACUUM FREEZE, не VACUUM FULL.

VACUUM FULL выполняет заморозку как побочный эффект, но его главная цена - эксклюзивная блокировка таблицы на всё время выполнения, что неприемлемо для продакшн-систем. VACUUM FREEZE работает без блокировки и специально оптимизирован для freeze.

Почему долгоживущая транзакция (open > 24h) опасна с точки зрения wraparound?

Итоги

  • **32-bit txid исчерпаем:** при ~4.3B транзакциях PostgreSQL уходит в wraparound - старые строки становятся невидимыми, база перестаёт читать данные
  • **Freeze - защитный механизм:** VACUUM заменяет старые xmin на FrozenTransactionId (=2), делая строки вечно видимыми и безопасными для wraparound
  • **Мониторинг age - обязателен:** нужно алертить на age(datfrozenxid) > 1.5B, иначе катастрофа может случиться незаметно в ночь с воскресенья

Связанные темы

Wraparound неразрывно связан с другими механизмами PostgreSQL:

  • MVCC и видимость строк — Wraparound - прямое следствие MVCC-архитектуры: xmin/xmax в каждой строке и есть то, что переполняется
  • VACUUM и autovacuum — Единственный механизм предотвращения wraparound - регулярный VACUUM FREEZE, который запускает autovacuum
  • Мониторинг PostgreSQL — age(datfrozenxid) - обязательная метрика в любом production-мониторинге PostgreSQL

Вопросы для размышления

  • Если таблица получает 1000 транзакций в секунду, за сколько дней age вырастет на 200M? Как это влияет на частоту autovacuum?
  • Почему pg_dump с --serializable-deferrable может спровоцировать wraparound на очень нагруженных системах?
  • Как бы выглядел incident response playbook для ситуации когда age(datfrozenxid) = 2.0B а VACUUM FREEZE выполняется слишком медленно?

Связанные уроки

  • db-13-transactions
Transaction ID Wraparound: тихий убийца

0

1

Войти