Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

PostgreSQL: что нужно знать о счётчике транзакций

Расскажем, как PostgreSQL справляется с высокими нагрузками, уделяя особое внимание его счетчику транзакций. Вы узнаете о MVCC (Multiversion Concurrency Control), о том, как идентификаторы транзакций (xmin, xmax) управляют версиями данных, и о проблемах 32-битного счетчика транзакций в системах с высокой нагрузкой.

805 открытий5К показов
PostgreSQL: что нужно знать о счётчике транзакций

Многие годы считалось, что PostgreSQL не подходит для систем с высокой нагрузкой. Но на практике всё чаще встречаются ситуации, когда даже крупные компании выбирают именно эту СУБД. В результате, всё больше внимания уделяется вопросам производительности и масштабируемости PostgreSQL. Один из таких вопросов — счётчик транзакций, о котором мы сегодня и поговорим.

Что такое счётчик транзакций и зачем он нужен?

PostgreSQL использует механизм многоверсионности (MVCC) благодаря которому читающие транзакции не блокируют пишущие, и наоборот. Каждая транзакция работает со своей версией данных, а доступ регулируется правилами видимости.

Чтобы понять, как функционирует MVCC, представьте, что у каждого пользователя базы есть свой «моментальный снимок» данных. Когда вы начинаете работать, вы видите состояние данных на этот конкретный момент времени. Изменения, которые делают другие пользователи, не влияют на ваш снимок, пока вы не завершите свою операцию и не начнете новую. Это позволяет нескольким пользователям одновременно читать и изменять данные, не мешая друг другу.

MVCC в PostgreSQL работает похожим образом:

  • Каждая транзакция — это как отдельный пользователь, работающий с документом. Транзакция видит «снимок» базы данных на момент своего начала. 
  • Когда транзакция изменяет данные, она не перезаписывает старые. Вместо этого создаётся новая версия изменённых строк.

Каждая версия строки имеет специальные метки:

  • xmin — идентификатор транзакции, которая создала версию.
  • xmax — идентификатор транзакции, которая удалила (или обновила) версию. Если строка ещё не удалена, xmax пустой.

Когда транзакция читает данные, PostgreSQL определяет, какую версию строки ей показывать, на основе правил видимости:

  • транзакция видит строки, созданные до её начала (xmin меньше её идентификатора), и не помеченные как удалённые (xmax пустой или больше её идентификатора);
  • транзакция видит строки, созданные ею самой;
  • транзакция не видит строки, созданные транзакциями, которые начались позже неё (xmin больше её идентификатора).

Пример:

Допустим, у нас есть таблица users с данными:

			id  | name  | age

—|——-|—-

1   | Alice  | 30

2  | Bob   | 25
		

1. Транзакция 1 (xid=100) начинается и читает таблицу Она видит обе строки, потому что они были созданы до её начала, и xmax у них пустой.

2. Транзакция 2 (xid=105) начинается и обновляет возраст Alice на 31. PostgreSQL не перезаписывает строку Alice, а создаёт новую версию:

			id | name  | age | xmin | xmax

—|——-|—–|——|—–

1   | Alice   | 30  | …      | 105

1   | Alice   | 31   | 105  |

2   | Bob    | 25  | …      |
		

3. Обратите внимание, что у старой версии Alice xmax теперь равен 105 (идентификатор транзакции 2), а у новой версии xmin равен 105.

4. Транзакция 1 снова читает таблицу. Она по-прежнему видит старую версию Alice (возраст 30), потому что новая версия была создана транзакцией, которая началась позже (105 > 100), а старая помечена как удалённая транзакцией с большим номером.

5. Транзакция 2 фиксируется.

6. Транзакция 3 (xid=110) начинается и читает таблицу Теперь она видит новую версию Alice (возраст 31), потому что транзакция 2, которая её создала, уже зафиксирована, и её xid меньше чем у транзакции 3.

Важно: номера транзакций (xid) не стоит сравнивать как обычные числа («больше» или «меньше»). Корректнее говорить «старше» или «младше». Дело в том, что номера транзакций сравниваются по модулю 232, образуя кольцо. Транзакции, отстающие от текущей на 231 назад, считаются «старше» («в прошлом»), а на 231 вперёд — «младше» («в будущем»).

Представьте себе круг:

  • текущая транзакция — это точка на круге;
  • «прошлые» транзакции — это точки слева от текущей, если двигаться против часовой стрелки;
  • «будущие» транзакции — точки справа.
PostgreSQL: что нужно знать о счётчике транзакций 1

Каждая запись в базе данных имеет служебные поля xmin и xmax. В xmin записывается номер транзакции, создавшей запись, а в xmax — номер транзакции, удалившей её. Учетом этих номеров и занимается счетчик транзакций.

Как развивался счётчик транзакций

До версии PostgreSQL 8.2 при достижении максимального значения счётчика транзакций (примерно 4 миллиарда) PostgreSQL просто «падал». Чтобы продолжить работу, нужно было сделать дамп базы данных и создать её заново.

В версии 8.2 появился механизм циклического перезапуска счётчика. Для этого в фоновом режиме запускается процесс очистки (VACUUM).

Как это работает:

  1. «Заморозка» старых транзакций: Когда на VACUUM передаётся значение, что счётчик транзакций скоро переполнится, он начинает помечать старые транзакции специальным идентификатором FrozenTransactionId. По сути, VACUUM говорит: «Эти транзакции настолько старые, что мы считаем их замороженными. Мы всегда будем считать их старше любых других транзакций.»
  2. Счётчик эпох: Чтобы PostgreSQL знал, сколько раз счётчик уже «перезапускался», используется счётчик эпох. Каждый раз, когда счётчик транзакций доходит до конца и начинает заново, счётчик эпох увеличивается на единицу.
  3. Циклический перезапуск: После того как VACUUM «заморозит» старые транзакции, счётчик транзакций может безопасно начать считать заново с трёх. PostgreSQL теперь знает, что все транзакции с идентификатором FrozenTransactionId старше любых других транзакций, даже если их номера меньше.

Это решило проблему «падения», но возникла новая: если VACUUM не успевает отработать вовремя, сервер может остановиться и потребовать запуска в монопольном режиме для очистки, что ведёт к простою.

В чём же проблема?

Сам по себе механизм счётчика транзакций работает хорошо. Проблема в том, что он создавался, когда 32-битное число (более 4 миллиардов) казалось огромным. Сейчас же, в системах с высокой нагрузкой (ретейл, крупные заводы, госучреждения), счётчик может оборачиваться раз в сутки.

Дополнительные сложности:

Среди служебных данных записи есть поле флагов. Для старых записей нужно периодически запускать VACUUM FREEZE, который помечает их как «замороженные» (FrozenTransactionId).

Для стабильной работы нужно, чтобы:

  • разница между номером текущей транзакции и номером самой старой записи не превышала 2^32;
  • VACUUM FREEZE выполнялся до нарушения первого условия.

В реальном мире это сложно:

  • «долгие» транзакции мешают VACUUM FREEZE;
  • сложно определить, когда транзакция «долгая», а когда «зависшая»;
  • непонятно, когда бить тревогу из-за исчерпания идентификаторов;
  • администратор БД несёт ответственность за потери бизнеса из-за своих действий.

Если вы можете остановить кластер БД, запустить его в монопольном режиме и подождать несколько часов, эта проблема вас не касается. Но в системах с высокими требованиями к доступности простой — недопустим.

Отдельно стоит упомянуть базы данных, в которые данные только добавляются (insert-only). В них VACUUM всё равно запускается для «заморозки» старых записей.

Решение: 64-битные идентификаторы транзакций

Казалось бы, простое решение — увеличить размер счётчика до 64 бит. Но это сложно:

  • весь код PostgreSQL ожидает «видеть» 32-битные числа;
  • в каждом кортеже (записи) хранятся xmin и xmax. Увеличение их размера с 8 до 16 байт значительно увеличит размер базы.

Мы в Postgres Professional разработали реализацию 64-битных идентификаторов транзакций (xid), увеличив их количество до 18 446 744 073 709 551 615 (264). Это решение используется во многих форках PostgreSQL. Проблема переполнения счётчика становится гипотетической (при высокой нагрузке 64-битный счётчик исчерпается примерно через 400 лет).

Патч с реализацией был предложен сообществу PostgreSQL, но пока не принят из-за его сложности. Однако многие разработчики форков PostgreSQL уже заявили о поддержке 64-битных xid в своих дистрибутивах.

Цель Postgres Professional — не продвигать свой вариант как единственно верный, а привлечь внимание к проблеме и стимулировать сообщество к поиску оптимального решения. Первая часть удалась: идея получила внимание. Но внедрение таких изменений — долгий процесс. Рост нагрузок на СУБД во всём мире показывает важность этой проблемы.

Мы надеемся, что PostgreSQL сможет адаптироваться к растущим нагрузкам и укрепить свои позиции на рынке.

Следите за новыми постами
Следите за новыми постами по любимым темам
805 открытий5К показов