7.3. Распределённые транзакции

7.3.1. Видимость и CSN

7.3.1.1. CSN — порядковый номер фиксации

Кластер Shardman использует механизм изоляции снимков для распределённых транзакций. Механизм предоставляет способ синхронизации снимков между различными узлами кластера и способ атомарной фиксации такой транзакции по отношению к другим параллельным глобальным и локальным транзакциям. Эти глобальные транзакции можно координировать, используя предоставленные SQL-функции или посредством postgres_fdw, который прозрачно использует эти функции на удалённых узлах.

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

Предположим, что CSN — это текущее физическое время на узле, и назовём его GlobalCSN. Если физическое время на разных узлах идеально синхронизировано, то такой снимок, полученный на одном узле, можно использовать на других узлах для обеспечения необходимого уровня изоляции транзакций. Но, к сожалению, физическое время никогда не бывает идеально синхронизировано и может смещаться, и это следует учитывать. Кроме того, в распределённой среде нет простого понятия блокировки или атомарной операции, поэтому атомарность фиксации на разных узлах в отношении одновременного получения снимков должна каким-то образом обрабатываться. Эта проблема решается следующим образом:

  1. Для достижения атомарности фиксаций разных узлов вводится промежуточный шаг: при первом запуске транзакция помечается как InDoubt на всех узлах, и только после этого каждый узел фиксирует её и ставит метку с заданным GlobalCSN. Все обработчики, столкнувшиеся с кортежами транзакции InDoubt, должны дождаться её завершения и перепроверить видимость.

  2. Когда координатор помечает транзакции как InDoubt на других узлах, он собирает ProposedGlobalCSN от каждого участника, которые представляют собой местное время на этих узлах. Затем он выбирает максимальное значение всех ProposedGlobalCSN и фиксирует транзакцию на всех узлах с этим максимальным GlobalCSN, даже если это значение больше, чем текущее время на этом узле из-за смещения. Таким образом, GlobalCSN для данной транзакции будет одинаковым на всех узлах. Каждый узел записывает свой последний сгенерированный CSN (last_csn) и не может генерировать CSN > last_csn. Когда узел фиксирует транзакцию с CSN > last_csn, last_csn настраивается для записи этого CSN. Благодаря этому механизму узел не может сгенерировать CSN, который меньше, чем CSN уже совершённых транзакций.

  3. Если локальная транзакция импортирует сторонний глобальный снимок с некоторым GlobalCSN и текущее время на этом узле меньше, чем входящего GlobalCSN, транзакция должна дождаться, когда время этого GlobalCSN наступит локально.

Два последних правила обеспечивают защиту от смещения по времени.

7.3.1.2. Задержка фиксации и внешняя согласованность

Соблюдение вышеприведённых правил по-прежнему не гарантирует актуальность снимков, созданных на узлах, которые не участвуют в транзакции. Операция чтения из такого узла может получить устаревшие данные. Вероятность аномалии напрямую зависит от рассогласования системных часов в кластере Shardman.

Особое внимание следует уделить синхронизации системных часов на всех узлах кластера. Значение рассогласования часов должно быть измерено. Если требуется внешняя согласованность, то такое рассогласование можно компенсировать задержкой фиксации. Эта задержка добавляется перед каждой фиксацией в системе, поэтому она увеличивает задержку транзакций. Эта задержка не влияет на транзакции только для чтения. Задержку можно установить при помощи параметра конфигурации csn_commit_delay.

7.3.1.3. Карта CSN

Описанный выше механизм видимости CSN не является общим способом проверки видимости всех транзакций. Он используется для обеспечения изоляции только распределённых транзакций. В результате каждый узел кластера использует механизм проверки видимости, основанный на xid и xmin. Чтобы иметь возможность использовать снимок CSN, указывающий на прошлое, нужно сохранить старые версии кортежей на всех узлах и, следовательно, отложить их очистку. Для этого каждый узел в кластере Shardman поддерживает сопоставление CSN и xid. Карта называется CSNSnapshotXidMap. Эта карта является циклическим буфером и хранит соответствие между текущими snapshot_csn и xmin в разреженном виде: snapshot_csn округляется до секунд (и здесь используется тот факт, что snapshot_csn — это просто метка времени), а xmin хранится в циклическом буфере, где округлённое значение snapshot_csn действует как смещение из текущего циклического буфера. Размер циклического буфера управляется параметром конфигурации csn_snapshot_defer_time. VACUUM не может очищать кортежи, у которых xmax новее, чем самый старый xmin в CSNSnapshotXidMap.

Когда приходит снимок CSN, проверяется, что его snapshot_csn всё ещё находится в карте, в противном случае приходит сообщение об ошибке «snapshot too old» (снимок слишком старый). Если snapshot_csn успешно сопоставлен, xmin серверной части заполняется значением из карты. Таким образом можно учитывать обслуживающие процессы с импортированным снимком CSN, и старые версии кортежей будут сохранены.

7.3.1.4. Сокращение карты CSN

Для поддержки глобальных транзакций каждый узел хранит старые версии кортежей не менее csn_snapshot_defer_time секунд. При больших значениях csn_snapshot_defer_time это отрицательно сказывается на производительности и связано с тем, что узлы сохраняют все версии строк в течение последних csn_snapshot_defer_time секунд, но в кластере может не быть больше транзакций, которые могут их прочитать. Специальная задача monitor периодически пересчитывает xmin в кластере и устанавливает для него на всех узлах минимально возможное значение. Она позволяет процедуре очистки удалить версию строки, которая больше не представляет интереса для какой-либо транзакции. Параметр конфигурации shardman.monitor_trim_csnxid_map_interval управляет рабочим процессом. Рабочий процесс активируется каждые monitor_interval секунд и выполняет следующие операции:

  1. Проверяет, является ли идентификатор группы репликации текущего узла наименьшим среди всех идентификаторов в кластере. Если это условие не выполняется, то работа на текущем узле прекращается. Таким образом, только один узел в кластере может определять глобальный горизонт.

  2. С каждого узла кластера Shardman координатор собирает самый старый снимок CSN среди всех активных транзакций на узле.

  3. Координатор выбирает наименьшее значение CSN и отправляет его каждому узлу. На каждом узле отбрасываются все значения csnXidMap, которые меньше значения этого CSN.

7.3.2. Разрешение двухфазных и подготовленных транзакций

Shardman реализует протокол двухфазной фиксации для обеспечения атомарности распределённых транзакций. Во время выполнения распределённой транзакции узел-координатор отправляет команду BEGIN узлам-участникам для инициирования их локальных транзакций.

Термин «узлы-участники» здесь и далее относится к подмножеству узлов кластера, которые участвуют в выполнении команды транзакции, пока узел занят записью.

Кроме того, на узле-координаторе создаётся локальная транзакция. Это гарантирует наличие соответствующих локальных транзакций на всех узлах, участвующих в распределённой транзакции.

Во время двухфазной фиксации транзакции узел-координатор отправляет команду PREPARE TRANSACTION узлам-участникам, чтобы инициировать подготовку их локальных транзакций к фиксации. Если подготовка прошла успешно, локальные данные транзакции сохраняются в хранилище на диске, что делает их постоянными. Если все узлы-участники сообщают об успешной подготовке узлу-координатору, узел-координатор фиксирует свою локальную транзакцию. Впоследствии узел-координатор также зафиксирует ранее подготовленные транзакции на узлах-участниках, используя команду COMMIT PREPARED.

Если во время выполнения команды PREPARE TRANSACTION на любом из узлов-участников происходит сбой, распределённая транзакция считается прерванной. Затем узел-координатор транслирует команду отмены ранее подготовленных транзакций ROLLBACK PREPARED. Если локальная транзакция уже была подготовлена, она прерывается. Однако если подготовленной транзакции с указанным именем не было, команда отката просто игнорируется. Впоследствии узел-координатор откатывает свою локальную транзакцию.

После успешной фазы подготовки на каждом из узлов-участников будет объект prepared transaction. Эти объекты фактически являются файлами на диске и записями в памяти сервера.

Возможно наличие подготовленной транзакция, которая была создана ранее посредством двухфазной операции и никогда не будет завершена. Это может произойти, например, если узел-координатор выходит из строя сразу после этапа подготовки, но до этапа фиксации. Это также может произойти в результате проблем с сетевым подключением. Например, если команда COMMIT PREPARED, переданная узлом-координатором узлу-участнику завершается ошибкой, локальные транзакции будут зафиксированы на всех узлах-участниках, кроме узла с ошибкой. Локальная транзакция также будет зафиксирована на узле-координаторе. Все участники, кроме одного с ошибкой, считают, что распределённая транзакция завершена. Однако один участник, всё ещё ожидающий команду COMMIT PREPARED, никогда не получит её, в результате чего подготовленная транзакция никогда не будет завершена.

Подготовленная транзакция потребляет системные ресурсы, такие как память и дисковое пространство. Незавершённая подготовленная транзакция заставляет другие транзакции, которые обращаются к строкам, изменённым этой транзакцией, ожидать завершения распределённой операции. Поэтому необходимо завершить подготовленные транзакции, даже в тех случаях, когда во время фиксации были сбои, чтобы высвободить ресурсы и обеспечить возможность выполнения других транзакций.

Для решения этой проблемы существует механизм разбора подготовленных транзакций, реализованный в составе Shardman monitor. Он реализован как фоновый рабочий процесс, который периодически активируется, действуя как внутреннее задание «crontab». По умолчанию период активации установлен на 5 секунд, но его можно настроить в параметре конфигурации shardman.monitor_dxact_interval. Рабочий процесс проверяет наличие подготовленных транзакций, которые были созданы ранее, через определённое время, указанное в параметре конфигурации shardman.monitor_dxact_timeout (который по умолчанию также равен 5 секундам), на том же узле где работает Shardman monitor.

Когда команда PREPARE TRANSACTION отправляется узлу-участнику, подготовленной транзакции присваивается специальное имя. Это имя содержит полезную информацию, позволяющую идентифицировать узел-координатор и его локальную транзакцию.

Если Shardman monitor обнаруживает устаревшие подготовленные транзакции, он извлекает идентификатор группы репликации координатора и идентификатор локальной транзакции координатора. Затем monitor отправляет запрос координатору

SELECT shardman.xact_status(TransactionId)

который запрашивает текущий статус локальной транзакции координатора. Если запрос завершается ошибкой, например из-за проблем с сетевым подключением, то подготовленная транзакция останется нетронутой до следующего раза, когда monitor будет активирован.

В случае успешного запроса узел-координатор может ответить с одним из следующих статусов:

committed

Локальная транзакция на узле-координаторе успешно завершена. Поэтому Shardman monitor также фиксирует эту подготовленную транзакцию, используя команду COMMIT PREPARED.

aborted

Локальная транзакция на узле-координаторе была прервана. Поэтому monitor также прерывает эту транзакцию, используя команду ROLLBACK PREPARED.

unknown

Транзакция с таким идентификатором никогда не существовала на узле-координаторе. Поэтому monitor прерывает эту транзакцию командой ROLLBACK PREPARED.

active

Локальная транзакция на узле-координаторе всё ещё находится где-то внутри потока CommitTransaction(). Поэтому monitor ничего не делает с этой транзакцией и повторит попытку с этой транзакцией при следующей активации.

ambiguous

Этот статус может быть возвращён, когда усечение CLOG включено на узле-координаторе. CLOG — это растровое изображение, в котором хранится состояние завершённых локальных транзакций. Когда транзакция фиксируется или прерывается, её статус отмечается в CLOG. Однако CLOG может быть усечён (собран мусор) процессом VACUUM, чтобы отбросить статусы старых транзакций, которые не влияют на видимость данных для любой существующей транзакции.

Когда CLOG усекается, существует вероятность того, что функция shardman.xact_status() не сможет однозначно решить, существовала ли транзакция в прошлом (с некоторым статусом) или никогда не существовала. В таких случаях функция возвращает неоднозначный статус. Это может привести к неопределённости фактического статуса транзакции и затруднить разбор подготовленной транзакции.

Когда функция shardman.xact_status() возвращает статус ambiguous для подготовленной транзакции, узел monitor вносит в журнал предупреждение, указывающее, что статус нельзя определить однозначно. Подготовленная транзакция остаётся нетронутой, и monitor снова попытается использовать эту транзакцию при следующей активации. Важно установить правильное значение параметра min_clog_size со значением 1024000 (что означает «никогда не усекать CLOG»), чтобы избежать неоднозначности в статусе подготовленных транзакций.

В ситуациях, когда механизм разбора подготовленных транзакций не может разобрать подготовленные транзакции из-за постоянных ошибок или неоднозначного статуса, администратору потребуется вручную вмешаться в разбор этих транзакций. Для этого может потребоваться изучить журналы сервера и выполнить операции отката или фиксации для подготовленной транзакции вручную. Обратите внимание, что оставление подготовленных транзакций неразобранными может приводить к проблемам с потреблением ресурсов и производительностью, поэтому важно устранять такие ситуации как можно скорее.