Состояния транзакций, сборка мусора, интересующиеся и активные транзакции, sweep, примеры

Автор: Ann Harrison, Harbor
Перевод: Дмитрий Кузьменко
Оригинальный текст: oitoat.txt
 
Внимание! Документ устарел, и хотя и содержит верные сведения, к прочтению не рекомендуется. Вместо него следует читать более современные и подробные статьи:

Пояснение для новичков – когда я говорю "транзакция", я имею в виду набор действий над базой данных, завершающихся Commit (подтверждением), Rollback (отменой), Prepare/Commit (подготовкой к подтверждению при two phase commit), и в том числе отсоединением от базы данных (обрыв связи, выключение клиентского компьютера и т. п.). Простое действие, как вставка, изменение или удаление записи, является оператором. Многие инструменты обеспечивают автоматическую поддержку транзакций, поэтому вам может быть неизвестно реальное количество транзакций, выполняемых при работе с данными. Но любой инструмент, производящий подтверждение (commit) на каждый оператор, не является лучшим выбором при импорте данных в БД.
 

Состояния транзакций

Транзакции могут находиться в четырех состояниях: активном, подтвержденном, limbo (подтвержденное но не зафиксированное по TPC) и отмененном.

Рассмотрим каждое состояние подробнее от самого сложного до самого простого:

Limbo: Транзакция, стартовавшая в режиме 2PC (two phase commit) вызовом процедуры Prepare. Эта транзакция может быть живой или нет. В любой момент такая транзакция может возобновиться и запросить подтверждение или отмену. Изменения, произведенные транзакцией, оставшейся в состоянии in limbo не могут быть приняты или игнорированы, соответственно они не могут быть удалены из БД.

Подтвержденное: Транзакция, которая завершила всю свою работу успешно
  1. вызовом процедуры COMMIT
  2. вызовом процедуры Rollback, но не произведя никаких изменений в БД.
В любом случае транзакция завершилась, и никогда не возобновится снова. Ее изменения теперь являются частью корректного состояния БД.

Отмененное: Транзакция, которая
  1. завершилась процедурой ROLLBACK, т. е. запросила удалить все произведенные ею изменения из БД.
  2. была помечена как Активная, но была обнаружена в неживом состоянии другой транзакцией, которая и помечает ее как отмененную.
В любом случае, изменения, сделанные такой транзакцией, должны быть игнорированы и удалены из базы данных

Активное: Такое состояние имеет транзакция, которая
  1. не стартовала.
  2. стартовала, но еще не завершилась.
  3. стартовала, но не закончилась вызовом любой процедуры завершения. (Например, из-за сбоя питания, обрыва соединения и т. п.)
 

Как транзакции узнают о состояниях друг друга?

Состояние каждой транзакции хранится на Transaction Inventory Page (TIP). Единственным измененим БД при подтверждении транзакции является смена состояния этой транзакции с Активной на Подтвержденную. Когда транзакция вызывает процедуру отмены, она проверяет свой Update Flag – если он не установлен, то значит никаких изменений БД не было произведено, и нужно сделать Подтверждение (COMMIT) вместо Отмены (ROLLBACK). Таким образом, отмена read-only транзакций не нагружает БД.

Каким образом транзакция переходит из Активного состояния в Отмененное если завершение происходит по сбою?

Это может произойти двумя путями.
  1. Когда транзакция стартует, она делает блокировку собственного идентификатора транзакции. Если транзакция B пытается изменить или удалить запись, и обнаруживает что версия записи была создана транзакцией A, состояние в TIP которой Активное, транзакция B пытается вызвать конфликт блокировки идентификатора транзакции A. Если блокировка прошла, то транзакция B решает что A умерла, и меняет состояние A в TIP с Активного на Отмененное.
  2. Когда транзакция стартует, она проверяет, можно ли установить полную блокировку на БД. Вообще транзакции при работе устанавливают разделяемые блокировки на БД. Следовательно, если транзакции удается поставить полную блокировку, то других транзакций нет, и она конвертирует все Активные состояни в TIP на Отмененные.
 

Мусор

Borland InterBase – СУБД с многоверсионностью данных. Когда запись изменяется, на страницу данных помещается ее копия с новыми значениями, однако старая запись остается. Старое значение называется "Back Version" (резервная версия), и является "историей отката" – если транзакця, изменившая запись, отменится, то старая версия записи тут как тут, на своем старом месте. Кроме этого, старые версии обеспечивают уровень изоляции Repeatable Read (воспроизводимое чтение) для длинных транзакций, которым на все время действия нужно видеть данные, существовавшие на момент начала такой транзакции.

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

Сборка мусора

Сборка мусора предотвращает наполнение интенсивно обновляемой БД ненужными старыми версиями записей. Также уничтожаются версии записей, созданные отмененными транзакциями (Rolled Back). Каждая транзакция участвует в сборке мусора, в том числе и только читающие (read-only) транзакции.

Когда транзакция считывает запись, на самом верхнем уровне она получает эту запись (это относится к любой записи в БД). Двумя уровнями ниже, где-то в недрах сервера, Borland InterBase вытаскивает с диска набор версий. (Прим. пер.: поскольку версии записей хранятся на страницах данных, а механизм работы со страницами оптимизирован для работы с версиями, лишних операций кроме чтения страниц данных не происходит). Каждая версия имеет идентификатор создавшей ее транзакции. Первой в списке версий идет самая старая запись. В этот момент сервер имеет две задачи:
  1. выдать правильную версию записи запросившей ее транзакции
  2. удалить все версии, являющиеся мусором – т. е. те версии, которые были созданы отмененными транзакциями или слишком старые версии записей, чтобы в них кто-то был заинтересован.

Существует и третий случай сборки мусора, который происходит в этот же момент. Кроме многоверсионных обновлений сервер использует и многоверсионные удаления. Когда транзакция удаляет запись, должна ли такая запись быть сразу удалена? Конечно нет! Удаление ведь может быть отменено. Поэтому вместо удаления записи, сервер выставляет метку удаления для старой версии записи. Если рано или поздно транзакция завершится, то вся запись вместе с меткой удаления и со своими предыдущими версиями станет мусором, и... (вы угадали!) когда-нибудь будет физически удалена.
 

Сборка мусора – итог

Сборка мусора является кооперативной. Это означает что все транзакции участвуют в ней (а не какая-то специально выделенная команда мусорщиков). Старые версии, удаленные записи, и отмененные изменения (и добавления) уничтожаются, когда транзакция пытается прочитать запись. (Прим. пер.: именно этим объясняются "странные" задержки в отработке запросов, например если вы выдали SELECT на таблице, из которой другой человек прямо перед вами удалил тысяч сто (100000) записей – вашей транзакции выпало "счастье" убирать мусор за предыдущей транзакцией).

Периодическое архивирование БД (backup) также производит сборку мусора, поскольку считывает абсолютно все записи из БД. (Прим. пер.: если в момент backup есть активные транзакции, то некоторый мусор после их завершени безусловно останется. если не хотите, чтобы backup собирал мусор, указывайте ключ -g в командной строке gbak).
 

Старейшая заинтересованная транзакция (OLDEST INTERESTING TRANSACTION)

Для того чтобы определить, какие версии записей могут быть удалены при сборке мусора, и какие изменения отменены и могут быть игнорированы, каждая транзакция имеет "маску" "заинтересованых" транзакций". Транзакция является "заинтересованной" по отношению к другой транзакции, если она конкурирует с ней – т. е. ее изменения не подтверждены, или она отменена (ее изменения должны игнорироваться или такая транзакция в состоянии limbo).

"Маска транзакций" – это снимок состояний всех транзакций от старейшей заинтересованной (OIT) до текущей. Размер снимка зависит от количества транзакций, стартовавших с момента старта старейшей заинтересованной транзакции. (Прим. пер.: собственно, здесь речь идет про TIP).
 

Старейшая активная транзакция (OLDEST ACTIVE TRANSACTION)

Примечание KDV. То, что Ann здесь именует как Oldest Active Transaction, на самом деле всегда было и есть Oldest Snapshot Transaction. См. www.ibase.ru/summary/, www.ibase.ru/ibtrans/.
Звучит просто, но на самом деле не очень. Старейшая активная транзакция это не старейшая, живущая до настоящего момента. И не старайшая транзакция помеченная как Активная в TIP. Это старейшая транзакция, которая была активной, когда началась старейшая активная в текущий момент транзакция. Читать такие выражения довольно трудно, и я не помню как это было сделано, но это так, и это работает.

(Прим. пер.: следующий абзац переводите сами)

Any record version behind a committed version created by a transaction older than the oldest transaction active when the oldest transaction currently active started is garbage and will never be needed ever again. That's pretty dense. Lets ignore the commit/rollback question briefly.

Простой случай: Я транзакция 20. Я нахожу запись созданную и подтвержденную транзакцией 15. Я изменяю ее и подтверждаю изменения. Вы – транзакция 25, и когда вы стартуете, вы являетесь единственной активной транзакцией. Вы читаете запись, и обнаруживаете что все активные транзакции (стартовавшие после вас) могут использовать версию записи, созданную мной, поэтому вы собираете мусор – оригинальную версию этой записи. В этом случае, право на сборку мусора (как старейшей активной транзакции) ваше.

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

Тяжелейший случай: Я транзакция 87, и когда я стартую, все транзакции до 75-ой завершились подтверждением, и все после 75-ой в настоящее время активны. Транзакция 77 модифицирует запись, созданную транзакцией 56. Я продолжаю читать версию 56-ой транзакции. Все нормально. Транзакция 77 завершается подтверждением. Вы – транзакция 95. Когда вы стартуете, я (87-ая) являюсь старейшей активной. Вы читаете запись созданную 56-ой и модифицированную 77-ой. Вы не можете собирать мусор для этой записи, поскольку я не могу читать записи созданные транзакцией после 74-ой (они еще не все завершены).

Надеюсь что вы теперь поняли, что понятие "старейшей активной" транзакции не так просто, как кажется.
 

Чистка (SWEEPING)

Чистка – это НЕ только сборка мусора. Основная работа, которую делает чистка, это перемещение старейшей заинтересованной транзакции "вверх", и уменьшение размера маски транзакций. Это делается переводом Отмененных транзакцией в Подтвержденные транзакции.

Вы скажете – "Она сошла с ума!"

Но это, действительно, основная работа чистки. Она удаляет все изменения, сделанные отмененными транзакциями, затем меняет их состояние на Подтвержденное. (Помните, было сказано, что отмененные read-only транзакции получают состояние подтвержденных. Удалите изменения, и можно считать что транзакция завершилась подтверждением).

В то же время, чистка собирает мусор так же, как и любая другая транзакция.

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

Транзакции "in limbo"

Транзакции in limbo не могут быть переведены в другое состояние автоматически, будут приводить к постоянному включению чистки, и будут блокировать попытки изменения или удаления созданных ими версий. В любом случае Borland InterBase предоставляет хорошую диагностику при возникновении сбоев при two phase commit (утилита Server Manager).
 

Несколько примеров

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

Случай 1. Поток неконкурирующих транзакций



Транзакция 1 вставляет запись 1 и завершается подтверждением. Транзакция 2 стартует и становится одновременно старейшей активной и старейшей заинтересованной. Она вставляет запись 2 и завершается подтверждением. Транзакция 3 стартует, также становится старейшей активной и заинтересованной, вставляет свою запись и завершается подтверждением. В конце концов транзакция 1000000 стартует, опять становится старейшей активной и заинтересованной. Чистки не происходит (в общем-то чистить и нечего).

Случай 2. Готовится засада



Транзакция 1 стартует, оглядывается, и идет покурить. Транзакция 2 стартует, обнаруживает что 1-ая является старейшей активной и
заинтересованной, вставляет запись 1 и завершается подтверждением. Транзакция 3 стартует, обнаруживает что 1-ая все еще старейшая активна и заинтересованная, вставляет запись 2 и завершается подтверждением. В конце концов транзакция 1000001 стартует, видит что 1-ая все еще старейша активная и заинтересованная, т. е. разница между OAT и OIT равна 0, и завершается. Опять чистка не возникает.

Случай 3. Кто попадет в засаду?



Транзакция 1 стартует, что-то делает и идет покурить. Транзакция 2 стартует, обнаруживает что 1-ая является старейшей активной и заинтересованной, вставляет запись 1 и завершается. Транзакция 3 стартует, обнаруживает что 1-ая является старейшей активной и заинтересованной, вставляет запись 2 и завершается. Вдруг транзакция 1 отравляется никотином и умирает прямо в курительной комнате (допустим, не так страшно, а просто происходит обрыв связи между клиентским компьютером и сервером). Транзакция 15034 стартует (к счастью), получает возможность установить монопольную блокировку на файл базы данных, и устанавливает состояние транзакции 1 в Отмененное. Теперь старейшая заинтересованная имеет номер 1, но старейшая активная уже имеет номер 15034. Разница составляет 15033, поэтому уборка (sweep) не начинается. Через 4967 транзакций происходит уборка. После того как она закончена, идентификаторы старейшей заинтересованной и активной становятся равными, и следущий процесс уборки может начаться только если возникнет заинтересованная транзакция, перешедшая в неактивное состояние.

Случай 4. Смертельные качели

Предположим, что в нашей системе происходят парные транзакции, одна из которых завершается подтверждением, а другая откатом. Проблема в том, что транзакция, завершенная откатом, становится старейшей заинтересованной (OIT), а транзакция завершенная подтверждением – соответственно старейшей активной (OAT). Если после транзакции, завершившейся откатом, произойдет еще одна откатываемая транзакция, то она не станет OIT, поскольку предыдуща является "старейшей". Таким образом после даже единственной завершенной откатом транзакции, каждая последующая подтверждаемая транзакция будет увеличивать разницу между OIT и OAT. И через 20001 завершенных подтверждением транзакций действительно произойдет ЧИСТКА (SWEEP).
 

Итог

Итак, мы с вами выяснили к чему может привести вставка 1000000 записей с транзакцией на каждую запись. Может быть стоит выключить forced write (параметр БД в Server Manager)? Или настала пора запустить дефрагментацию диска?

Подпишитесь на новости Firebird в России

Подписаться