Контроль целостности в ADB (изоляция транзакций)

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

В ADB используется PostgreSQL Multiversion Concurrency Control (MVCC) для управления параллельными транзакциями heap-таблиц. С MVCC каждый запрос работает со снапшотом базы данных, создаваемым в тот момент, когда запрос объявляется. Пока выполняется снапшот, запрос не может видеть сделанные другими параллельными транзакциями изменения. Это гарантирует, что запрос видит последовательное представление базы данных. Запросы, которые читают строки, никогда не блокируют транзакции, пишущие строки. И наоборот, запросы, которые записывают строки, не могут быть заблокированы транзакциями, которые читают строки. Это позволяет значительно увеличить параллелизм в сравнении с традиционными базами данных, в которых используется блокировка для координации доступа между транзакциями, которые считывают и записывают данные.

Important

Append optimized таблицы управляются с помощью иной модели управления параллелизмом, нежели чем модель MVCC, которая обсуждалась в данном разделе. Они предназначены для приложений write-once, read-many, что никогда (или только очень редко) подразумевает обновления на уровне строк

Снапшоты

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

Каждой транзакции присваивается уникальный идентификатор (XID), 32-битное значение. Оператор SQL, не являющийся частью транзакции, обрабатывается как одиночная транзакция (с добавлением BEGIN и COMMIT). Это похоже на autocommit, используемый в некоторых базах данных.

Когда транзакция вставляет строку, XID сохраняется со строкой в столбце xmin. Когда транзакция удаляет строку, XID сохраняется в столбце системы xmax. Обновление строки рассматривается как delete и insert, поэтому XID сохраняется в xmax текущей строки и xmin только что вставленного ряда. Столбцы xmin и xmax вместе со статусом завершения транзакции определяют диапазон транзакций, для которых видна текущая версия строки. Транзакция видит результаты работы всех транзакций меньше, чем xmin, но не может видеть результаты работы любой транзакции больше или равной xmax.

Операции с несколькими действиями также должны записывать, какая команда в транзакции вставляла строку (cmin) или удаляла строку (cmax), чтобы транзакция могла видеть изменения, сделанные предыдущими командами в транзакции. Последовательность команд имеет смысл только во время выполнения транзакции, поэтому она сбрасывается до 0 в самом начале.

Каждый сегмент ADB имеет свою собственную последовательность XID, которая не может сравниваться с XID других инcтансов. Мастер координирует распределенные транзакции с сегментами, использующими идентификационный номер сеанса, называемый gp_session_id. Сегменты сопоставляют идентификаторы распределенных транзакций с их локальными XID. Мастер координирует распределенные транзакции с помощью двухфазового протокола фиксации. Если транзакция не выполняется на одном из сегментов, транзакция прекращается на всех сегментах, и данные возвращаются в первоначальный вид.

Столбцы xmin, xmax, cmin и cmax для любой строки можно увидеть при помощи оператора SELECT:

SELECT xmin, xmax, cmin, cmax, * FROM tablename;

Поскольку команда SELECT выполняется на мастере, XID являются идентификаторами распределенных транзакций. В случае если команда выполняется в отдельной базе данных сегментов, значения xmin и xmax соответствуют локальным значениям XID.

Зацикливание ID Транзакций

Модель MVCC использует идентификаторы транзакций (XID), чтобы определить, какие строки видны в начале запроса или транзакции. XID – это 32-битное значение, поэтому база данных может теоретически выполнить более четырех миллиардов транзакций до того, как значение переполнится и обнулится. Тем не менее, в базе данных ADB используется арифметика по модулю 2^32, что позволяет XID зацикливаться. Для любого XID может быть около двух миллиардов предыдущих и новых XID. Это работает до тех пор, пока текущая версия строки примерно через два миллиарда транзакций неожиданно не станет новой строкой. Чтобы предотвратить это, ADB имеет специальный XID, называемый FrozenXID, который всегда считается старше обычного XID, с которым он сравнивается. Xmin строки должен быть заменен на FrozenXID в течение двух миллиардов транзакций, и это одна из функций, выполняемых командой VACUUM.

Очистка (vacuuming) как минимум раз в два миллиарда транзакций предотвращает зацикливание XID. База данных ADB отслеживает XID и предупреждает, когда требуется произвести очистку (операция VACUUM).

Когда значительная часть идентификаторов больше недоступна и до того, как происходит зацикливание XID, выдается предупреждение:

WARNING: database "database_name" must be vacuumed within number_of_transactions

Transactions

Если операция VACUUM не выполняется, база данных ADB перестает создавать транзакции, чтобы избежать возможной потери данных. При достижении лимита, система сообщает об ошибке:

FATAL: database is not accepting commands to avoid wraparound data loss in database

"database_name"

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

Режимы изоляции транзакций

Стандарт SQL описывает три явления, которые могут возникать при запуске транзакций базы данных одновременно:

  • Грязное чтение – явление, которое возникает, когда транзакция считывает незафиксированные данные из другой параллельной транзакции;
  • Неповторяющееся чтение – ситуация, когда при повторном чтении в рамках одной транзакции ранее прочитанные данные оказываются измененными;
  • Чтение фантомов – ситуация, когда при повторном чтении в рамках одной транзакции одна и та же выборка дает разные множества строк.

Стандарт SQL определяет четыре режима изоляции транзакций, которые должны поддерживать системы баз данных:

Команды SQL базы данных ADB позволяют запросить READ UNCOMMITTED, READ COMMITTED, или SERIALIZABLE. База данных ADB рассматривает READ UNCOMMITTED так же, как READ COMMITTED. Запрос REPEATABLE READ вызывает ошибку; вместо этого необходимо использовать SERIALIZABLE. Режим изоляции по умолчанию – READ COMMITTED.

Разница между READ COMMITTED и SERIALIZABLE заключается в том, что в режиме READ COMMITTED каждый оператор в транзакции видит только строки, обновленные до начала выполнения оператора, в то время как в режиме SERIALIZABLE mode все операторы транзакции видят строки, обновленные до начала транзакции.

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

Режим изоляции SERIALIZABLE предотвращает как неповторяющиеся чтения, так и фантомные чтения, но за счет ухудшения распараллеливания и производительности. Все параллельные транзакции имеют одинаковый снимок базы данных, полученный перед началом транзакций. Параллельная транзакция, которая пытается изменить данные, уже измененные другой транзакцией, получает запрет на это действие. Приложения, выполняющие транзакции в режиме SERIALIZABLE, должны быть подготовлены к обработке транзакций, не выполняющихся из-за ошибок сериализации. Если режим изоляции SERIALIZABLE жестко не требуется, лучше использовать режим READ COMMITTED.

Стандарт SQL подразумевает, что параллельные упорядоченные транзакции приводят базу данных в одно состояние в независимости от типа их обработки (т.е. в параллели или друг за другом). Модель изоляции снапшотов MVCC предотвращает грязные чтения, неповторяющиеся чтения и фантомные чтения, не используя блокировку. Однако, иные взаимодействия, которые могут возникнуть между некоторыми транзакциями SERIALIZABLE в базе данных ADB, не позволяют им называться полностью упорядоченными. Эти аномалии можно отнести к тому факту, что база данных ADB не выполняет блокировку предикатов, а это означает, что запись одной транзакции может повлиять на результат предыдущего чтения в другой параллельной транзакции.

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

Оператор SQL SET TRANSACTION ISOLATION LEVEL устанавливает режим изоляции для текущей транзакции. Режим должен быть установлен перед любыми операциями SELECT, INSERT, DELETE, UPDATE или COPY:

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;

Режим изоляции также может быть указан как часть инструкции BEGIN:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Режим изоляции транзакции по умолчанию можно изменить с помощью свойства default_transaction_isolation.

Удаление неиспользуемых строк из таблиц

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

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

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

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

Команда VACUUM FULL перезаписывает таблицу без неиспользуемых строк, сводя ее к минимальному размеру. Для создания новой таблицы должно быть достаточно места на диске. При этом таблица блокируется до тех пор, пока команда VACUUM FULL не завершится. Это очень ресурсоемко по сравнению с обычной командой VACUUM, и ее можно избежать или отложить путем регулярной чистки. Лучше всего запускать VACUUM FULL в течение периода технического обслуживания. Альтернативой VACUUM FULL является воссоздание таблицы с помощью инструкции CREATE TABLE AS с последующим удалением старой таблицы.

Карта свободного пространства находится в общей памяти и отслеживает свободное пространство для всех таблиц и индексов. Каждая таблица или индекс использует около 60 байт памяти, и каждая страница со свободным пространством занимает 6 байт. Два параметра конфигурации системы определяют размер карты свободного пространства max_fsm_pages и max_fsm_relations.

  • max_fsm_pages
Данный параметр устанавливает максимальное количество дисковых страниц, которые могут быть добавлены в общую карту свободного пространства. Для каждого слота страницы потребляется 6 байт общей памяти. Значение по умолчанию - 200000. Этот параметр должен быть установлен как минимум в 16 раз больше значения max_fsm_relations.
  • max_fsm_relations
Параметр устанавливает максимальное количество отношений, которые отслеживаются в карте свободного пространства. Этот параметр должен быть установлен больше, чем общее количество таблиц + индексов + системных таблиц. Значение по умолчанию - 1000. Для каждого отношения к каждому сегменту потребляется около 60 байт памяти. Рекомендуется устанавливать параметр на более высокое значение.

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

Можно запустить VACUUM VERBOSE tablename, чтобы получить отчет по сегменту о количестве удаленных строк, количестве затронутых страниц и количестве страниц с полезным свободным пространством.

Чтобы узнать, сколько страниц таблица использует во всех сегментах, необходимо запросить системную таблицу pg_class. Чтобы получить точные данные обязательно следует сначала выполнить ANALYZE для таблицы.

SELECT relname, relpages, reltuples FROM pg_class WHERE relname = 'tablename';

Другим полезным инструментом является gp_bloat_diag в схеме gp_toolkit, который идентифицирует раздутие (bloat) в таблицах путем сравнения фактического количества используемых таблицей страниц с ожидаемым числом.