Pull to refresh

Comments 18

Рад! Оставайтесь с нами (:

Егор, большое спасибо за статью!

Традиционно, несколько вопросов:

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


То есть имеем операции «создания»/«удаления» снимка. Их тем больше, чем больше операторов в транзакции

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


То есть имеем всего одну операцию «создания»/«удаления» снимка.

Сам вопрос: Насколько затратны операции «создания»/«удаления»? Можно ли сказать, что уровни Repeatable Read и Serializable
значительно меньше нагружают сервер БД? Или разница ничтожно мала по сравнению с другими действиями для этих транзакций? Я имею ввиду порядки затрат, соизмеримые с порядками затрат оптимизатора, то есть микросекунды. Понятно, что по сравнению с IO операциями передачи данных по сети эти затраты будут ничтожно малы.

Имеется ввиду затратность по:
* RAM — насколько «тяжеловесна» информация о снимке.
* CPU/IO — насколько трудоемко создавать новые снимки (и видимо помечать неактивные к удалению).

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


Что означает понятие «снимок активен» в применении к Read Committed? Это означает, что при создании нового снимка, ранее созданный (например, на момент создания транзакции) уже никак не участвует в транзакции? Как удаляются такие снимки? Они помечаются к удалению или удаляются в рамках той же транзакции?

№3
такие интервалы не пересекаются, поэтому одна строка представлена в любом снимке максимум одной своей версией.


То есть невозможна ситуация, при которой xmin xmax одной и той же строки будут перезаписаны в разных транзакциях? То есть если xmin/xmax когда-либо были записаны — измениться они уже не могут (immutable). При условии, если транзакция изменившая их первой — зафиксировалась.

№4
А для DDL-запросов (тоже транзакционны) изоляции и снимки тоже используются? Насколько механизмы изоляций и снимков отличаются от уже описанных механизмов в этой и в предыдущих статьях?
За Serializable Snapshot Isolation(SSI) мы расплачиваемся раздуванием таблиц и autovacuum.
Ну, не совсем. Расплачиваемся мы не за SSI, а за SI (Serializable в этом смысле ничего нового не дает). И даже не за сам SI, а за его реализацию со сборкой мусора.
Есть реализации, которые этим не страдают (как в Оракле, или вот zheap). Но zheap — это пока эксперимент, о нем рановато говорить.
Владимир, спасибо. По порядку.
1.
Разницу можно считать ничтожно малой. Это точно не повод задумываться о Repeatable Read (таким поводом должна быть изоляция).
Информация о снимке — это всего несколько чисел, ничего тяжелого. Самое неприятное, с чем там приходится иметь дело — это чтение ProcArray для того, чтобы запомнить список активных транзакций. Это требует блокировки, хоть и разделяемой. Так что при увеличении количества соединений (больше нескольких сотен) ProcArray начинает становиться узким местом, и надо думать о пуле соединений.

Спасибо, а как пул соединений поможет в данном случае? Насколько я понял, источник информации о состоянии транзакций «один на всех»

Просто соединений (и, следовательно, одновременных транзакций) будет меньше. Толкотня за ProcArray уменьшится.
А можно больше подробностей?

Чтобы получить снимок, требуется прочитать список всех транзакций (ProcArray), и на время чтения его приходится блокировать. Чем больше соединений, тем больше транзакций (и, следовательно, чаще надо получать снимки) и тем больше времени удерживается блокировка.
В итоге получается так, что при увеличении количества соединений производительность начинает падать, даже если железяка мощная и ее ресурсы не исчерпаны.
Ну и другие неприятности могут быть, например проблемы с новыми подключениями, которые долго не могут взять монопольную блокировку ProcArray.
А пул ограничивает количество одновременно открытых соединений и становится легче.

2.
На каком-то уровне абстракции (достаточном для понимания происходящего) можно просто считать, что снимок живет столько, сколько запрос/транзакция, и потом исчезает.
Если влезать глубже, то начинаются всякие детали, в которых легко погрязнуть. Если прям хочется копнуть, то можно начинать с src/backend/utils/time/snapmgr.c. Но не уверен, что это сильно полезно.
3.
Одну и ту же строку две разные транзакции не могут изменять одновременно; одна из них будет заблокирована. Ну и в целом версии идут одна за другой — одна стала неактуальной, появилась другая актуальная, xmax одной будет равен xmin другой.
4.
DDL — это по сути изменение таблиц системного каталога. Для системного каталога тоже используется многоверсионность и снимки, это и дает нам транзакционный DDL. Но отличия все же есть.
Внешне все выглядит так, как будто для таблиц системного каталога всегда действует уровень Repeatable Read, даже на Read Committed.
Но я в это глубоко не влезал, а надо бы. Так что за этот вопрос отдельное спасибо, поизучаю.

Статья старая, но я не побоюсь задать вопрос на тему.


Как видно, PostgreSQL вообще нигде ничего не делает на тему линеаризуемости: последующая транзакция даже от того же клиента вообще может не заметить эффектов уже применённых транзакций, в том числе его же. В моём представлении это возникает из-за гонок, когда новая начинающаяся транзакция берёт тот самый список живых транзакций и может, в теории, не успеть увидеть там предшественника. На самом деле это ещё один целый пласт аномалий консистентности.


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

Правильно, бояться не надо.


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

Но это же не так.
Любая транзакция обязательно увидит все сделанное зафиксированными ранее транзакциями. Никаких гонок там нет и быть не может.
А почему вы так решили? Есть какой-то пример, который заставляет вас так думать?


Да, ну и еще надо понимать, что сериализуемость и линеаризуемость — вещи разные. Но это, мне кажется, нет смысла обсуждать, пока мы не разберемся с предыдущим вопросом.

Я задался этим вопросом потому что, как мне казалось, в паре быстро работавших моих тестов я ловил аномалию что следующая читающая транзакция как будто была выполнена до COMMIT предыдущей и не увидела изменений (хотя остальное было консистентно, никаких частичных / фантомных / неповторяемых чтений) и начал думать, чем там можно помочь кроме втыкания sleep. Формально сериализуемость, насколько я знаю, не запрещает таких аномалий: гарантируется лишь существование пост-фактум какого-то порядка применения с теми же наблюдаемыми эффектами; как раз линеаризуемость (и её надмножества по допускаемым аномалиям) говорит о том, что порядок какой-то известен. Тем более что во всех анализах Postgres на консистентность, которые я читал (в том числе отсылающих к Jespen), уделялось внимание в первую очередь ветке сериализуемости и её фрагментам (Snapshot Isolation, Repeatable Read, ...), а о линеризуемости не говорилось ничего. В документации, насколько я помню, этому много внимания не уделяется, хотя зная все детали реализации это может быть и очевидно.


Вы сейчас говорите, что всё же транзакция, начатая после завершения другой (можно ли это формализировать? happens after сообщения об успешности COMMIT?) точно будет в сериализуемом порядке после неё?

В мире (реляционных по крайней мере) СУБД действительно никогда не говорят о линеаризуемости, потому что она плохо сочетается с транзакциями. Одно дело, когда есть "точечные" события, которые можно выстроить одно за другим, а другое — когда есть продолжительные транзакции, части которых могут к тому же перепутываться с частями других транзакций. Поэтому сериализуемость — наше все.


Но вот то, о чем вы пишете (следующая читающая транзакция как будто была выполнена до COMMIT предыдущей и не увидела изменений) — это невозможно. Когда транзакция начинает выполняться, она строит снимок данных, в который гарантированно попадут изменения всех зафиксированных на тот момент транзакций (на любом уровне изоляции).


Вы сейчас говорите, что всё же транзакция, начатая после завершения другой (можно ли это формализировать? happens after сообщения об успешности COMMIT?) точно будет в сериализуемом порядке после неё?

Да.


Если можете показать свои тесты, было бы интересно посмотреть.

Спасибо! Там внутренняя кухня и наверняка беда была не только в общении с базой, но я уже думал подозревать своё неправильное понимание что Postgres обещает, а что нет. А в результате обещает больше чем я думал, и это радует, спасибо огромное за разъяснение и ваши статьи!

Sign up to leave a comment.