На конференции PgBootcamp 2025 был доклад Евгения Воропаева "Разработка и отладка 64-битного счётчика транзакций". В докладе рассматривались проблемы, которые встретились при переносе патча с 16 на 18 версию PostgreSQL. В статье описывается история патча.
Введение
В PostgreSQL используется 32-битный счетчик транзакций. В 32-битном счетчике максимальное значение номера транзакции 4 миллиарда, дальше счетчик должен перейти через ноль (выполнить wraparound). Для беспроблемного перехода через ноль в PostgreSQL есть функционал "заморозки" версий строк в блоках таблиц.
У российских форков PostgreSQL есть сборки с 64-битным счетчиком транзакций. Почему заморозки не достаточно и зачем понадобился 64-битный счетчик? При быстром росте размера таблицы и большой частоте транзакций есть вероятность, что автовакуум не успеет обработать блоки какой-нибудь таблицы и заморозить их, что при этом произойдёт описано ближе к концу статьи.
64-битный счетчик можно было реализовать по-разному. Можно было увеличить размерность xid с 4 байт до 8 байт, но это бы привело к увеличению объема служебных данных и структур памяти. Размер файлов кластера увеличился бы, так как в заголовке каждой версии строки хранится min и xmax. Заголовок каждой строки увеличился бы на 8 байт. Если в блоке хранится 100 строк, то добавилось бы 10%, что много. Такой способ неоптимален. В 64-битном счетчике, который используется в российских форках PostgreSQL, накладных расходов немного.
История
Дата рождения патча 22.06.2017г., когда Александр Коротков опубликовал первую версию патча.
Патч не был готов к применению ("not so good shape"), хотя бы потому, что не была реализована миграция с помощью утилиты pg_upgrade. Александр Коротков загрузил патч для самоконтроля, чтобы не останавливаться в доработке патча ("in order to not stop progress in this area").
Вторая версия патча была опубликована Александром 14.09.2017г.
Третья версия была опубликована спустя несколько часов, потому что Александр загрузил не ту версию ("messed up with git").
Amit Kapila хотел посмотреть патч, но патч не был откомментирован и Amit попросил написать хотя бы README с описанием что вообще делает патч, как будет работать PostgreSQL с патчем или послать ему описание по электронной почте. Александр Коротков пообещал написать README и провести тесты.

История с просьбой написать README повторилась во второй раз 29.04.2025 с частью патча.
Отсутствие документирования и объяснения, как что работает, вероятно, было основным препятствием к принятию патча, сразу после его появления. Если бы патч был принят "вслепую", то у активных разработчиков сообщества уменьшилось бы понимание того, как работает PostgreSQL и разработка нового функционала замедлилась.
9.01.2018 был сделан первый перенос кода патча (rebase) на новую основную версию PostgreSQL и патч получил версию номер 4. Эта версия не компилировалась и через один день была выложена 5 версия.
Andres Freund попросил прокомментировать какие изменения вносятся в патч и предложил выделить поддержку 64-битных параметров конфигурации в отдельный патч. Александр Коротков пообещал и перестал отвечать.
01.11.2019 появился пост неизвестного (человека или собирательного образа) примечательный тем, что уважаемые члены сообщества разработчиков PostgreSQL дали развернутое мнение о 64-битном счетчике транзакций.
3.11.2019 Томас Манро создал страницу про патч https://wiki.postgresql.org/wiki/FullTransactionId

1.10.2021 Michael Paquier написал: три недели не было ответа от автора патча на ревью, поэтому патч отклоняется (Returned with Feedback). К тому времени патч перестал компилироваться и требовал переноса на новую версию.
Возрождение патча
30.12.2021 Максим Орлов открыл новое обсуждение, в котором предложил 6 версию патча, созданную на основе 5 версии патча Короткова, которую отрефакторил и перенес на 15 версию PostgreSQL с немногочисленными исправлениями ошибок. Патч состоял из двух частей размерами 26Кб и 720Кб и содержал рекордные 17259 строк.
Снова был задан вопрос сразу двумя членами сообщества: "есть ли какая-либо документация или файл README, объясняющие весь этот механизм 64-битного XID?" В обсуждении написал Брюс Момжан и только ему, после долгого молчания, ответил Александр Коротков, что с патчем есть 5 проблем.
6.01.2022 Simon Riggs (EnterpriseDB) написал ревью на первую часть патча Максима Орлова, что не нашел ошибок и предложил закоммитить первую часть патча.
15.01.2022 Злой рок с README продолжал преследовать патч. Оказалось, что третью часть патча на 152 строки, в которой и был README, "съел" и "никому не показывал" commitfest bot. Третью часть патча нашли и сделали видимой. README написали Максим Орлов, Павел Борисов, Юрий Соколов.
Патч немного пообсуждали и 02.02.2022, по результатам обсуждения, Павел Борисов (из Supabase) опубликовал 8 версию патча.
Другой член сообщества написал, что патч не применяется и предположил, что патч не для ванильного PostgreSQL, а для какого-то неизвестного форка.
Andres Freund написал, что в 15 версию патч точно не успеет войти и предложил вынести 64-битный SLRU в отдельный патч, так как изменения независимы и он видит потенциал для коммита в этой части.
Как написал Andres, так и произошло: патч "Index SLRUs by 64-bit integers rather than by 32-bit integers" перемещался на следующие коммитфесты два года и был закоммичен в январе 2024 года https://commitfest.postgresql.org/patch/3489/ Александром Коротковым. Авторы патча - представители трёх компаний: Александр Алексеев (TimescaleDB), Максим Орлов (Postgres Pro), Павел Борисов (Supabase).
03.03.2022 Александр Алексеев обновил патч, чтобы патч работал с текущей версией (выполнил "rebase") и выложил 10 версию патча, написав, что патч для ванильного PostgreSQL, а не для неизвестного форка, а не накладывался патч потому, что авторы использовали устаревший репозитарий и дал оценку, что патч довольно сырой ("patch is pretty raw in its current state").
03.03.2022 Максим Орлов опубликовал 11 версию патча. Обсуждение, инициированное Максимом Орловым, привело к добавлению патча в коммитфест в мае 2022 года https://commitfest.postgresql.org/patch/3594/ . В течение всего года патч активно дорабатывался и неоднократно ребейзился, так как патч переставал компилироваться.
14.11.2022 Максим опубликовал юбилейную 50 версию патча, который к тому времени состоял из 8 частей. Одна из частей патча была по-прежнему огромной. В июле 2023 года патч был возвращен, а не перенесен на следующий коммитфест.
28.11.2022 Максим опубликовал 51 версию патча.
13.12.2023 Максим опубликовал 52 версию патча, разбитую на 7 частей, и опубликовал статью.
В январе 2024 года патч был снова добавлен в коммитфест и в сентябре 2024 года снова возвращен.
19.01.2024 написали, что текущая версия PostgreSQL с патчем не компилируется и нужен перенос патча на текущую версию PostgreSQL.
23.04.2024 Максим Орлов начал новое обсуждение.
19.06.2024 Александр Алексеев опубликовал 53 версию части патчей, которые смог перенести и сменил нумерацию частей. В тот же день он опубликовал 54 версию патча с исправлением бага, обнаруженного commitfest ботом.
При первом прочтении истории сообщений, у меня возникло впечатление: если в течение суток публикуют новую версию патча с обнаруженным багом, то сколько еще багов содержится в патче и как можно такой патч коммитить. При повторном прочтении истории, такое максималистское отношение к патчу у меня уменьшилось.
23.07.2024 Александр Алексеев перенёс патч на текущую версию PostgreSQL (выполнил rebase) и опубликовал 55 версию патча. Однако, основной и самый большой патч он не смог перенести (0006+ are difficult to rebase / review and I'm a bit worried for the committer who will merge them. We can return to 0006+ when we deal with the first 5 patches.).
12.09.2024 Александр Алексеев написал: консенсус не достигнут, патч трудно перебазировать и ревьювить и предлагаю сосредоточиться на чем-то, в чём достигнут консенсус (I suggest focusing on something we reached a consensus for). Я собираюсь вернуть патч без комментариев. Мы всегда можем вернуться к патчу позже, желательно найдя коммитера, у котого есть время и силы.
Александр вернул патч в сентябре 2024 года.
Александр Алексеев, не работая в компании, которая эксплуатировала патч, героически тратил силы и время на несколько перебазирований патча и ему эта благотворительность надоела. Рассказы о том, как "злое сообщество" отвергает "суперпатчи" и возвращает без комментариев - это "разговоры в пользу бедных".
В том же сентябре 2024 года был выделен патч "Make MultiXactOffset 64-bit type" Максимом Орловым и пока ревьювится. В ревьюверы записался Heikki Linnakangas.
16.04.2025 Максим Орлов перенёс патч в июльский коммитфест 07.2025.
29.04.2025 ревьювер патчей июльского коммитфеста предложил документировать в transam/README, как используются файлы pg_multixact/members and pg_multixact/offset и описать формат этих файлов, что позволило бы быстрее ревьювить патч. Максим согласился с этим: "I agree, this is a big overlook, I think. Anyone who tries to understand how pg_multixact works has to deal with the code. Certainly, we need to address this issue." 14.07.2025 Максим загрузил обновление части патча, в файлах слова README нет.
Александр Алексеев, единственный, кто из сообщества ребейзил патч, не смог перенести его на новую версию. Патч перестал просматриваться кем-либо, так как с ним никто не мог собрать работающий PostgreSQL.
Использовать патч для самостоятельной сборки и использования компании не могут, так как если и скомпилируют PostgreSQL, то не смогут обновиться на новый релиз и будут вынуждены выгружать данные на логическом уровне (pg_dump, логическая репликация). Для маленьких СУБД с небольшой транзакционной нагрузкой 64-битный счетчик бесполезен, значит, размер баз большой и выгрузка на логическом уровне будет идти долго.
Для переноса патча нужны значительные ресурсы, которые есть только у нескольких производителей форков. Некоторым производителям форков может быть сложно выполнять перенос и они пропускают мажорные релизы PostgreSQL. Мажорные релизы не пропускали Tantor и Postgres Professional.
Наше время
8.12.2024 Евгений Воропаев (Тантор Лабс) загрузил 56 версию патча. В 56 версии Евгений:
собрал воедино все изменения MXIDOFFSETv9, GUC64v2, xid64 (v52-v55) the latest versions from the relevant threads have been used. Получилось 12 патчей суммарным размером 1Мб
отребейзил патчи, в том числе, самый большой
доработал патч, создав функцию HeapTupleCopyRawXidsFromPage, которая возвращает реальные значения xid даже для замороженных строк
исправил ошибки, которые обнаружили стандартные регрессионные тесты
исправил баг, который был в функции heapam_tuple_satisfies_snapshot(..). Баг проявлялся как ошибка "missing chunk" в TOAST, чаще всего на таблице pg_statistic.
18.12.2024 была опубликована статья, где приветствовали доработки: «наша позиция состоит не в продвижении нашего варианта как единственно возможного или самого правильного. Скорее, мы рекомендуем сообществу уделить больше времени реализации идеи 64-битных идентификаторов транзакций и приветствуем улучшения и предложения». Статья примечательна тем, что, в соответствии с принципом Арнольда (принцип открыт Стиглером, но в соответствии с самим принципом назван не по имени Стиглера), в статье ни разу не упоминается автор патча.
16.12.2024 Евгений Воропаев загрузил 58 версию патча. При каждой загрузке он описывал, какие вносились изменения.
26.03.2025 Евгений Воропаев сделал ребейз и загрузил 60 версию патча из 15 частей с исправлениями нескольких багов.
16.05.2025 Максим Орлов загрузил 61 версию части патчей, в количестве 8 файлов.
Работа над патчем возобновилась.
Wraparound
В чем проблема 32-битного счетчика транзакций? Напишу, в меру своих скромных познаний, как я понимаю проблему. Если вы найдёте ошибки, не судите строго, поправьте в комментариях. Переход через ноль (wraparound) счетчиков транзакций и мультитранзакций может происходить часто.
При генерации 20 тыс. транзакций в секунду переход через ноль произойдет через 2,5 суток. Первая транзакция после перехода через ноль будет иметь номер 3 (у счетчика мультитранзакций начинаются с 1).
Проблемы начнутся гораздо раньше - транзакции не смогут получать xidы через 2млрд минус 3млн. транзакций (xidStopLimit = xidWrapLimit - 3000000). Будет выдаваться ошибка:
postgres=# begin transaction;
postgres=*# update t set id = 1 where id=1;
ERROR: database is not accepting commands that assign new transaction IDs to avoid wraparound data loss in database with OID 5
HINT: Execute a database-wide VACUUM in that database.
You might also need to commit or roll back old prepared transactions, or drop stale replication slots.
postgres=!# commit;
ROLLBACK
Предупреждения будут выдаваться за 2 млрд. минус 40 млн. транзакций (xidWarnLimit = xidWrapLimit - 40000000).
Если разница между xmin и xmax строк в одном блоке станут отличаться больше чем на 4млрд.? Такое может произойти, если горизонт базы удерживается так, что за время удержания проходит 4 млрд. транзакций. При 20 тыс. транзакций в секунду удерживать надо всего лишь 2,5 суток. Заморозить строку или сдвинуть старшие 32 бита 64-разрядного счетчика транзакций не удастся. Даже с патчем за долгими транзакциями и запросами нужно будет следить. Для защиты от долгих запросов и транзакций можно использовать параметры конфигурации statement_timeout и transaction_timeout.
Заморозка
По умолчанию, заморозка начинается, когда она не была выполнена autovacuum_freeze_max_age (по умолчанию 200 млн.) транзакций, отстоящих от самой последней транзакции. Если заморозка не успеет выполниться и пройдёт 2млрд. транзакций, то номера транзакций перестанут выдаваться, пока не будет выполнена заморозка.
По умолчанию, процессы автовакуума (которые выполняют заморозку) работают с задержкой, заданную параметром конфигурации autovacuum_vacuum_cost_delay и могут не успеть обработать таблицы. Задержку стоит отключить, но по-умолчанию она включена. Задержка начинает игнорироваться только по достижении vacuum_failsafe_age (по умолчанию 1млрд.600млн.) транзакций, то есть когда останется 400млн. транзакций до аварийной остановки экземпляра, которая произойдёт, если заморозка не успеет завершиться.
Также, заморозка таблицы не сможет выполниться, если в её файлах имеется сбойный блок.
Доклад Евгения Воропаева
В докладе рассказывалось, как выполнялся ребейзинг патча 52 версии, совместимого с веткой между 16 и 17 версиями PostgreSQL. Были фрагменты, вынесенные в отдельные патчи. Задача - сделать патч совместимым с 18 версией PostgreSQL. Сложность была в том, что Heikki Linnakangas внёс изменения в формирование журнальных записей, объединив prune с freeze в одно действие при вакуумировании и в единой журнальной записи.
Евгений сказал, что первый ребейзинг занял 5 месяцев, второй три месяца, третий две недели. Другими словами, чтобы вникнуть в патч к.ф.-м.н. нужно полгода с поддержкой 5 программистов на языке C и одного программиста уровня коммитера.

К докладу идёт пример готовой сборки PostgreSQL с патчем и скриптом тестирования. Можно попробовать скомпилировать PostgreSQL, посмотреть как выглядит ошибка, попробовать её найти и исправить. Также можно скачать презентацию доклада.
Вопросы после доклада на конференции PgBootcamp
Вопросы стоит посмотреть самостоятельно, они довольно прямые.
Вопрос. При неудачном переносе патча или наличии неизвестного бага в нем он портит данные. Высоконагруженные системы должны использовать проверенные технологии. В ванильный Postgres коммитить не хотят. Какие перспективы патча?
Ответ. Во всех коммерческих версиях трёх основных форков этот патч есть и эксплуатируется всеми клиентами. В системах с большим TPS нет выхода - либо использовать сборку с патчем, либо не использовать PostgreSQL. Если что-то сломается, то техподдержка будет чинить. Сложность патча: 10тыс. строк кода, Андрей Бородин за год закоммитил (написал) 2000 строк кода.
Вопрос: "Вот вы исправили патч, вот вам новая версия, исправили, вот вам новая версия. Сообществу вообще удобно смотреть на такое, они вообще разберутся в патче?"
Политкорректный вопрос намекает на то, что надо детально описывать в обсуждении, где постится новая версия патча, какие исправления внесены и почему. Хороший пример таких объяснений - это сам доклад, в котором рассматривается несколько примеров ошибок.
Вопрос: "Будет ли закоммичен патч?"
Ответ. Не в 18 версии и вряд ли это будет в ближайших версиях.
Тайна конца блока
В СУБД Tantor Postgres и патче, предложенном в сообщество, используются 16 байт в конце блока таблиц. Структура зоны special:
/*
* HeapPageSpecialData -- data that stored at the end of each heap page.
*
* pd_xid_base - base value for transaction IDs on page
* pd_multi_base - base value for multixact IDs on page
*
* pd_xid_base and pd_multi_base are base values for calculation of transaction
* identifiers from t_xmin and t_xmax in each heap tuple header on the page.
*/
typedef struct HeapPageSpecialData
{
TransactionId pd_xid_base; /* base value for transaction IDs on page */
TransactionId pd_multi_base; /* base value for multixact IDs on page */
} HeapPageSpecialData;
Под хранение старших разрядов 64-битного счетчика используется 16 байт в конце блока (зона "special"):
postgres=# create table t (n numeric);
postgres=# insert into t values (1);
postgres=# create extension pageinspect;
postgres=# select pagesize-special special from page_header(get_raw_page('t', 0));
special
---------
16
В статье Максима Орлова https://habr.com/ru/companies/postgrespro/articles/707968/ написано, что В СУБД Postgres Pro используется структура:
* pd_prune_xid is a hint field that helps determine whether pruning will be
* useful. It is currently unused in index pages.
*
* pd_magic allows identified an type of object heap page belongs to.
* Currently, heap page may belong to an regular table heap or sequence heap.
typedef struct HeapPageSpecialData
{
TransactionId pd_xid_base; /* base value for transaction IDs on page */
TransactionId pd_multi_base; /* base value for multixact IDs on page */
ShortTransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */
uint32 pd_magic; /* magic number identifies type of page */
} HeapPageSpecialData;
которая занимает 24 байта в конце каждого блока.
В Postgres Pro эксплуатируется патч немного отличный от того, который который предложен в сообщество. Предполагаю, что это сделано не специально, так как 24-байтная структура использовалась до первой версии патча. Упоминания о полях pd_prune_xid
, pd_magic
и макросах HEAP_PAGE_MAGIC, SEQ_PAGE_MAGIC присутствуют с первой по четвертую версию патча Короткова, то есть поля были созданы и убраны им самим. Коротков убрал лишние поля, после того, как устранил проблемы с блоками последовательностей, для которых эти поля и были добавлены. Причина того, что в Postgres Pro используется структура из бета-версии патча неизвестна. Могу предположить, что эти поля остались в Postgres Pro из-за того, что были уже вставлены, убрать эти поля оказалось сложно, поля остались для совместимости с ранними версиями патча, чтобы клиенты могли обновляться на новые версии без обновления всех блоков с 24-битной структуры на 16-битную. Возможно, решили оставить лишние 8 байт на будущее, вдруг пригодятся для хранения чего-нибудь связанного с компрессией.
Артефакты двух полей присутствуют в патче Короткова с 1 по 4 версию не только в комментариях, но и в коде патча:pd_magic - magic number identifies type of page
HEAP_PAGE_MAGIC
Коротков правильно убрал лишние поля, так как неоправданное использование магических номеров считается плохим стилем программирования, а сообщество старается поддерживать качество кода.

Вопросы, которые можно было бы задать
Зачем выложили обновленный патч, а не скрывали его как рецепт Кока-Колы?
Переносить патч на новые версии PostgreSQL сложно. При каждом переносе код патча у каждого производителя расходится. Заниматься переносом - не самый творческий труд и разработчику может надоесть тратить время на это, он захочет заняться более интересными задачами. Например, Коротков, автор патча, перестал им заниматься и занялся OrioleDB. После переноса патчей нужно переносить остальные доработки форков, так как патч затрагивает почти весь код. Для компаний-производителей необходимость переносить каждый год патчи - это риски, так как все клиенты используют сборки с патчем. Можно обучить новых разработчиков переносу, но вероятность добавления бага при переносе возрастает, нагрузка на техническую поддержку производителя может резко возрасти.
С другой стороны, сложность переноса патча полезна тем, что создаёт барьер для появления новых форков. Но это не перевешивает пользу от принятия патча, так как есть и другие сложные патчи типа поддержки автономных транзакций и без квалифицированных разработчиков, которых немного, форки не смогут выполнять ребейзинг.
Стоит ли переходить с 64-битного счетчика на 32-битный? Не стоит, так как есть вероятность появления или присутствия багов, проявляющихся при wraparound, а это опасность полной остановки обслуживания.
Можно ли было в принципе ожидать, что патч был бы закоммичен?
Даже с маленьким патчем, находящимся сейчас на коммитфесте, есть проблема - сначала он не компилировался под одну из операционных систем. Не то чтобы MacOS была нужна, компиляция под малораспространённые операционные системы это показатель качества патчей и простой способ выявить ошибки.

Текущее состояние этого патча https://commitfest.postgresql.org/patch/5205/ - вообще перестал компилироваться и "Needs rebase!":

По обсуждениям патча можно убедиться, что подход сообщества взвешенный и нет неприятия патча. Например, Александр Алексеев из TimescaleDB потратил много усилий, перенося патч. Павел Борисов из Supabase активно дорабатывал патч. Другие члены сообщества пытались посмотреть части, в которых разбирались.
Просмотеть 720Кб нереально, мало кто заставит себя даже открыть патч такого размера. Также стоит устранить проблему долгих транзакций, о которой писалось в статье 2022 года "мы уже работаем над устранением этого ограничения. И, возможно, мы сможем от него избавиться в ближайшее время".
Скорее всего, путь выбранный сообществом - выделять части и коммитить их отдельно разумен.

Является патч качественным и заслуживает ли коммита? Это может сказать тот, кто его просматривал, например, Евгений Воропаев и Максим Орлов. Коротков написал своё мнение со списком проблем, которые нужно устранить. Если баги продолжают выявляться, то по моему мнению, такой патч вряд ли закоммитят.
Нужен ли вообще 64-битный счетчик, может 32-разрядный с freezом достаточен? В Oracle Database использовался 48-битный SCN, начиная с версии 12.2 стал использоваться 64-битный SCN. Наверное, и PostgreSQL стоит перейти на 64-битный счетчик.
Можно ли закоммитить патч и начать жить в "новых реалиях"? Можно, но вряд ли интересно сообществу по многим причинам. Например, сообщество не поощряет неестественные потребности пользователей PostgreSQL. Их и Oracle не поощряет: есть "reasonable SCN limit" (разумная частота транзакций), которая до 19 версии Oracle Database составляла 16384 транзакций в секунду, после - 98304 транзакции в секунду, что равно 2 млрд. транзакций за 6 часов, если бы они генерировались 2008 года. Если генерация началась позже, то ошибки в выдаче SCN начнутся, при большей частоте транзакций. При применении патча будут проблемы с 32-битными версиями PostgreSQL - неатомарность доступа к 64-битным значениям в разделяемой памяти. Вдруг появится и станет популярной какая-нибудь 32-битная встраиваемая архитектура процессоров (SoC) и PostgreSQL сможет найти себя там.
Заключение
В статье рассмотрена история патча, вводящего 64-битные транзакции в PostgreSQL. Возможно, патча дошел до того состояния, когда в нем были выявлены все существенные ошибки. Возможно, несущеcтвенные части патча будут приняты в будущие версии PostgreSQL.
benjik
README так и не появился, судя по гитхабу?
OlegIct Автор
не появился :) Какой из этого я бы сделал вывод: как работают мультитранзакции (зачем там два файла member и offset) никто не знает или знает, но не может описать.