Обновить

MVCC без VACUUM: что нам дал UNDO-лог, какую цену мы заплатили и зачем нам 5 механизмов сборки мусора

Уровень сложностиСредний
Время на прочтение18 мин
Охват и читатели12K
Всего голосов 10: ↑10 и ↓0+16
Комментарии7

Комментарии 7

одна забытая сессия с открытой транзакцией останавливает очистку для всех.

Ну почему же, тот undo, который относится к транзакциям, начавшимся позже и завершившимся раньше, вполне можно чистить. В этом случае длинная транзакция может наткнуться на строки, для которых не существует версии, актуальной на момент её начала. Но это лучше, чем держать ради неё undo (imho, разумеется)

выросшая строка переезжает в новый слот, а старый помечается мёртвым и ждёт возврата. Это уже не MVCC, а механика страниц с записями переменной длины — от неё не свободны ни InnoDB, ни Oracle (row migration)

У Oracle строки мигрируют только если меняется ключ секционирования. Если это обычная таблица, то в старой странице её корень остаётся навечно (до ALTER TABLE ... MOVE), и индекс указывает на корень. А корень в свою очередь указывает на то место, где лежат настоящие данные. Это явление называется chained rows.

Ну почему же, тот undo, который относится к транзакциям, начавшимся позже и завершившимся раньше, вполне можно чистить. В этом случае длинная транзакция может наткнуться на строки, для которых не существует версии, актуальной на момент её начала. Но это лучше, чем держать ради неё undo (imho, разумеется)

Тут как раз и проходит развилка. Undo, который нужен «зависшей» длинной транзакции для восстановления её снимка, выкинуть нельзя: если транзакция, что началась позже и закоммитилась раньше нашей длинной, поменяла строку — длинная не должна видеть её изменений и обязана собрать до-версию, а для этого как раз и нужен тот самый undo. Так что «лишним» он только кажется.

Дальше — вопрос политики. Можно разрешить этот случай в пользу очистки: тогда длинная транзакция наткнётся на строку, для которой нет версии на момент её старта, и получит «snapshot too old» — ровно ORA-01555 у Oracle. А можно в пользу транзакции — и тогда undo приходится держать. Мы выбрали второе: читающая транзакция получает консистентный снимок и не падает на ровном месте. ORA-01555 — боль хорошо известная, особенно на долгих отчётах и выгрузках, и тащить её к себе не хотелось.

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

У Oracle строки мигрируют только если меняется ключ секционирования. Если это обычная таблица, то в старой странице её корень остаётся навечно (до ALTER TABLE ... MOVE), и индекс указывает на корень. А корень в свою очередь указывает на то место, где лежат настоящие данные. Это явление называется chained rows.

Как мне кажется тут небольшая путаница в терминах. То, что вы описали — «голова» строки остаётся на месте, индекс указывает на неё, а она перенаправляет на новое расположение данных — это и есть row migration. Chained rows — про другое: когда строка целиком не влезает в один блок (длиннее блока или больше 255 столбцов) и нарезается на куски по нескольким блокам. Инструмент ANALYZE … LIST CHAINED ROWS ловит и те, и другие в один список.

И мигрируют строки в Oracle не только при смене ключа секционирования. Обычный UPDATE, который вырастил строку так, что она перестала помещаться в свой блок с учётом PCTFREE, даёт ровно ту самую миграцию — секционирование тут ни при чём. А переезд между секциями при смене ключа (ROW MOVEMENT) — это уже отдельный, третий механизм, и там строка получает новый ROWID.

Так что исходный тезис как раз про это и был: выросшая запись переезжает, старый слот остаётся висеть — это общая механика страниц с записями переменной длины, и Oracle от неё не свободен, как и InnoDB. К MVCC как таковому это отношения не имеет.

К вопросу «а как у нас» мы от переезда выросшей строки тоже не свободны — записи переменной длины, тут чудес не бывает. Но решаем иначе, чем Oracle. У нас слотированная страница: если строка выросла и не влезла, старый слот помечается мёртвым, новая версия едет в новый слот, а вторичные индексы перенацеливаются на новый идентификатор строки. Forwarding-стаба, как у Oracle, не остаётся — а значит, нет и вечного лишнего I/O на чтении мигрировавшей строки до ALTER TABLE … MOVE. Размен прямой: мы платим один раз на записи (правка указателей в индексах) вместо постоянного чтения. Старая версия при этом уходит в undo-лог, а не дублируется на странице. Если же строка не растёт и индексные колонки не меняются — апдейт идёт на месте, индексы не трогаются вовсе.

Цена UPDATE для писателя

PGSQL : новая версия tuple в heap

UNDO-MVCC (наш вариант) : in-place перезапись + ~159 байт в UNDO

Напоминает подход SQL Server.

Да, семейство то же — и это наш сознательный выбор. Update-in-place плюс старая версия в отдельный сегмент — так живут Oracle (UNDO tablespace), InnoDB (undo logs) и SQL Server (version store в tempdb). Мы в этом же лагере, напротив Postgres с его «новый tuple в heap + VACUUM». Если уж искать ближайшего родственника — это скорее Oracle/InnoDB, у которых undo нативный, а не приделанный сбоку; SQL Server приходит к тому же, но через версионник в tempdb.

Только пара уточнений по нашей механике, чтобы картина была точной.

In-place — это быстрый путь, когда новая версия укладывается в слот по размеру (типично для апдейта полей фиксированной ширины). Если строка выросла и не влезла — она переезжает в новый слот, старый помечается мёртвым; то самое row migration из соседней ветки. Так что «всегда in-place» — неверно: оно там, где размер не поехал.

UNDO у нас дельта-кодирован — в запись идут не вся строка, а изменённые колонки плюс служебка (указатель на предыдущую версию, commit_ts, lsn, txn_id и т.п.). Поэтому ~159 байт — это про конкретный апдейт: на узком изменении запись короче, на «переписали всю строку» — длиннее.

И отличие от tempdb-версионника SQL Server: там version store общий и живёт в tempdb, который от этого пухнет и временами становится бутылочным горлышком на ровном месте. У нас undo — собственный стор движка, а не общая свалка на всю инсталляцию. Цена при этом ровно та же, что у всего лагеря: VACUUM не нужен, но undo приходится держать, пока его видит хоть один активный снимок — ровно то, с чего началась ветка про длинную транзакцию.

а еще есть firebird с версиями в heap но без вакуума. Чиститься как у вас индексы - совместно с обычными чтениями

а еще есть firebird с версиями в heap но без вакуума.

А он точно без вакуума? А то ведь его предшественник, Interbase, имел внутри себя сборку мусора, правда под другим названием: sweep.

Firebird тут стоит чуть особняком — он не совпадает с нами по двум осям.

Первое — где лежат старые версии. Firebird держит их прямо в страницах данных: к записи цепляются back-версии, и heap пухнет от истории. По размещению это ближе к Postgres, чем к нам. У нас heap-страница несёт только последнюю версию, а вся история уходит в отдельный append-only undo-лог (Oracle/InnoDB-стиль). Так что в части хранения старых версий Firebird и AngaraBase расходятся: у одного история размазана по страницам данных, у другого вынесена в отдельный лог.

Второе, и самое главное: сборка у нас не на пути чтения. Чтение лишь дёшево помечает «горячие» страницы как кандидатов и ставит в очередь; саму чистку — эксклюзивный лок страницы, синхронная запись в WAL, перестроение — делает фоновый воркер по watermark самого старого активного снимка. Кооперативная сборка прямо в SELECT — ровно то, от чего мы ушли: сначала и у нас так и было, и читающий запрос начинал брать эксклюзивные локи и писать в журнал, проседая на ровном месте. Это, собственно, известная цена firebird'овской модели — SELECT, который внезапно делает запись и I/O.

А вот в чём совпадение полное: и у Firebird, и у нас сборку держит самый старый живой снимок. У Firebird это OIT — длинная «интересная» транзакция стопорит чистку back-версий; у нас тот же горизонт, с которого началась ветка про длинную транзакцию. Разные места хранения, разный момент сборки — но упирается всё в одно и то же.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации