MVCC в Oracle и PostgreSQL
Многоверсионность (multiversion concurrency control) – один из возможных способов организации доступа к данным. Из четырех классических требований ACID к управлению транзакциями этот механизм имеет непосредственное отношение к атомарности (транзакция либо выполняется полностью, либо полностью отменяется), согласованности (транзакция сохраняет целостность данных) и изоляции(одновременно выполняющиеся транзакции не должны влиять друг на друга).
Механизм состоит в поддержке на низком уровне одновременно нескольких версий данных. Транзакции не видят этого; они работают соснимком, который из многих версий составляет согласованную на определенный момент времени картину данных. В зависимости от уровня изоляции, снимок может определяться в момент начала транзакции (уровени repeatable read, serializable) или отдельно для каждой операции (уровень read committed).
Таким образом, транзакции смотрят на данные через призму снимков и могут видеть разную (но согласованную) информацию. Разумеется, снимок не является полной физической копией всех данных: это только логическое представление, и его можно организовать по-разному. Простой способ состоит в полном ограничении одновременного доступа: и изменений, и чтений. Но при эффективной реализации – как в Постгресе и Оракле – читающая транзакция никогда не будет заблокирована другими транзакциями, читающими или изменяющими те же данные – каждая из них будет независимо работать со своей версией. Блокироваться будут только попытки изменить данные, которые уже изменены другой транзакцией, но еще не зафиксированы.
Внутренние детали реализации в двух системах существенно отличаются. Постгрес хранит в блоке все варианты строк, которые получаются при их изменении и даже удалении. Снимок определяет, какая именно из имеющихся версий строки должна быть видна. Время от времени блоки очищаются от тех версий, которые больше не видны никому. В Оракле в блоке находятся только актуальные на определенный момент строки, а вместо непосредственного хранения предыдущих вариантов формируется журнал отката. Если блок не соответствует снимку, изменения в нем откатываются с помощью журнала, формируя новую версию этого блока.
Далее оба подхода рассмотрены более подробно, насколько это необходимо для выявления сути отличий. В конце статьи приведены документы, в которых можно познакомиться со многими опущенными здесь деталями.
В реализациях особенно интересны несколько моментов:
- Чем определяется «момент времени», как упорядочены события в системе?
- Что является объектом многоверсионности? Несколько версий чего именно поддерживается в системе?
- Как организована многоверсионность на низком уровне?
- Как происходит фиксация и отмена изменений?
- Как устроен снимок данных?
Постгрес использует последовательные номера транзакций; эти номера и определяют порядок событий.
Единицей многоверсионности служат строки таблиц. Табличный блок содержит набор версий строк (tuples), для каждой из которых хранятся номера двух транзакций: начальной (xmin) и конечной (xmax).
При вставке строки номер транзакции записывается в нее как начальный. При удалении строки она не стирается из блока; вместо этого номер удаляющей транзакции записывается как конечный. Обновление строки работает просто как удаление и вставка новой. Таким образом, внутри одного блока могут находиться разные версии одной и той же строки, причем известно, когда версия появилась и когда она исчезла.
Индексные блоки не содержат никакой информации о версионности, записи в индексах ссылаются на каждую из версий строк.
В системе имеется список статусов всех транзакций (CLOG). Фиксация или отмена транзакций выполняются изменением статуса в этом списке. Начальный и конечный номера транзакций в строках могут стать неактуальными, например, если транзакция была отменена. Затронутые блоки исправляются не сразу – это было бы слишком накладно, – а позже, сверяясь с CLOG.
Снимок данных состоит из ближайшего номера транзакции (который определяет текущий момент времени) и списка активных транзакций, еще не завершенных на данный момент. Когда транзакция читает данные, она должна увидеть в блоке только те строки, которые уже были зафиксированы и еще не были удалены в момент создания снимка (а также строки, созданные самой транзакцией). Информации в снимке и строках как раз достаточно, чтобы вычислить это условие.
В Оракле за упорядоченность отвечает SCN (system change number) – счетчик, увеличивающийся как минимум при каждой фиксации или откате изменений.
Единицей многоверсионности служит блок; он всегда содержит актуальный на некоторый момент набор строк. При любом изменении данных в блоке (будь то вставка, удаление или обновление) в журнал отката записывается минимальная информация, необходимая для отмены этих изменений. При вставке строки это указание о ее удалении, при изменении – старое значение изменившихся полей, и только при удалении – вся строка полностью. Номер транзакции не является последовательным, а представляет собой ссылку на цепочку записей в журнале отката. Точно так же обстоит дело и с блоками индексов.
Каждый блок, будь то табличный или индексный, содержит ITL (interested transactions list) – список транзакций, изменяющих этот блок, с указанием их статуса, момента начала и окончания. Эта информация определяет SCN блока – момент, на который данные в блоке актуальны. Размер ITL ограничен, но его элементы могут использоваться повторно, как только соответствующая транзакция завершится.
Снимок данных определяется исключительно значением SCN. Чтобы получить в блоке согласованные данные, сначала откатываются изменения незафиксированных транзакций, если такие присутствуют в ITL. Затем одно за другим откатываются изменения уже зафиксированных транзакций, пока SCN блока не достигнет заданного снимком значения. Чтобы не повторять каждый раз эти действия, новая версия блока сохраняется в буферном кэше и используется транзакциями, пока не будет вытеснена.
Фиксация изменений выполняется простой сменой статуса транзакции в журнале отката. А вот отмена транзакции требует отката всех ее изменений, и это может занять столько же времени, сколько уже было потрачено на выполнение транзакции. И в том, и в другом случае статус транзакции в ITL измененных блоков может быть исправлен не сразу, а при первом обращении к блоку.
Таковы основные идеи, заложенные в двух реализациях. Безусловно, в каждой есть свои тонкости, свои плюсы и минусы. Вот некоторые из них.
Переполнение счетчика. Под номер транзакции в Постгресе отведено 32 бита, поэтому в нагруженной системе вполне реально получить переполнение. Более того, поскольку в CLOG хранится список всех транзакций (хоть и очень компактный), увеличение разрядности привело бы к дополнительному расходу места на диске. Решение состоит в том, что CLOG считается кольцевым буфером, перезаписываются только старые транзакции, уже не влияющие на изоляцию, а точка «начала отсчета» периодически сдвигается вперед.
Разрядность SCN в Оракле составляет 48 бит, что позволяет не заботиться о переполнении.
Хранение дополнительных версий требует дискового пространства. Особенно остро вопрос стоит для Постгреса, ведь хранить приходится полные версии строк, а также ссылающиеся на них записи в индексных блоках. Чтобы освободить место, необходимо периодически очищать блоки от тех версий, которые не видны ни одной транзакции. Это действие может происходить как при обращении к блоку (что приводит к выполнению транзакцией «не своей» работы), так и на периодической основе в специальном процессе (VACUUM). Большое значение имеет оптимизация (HOT update), позволяющая не создавать записи в индексных блоках, если в новой версии строки не изменились проиндексированные поля, а заодно и выполняющая частичную очистку в рамках одного блока таблицы.
В Оракле размер данных, необходимых для поддержки версионности, несколько меньше за счет использования журнала отката. Но есть и проблема: место под журналы отката ограничено и поэтому журнал уже зафиксированной транзакции может быть перезаписан. Это может привести к невозможности отката блока до SCN снимка, особенно в случае долгоиграющей транзакции на фоне большой активности в системе (ошибка «snapshot too old»).
Индексы Постгреса не содержат информации о версионности, и по одному только индексу невозможно определить, какие значения попадают в снимок. Наличие «карты видимости», в которой отмечены гарантированно видимые всем транзакциям строки, позволяет методам доступа на основе индекса (index-only scans) работать эффективно, но в ряде случаев все равно приходится заглядывать в таблицу. Ораклу проще: в плане поддержки многоверсионности индекс ничем не отличается от таблицы.
Фиксация и отмена транзакций в Постгресе выполняются одинаково быстро. В случае Оракла это верно только для фиксации; отмена – затратная операция.
Блокировки в Оракле устроены несколько более сложно. Перед тем, как первый раз изменить блок, транзакция должна записаться в ITL, а размер этого списка ограничен. Если с блоком уже работает максимально возможное количество транзакций, следующей придется ожидать освобождения элемента ITL. Это не только ограничивает степень параллелизма, но и может привести к неожиданным взаимоблокировкам.
Снимок данных Постгреса содержит список незавершенных транзакций, но эта информация доступна только на текущий момент. Это означает невозможность получить данные, согласованные на произвольный момент в прошлом, если в тот момент не был сделан снимок.
В Оракле снимок определяется только значением SCN, поэтому согласованные данные можно получить на любой момент в прошлом (flashback query), лишь бы в сегментах отката оставалась информация, достаточная для построения необходимых версий блоков данных.
Источники информации
Постгрес
-Документация: http://www.postgresql.org/docs/current/static/mvcc-intro.html
-Презентация Брюса Момджана: http://momjian.us/main/writings/pgsql/mvcc.pdf
Оракл
-Документация http://docs.oracle.com/database/121/CNCPT/part_txn.htm
-Jonathan Lewis, «Oracle Core: Essential Internals for DBAs and Developers»