又一份講基本 RDBMS 的文件

前幾天在 Hacker News Daily 看到「Things You Should Know About Databases」這篇文章,裡面講了很多基本的 RDBMS 的概念,另外 Hacker News 上對應的討論在「Things to know about databases (architecturenotes.co)」這邊。

裡面講了 B-treeB+tree 的差異:

不過這點在維基百科上也蠻清楚的文字說明:

A B+ tree can be viewed as a B-tree in which each node contains only keys (not key–value pairs), and to which an additional level is added at the bottom with linked leaves.

另外裡面的 sorted 的那張圖:

這邊的說明不完全正確,在維基百科上的「Database index」這個條目裡面有提到 Non-clustered、Clustered 與 Cluster 三種架構,這邊圖片所表示的是 Non-clustered。在 InnoDB 裡面 data 是照 primary key 順序存放的 (沒有指定時會有一套邏輯選出哪個欄位當 PK,最後的情況是有 hidden key)。

再來就是提到 isolation,這邊也講的比較淺,只提到 ANSI 標準裡面的 SERIALIZABLEREPEATABLE READ (RR)、READ COMMITTED (RC) 與 READ UNCOMMITTED (RU) 四個,但沒提到像是 SNAPSHOT ISOLATION (SI) 這類的也很常見的標準。

說到 SI,在查 Snapshot isolation 的資料時整理了一下 PostgreSQL 的混亂情況。

在 PostgreSQL 9.0 以及更早前的版本,你指定 SERIALIZABLE 其實只有做到 Snapshot isolation 的等級,到了 9.1+ 後,SERIALIZABLE 才是真正做到 ANSI 定義的強度:

Snapshot isolation is called "serializable" mode in Oracle and PostgreSQL versions prior to 9.1, which may cause confusion with the "real serializability" mode.

另外 ANSI 定義的 isolation level 很難「用」 (但還是值得學起來,算是基本的東西),實際上的使用都是看各家資料庫對 isolation level 的保證程度來設計。

PostgreSQL 的 SERIALIZABLE 的 bug

這是 Jespen 第一次測試 PostgreSQL,就順利找出可重製的 bug 了:「PostgreSQL 12.3」。

第一個 bug 是 REPEATABLE READ 下的問題,不過因為 SQL-92 定義不夠嚴謹的關係,其實算不算是 bug 有討論的空間,這點作者 Kyle Kingsbury 在文章裡也有提出來:

Whether PostgreSQL’s repeatable-read behavior is correct therefore depends on one’s interpretation of the standard. It is surprising that a database based on snapshot isolation would reject the strict interpretation chosen by the seminal paper on SI, but on reflection, the behavior is defensible.

另外一個就比較沒問題了,是 SERIALIZABLE 下的 bug,在 SQL-92 下對 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 concurrently 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 下一堆 transaction 的執行結果,你至少可以找到一組排序,使得這些 transaction 的結果是等價的。

而 Jespen 順利找出了一組 transaction (兩個 transaction),在 SERIALIZABLE 下都成功 (但不應該成功):

對於這兩個 transaction,不論是上面這條先執行,還是下面這條先執行,都不存在等價的結果,所以不符合 SERIALIZABLE 的要求。

另外也找到一個包括三個 transaction 的情況:

把 transaction 依照執行的結果把 dependency 拉出來,就可以看出來裡面產生了 loop,代表不可能在 SERIALIZABLE 下三個都成功。

在 Jespen 找到這些 bug 後,PostgreSQL 方面也找到軟體內產生 bug 的部份,並且修正了:「Avoid update conflict out serialization anomalies.」,看起來是在 PostgreSQL 引入 Serializable Snapshot Isolation (SSI) 的時候就有這個 bug,所以 9.1 以後的版本都有這個問題...

這次順利打下來,測得很漂亮啊... 翻了一下 Jespen 上的記錄,發現好像還沒測過 MySQL,應該會是後續的目標?

MySQL 上不同 Isolation Level 對效能的影響

目前看到的結論都是:MySQL (InnoDB) 上因為高度對 RR (REPEATABLE-READ) 最佳化,使得 RR 的效能反而比 RC (READ-COMMITTED) 以及 RU (READ-UNCOMMITTED) 都好。

不清楚 RR/RC/RU 差異的可以參考維基百科上「Isolation (database systems)」的解釋...

從 2010 年在測 5.0 的「Repeatable read versus read committed for InnoDB」到 2015 年測 5.7 的「MySQL Performance : Impact of InnoDB Transaction Isolation Modes in MySQL 5.7」都測出 RR 的效能比 RC/RU 好... 三段分別是 RR/RC/RU:

所以在 MySQL 上沒有使用 RC/RU 的必要... (抱頭)

InnoDB 的 Isolation Level 以及 Performance Schema 對效能的影響

雖然 Mark Callaghan 現在的主力都在 MyRocks 上,但他還是對 InnoDB 上的效能頗關注 (畢竟是個成熟而且競爭的產品)。而這篇「Sysbench, InnoDB, transaction isolation and the performance schema」講到 MySQL 5.6.26 裡的 InnoDB,了解 isolation level 與 performance schema 對效能的差異。結果可以在這邊翻到。

關掉 performance schema 會讓效能變好是預期的,不過看起來比預期小很多。另外某些情況下 RR (REPEATABLE-READ) 的效能會比 RC (READ-COMMITTED) 好倒是頗意外,這邊也有給出原因:

Using repeatable-read boosts performance because it reduces the mutex contention from getting a consistent read snapshot as that is done once per transaction rather than once per statement.

不過看了看數據,純粹讀取的部份 RC 會在某些地方快一些,不過整體來說在 MySQL 5.6.26 上的 RR 與 RC 差異真的不算太明顯了...

繼續學 Isolation...

在「測試 MariaDB 上 Galera Cluster 的 Isolation」這邊與「從頭學一次 Isolation level 的 REPEATABLE-READ 與 SERIALIZABLE」對應了 Stripe 的「Call me Maybe: MariaDB Galera Cluster」以及 Percona 的「Clarification on “Call me Maybe: MariaDB Galera Cluster”」這兩篇文章,兩邊在空中交火...

而「Call me Maybe: Percona XtraDB Cluster」這篇是目前最新的一篇 (文章日期有點怪怪的,不應該是 2015/09/04)。

畢竟 Galera Cluster 是目前現在在用的產品,雖然跑起來也沒什麼大問題,但預先多了解一點總是比較保險。而要參與這場 Isolation 戰爭,又花了不少時間看幾份資料。(包括重頭讀的)

第一個當然是 1992 年發表的 SQL-92:「ISO/IEC 9075:1992, Database Language SQL」,在 SQL-92 定義了四個 Isolation 的層級,分別是 SERIALIZABLEREPEATABLE READREAD COMMITTEDREAD UNCOMMITTED

另外一篇常被拿出來討論的是 1995 年由微軟帶頭發表的「A Critique of ANSI SQL Isolation Levels」,主要是提出 Snapshot Isolation 的定義。

後來讀到覺得真的很怪的時候到 Stack Exchange 上的 Database Administrators 問:「Does SQL92's REPEATABLE-READ preclude Write Skew (A5B)?」,確認了我的想法。

這篇算是讀書心得吧,留給自己用的 :p

首先是 REPEATABLE READ 的定義是透過「現象」定義,只要符合這些條件就可以被稱作 REPEATABLE READ,這包括了保證不會出現 P1 ("Dirty read"):

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.

以及保證不會出現 P2 ("Non-repeatable read"):

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.

並且保證 Atomicity 以及 no updates will be lost:

The four isolation levels guarantee that each SQL-transaction will be executed completely or not at all, and that no updates will be lost.

只要做到這些條件,就可以說符合 SQL-92 的 REPEATABLE READ 要求。


另外一方面,在「A Critique of ANSI SQL Isolation Levels」定義的 SQL-92 認為 REPEATABLE READ 是透過 locking 機制實作的 (整篇的寫法的確是朝這個方向走),並且基於 locking mechanism 來推論後續的行為,所以這篇 paper 裡面很多說明是帶著這個大前提的。

而當時已經有資料庫是使用 MVCC 架構在實作,可以透過 read snapshot 的方式找出另外一條路。

舉例來說,InnoDB 對 REPEATABLE READ 實作的方式是透過 snapshot 實作:(出自「MySQL :: MySQL 5.6 Reference Manual :: 13.3.6 SET TRANSACTION Syntax」)

For consistent reads, there is an important difference from the READ COMMITTED isolation level: All consistent reads within the same transaction read the snapshot established by the first read. This convention means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other.

這樣避開了 P1、P2 (甚至多做避開到了 P3,不過這邊不重要),並且 Atomicity。而 Update Lost 問題則再透過 lock 機制避免。所以 InnoDB 的 REPEATABLE READ 其實比 SQL-92 的要求多做到了很多事情...


到這邊再回頭看原來的討論文章,才會發現 Isolation 裡有滿滿的地雷可以採 XD

從頭學一次 Isolation level 的 REPEATABLE-READ 與 SERIALIZABLE

在「測試 MariaDB 上 Galera Cluster 的 Isolation」這篇提到了 StripeKyle Kingsbury 寫了「Call me Maybe: MariaDB Galera Cluster」這篇文章,在討論 Galera Cluster 的 Isolation 問題。

Percona 的 CTO Vadim Tkachenko 寫了回應的文章:「Clarification on “Call me Maybe: MariaDB Galera Cluster”」。

看完後再跑去翻了一些資料,首先是 SQL 92 對 Isolation 的原始定義,尤其是對 REPEATABLE-READ 的定義。

REPEATABLE-READ 是透過 P2 ("Non-repeatable read") 來定義的,只要保證不會發生 P2 就是 REPEATABLE-READ 了:

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.

也就是說,T1 讀過的 row 只要在 T1 transaction 的期間內都保持一樣就可以。由於沒有多餘其他保證,所以有機會產生 P3 ("Phantom") 問題:

P3 ("Phantom"): SQL-transaction T1 reads the set of rows N that satisfy some . SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the 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.

SERIALIZABLEREPEATABLE-READ 的定義方式不同,並不是避免 P3 就可以叫做 SERIALIZABLE。SQL 92 定義說明必須與序列執行的結果一樣才能叫做 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 concurrently 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 一定可以避免 P3 發生,但避免了 P3 不代表做到 SERIALIZABLE

另外一份資料是「MySQL :: MySQL 5.6 Reference Manual :: 13.3.6 SET TRANSACTION Syntax」對於 MySQL 5.6 中 InnoDB 實作 REPEATABLE-READ 的細節:

This is the default isolation level for InnoDB. For consistent reads, there is an important difference from the READ COMMITTED isolation level: All consistent reads within the same transaction read the snapshot established by the first read. This convention means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other. See Section 14.2.2.2, “Consistent Nonlocking Reads”.

InnoDB 的 REPEATABLE-READ 會在第一次讀取時建立一份 snapshot,所以 MySQL 的 REPEATABLE-READ 有達到不產生 P2 的要求,但也因為 snapshot 的關係而不會產生 P3,不過也就僅此而已,寫入的部份還是沒有達到 SERIALIZABLE 的要求。

回到原來的文章所整理出來的測試資料 (我稍微排版過),在這兩個不同的 transaction 同時發生時:(不同人都轉帳給 id = 8)

BEGIN; -- T1
SELECT balance FROM accounts WHERE account_id = 5;
SELECT balance FROM accounts WHERE account_id = 8;
UPDATE account SET balance = 75 WHERE account_id = 5;
UPDATE account SET balance = 125 WHERE account_id = 8;
COMMIT;
BEGIN; -- T2
SELECT balance FROM accounts WHERE account_id = 6;
SELECT balance FROM accounts WHERE account_id = 8;
UPDATE account SET balance = 80 WHERE account_id = 6;
UPDATE account SET balance = 120 WHERE account_id = 8;
COMMIT;

SERIALIZABLE 保證執行結果會與某種循序的結果相符 (可能是 T1 -> T2,也可能是 T2 -> T1)。但 REPEATABLE-READ 只保證在 transaction 裡面看到的 row 不會變化。

在 MySQL 裡,如果你想要跟 MySQL 告知「變更的結果會收到某些欄位的值的影響」,你至少要用 LOCK IN SHARE MODE 來做 (或是用 FOR UPDATE 會更清楚表示出意思),也就是:

BEGIN; -- T1
SELECT balance FROM accounts WHERE account_id = 5 LOCK IN SHARE MODE;
SELECT balance FROM accounts WHERE account_id = 8 LOCK IN SHARE MODE;
UPDATE account SET balance = 75 WHERE account_id = 5;
UPDATE account SET balance = 125 WHERE account_id = 8;
COMMIT;
BEGIN; -- T2
SELECT balance FROM accounts WHERE account_id = 6 LOCK IN SHARE MODE;
SELECT balance FROM accounts WHERE account_id = 8 LOCK IN SHARE MODE;
UPDATE account SET balance = 80 WHERE account_id = 6;
UPDATE account SET balance = 120 WHERE account_id = 8;
COMMIT;

這樣至少會有一個 transaction 會出現 deadlock 訊息,於是就會確保是正確的結果:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

值得一提的是,Percona 文章提到的第二種解法是有問題的:

BEGIN;
SELECT balance FROM accounts WHERE account_id = 5;
SELECT balance FROM accounts WHERE account_id = 8;
UPDATE account SET balance = balance - 25 WHERE account_id = 5;
UPDATE account SET balance = balance + 25 WHERE account_id = 8;
COMMIT;

這樣「總和」會因為保證原子性 (atomicity) 所以不會產生問題,但 balance 可能會產生出負數。

所以結論是 Kyle Kingsbury 那篇文章測試的方法本來就有問題,在 SELECT 時少了 LOCK IN SHARE MODE 或是 FOR UPDATE 的要求。

測試 MariaDB 上 Galera Cluster 的 Isolation

雖然人還在大阪員旅,不過看到這篇很精彩的測試還是先寫下來:「Call me Maybe: MariaDB Galera Cluster」,作者在 Stripe 花了很多時間在測試各種資料庫的資料正確性。

由於 PerconaPercona XtraDB Cluster (PXC) 是基於 Galera Cluster 改出來的,再加上敝公司裡面大量使用 PXC,決定還是趕緊花時間看完整篇文章確認到底講了什麼。

先講目前看到的結論:

  • 作者認為 Galera Cluster 官方宣稱的 SNAPSHOT-ISOLATION (SI) > REPEATABLE-READ (RR) 是有問題的講法,在學理上 SI 與 RR 都低於 SERIALIZABLE (1SR),也都高於 READ-COMMITTED (RC),但兩者之前沒有直接包含。
  • Galera Cluster 的測試結果看起來沒有做到 SI 的要求,不過測試中看起來有達到 READ-UNCOMMITTED (RU),在 RC 的測試中也沒找到問題,不過作者還是懷疑系統有達到 RC 的要求。

另外從其他討論串看起來,當所有人寫入都到同一台就沒有這些狀況,可以達到 1SR 的要求,這也是目前還蠻常見的系統設計,所以這次的問題看起來沒有這麼大。

後面也提到裝 MySQL Cluster 裝了好幾個禮拜還是裝不起來的事情:

Galera is easy to install–I spent weeks trying to set up MySQL Cluster to no avail, and got Galera Cluster running in a matter of hours.

有種,不是很意外的感覺...

Anyway,原文下面的 comment 可以看到 Baron Schwartz 也跑出來討論,另外在 GitHub 上有開始有討論了:「Snapshot Isolation may not be as isolated as one would like」,看起來直接戳破 Galera Cluster 支援 SI 的宣傳詞了,接下來拉板凳繼續看戲吧... XD