從頭學一次 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

Percona 對 mysql_query_cache 的測試 (以 Magento 為例)

Percona 的人以現在的觀點來看 mysql_query_cache:「The MySQL query cache: Worst enemy or best friend?」。

起因主要也是懷疑 query cache 是 global mutex 在現在的硬體架構 (主要是 CPU 數量成長) 應該是個負面的影響,但不確定影響多少:

The query cache is well known for its contentions: a global mutex has to be acquired for any read or write operation, which means that any access is serialized. This was not an issue 15 years ago, but with today’s multi-core servers, such serialization is the best way to kill performance.

這邊就有點怪了,PK search 應該是個位數 ms 等級才對 (一般 EC 網站的資料量都應該可以 memory fit),不知道他是怎麼測的:

However from a performance point of view, any query cache hit is served in a few tens of microseconds while the fastest access with InnoDB (primary lookup) still requires several hundreds of microseconds. Yes, the query cache is at least an order of magnitude faster than any query that goes to InnoDB.

anyway,他實際測試兩個不同的 configuration,首先是打開 query cache 的:

再來是關閉 query cache 的:

測試的方式在原文有提到,這邊就不抄過來了。測試的結果可以看到關閉 query cache 得到比較好的 thoughput:

Throughput scales well up to somewhere between 10 and 20 threads (for the record the server I was using had 16 cores). But more importantly, even at the higher concurrencies, the overall throughput continued to increase: at 20 concurrent threads, MySQL was able to serve nearly 3x more queries without the query cache.

跟預期的差不多,硬體的改變使得演算法也必須跟著改,不然就會遇到問題...

Amazon S3 的改善

在「Amazon S3 Introduces New Usability Enhancements」這邊提到了 Amazon S3 的兩個改善。

第一個是業務面的改善,以前應該是開 support ticket 請人調整 S3 bucket 數量上限,現在則是可以直接透過界面申請?(沒有遇過瓶頸,不知道以前是不是在界面上看不到...)

第二個才是重頭戲:Read-after-write Consistency。

With this enhancement, Amazon S3 now supports read-after-write consistency in all regions for new objects added to Amazon S3.

也就是說,Amazon S3 現在保證「新增的 object」「可以在建立後馬上被讀取」。之前還沒修正前,這個問題有多嚴重呢?可以看 2014 年時「Netflix 對 S3 的 Eventually Consistency 的補強...」這邊 Netflix 在跑 PigHive 遇到的問題。

Netflix 這邊舉的例子是兩個 Pig cluster 在跑,其中 Pig-2 需要 Pig-1 跑出來的資料,在這次公告前,如果 Pig-1 的資料寫回 Amazon S3 時不會馬上出現,那麼 Pig-2 就會拿不完整的資料執行:

Pig-2 is activated based on the completion of Pig-1 and immediately lists the output directories of the previous task. If the S3 listing is incomplete when the second job starts, it will proceed with incomplete data.

而現在總算是保證新的 object 可以馬上被讀取,所以 Netflix 可以利用一個檔案列出所有的 filename,確保知道所有的檔案名稱... (LIST 指令還是 eventually consistent,所以這部份還是要自己處理)

RAID 卡的電池維護

實際的世界都是由 workaround 疊 workaround 解決問題的...

MySQL 資料庫一般都用 RAID 10,利用 RAID 1 的特性保護資料,並且利用 RAID 0 的特性提昇 IOPS 能力。

而這些 RAID 卡通常都會提供 cache,預設應該都會開 read cache,可以大幅增加 random read 的速度。而另外也可以打開 write cache (也就是 write-back),寫入時先寫到 cache 裡,RAID 卡馬上就會跟作業系統回報完成,藉以加速 random write 的速度。

但這樣就會有風險,當資料還沒寫入硬碟就斷電時就會遺失資料。所以在設定 write-back 的 RAID 卡上安裝電池就變成解法之一。

而電池會有壽命問題,所以配電池的 RAID 卡會每隔一陣子就放電測試電池可以撐多久,但在放電測試時,如果斷電就有可能造成資料遺失,於是又冒出很多方法解決。

也就是在「Learning to Deal With Learning」這篇提到 RAID 卡電池維護的事情。

每一層都是 workaround 想辦法解決問題,然後再用 workaround 解決前面造成的問題...

Anyway,有幾種解法,其中仍然對上層作業系統與應用程式透明的解法是:

  • 雙電池架構,很明顯的可以一次只測一顆。
  • 改用 NVRAM,就不需要電池了,不過速度以及成本會是另外一個問題。

另外,對上層作業系統與應用程式有影響的方式:

  • 放電測試時將 write cache 關閉,切回 write-through。這點在原文裡也有提到,效能其實會受到蠻大的影響。
  • 不放電測試了,但這樣的缺點就是拿安全性交換,當斷電時不知道能不撐過去。
  • 或是自己控制放電測試的時間,這可以配合上面切回 write-through 的方式,挑負載比較輕的離峰時間做。

看了下來雙電池架構還不錯,增加的成本還算可以接受,而且因為效能不受到影響,也確保資料安全性,整體維護起來比較簡單。而之後在規模更大的時候,應該就會直接考慮跳到自己放電測試的方式來處理電池問題...

Netflix 對 S3 的 Eventually Consistency 的補強...

眾所皆知的,Netflix 幾乎將所有服務都放在 AWS 上,這當然也包括了 Amazon S3

在 Amazon S3 上會有 Eventually Consistency 的問題:寫入後可能會讀到舊的資料,於是就算錯資料了...

Netflix 的人討論了幾種方案,後來開發 s3mper 用來解決 Amazon S3 的 Eventually Consistency 問題:「S3mper: Consistency in the Cloud」。

s3mper 透過 AWS DynamoDB 儲存檔案的 metadata,藉以得知是否 consistency。而 Amazon DynamoDB 本身雖然也是 Eventually Consistency,但多了 API 可以得知是否 Consistency。

Supported Operations in DynamoDB 可以看到 Data Read and Consistency Considerations 這段提供了兩種 read mode:

  • Eventually Consistent Reads
  • Strongly Consistent Reads

在 Strongly Consistent Reads 中,可以確認讀到的是不是最新的資料。只有當 DynamoDB 與 S3 的資料都正確時才繼續往下跑...

這個解法相當於在 Amazon S3 上面架了一層防護網,算是 workaround 吧 :p 如果 Amazon S3 可以提供 consistency 資訊的話,也就不用這樣搞了...

AWS 提供跨區的 MySQL Read Replica...

Amazon RDS 將提供跨區的 MySQL read replication。看起來是針對 5.6+ 的版本提供這個功能...

有兩篇官方文章,一篇是 CTO 發了一篇「Expanding the Cloud: Enabling Globally Distributed Applications and Diaster Recovery」,另外一篇是官方網誌上的「Cross-Region Read Replicas for Amazon RDS for MySQL」。

用圖表示比較容易懂:

在 US-East 建立 MySQL master,另外在 EU 與 Tokyo 建立 slave replication。不知道中間的 traffic 有沒有過 IPSec 或是 SSL?

Amazon RDS 可以直接產生 Read Replica Replication 了...

以往要在 Amazon RDS 產生 Read Replica Replication 需要複雜的 snapshot 處理,但現在 AWS 直接提供這個功能了,而且可以同時生很多台:「New Read Replica Capabilities for Amazon RDS」。

這有多重要呢?以前因應流量瞬間爆增時的方式是增加 web server,並且利用 cache (可能是 memcached) 降低對後端的 query 數量。但因為引入 cache,平常就得處理 cache invalidate 的問題。

而這個方式平常只要處理讀寫分離就可以了。當量爆增時除了 web server 增加,直接增加後端的 RDS server (Read Replica Replication),甚至可以分層:

以目前的步調來看,之後有可能會推出 Master-Master 的 HA 架構?

Update:照 comment 提到的,Multi-AZ 本身就是 HA 架構了...

Percona 將辦 Webinar 說明資料庫讀寫分離時的處理...

MySQL replication 通常是資料庫擴充的第一步,因為架設很簡單。但一般 MySQL replication 的讀寫必須分開 (寫入只能在 master)。

在「Webinar on Read/Write Splitting with PHP」看到 Percona 下星期會辦 Webinar,說明在 MySQL replication 架構下要如何處理讀寫分離。

看起來包括對 replication lag 時的處理 (slave 因為各種原因,導致跟不上 master),有興趣的人可以去報名聽聽看... 雖然是講 PHP,但這個問題在其他的語言也會遇到,聽觀念也應該有幫助。