Содержание

Предыдущий раздел

Cнапшоты

Следующий раздел

Дата и время (вводная статья)

MVCC и VACUUM

MVCC - одна из ключевых технологий доступа к данным, которая используется в PostgreSQL. Она позволяет осуществлять параллельное чтение и изменение записей (tuples) одних и тех же таблиц без блокировки этих таблиц. Чтобы иметь такую возможность, данные из таблицы сразу не удаляются, а лишь помечаются как удаленные. Изменение записей осуществляется путем маркировки этих записей как удаленных, и созданием новых записей с измененными полями. Таким образом, история изменений одной записи сохраняется в базе данных и доступна для чтения другими транзакциями. Этот способ хранения записей позволяет параллельным процессам иметь неблокирующий доступ к записям, которые были удалены или изменены в параллельных незакрытых транзакциях. Техника, используемая в этом подходе, относительно простая. У каждой записи в таблицы есть системные скрытые поля xmin, xmax.

  • xmin - хранит номер транзакции, в которой запись была создана.
  • xmax - хранит номер транзакции, в которой запись была удалена или изменена.

С помощью этих полей и осуществляется фильтрация записей (есть еще поля cmax/cmin, на них я не буду останавливаться). Перед началом выборки данных PostgreSQL сохраняет снапшот текущего состояния БД. На основании данных снапшота, полей xmin, xmax осуществляется фильтрация записей.

В очень упрощенном виде работу MVCC можно представить таким образом

Если к таблице table делается запрос

SELECT * FROM table WHERE ваши условия

PostgreSQL обрабатывает запрос и добавляет в него доп. условия

SELECT * FROM table WHERE ваши условия AND is_tuple_visible(xmin, xmax, snapshot)

где is_tuple_visible - функция, которая по значениям полей записи xmin и xmax и сохраненного снапшота определяет, видна ли эта запись для текущего запроса

У данной технологии есть одна неприятная особенность. Суть её заключается в том, что при изменении записи, в таблицу добавляется новая запись, а старая копия помечается как удаленная, что ведет к росту размера таблицы. И есть реальная опасность - номерная емкость пула транзакций имеет свой предел (~ 2 в степени 32 или 4 млрд номеров). Что произойдет, если произойдет переполнение счетчика транзакций? Получится, что записи, сделанные старыми транзакциями, окажутся в будущем и не будут видны новым транзакциям. Этого допустить нельзя. Надо как-то выходить из ситуации. Разработчики PostgreSQL вышли из ситуации таким образом. Были введены специальные номера транзакций, которые всегда находятся в прошлом, относительно номеров обычных транзакций. Была создана процедура, которая системные поля записей xmin заменяет на специальный номер транзакции (FrozenXID), таким образом старые записи всегда будут находится в прошлом для всех новых транзакций. Этот демон периодически сканирует все таблицы в базе данных и заменяет номера транзакций на FrozenXID. Таким образом исключается потеря данных в результате переполнения счетчика номеров транзакции. Вроде, всё достаточно просто. Но это не так. Если бы в реальности всё было реализовано прямо так, как описано выше, то любому разработчику после прочтения алгоритма стало бы понятно, что в нем есть слабое место, а именно крайний случай, когда выдается максимально допустимый номер транзакции. В этом случае необходимо в срочном порядке все xmin всех записей в базе данных обнулить в ForzenXID. А так как это процесс не быстрый, то, чтобы избежать потери данных, СУБД придётся приостановить все операции на время обнуления записей, а это недопустимо. Поэтому разработчики придумали следующий финт. Они реализовали циклическую номерную емкость номеров транзакций так, что для любого номера транзакции имеется 2 млрд номеров транзакций, которые, как считается, находятся в прошлом, относительно текущей транзакции, и имеются 2 млрд номером транзакций, которые, как считается, что находятся в будущем относительно текущей транзакции. Это реализовано арифметикой по модулю 2v31.

Пример для номерной емкости в 16 номеров.

Получается, что надо использовать арифметику по модулю 2v3. Предположим, что номер текущей транзакции 4. В этом случае номера транзакций с 5 по 12 должны был в будущем, а номера с 13 по 15 и с 0 по 3 должны быть в прошлом. Как же этого добиться?

Надо вычесть один номер транзакции из другого с использованием беззнаковой арифметики, а результат перевести в дополнительный код (целое число со знаком) (или по-другому говоря в знаковую арифметику)

4 - 5 = -1 или 1111 (в доп. коде)
4 - 6 = -2 или 1110 (в доп. коде)
...
4 - 12 = -8 или 1000 (в доп. коде)
4 - 13 = -9 или 0111 (в доп. коде)
4 - 15 = -11 или 0101 (в доп. коде)
4 - 3 = 1 или 0001 (в доп. коде)

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

В итоге получается, что СУБД должна не позднее, чем через 2 млрд транзакций обнулять транзакционную информацию у записи. Этот растянутый во времени процесс позволяет избежать пиковой нагрузки на базу данных при работе VACUUM'а.

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

Дополнительные материалы

PostgreSQL Documentation

comments powered by Disqus