Snapshot oracle что это

Обновлено: 07.07.2024

Вчера-сегодня пришлось испытать на собственной шкуре, что такое версионность в СУБД, как работает READ_COMMITTED_SNAPSHOT в SQL Server и когда его включать не следует.

Что делает READ_COMMITTED_SNAPSHOT?

Для транзакций, выполняющихся на уровне изоляции READ COMMITTED, инструкция ALTER DATABASE [. ] SET READ_COMMITTED_SNAPSHOT ON изменяет стратегию избегания "грязного чтения". "Грязное чтение" это плохо — это когда читающая транзакция прочитывает данные незавершенной пишущей транзакции, то есть данные, которые в итоге могут быть отменены. Все промышленные СУБД (по умолчанию) запускают транзакции в режимах изоляции, исключающих "грязное чтение".

Microsoft SQL Server на уровне изоляции READ COMMITTED (уровень изоляции по умолчанию) может использовать две стратегии:

  1. При выключенной опции READ_COMMITTED_SNAPSHOT, SQL Server прибегает к разделяемой (shared) блокировке объектов, чтение которых производится транзакцией.
  2. При включенной опции READ_COMMITTED_SNAPSHOT, SQL Server для каждого изменения создает новую версию строки, возвращая читающим транзакциям ранее принятую (committed) версию, а изменяющей транзакции предоставляя собственную рабочую версию.

Есть направление священных войн (holywars) — "СУБД версионная, против СУБД с блокировками", где SQL Server противопоставляется Oracle или Firebird именно как СУБД, в которой целостность данных обеспечивается только блокировками. 10 лет назад это было так.

В чем плюс версионности (включенного режима READ_COMMITTED_SNAPSHOT)? Плюс в том, что читающие транзакции (а таких большинство) никогда не блокируют пишущие.

А минус? За версионность приходится платить ресурсами — в первую очередь ОЗУ (поэтому Oracle так любит память). Выключенная по умолчанию версионность, видимо, одна из причин, почему SQL Server может прекрасно работать на ограниченных ресурсах.

Теперь подтвержу практикой.

Исходная ситуация

Старый сервер с 2 CPU Intel Xeon, 8 ГБ ОЗУ и несколькими RAID массивами. На нем работает SQL Server, где база одного OLTP-приложения (сильно нагружена, в том числе job-ами синхронизации) и другие базы.

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

В данном случае, базы находятся на G:, журнал транзакций на T:, файл подкачки на S: и это разные RAID массивы.

Кроме того, на сервере находится второй экземпляр SQL Server, предназначенный для разработки. Память поделена поровну между основным инстансом и девелоперским. Хотя экземпляр для разработки используется, им можно пожертвовать в критической ситуации (это и потребовалось).

На сервере постоянно собираются (некоторые) данные со счетчиков производительности при помощи системного монитора.

Ситуация вчера

Ситуация сегодня

Проиллюстрирую картинкой системного монитора:

Голубой (частый вначале) сплошной график - Transactions/sec (масштаб 0,1).
Светло-голубой сплошной график - Lock waits/sec (масштаб 100,0).
Красный пунктирный график - Средняя длинна очереди записи на массив с базами (масштаб 10,0).
Красный сплошной график - Средняя длинна очереди чтения с массива с базами (масштаб 0,1).
Розовый сплошной график (с двумя заметными пиками) - Кол-во обращений к диску, где лежит файл подкачки (масштаб 10,0).
Синий пунктирный график (горизонтальный с единственным перепадом) - Total Server Memory KB (масштаб 0,00001).

Первое на что хочется обратить внимание — голубой график (количество транзакций), показывает сколько транзакций сервер смог провести, а не сколько требовалось. Если ресурсов не хватает, количество транзакций будет снижаться из-за ожиданий. Этот график удобно сравнивать с количеством компиляций SQL запросов, например, что бы понять, что не нагрузка падает, а мы проседаем, но тут и так всё понятно.

Первый пик светло-голубого графика (блокировки) приходится на 9 утра. Это можно объяснить тем, что с 9 утра начинают работать некоторые job-ы, которые ночью спят. Но я предполагаю, что блокировки, которые ими вызываются, относятся к другим базам, ведь наша основная база находится в режиме READ_COMMITTED_SNAPSHOT. Хотя возможно, что это блокировки операций записи между собой (из разных job-ов).

Где-то с 9:40, когда люди реально начинают работать, наблюдается снижение количества блокировок и снижение количества транзакций (специально подчеркиваю), зато растет очередь чтения с массива с базами (красный график).

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

Я предполагаю, что с 10 утра до 10:45 происходил пик получения отчетов, обычно у нас с утра есть такой, приблизительно в это время. Так или иначе, около 10:45 очередь чтения с массива с базами снизилась и количество проводимых транзакций подросло, однако, субъективно, было плохо — на сервере тормозило даже RDP.

В 11:16 я выключил READ_COMMITTED_SNAPSHOT — это видно по пику светло-голубого графика. Дальше пошли блокировки, они стали несколько плотнее чем до 9:40. Сервер субъективно растормозился (практически мгновенно), но вернулись вчерашние проблемы — иногда возникающее "Превышено время ожидания блокировки".

В 12:50, я выключил инстанс Developer Edition и удвоил память основному инстансу SQL Server, что показывает синий график (Total Server Memory). В результате, плотность и высота пиков светло-голубого графика (Lock waits/sec) упала, а голубой график (Transactions/sec) вырос.

Выводы

Механизм версионности при одинаковых нагрузках требует больше памяти. Если нет памяти, лучше решить вопрос установкой большего LOCK TIMEOUT (если приложение его выставляет).

Вопрос "что лучше: версионность или блокировки?" не решается, очевидно, в пользу версионности, вот почему:

Если памяти для СУБД мало — версионность работать будет, но пользоваться приложением будет нельзя (будет работать слишком медленно).

Если памяти для СУБД много — время блокировок само по себе сократится (из-за ускорения записи и сокращения очередей на дисках) и преимущество неблокирующего чтения сойдет на нет.

В итоге, версионная у вас СУБД или блокирующая неважно, требуется адекватный объем памяти. Единственный плюс блокирующей СУБД, во всяком случае SQL Server перед Oracle в том, что при маленькой нагрузке можно работать на небольшом объеме памяти.

Рассмотрев вопросы, связанные с изоляцией, и сделав отступление об организации данных на низком уровне, мы в прошлый раз подробно поговорили о версиях строк и проследили, как изменяется служебная информация в заголовке версии при различных операциях.

Сегодня мы посмотрим на то, как из версий строк получаются согласованные снимки данных.

Физически в страницах данных могут находиться несколько версий одной и той же строки. При этом каждая транзакция должна видеть только одну (или ни одной) версию каждой строки так, чтобы вместе они составляли согласованную в ACID-смысле картину данных на определенный момент времени.

Изоляция в PostgreSQL строится на основе снимков данных (snapshot): каждая транзакция работает со своим снимком данных, который «содержит» данные, которые были зафиксированы до момента создания снимка, и не «содержит» еще не зафиксированные на этот момент данные. Мы уже видели, что изоляция при этом получается более строгая, чем требует стандарт, но не лишенная аномалий.

На уровне изоляции Read Committed снимок создается в начале каждого оператора транзакции. Такой снимок активен, пока выполняется оператор. На рисунке момент создания снимка (который, как мы помним, определяется номером транзакции) показан синим цветом.


На уровнях Repeatable Read и Serializable снимок создается один раз в начале первого оператора транзакции. Такой снимок остается активным до самого конца транзакции.


Правила видимости

Конечно, снимок не является физической копией всех необходимых версий строк. Фактически снимок задается несколькими числами, а видимость версий строк в снимке определяется правилами.

Будет или нет данная версия строки видна в снимке, зависит от двух полей ее заголовка — xmin и xmax, — то есть от номеров создавшей и удалившей транзакций. Такие интервалы не пересекаются, поэтому одна строка представлена в любом снимке максимум одной своей версией.

Точные правила видимости довольно сложны и учитывают множество различных ситуаций и крайних случаев.

В этом легко убедиться, заглянув в src/backend/utils/time/tqual.c (в версии 12 проверка переехала в src/backend/access/heap/heapam_visibility.c).

Упрощая, можно сказать, что версия строки видна, когда в снимке видны изменения, сделанные транзакцией xmin, и не видны изменения, сделанные транзакцией xmax (иными словами, уже видно, что версия строки появилась, но еще не видно, что ее удалили).

В свою очередь, изменения транзакции видны в снимке, если либо это та же самая транзакция, что создала снимок (она сама видит свои собственные изменения), либо транзакция была зафиксирована до момента создания снимка.

Можно изобразить транзакции графически в виде отрезков (от момента начала до момента фиксации):


  • изменения транзакции 2 будут видны, потому что она завершились до создания снимка,
  • изменения транзакции 1 не будут видны, потому что она была активна на момент создания снимка,
  • изменения транзакции 3 не будут видны, потому что она начались позже создания снимка (не важно, закончилась она или нет).

Все, что мы можем — это узнать текущий статус транзакций при создании снимка. Эта информация есть в общей памяти сервера в структуре ProcArray, которая содержит список всех активных сеансов и их транзакций.

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

Из сказанного следует, что в PostgreSQL нельзя создать снимок, показывающий согласованные данные по состоянию на произвольное время назад, даже если все необходимые для этого версии строк существуют в табличных страницах. Часто приходится слышать вопрос, почему в PostgreSQL нет ретроспективных (или темпоральных; в Oracle это называется flashback query) запросов — вот одна из причин.

Забавно, что изначально такая функциональность была, но позже ее убрали из СУБД. Про это можно прочитать в статье Джозефа Хеллерштейна.

Итак, снимок данных определяется несколькими параметрами:

  • моментом создания снимка, а именно, номером следующей, еще не существующей в системе, транзакции (snapshot.xmax);
  • списком активных транзакций на момент создания снимка (snapshot.xip).

Также в снимке сохраняются еще несколько параметров, но они для нас не важны.


Пример

Чтобы посмотреть, как видимость определяется снимком, воспроизведем ситуацию с тремя транзакциями, рассмотренную выше. В таблице будут три строки, причем:

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

Первая транзакция (еще не завершилась):


Вторая транзакция (завершилась до создания снимка):


Создаем снимок в транзакции в другом сеансе.


Завершаем первую транзакцию после того, как создан снимок:


И третья транзакция (появилась позже создания снимка):


Очевидно, что в нашем снимке по-прежнему видна одна строка:


Вопрос в том, как это понимает PostgreSQL.

Все определяется снимком. Посмотрим на него:


Здесь через двоеточие перечислены snapshot.xmin, snapshot.xmax и snapshot.xip (в данном случае один номер, но в общем — список).

По сформулированным выше правилам, в снимке должны быть видны изменения, сделанные транзакциями с номерами snapshot.xmin <= xid < snapshot.xmax, за исключением попавших в список snapshot.xip. Посмотрим на все строки таблицы (в новом снимке):


Первая строка не видна — она создана транзакцией, которая входит в список активных (xip).
Вторая строка видна — она создана транзакцией, которая попадает в диапазон снимка.
Третья строка не видна — она создана транзакцией, которая не входит в диапазон снимка.

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

Для этого в заголовке версии строки есть специальное поле (которое отображается в псевдостолбцах cmin и cmax), показывающее порядковый номер операции внутри транзакции. Cmin представляет номер для вставки, cmax — для удаления, но для экономии места в заголовке строки это на самом деле одно поле, а не два разных. Считается, что вставка и удаление той же строки в одной транзакции выполняется редко.

Если же это все-таки происходит, то в то же самое поле вставляется специальный «комбо»-номер, про который обслуживающий процесс запоминает реальные cmin и cmax. Но это уже совсем экзотика.

Простой пример. Начнем транзакцию и добавим в таблицу строку:


Выведем содержимое таблицы вместе с полем cmin (но только для строк, добавленных нашей транзакцией — для других оно не имеет смысла):


Теперь откроем курсор для запроса, возвращающего число строк в таблице.


И после этого добавим еще одну строку:


Запрос вернет 4 — строка, добавленная после открытия курсора, не попадет в снимок данных:


Почему? Потому что в снимке учитываются только версии строк с cmin < 1.

Номер самой ранней из активных транзакций (snapshot.xmin) имеет важный смысл — он определяет «горизонт событий» транзакции. А именно, за своим горизонтом транзакция всегда видит только актуальные версии строк.

Действительно, неактуальную версию требуется видеть только в том случае, когда актуальная создана еще не завершившейся транзакцией, и поэтому пока не видна. Но за «горизонтом» все транзакции уже гарантированно завершились.


«Горизонт событий» транзакции можно увидеть в системном каталоге:


Также можно определить и «горизонт событий» на уровне базы данных. Для этого надо взять все активные снимки и среди них найти наиболее старый xmin. Он и будет определять горизонт, за которым неактуальные версии строк в этой БД уже никогда не будут видны ни одной транзакции. Такие версии строк могут быть очищены — именно поэтому понятие горизонта так важно с практической точки зрения.

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

А это означает, что неактуальные версии строк в этой БД не могут быть очищены. При этом «долгоиграющая» транзакция может никак не пересекаться по данным с другими транзакциями — это совершенно не важно, горизонт базы данных один на всех.

Если теперь в виде отрезка изобразить не транзакции, а снимки (от snapshot.xmin до snapshot.xmax), то ситуацию можно представить себе так:


На этом рисунке самый нижний снимок относится к незавершенной транзакции, а в остальных снимках snapshot.xmin не может быть больше ее номера.

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


И только после завершения транзакции горизонт продвигается вперед, позволяя очищать неактуальные версии строк:


Если описанная ситуация действительно создает проблемы и нет способа избежать ее на уровне приложения, то, начиная с версии 9.6, доступны два параметра:

  • old_snapshot_threshold определяет максимальное время жизни снимка. После этого времени сервер получает право удалять неактуальные версии строк, а если они понадобятся «долгоиграющей» транзакции, но она получит ошибку snapshot too old.
  • idle_in_transaction_session_timeout определяет максимальное время жизни бездействующей транзакции. После этого времени транзакция прерывается.

Бывают ситуации, когда несколько параллельных транзакций должны гарантированно видеть одну и ту же картину данных. В качестве примера можно привести утилиту pg_dump, умеющую работать в параллельном режиме: все рабочие процессы должны видеть базу данных в одном и том же состоянии, чтобы резервная копия получилась согласованной.

Разумеется, нельзя полагаться на то, что картины данных совпадут просто из-за того, что транзакции запущены «одновременно». Для этого есть механизм экспорта и импорта снимка.

Функция pg_export_snapshot возвращает идентификатор снимка, который может быть передан (внешними по отношению к СУБД средствами) в другую транзакцию.


Другая транзакция может импортировать снимок с помощью команды SET TRANSACTION SNAPSHOT до выполнения первого запроса в ней. Предварительно надо установить и уровень изоляции Repeatable Read или Serializable, потому что на уровне Read Committed операторы будут использовать собственные снимки.


Теперь вторая транзакция будет работать со снимком первой и, соответственно, видеть три строки (а не ноль):


Время жизни экспортированного снимка совпадает со временем жизни экспортирующей транзакции.

Читайте также: