Участвую в стартапе, в котором разрабатывается СУБД нового типа (работает поверх некоторых kv-движков, кардинально расширяя их возможности, про это немного можно прочитать здесь). Для того, чтобы сравнить то, что понемногу получается, с тем, что имеется в индустрии, пришлось на глубоком уровне проработать первоисточники по темам Isolation и Consistency (уточню, что имеется ввиду не та Consistency, что в ACID). Обнаружил интересные нюансы, которые и излагаю в этой статье.


Тезисно:


  • Термин Phantom Read является продуктом испорченного телефона
  • Смысл понятий Lost Update, Write Skew и Read Skew для разделения уровней изоляций неочевиден и относителен
  • Движок, который обеспечивает уровень изоляции Serializable, в распределённом мире может вести себя весьма причудливо, например, всегда возвращать пустой результат для read-only транзакций — и ему за это по стандарту "ничего не будет"
  • Strong consistency в Cosmos DB — предел мечтаний? (спойлер: нет)

Ну, и ещё кое-что по мелочи. В конце рассмотрим вот такой венец творения человеческого разума:



Содержание



SQL-92


Тридцать лет назад появился документ ISO/IEC 9075:1992, Database Language SQL- July 30, 1992 (далее SQL-92). Посмотрим, что там есть интересного, в рамках этой статьи.


Определяются три феномена:


    1) P1 ("Dirty read"): SQL-transaction T1 modifies a row. SQL-
    transaction T2 then reads that row before T1 performs a COMMIT.
    If T1 then performs a ROLLBACK, T2 will have read a row that was
    never committed and that may thus be considered to have never
    existed.

    2) P2 ("Non-repeatable read"): SQL-transaction T1 reads a row. SQL-
    transaction T2 then modifies or deletes that row and performs
    a COMMIT. If T1 then attempts to reread the row, it may receive
    the modified value or discover that the row has been deleted.

    3) P3 ("Phantom"): SQL-transaction T1 reads the set of rows N
    that satisfy some <search condition>. SQL-transaction T2 then
    executes SQL-statements that generate one or more rows that
    satisfy the <search condition> used by SQL-transaction T1. If
    SQL-transaction T1 then repeats the initial read with the same
    <search condition>, it obtains a different collection of rows.

Пока ничего интересного, за исключением того, что Phantom Read, на самом деле, надо называть просто Phantom.


Стандарт определяет уровень изоляции SERIALIZABLE:


The execution of concurrent SQL-transactions at isolation level SERIALIZABLE is guaranteed to be serializable. A serializable execution is defined to be an execution of the operations of concur rently executing SQL-transactions that produces the same effect as some serial execution of those same SQL-transactions. A serial execution is one in which each SQL-transaction executes to completion before the next SQL-transaction begins.

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


Кроме того, в стандарте можно найти удивительный тезис:


    Significant new features are:
    ...
    15)Support for transaction consistency levels,      

Удивительный он потому, что далее в тексте нет ничего про transaction consistency levels, по всей видимости, авторы имели ввиду "SQL-transaction isolation levels". Тем не менее, отметим смешение понятий Сonsistency и Isolation.


Идём дальше.


SQL-92?


A Critique of ANSI SQL Isolation Levels, Jun 1995, Microsoft Research — совместная работа Microsoft, Sybase и UMass (University of Massachusetts Amherst), опубликованная в 1995 году. В ней авторы критикуют ANSI X3.135-1992 (он же ISO 9075:1992) и предлагают улучшения и дополнения. Несмотря на почтенный возраст, работа ключевая, именно её определения постоянно используют и пересказывают (например, тут).


Авторы предлагают весьма полезную нотацию:


Histories consisting of reads, writes, commits, and aborts can be written in a shorthand notation: “w1[x]” means a write by transaction 1 on data item x (which is how a data item is “modified’), and “r2[x]” represents a read of x by transaction 2. Transaction 1 reading and writing a set of records satisfying predicate P is denoted by r1[P] and w1[P] respectively. Transaction 1’s commit and abort (ROLLBACK) are written “c1” and “a1”, respectively.

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


Авторы отказываются называть ранее определенные феномены "феноменами", давая им название "аномалии", и дают такую формализацию в своей нотации:


Anomaly History
A1 (Dirty Read) w1[x]...r2[x]...(a1 and c2 in either order)
A2 (Fuzzy or Non-Repeatable Read) r1[x]...w2[x]...c2...r1[x]...c1
A3 (Phantom) r1[P]...w2[y in P]...c2...r1[P]...c1

Под феноменами подразумеваются истории более общего характера, которые могут привести к аномалиям, и, соответственно, они должны исключаться "на корню" движками БД. Работа (пере)определяет шесть феноменов и вводит две новых аномалии:


  • P0 (Dirty Write)
  • P1 (Dirty Read)
  • P2 (Fuzzy or Non-Repeatable Read)
  • P3 (Phantom)
  • P4 (Lost Update)
  • P4C (Cursor Lost Update)
  • A5A (Read Skew)
  • A5B (Write Skew)

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


P0 (Dirty Write)


Этот феномен отсутствует в SQL-92, определяется так:


w1[x]...w2[x]...((c1 or a1) and (c2 or a2) in any order)

Т.е. нельзя менять то, что кто-то сейчас меняет. Такая история (феномен) может привести к аномалии:


w1[x=1]...w2[x=2]...w2[y=2]...c2...w1[y=1]...c1

  • На базу наложено ограничение целостности x == y
  • Первая транзакция записывает в x и y значение 1, вторая — значение 2
  • В результате x == 2, y == 1, что нехорошо
  • Авторы считают, что любой уровень изоляции из SQL-92 должен исключать Dirty Write

P1 (Dirty Read)


Формализация определения из стандарта:


A1: w1[x]...r2[x]...(a1 and c2 in either order)

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


Критики утверждают, что на вопрос надо смотреть шире:


P1: w1[x]...r2[x]...(c1 or a1)

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


H1: r1[x=50]...w1[x=10]...r2[x=10]...r2[y=50]...c2...r1[y=50]...w1[y=90]...c1

  • T2 получает аномальный баланс 60
  • Историю невозможно сериализовать, т.е. не существует последовательности из заданных транзакций, которая даёт такой же результат
  • История не содержит никаких феноменов из SQL-92

Как можно убедиться в истинности последнего утверждения?


  • A1 требует отката транзакции — в истории отсутствует
  • A2 требует двойного чтения одной и той же переменной — в истории отсутствует
  • A3 требует операции над множеством — в истории отсутствует

Ч.Т.Д. (ну, или Q.E.D. — кому что милее).


В общем, — "резать, не дожидаясь перитонита". Замечу, что в приводимой ранее статье в качестве Dirty Read рассматривается аномалия, а не феномен.


P2 (Fuzzy or Non-Repeatable Read)


Формализация определения из стандарта:


A2: r1[x]...w2[x]...c2...r1[x]...c1

Т.е. первая транзакция читает значение, вторая его "портит" и завершается, затем первая повторяет чтение, получая результат, отличный от результата первого чтения.


Критики утверждают, что на вопрос надо смотреть шире:


P2: r1[x]...w2[x]...(c1 or a1)

Т.е. нельзя менять то, что кто-то читает. Вторая транзакция вроде как не мешает первой, Non-Repeatable Read отсутствует, в чем проблема?! Опять с переводом денег:


H2: r1[x=50]...r2[x=50]...w2[x=10]...r2[y=50]...w2[y=90]...c2...r1[y=90]...c1

  • T1 получает аномальный баланс 140
  • Историю невозможно сериализовать
  • История не содержит феноменов из SQL-92

P3 (Phantom)


То, что ошибочно называют Phantom Read. Формализация определения из стандарта:


A3: r1[P]...w2[y in P]...c2...r1[P]...c1

Т.е. читаем множество P, затем его "портим" и повторно читаем "порченное". "Попортить" можно как вставкой нового элемента, в результате чего он пополнит собой множество, так и обновлением существующего, так что он "перейдет" в множество.


Критики утверждают, что на вопрос надо смотреть шире:


P3: r1[P]...w2[y in P]...(c1 or a1)

  • Аргументация та же, что и для P2

P4 (Lost Update)


В стандарте отсутствует, предлагается такая формулировка:


P4: r1[x=1]...w2[x=10]...w1[x=1+1]...c1

Тут Т1 "перетирает" записанное T2 значение.


  • В таком виде смысла не имеет, так как содержит в себе феномен P0 (Dirty Write, w2[x=10]...w1[x=1+1]), который, по замыслу авторов, должен исключаться любым уровнем изоляции
  • Можно переписать так: r1[x=1]...w2[x=10]...c2...w1[x=1+1]...c1
  • Теперь внутри P2: r1[x=1]...w2[x=10]
  • Неочевидный нюанс — нестандартный уровень изоляции Snapshot предотвращает P4, но не P2 (см. далее)

P4C (Cursor Lost Update)


В стандарте отсутствует, предлагается такая формулировка:


rc1[x=1]...w2[x=10]...w1[x=1+1]...c1

Здесь под rc подразумевается операция чтения через курсор, смысл феномена — запретить менять переменную, на которой находится курсор


  • История предотвращается нестандартным уровнем изоляции Cursor Stability
  • Я бы записал как: rc1[x=1]...w2[x=10]...c2...w1[x=1+1]...c1
    • Иначе опять имеет место P0
  • Можно менять то, что не под курсором: rc1[x]...rc1[y]...w2[x]...c2...w1[y]...c1

A5A (Read Skew)


В стандарте отсутствует, предлагается такая формулировка:


r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1)

Здесь происходит чтение согласованных значений, одно из которых в процессе чтения "портит" другая транзакций, чтение получается "кривое", прочитанные значения — несогласованные.


Пример истории с проблемой, опять переводим 40 между счетами с начальным остатком в 50:


H2: [x=50, y=50]...r1[x=50]...w2[x=10]...w2[y=90]...c2...r1[y=90]...(c1 or a1)

  • T1 получает аномальный баланс 140
  • Внутри аномалии содержится P2: (r1[x]...w2[x])
  • Уровень изоляции, предотвращающий A5A, но не P2 — Snapshot (см. далее)

A5B (Write Skew)


В стандарте отсутствует, предлагается такая формулировка:


r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2 occur)

Имеем два согласованных значения, T1 пишет второе, исходя из прочитанного первого. Одновременная T2 делает наоборот, результирующие значения в базе несогласованные.


Пример с деньгами тут уже не подходит, пусть у нас ограничение целостности x*2 <= y, x=3, y=4, история:


[x=3, y=4]...r1[x=3]...r2[y=4]...w1[y=6]...w2[x=2]...c1...c2...[x=2, y=6]

  • В истории содержится P2: r1[x=3]...w2[x=2]
  • Отсутствует уровень изоляции, предотвращающий A5B, но не P2

Рассмотрим ещё уровень изоляции Snapshot; и с этой работой — всё.


Snapshot Isolation


Уровень изоляции Snapshot определяется такими требованиями:


  1. Each transaction reads data from a snapshot of the (committed) data as of the time the transaction started, called its Start-Timestamp. This time may be any time before the transaction’s first Read.
  2. Updates by other transactions active after the transaction StartTimestamp are invisible to the transaction
  3. A transaction running in Snapshot Isolation is never blocked attempting a read
  4. The transaction's writes (updates, inserts, and deletes) will also be reflected in this snapshot
    • The transaction successfully commits only if no other transaction T2 with a Commit-Timestamp in T1’s execution interval [StartTimestamp, Commit-Timestamp] wrote data that T1 also wrote
    • First-committer-wins prevents lost updates (phenomenon P4)

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


Посмотрим, как обстоят дела с A2/P2 (Non-Repeatable Read):


A2: r1[x]...w2[x]...c2...r1[x]...c1


  • Если подходить строго, то в таком виде история не предотвращается
  • Можно сформулировать с учётом версий: r1[x1]...w2[x2]...c2...r1[x2]...c1 — вот такие истории, действительно, не допускаются

P2: r1[x]...w2[x]...(c1 or a1)


  • Такие истории Snapshot допускает

Аналогично обстоят дела с A3/P3, предотвращается A3:


A3: r1[P1]...w2[y in P2]...c2...r1[P2]...c1

но не P3:


P3: r1[P1]...w2[y in P2]...(c1 or a1)

A5A (Read Skew) предотвращается в формулировке:


  • r1[x1]...w2[x2]...w2[y2]...c2...r1[y2]...(c1 or a1)

Исходя из вышесказанного Table 4, похоже, содержит ошибку:



  • Отношение P2 и Shapshot должно быть "Sometimes Possible", т.е. A2 невозможно, а вот P2 — вполне

Табличку я бы переписал в таком виде:



  • Если рассматривать феномены так, как они определены в SQL-92, то Lost Update и Read Skew не имеют смысла для разделения уровней изоляции, т.к. они эквивалентны (в указанном смысле) A2
  • Если рассматривать феномены так, как это делают "критики", то для разделения уровней изоляции не имеют смысла Read Skew (эквивалентен Lost Update) и Write Skew (эквивалентен Fuzzy Read)
  • Что такое Sometimes Possible для Cursor Stability — не осилил

Идем дальше.


D. Abadi: Introduction to Transaction Isolation Levels


Работа [ABAISO] профессора D. Abadi интересна следующим:


Во-первых, личностью профессора:


He is best-known for the development of the storage and query execution engines of the C-Store (column-oriented database) prototype, which was commercialized by Vertica and eventually acquired by Hewlett-Packard in 2011, for his HadoopDB research on fault tolerant scalable analytical database systems which was commercialized by Hadapt and acquired by Teradata in 2014, and deterministic, scalable, transactional, distributed systems such as Calvin which is currently being commercialized by Fauna

Во-вторых, тем, что ничего нового из этой статьи мы не узнаем, несмотря на то, что статья датируется 2019 годом:


There are many, many problems with how the SQL standard defines these isolation levels. Most of these problems were already pointed out in 1995, but inexplicably, revision after revision of the SQL standard have been released since that point without fixing these problems.

Не зря, выходит, так вдумчиво читали критику стандарта — это актуально и в нынешнем тысячелетии. Есть, правда, нюанс — телефон таки испортился и Phantom превратился в Phantom Read.


Phantom или Phantom Read?

Почему же Профессор использует термин Phantom Read? В SQL-92 фигурирует Phantom, может быть, в более поздних стандартах этот термин заменили на Phantom Read?


ISO/IEC 9075-2:1999:


3) P3 (‘‘Phantom’’): SQL-transaction T1 reads the set of rows N that satisfy some <search condition>. SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the <search condition> used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same <search condition>, it obtains a different collection of rows.

SQL:2011 or ISO/IEC 9075:2011:


3) P3 (“Phantom”): SQL-transaction T1 reads the set of rows N that satisfy some <search condition>. SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the <search condition> used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same <search condition>, it obtains a different collection of rows.

SQL:2016 or ISO/IEC 9075-2:2016:


3) P3 ("Phantom"): SQL-transaction T1 reads the set of rows N that satisfy some <search condition>. SQL- transaction T2 then executes SQL-statements that generate one or more rows that satisfy the <search condition> used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same <search condition>, it obtains a different collection of rows.

Стандарт SQL:2019 выпущен June 2019, это позже, чем работа Профессора, так что...


Напоследок стоит отметить определение понятия Perfect Isolation:


The key point for our purposes is that we are defining “perfect isolation” as the ability of a system to run transactions in parallel, but in a way that is equivalent to as if they were running one after the other. In the SQL standard, this perfect isolation level is called serializability.

D. Abadi: Correctness Anomalies Under Serializable Isolation


В статье [ABACASER] Профессор демонстрирует, что в распределенных системах уровень изоляции Serializable — далеко не предел мечтаний, всё не так, как в старые добрые времена мейнфреймов, монолитов и вертикального масштабирования:


In the good old days of having a “database server” which is running on a single physical machine, serializable isolation was indeed sufficient, and database vendors never attempted to sell database software with stronger correctness guarantees than SERIALIZABLE. However, as distributed and replicated database systems have started to proliferate in the last few decades, anomalies and bugs have started to appear in applications even when running over a database system that guarantees serializable isolation. As a consequence, database system vendors started to release systems with stronger correctness guarantees than serializable isolation, which promise a lack of vulnerability to these newer anomalies. In this post, we will discuss several well known bugs and anomalies in serializable distributed database systems, and modern correctness guarantees that ensure avoidance of these anomalies.

Для распределенных систем придумали One Copy Serializability (1SR), в работе [YB] он определяется так:


The One Copy Serializability [7] is the highest correctness criterion for replica control protocols… In order to achieve this correctness criterion, it is required that interleaved execution of transactions on replicas be equivalent to serial execution of those transactions on one copy of a database.

Ну т.е. опять требуется эквивалентность некоторому последовательному выполнению, в этот раз требование относится к транзакциям в распределенных системах. Всё ли хорошо в системе, которая удовлетворяет 1SR? Нет, не всё. Профессор приводит три аномалии, используя такую терминологию:


The next few sections describe some forms of time-travel anomalies that occur in distributed and/or replicated systems, and the types of application bugs that they may cause.

1. Immortal Write (Бессмертная Запись)


Пользователь меняет своё отображаемое имя, Daniel => Danny => Danger:


  • История в реальном времени: w1[x=Daniel]...c1...w2[x=Danny]...c2...w3[x=Danger]...c3
  • История в журнале 1SR-системы: w1[x=Daniel]...w3[x=Danger]...w2[x=Danny]

Наблюдаем анахронизм (time-travel anomaly) — значение Danger было послано последним, но движок БД в силу каких-то причин переставил w3 на второе место. Причиной может быть, например, рассинхронизация часов на серверах или же некие "соображения" движка БД по оптимизации времени выполнения транзакций. От такой "перестановки" требования Serializability никак не нарушаются, так что движок в своем праве поступать таким образом.


2. Stale Read (Несвежее Чтение)


Пишем в некоторую переменную 50, затем 0, потом читаем, ожидая 0:


  • История в реальном времени: w1[x=50]...с1...w2[x=0]...c2...r3...c3
  • История в журнале 1SR-системы: w1[x=50]...r3[x=50]...w2[x=0]

Кластер БД опять "переставил" транзакции, внезапно получаем 50.


3. Causal Reverse (Обратная Причинность, "реверс козла")


Ситуация: пользователь имеет 1.000.000 на счету x и 0 на счету y. Читаем остатки, снимаем в банкомате 1.000.000 со счета x и затем кладем миллион на счет y.


  • История в реальном времени: r1[x, y]...w2[x=0]...c2...w3[y=1.000.000]...c3...с1
  • История в журнале 1SR-системы: w3[y=1.000.000]...r1[x=1.000.000, y=1.000.000]...w2[x=0]

БД опять повела себя дерзко и переставила r1 на второе место, так что r1 "вычитала" по миллиону с обоих счетов.


Утверждается, что такая аномалия возможна в CockroachDB (aka CRDB) из-за особенностей системы (STRONG PARTITION SERIALIZABLE) и нерешаемой проблемы с синхронизацией часов: "this enables a read (in CockroachDB’s case, this read has to be sent to the system before the two write transactions) to potentially see the write of the later transaction, but not the earlier one".


С негодованием отвергнем CockroachDB? Мне кажется, не стоит, работая с другими системами, мы можем просто пребывать в счастливом неведении о тех ужасах, что происходят под "распределенным капотом".


Гарантии свободной от "ужасов" системы Профессор называет Strict Serializability (т.е. Строго-Совершенная Изоляция, да) и выделяет такие ступеньки на пути к идеалу:


  • Strong Session Serializable: система гарантирует Strict Serializability в рамках сессии, иначе 1SR
    • Очевидная реализация это маршрутизация "sticky session", т.е. все запросы с одинаковой сессией направляются к одному и тому же узлу
  • Strong Write Serializable: система гарантирует Strict Serializability для пишущих транзакций, read-only транзакции довольствуются уровнем 1SR
    • Очевидная реализация — кластер с одной master-репликой, на которой можно менять данные, и множеством read-only реплик
  • Strong Partition Serializable: cистема гарантирует Strict Serializability в рамках разделов
    • Именно такой подход, к слову, принят в CocroachDB и в нашем стартапе
    • Можно использовать shard в качестве partition, но у нас, к примеру, не так, и partition включает в себя множество shard

В заключении Профессор приводит табличку:


System Guarantee Dirty read Non-repeatable read Phantom Read Write Skew Immortal write Stale read Causal reverse
READ UNCOMMITTED Possible Possible Possible Possible Possible Possible Possible
READ COMMITTED - Possible Possible Possible Possible Possible Possible
REPEATABLE READ - - Possible Possible Possible Possible Possible
SNAPSHOT ISOLATION - - - Possible Possible Possible Possible
SERIALIZABLE / ONE COPY SERIALIZABLE / STRONG SESSION SERIALIZABLE - - - - Possible Possible Possible
STRONG WRITE SERIALIZABLE - - - - - Possible -
STRONG PARTITION SERIALIZABLE - - - - - - Possible
STRICT SERIALIZABLE - - - - - - -

Идем дальше.


Consistency levels in Azure Cosmos DB


“Суха теория, мой друг, но древо жизни зеленеет”. Сильные духом могут почитать [COSMOS], Microsoft, Consistency levels in Azure Cosmos DB, 2022, здесь ограничусь перечислением Consistency levels, принятых в этой СУБД:


  • Strong (Сильная)
  • Bounded staleness ([baʊndɪd ˈsteɪlnəs], Запаздывание, Ограниченное устаревание, Ограниченная несвежесть)
    • Т.е. читатель может получить несвежие данные, но их запаздывание ограничено по времени и/или по количеству операций
  • Session (Сеанс, Сессионная)
  • Consistent prefix ([kənˈsɪstənt ˈpriːfɪks], Согласованный префикс)
    • This guarantee says that if a sequence of writes happens in a certain order, then anyone reading those writes will see them appear in the same order [CLEPP] (порядок соблюдается, но читаем с произвольным запаздыванием)
  • Eventual (Итоговая, Светлое будущее)
    • Когда-то всё будет хорошо

Для Strong дается такая картинка:



Т.е. пишем в одну зону, в других читаем без всяких time-travel аномалий.


Вроде как Strong аналогична Strict Serializable у Профессора и c Cosmos DB всё хорошо, так? Нет, не так...


jepsen.io: Consistency Models


Может показаться, что всё очень запутано????. Чтобы "всё" распутать, рассмотрим ациклический направленный граф, у которого только одна вершина имеет нулевую степень исхода (как бы дерево, но с направленностью от листьев к корню):



Картинка приведена на сайте группы, которая себя позиционирует таким образом:


Jepsen is an effort to improve the safety of distributed databases, queues, consensus systems, etc. We maintain an open source software library for systems testing, as well as blog posts and conference talks exploring particular systems’ failure modes

Из достижений [2020-12-23]:


Together with the ScyllaDB team, we found seven problems in Scylla, including lightweight transaction (LWT) split-brain in healthy clusters due to a.) incomplete row hashcodes and b.) multiple problems with membership changes. We also identified incomplete or inaccurate documentation, including claims that non-LWT operations were isolated and atomic, and undocumented rules about what kinds of membership operations were legal. Scylla has corrected almost all of these errors via patches and documentation; remaining cases of split-brain appear limited to concurrent membership changes.

Ну т.е. ребята, похоже, своё дело знают.


Рекомендую "кликнуть" по картинке, перейти по ссылке и далее попутешествовать по узлам графа — откроется многое — здесь же ограничусь краткими замечаниями.


Левая ветвь дерева посвящена аномалиям одновременного выполнения транзакций, а правая — аномалиям последовательного выполнения операций. Будь моя воля, я бы дал такие определения:


Isolation — предотвращение аномалий одновременного выполнения транзакций

Consistency — предотвращение аномалий последовательного выполнения операций

Ещё раз замечу, что Consistency тут не та, что имеется ввиду в аббревиатуре ACID.


По левой ветви можно прочитать такие нетривиальные утверждения:


  • Note that read uncommitted does not impose any real-time constraints. If process A completes write w, then process B begins a read r, r is not necessarily guaranteed to observe w...In fact, a process can fail to observe its own prior writes, if those writes occurred in different transactions
    • Т.е. не гарантируется даже чтение своих собственных записей, если они были выполнены в разных транзакциях


  • Like serializability, read uncommitted allows pathological orderings. For instance, a read uncommmitted database can always return the empty state for any reads, by appearing to execute those reads at time 0. It can also discard write-only transactions by reordering them to execute at the very end of the history, after any reads. Operations like increments can also be discarded, assuming the result of the increment is never observed. Luckily, most implementations don’t seem to take advantage of these optimization opportunities
    • Иными словами, read-only транзакции даже для уровня изоляции Serializable могут возвращать всегда пустые результаты для всех операций, а write-only транзакции могут быть отброшены. К счастью, реальные СУБД не пользуются такими возможностями по оптимизации



Вооружившись новыми знаниями, можно по-новому взглянуть на гарантиии Cosmos DB. Уровень согласованности Strong, при котором нотки читаются сразу после записи и именно в том порядке, в котором записаны, является не более (но и не менее) чем Linearizable — т.е. максимальной гарантией касательно аномалий последовательного выполнения операций. Но мы теперь знаем, что есть ещё и другая ветвь гарантий. С ней что?


The database engine in Azure Cosmos DB supports full ACID (Atomicity, Consistency, Isolation, Durability) compliant transactions with snapshot isolation.

https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/database-transactions-optimistic-concurrency

Snapshot isolation — ага, возможен Write Skew. Кто предупрежден, тот вооружен.


Для сравнения, DynamoDB от Amazon поддерживает SERIALIZABLE/READ-COMMITTED в качестве уровней изоляции, а также Strongly Consistent Reads/Eventually Consistent Reads в качестве уровней согласованности. Strongly Consistent Reads соответствует Strict Serializable в терминах jepsen.io.



Заключение


  • Как страшно и трудно жить!
  • Для распределенных СУБД требуйте долива пива после отстоя пены четких гарантий по Isolation и Consistency
  • Капитан Очевидность утверждает, что чем выше гарантии, тем ниже производительность


Ссылки:


  • [SQL-92]: ISO/IEC 9075:1992, Database Language SQL- July 30, 1992
  • [ABAISO]: D. Abadi, Introduction to Transaction Isolation Levels, blogspot.com, May 2019
  • [ABACASER]: D. Abadi, Correctness Anomalies Under Serializable Isolation, blogspot.com, June 2019
  • [CLEPP]: Martin Kleppmann, Designing Data-Intensive Applications
  • [COSMOS], Microsoft, Consistency levels in Azure Cosmos DB, 2022, microsoft.com
  • [YB], D. Yadav, M. Butler, Rigorous Design of Fault-Tolerant Transactions for Replicated Database Systems using Event B, School of Electronics and Computer Science University of Southampton

Комментарии (22)


  1. vagon333
    20.12.2022 00:59
    +2

    В чем коммерческая выгода (стартап-же) от новой СУБД, да еще как надстройка на KV-базе (допустим Redis) ?
    Какие проблемы решает ваша новая СУБД?


    1. maxim_ge Автор
      20.12.2022 01:15
      +1

      Выгода в уменьшении на порядок бюджетов на разработку ну и в разы на сопровождение. Наши рестораны мы бы запустили за пару месяцев на hetzner с нуля, ну а так несколько человеко-лет ушло бы (как у извеcтных мне коллег на Azure). Сейчас, конечно, уйдет несколько больше - ввиду глобальности целей.

      Касательно Redis - нет, пока не поддерживаем даже в планах, только Cassandra/Scylla/FoundationDB/bbolt. Поправил в статье.

      Кластер будет ставиться "прозрачно" для пользователя, просто даете IP-адреса, и все. В случае выхода из строя узла нужен IP адрес нового узла, произойдет горячая замена, без простоев.

      От разработчиков будет требоваться уметь в командную строку Linux и проектировать БД на уровне схем. Знание Cassandra и проч. приветствуется, но не требуется.

      Таков план. Реальность, возможно, внесет некоторые коррективы, но пока все в допустимых рамках.


  1. debagger
    20.12.2022 05:32
    +1

    Как страшно и трудно жить!

    Спасибо, статья замечательная!

    Прочитал, осознал, ушел плакать...


  1. sshikov
    20.12.2022 10:38

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

    А давайте попробуем определить, что значит «нельзя»? Ну вот как бы чтение результатов транзакций, которые еще не сделали commit, его ведь разрешали не просто так, а ради решения каких-то задач. Которые видимо допускают, что результат задачи не будет консистентным. Или самым свежим. Ну так, условно — если нам нужно реплицировать данные в другую БД, нас больше устраивают данные, пусть отстающие по времени (с известным отставанием), но зато гарантированно полученные к определенному моменту.


    1. maxim_ge Автор
      20.12.2022 11:22

      А давайте попробуем определить, что значит «нельзя»?

      Давайте попробуем. Если уровень изоляции исключает P1, в формулировке "критиков", то движок БД не должен допускать вот таких историй: w1[x]...r2[x]...(c1 or a1)


      1. sshikov
        20.12.2022 11:30

        Ну вот движок их не допускает, а что взамен? Вторая транзакция прочитала x, и потом первая откатилась… и? Вместо чтения вот таких вот данных, которые потом не были зафиксированы, вторая транзакция (то есть второе приложение) делает что? Получает ошибку? Так она же данные уже прочитала (некий вызов API уже выполнен), поэтому возникает куча вопросов, в какой момент движок вообще ей эту ошибку вернет? В терминах существующих протоколов типа JDBC, далеко не очевидно, как этому приложению про ошибку сообщить, и что оно должно сделать, получив эту ошибку. Не выйдет ли так, что запретив такие истории, мы получим другие истории, с непредсказуемым усложнением поведения приложения?


        1. PrinceKorwin
          20.12.2022 17:56

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

          Не все сценарии работы с данными требуют жёсткой консистентности.


          1. sshikov
            20.12.2022 18:08

            >А почему сразу ошибка?
            А какая разница? Я лишь про то, что одна транзакция уже прочитала данные, которые потом (вдруг) оказались не консистентными, потому что другая транзакция (позже) откатилась. То есть мы тут имеем дело с асинхронной коммуникацией. Которая всегда сложнее синхронной, и вообще не всегда возможна — потому что клиент тупо уже отрубился недоступен.


  1. nin-jin
    20.12.2022 11:47

    А где в этой картине мира находятся Local-First СУБД? Я вижу так:

    • Они Snapshot Isolation, так как каждый процесс исполняет свои операции последовательно над локальной копией базы.

    • Они Total Available так как для работы связь с серверами и другими клиентами не обязательна. Аутентификация и авторизация обеспечивается криптографией.

    • Они Causal Consistency, так как следствия никогда не обгоняют причины.

    • Они Strong Eventual Consistency, так как после синхронизации приходят к единому состоянию.

    При этом возможны лишь следующие аномалии:

    • Write Skew, так как причинность не отслеживается, а явного конфликта изменений нет. Хотя, если всё же отслеживать причинность, то можно и от этой аномалии избавиться.

    Ничего не упустил?


    1. maxim_ge Автор
      20.12.2022 12:15

      Тут надо на архитектуру смотреть. Мы сейчас "пилим" вот такое:

      База распределенная (две копии), репликация асимметрична и асинхронна.

      Snapshot Isolation

      На клонированной из облака копии Strong Partition Serializable для пишущих транзакций, Eventual Consistency для транзакций на чтение.

      Total Available

      В нашем случае нет (хотя, видимо, зависит от определений), в "облако" писать нельзя пока активен Local Box (aka Edge Node)

      Causal Consistency, Strong Eventual Consistency

      Пожалуй, но, в нашем случае, только с применением оргмер. Без них, например, можно получить split brain - в облаке отметить что Local Box "умер", соответственно, с облачной копией теперь можно работать, но и с Local Box технически можно продолжать работать, и тогда, конечно, настанет время удивительных чудес с перемещениями во времени.


      1. nin-jin
        20.12.2022 13:35

        У нас попроще архитектура:

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

        Strict Serializable предполагает, что на всех узлах результаты транзакций будут появляться в одном и том же прядке, что не обеспечить при работе только с локальной базой. Так что SPS тут совсем не выходит. А вот Total Available как раз получается, так как можно писать в базу даже в оффлайне, а при синхронизации, через CvRDT все изменения конвергируют. Так что временный Split Brain совсем не страшен.


        1. maxim_ge Автор
          20.12.2022 19:14

          А вот Total Available как раз получается, так как можно писать в базу даже в оффлайне

          Согласно jepsen.io Cursor Stability и выше а также Snapshot Isolation и выше не могут быть Total Available.

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


          1. nin-jin
            20.12.2022 19:57

            Это потому, что там рассматриваются только Cloud-First субд, где клиент может сначала к одному узлу подключиться, а потом к другому и получить разные результаты. В случае Local-First можно спокойно коннектиться к любым узлам, даже если у них Split-Brain, тогда сервера синхронизируются не напрямую, а опосредованно, через клиента.


            1. maxim_ge Автор
              21.12.2022 16:17
              +1

              У Вас все-таки такая архитектура:

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

              Это случай "Clients only talk to the same servers, instead of switching to new ones". В таком сценарии обеспечиваются уровни изоляции и согласованности, обозначенные синим и желтым цветами.

              Конкретно с определением Snapshot дело обстоит так - можно обеспечить все требования кроме First-committer-wins. Это требование, в случае использования CRDTs, является излишним (так как нацелено на Lost Update, каковой невозможен с такими типами) тем не менее, оно есть в приводимых формулировках.

              Можно как-то по другому назвать, например, Snapshot-CRDTs.


              1. nin-jin
                21.12.2022 17:22

                Это случай "Clients only talk to the same servers, instead of switching to new ones"

                Не совсем. Клиент может спокойно менять сервер. То есть это голубой цвет. Но при этом обеспечивается Causal Consistency - всё, что ты видел на одном сервере, ты увидишь и на любом другом + что-то свежее.


                1. maxim_ge Автор
                  23.12.2022 00:04

                  Интересно, как при свободной смене сервера обеспечить Read-Your-Writes? Пишем в x=x+1 (=9+1=10 ) в SRV1, репликация асиннхронная(?), после записи SRV1 выпадает из сети, репликация не завершена. Идем на SRV2 и там читаем x==9.

                  Как избежать такой ситуации?


                  1. nin-jin
                    23.12.2022 00:18

                    Это возможно только в Local-First субд, где сам клиент неявно выступает в качестве посредника в синхронизации серверов. То есть, когда он подключается к SRV2, то передаёт ему и это изменение и любое другое, которое SRV2 ещё не видел.


                    1. maxim_ge Автор
                      23.12.2022 00:25

                      У клиента для этого еще одна база должна быть и именно в нее он и должен всегда писать First.


    1. funny_falcon
      21.12.2022 09:03

      Strong Eventual Consistency: в одной «локальной копии» я купил Бентли, в другой - Ламборгини. Денег было на одну машину. Как это будет синхронизироваться?

      «Овердрафт» - видимо так?


      1. nin-jin
        21.12.2022 09:40

        https://en.m.wikipedia.org/wiki/Eventual_consistency

        Да, будет овердрафт. Вы возможно путаете SEC со Strict Serializable.


  1. vanxant
    20.12.2022 12:11
    +1

    1. Очень крутое введение в проблематику

    2. Я лично удивлён отсутствием ссылки на CAP-теорему. Без опоры на неё у читающего может возникнуть светлая идея сделать "вжух" и чтобы всё заработало.


    1. maxim_ge Автор
      20.12.2022 12:22
      +1

      Я лично удивлён отсутствием ссылки на CAP-теорему

      Это следующий этап, КМК. Сначала определяем понятие Consistency, а затем уже можно его связывать с другими, например с Availability и Partition tolerance.