Async Commit 原理介紹

語言: CN / TW / HK

本文作者:陳奕霖(sticnarf),PingCAP 研發工程師,TiKV Committer,熱衷於開源技術,在分散式事務領域有豐富經驗,目前致力於優化 TiDB 的分散式事務效能。

TiDB 提供了原生的分散式事務支援,實現低延遲的分散式事務是持續的優化方向。TiDB 5.0 引入的 Async Commit 特性大大改善了事務提交的延遲,這一特性主要由本文作者陳奕霖(sticnarf),以及趙磊(youjiali1995),Nick Cameron(nrc)和周振靖(MyonKeminta)實現。 本文將向大家介紹 Async Commit 的設計思路、原理以及關鍵的實現細節。

Percolator 的額外延遲

TiDB 事務基於 Percolator 事務模型。讀者可以參考我們之前的部落格詳細瞭解 Percolator 事務模型的提交過程。

上圖是引入 Async Commit 之前的提交流程示意圖。使用者向 TiDB 傳送 COMMIT 語句之後, TiDB 至少要經歷以下的步驟才能向用戶返回提交結果

1. 併發地 prewrite 所有的 keys;

2. 從 PD 獲取時間戳作為 Commit TS;

3. 提交 primary key。

整個提交過程的關鍵路徑包括了至少兩次 TiDB 和 TiKV 之間的網路互動。只有提交 secondary keys 的操作在 TiDB 後臺非同步完成。

在 TiDB 的事務模型中,我們可以大致將 TiDB 節點認為是事務的協調者,而 TiKV 節點是事務的參與者。傳統二階段提交中我們一般預設協調者的資料儲存在節點本地,但 TiDB 事務模型中,所有的事務相關資料都在 TiKV 上。也正是因此,傳統二階段提交中,第一階段結束後事務狀態即可確定;而 TiDB 需要完成第二階段的一部分,將事務狀態儲存到 TiKV 上,才能向用戶響應。

不過這也意味著,TiDB 之前的事務模型是有提升空間的。能否改進 Percolator 事務模型,讓事務的狀態在完成第一階段後,無需額外的網路互動即可確定呢?

改進事務完成條件

引入 Async Commit 之前,事務的 primary key 被提交才意味著這個事務被提交。Async Commit 力圖實現的,就是把確定事務狀態的時間提前到完成 prewrite 的時候,讓整個提交的第二階段都非同步化進行。也就是說, 對於 Async Commit 事務,只要事務所有的 keys 都被成功 prewrite,就意味著事務提交成功

下圖是 Async Commit 事務的提交流程(你可能發現原來獲取 Commit TS 的環節沒有了,在 prewrite 前多了從 PD 獲取時間戳作為 Min Commit TS 的操作,這裡的原因會在後文中介紹):

為了達到這個目標,我們有兩個主要問題要解決:

  • 如何確定所有 keys 已被 prewrite。
  • 如何確定事務的 Commit TS。

如何找到事務所有的 keys

引入 Async Commit 之前,事務的狀態只由 primary key 決定,所以只需要在所有 secondary key 上儲存到 primary key 的指標。如果遇到未提交的 secondary key,查詢 primary key 的狀態即可知道當前事務的狀態:

判斷 Async Commit 事務則需要知道所有 keys 的狀態,所以我們需要能從事務的任意一個 key 出發,查詢到事務的每一個 key。於是我們做了一點小的修改,保留從 secondary key 到 primary key 指標的同時,在 primary key 的 value 裡面儲存到到每一個 secondary key 的指標:

Primary key 上儲存了所有 secondary keys 的列表,但顯然,如果一個事務包含的 keys 的數量特別多,我們不可能把它們全部存到 primary key 上。所以 Async Commit 事務不能太大,當前我們只對包含不超過 256 個 keys 且所有 keys 的大小總和不超過 4096 位元組的事務使用 Async Commit。

過大的事務的提交時長本身較長,減少一次網路互動帶來的延遲提升不明顯,所以我們也不考慮用類似多級結構的方式讓 Async Commit 支援更大的事務。

如何確定事務的 Commit TS

Async Commit 事務的狀態在 prewrite 完成時就必須確定了,Commit TS 作為事務狀態的一部分也不例外。

預設情況下,TiDB 事務滿足快照隔離的隔離級別和線性一致性。我們希望這些性質對於 Async Commit 事務同樣能夠成立,那麼確定合適的 Commit TS 非常關鍵。

對於 Async Commit 事務的每一個 key,prewrite 時會計算並在 TiKV 記錄這個 key 的 Min Commit TS,事務所有 keys 的 Min Commit TS 的最大值即為這個事務的 Commit TS。

下文會介紹 Min Commit TS 的計算方式,以及它們是如何使 Async Commit 事務滿足快照隔離和線性一致性的。

保證快照隔離

TiDB 通過 MVCC 實現快照隔離。事務在開始時會向 TSO 獲取 Start TS,為實現快照隔離,我們要保證以 Start TS 作為快照時間戳始終能讀取到一個一致的快照。

為此,TiDB 的每一次快照讀都會更新 TiKV 上的 Max TS1。Prewrite 時,Min Commit TS 會被要求至少比當前的 Max TS 大2,也就是比所有先前的快照讀的時間戳大,所以可以取 Max TS + 1 作為 Min Commit TS。在這個 Async Commit 事務提交成功後,由於其 Commit TS 比之前的快照讀的時間戳大,所以不會破壞快照隔離。

下面的例子中,事務 T1 要寫 x 和 y 兩個 keys。T2 讀取 y 將 Max TS 更新到 5,所以接下來 T1 prewrite y 時,Min Commit TS 至少為 6。T1 prewrite y 成功即意味著 T1 提交成功,而 T1 的 Commit TS 至少為 6。所以之後 T2 再讀取 y 時,不會讀取到 T1 更新的值,事務 T2 的快照保持了一致。

保證線性一致性

線性一致性實際上有 兩方面的要求

  • 循序性(sequential)
  • 實時性(real-time)

實時性要求在事務提交成功後,事務的修改立刻就能被新事務讀取到。新事務的快照時間戳是向 PD 上的 TSO 獲取的,這要求 Commit TS 不能太大,最大不能超過 TSO 分配的最大時間戳 + 1。

在快照隔離一節提到,Min Commit TS 的一個可能的取值是 Max TS + 1。用於更新 Max TS 的時間戳都來自於 TSO,所以 Max TS + 1 必然小於等於 TSO 上未分配的最小時間戳。除了 TiKV 上的 Max TS 之外,協調者 TiDB 也會提供 Min Commit TS 的約束(後面會提到),但也不會使其超過 TSO 上未分配的最小時間戳。

循序性要求邏輯上發生的順序不能違反物理上的先後順序。具體地說,有兩個事務 T1 和 T2,如果在 T1 提交後,T2 才開始提交,那麼邏輯上 T1 的提交就應該發生在 T2 之前,也就是說 T1 的 Commit TS 應該小於 T2 的 Commit TS。3

為了保證這個特性,TiDB 會在 prewrite 之前向 PD TSO 獲取一個時間戳作為 Min Commit TS 的最小約束。由於前面實時性的保證,T2 在 prewrite 前獲取的這個時間戳必定大於等於 T1 的 Commit TS,而這個時間戳也不會用於更新 Max TS,所以也不可能發生等於的情況。綜上我們可以保證 T2 的 Commit TS 大於 T1 的 Commit TS,即滿足了循序性的要求。

綜上所述, 每個 key 的 Min Commit TS 取 prewrite 時的 Max TS + 1 和 prewrite 前從 PD 獲取的時間戳的最大值,事務的 Commit TS 取所有 key 的 Min Commit TS 的最大值,就能夠同時保證快照隔離和線性一致性。

一階段提交 (1PC)

如果一個事務只更新一條記錄的非唯一索引,或是隻插入一條沒有二級索引的記錄,它只會涉及到單個 Region。在這種只涉及一個 Region 的場景下,是不是可以不使用分散式事務提交協議,只用一個階段完成事務的提交?這當然是可行的,但困難就在於一階段提交的事務的 Commit TS 如何確定。

有了 Async Commit 計算 Commit TS 的基礎,一階段提交實現的困難點也解決了。我們用和 Async Commit 相同的方式去計算出一階段提交事務的 Commit TS,通過一次和 TiKV 的互動直接將事務提交即可:

一階段提交沒有使用分散式提交協議,減少了寫 TiKV 的次數。所以如果事務只涉及一個 Region,使用一階段提交不僅可以降低事務延遲,還可以提升吞吐。4

一階段提交特性在 TiDB 5.0 中作為 Async Commit 的一部分被引入。

因果一致性

上文提到向 TSO 獲取 Min Commit TS 可以保證循序性。那麼如果把這一步省去會怎樣?這樣不就又省了一次 PD 和 TiDB 的網路互動延時嗎?

然而在這種情況下,我們可以找到違反循序性的例子。假設 x 和 y 位於不同的 TiKV 節點上,事務 T1 會修改 x,事務 T2 會修改 y。T1 比 T2 開始得早,但使用者在 T2 在提交成功後才通知 T1 提交。這樣,對於使用者來說,事務 T1 的提交發生在事務 T2 提交完成之後,如果滿足循序性,邏輯上 T1 應該晚於 T2 提交。

如果省去了 prewrite 前獲取 Min Commit TS 的操作,T1 的 Commit TS 可能為 2,小於 T2 的 Commit TS = 6。如果有一個 Start TS 為 3 的事務 T3,它可以觀察到 T2 在邏輯上晚於 T1 的事實。所以此時是沒有線性一致性的。

此時,快照的概念也可能和預期的不太一樣。下面的例子中,晚開始的 T2 通知事務 T1 提交,T1 的 Commit TS 可能會小於 T2 的 Start TS。

對於使用者來說,T2 在後續讀取到 T1 對 x 的修改是不符合預期的。這種情景下,可重複讀的性質沒有被破壞,但是否還符合快照隔離就存在爭議了5。

我們將這樣更弱的一致性稱為因果一致性: 有因果關係的事務的順序和它們物理上提交的順序一致,但沒有因果關係的事務之間的提交順序則是不確定的。 當且僅當兩個事務加鎖或寫入的資料有交集時,我們認為它們有因果關係。事實上,這裡的因果關係只包含資料庫可知的因果關係,不涉及上面例子中“應用層通知”這種外部的因果關係。

發生這樣的異常場景的條件比較苛刻,所以我們給使用者提供了省去獲取 Min Commit TS 的方式:使用 START TRANSACTION WITH CAUSAL CONSISTENCY ONLY 開啟的事務,在提交時不獲取 Min Commit TS。如果你的使用場景裡,不涉及上面這種在資料庫外部控制兩個同時執行的事務提交順序的情況,可以嘗試將一致性級別降低,減少一次 TiDB 從 PD TSO 獲取時間戳的耗時。

效能提升

Async Commit 使事務完成的時間點提前到 prewrite 結束時,使提交 primary key 的操作非同步化。提交 primary key 這一步操作在整個事務中耗時的佔比越大,那 Async Commit 的提升就越顯著。互動少的小事務通常能依靠 Async Commit 得到較大的提升。反之, 也有一些 Async Commit 提升不明顯的場景

  • 包含很多條語句,有較長的互動邏輯的事務,事務提交的耗時佔比較低,Async Commit 的提升則不會很明顯。
  • 包含 keys 較多,寫入資料量較大的事務,prewrite 的耗時明顯長於提交 primary key 的耗時,Async Commit 的提升也不會很明顯。
  • Async Commit 沒有減少對 TiKV 的讀寫量,所以不能提升極限吞吐。所以如果系統本身已接近吞吐極限,Async Commit 不會帶來明顯提升。

Sysbench oltp_update_index 場景下,一個事務只寫入行記錄和索引兩個 keys,同時也是沒有額外互動的 auto commit 事務,所以理論上 Async Commit 能大幅降低其延時。

實際測試也能證明這一點。如上圖所示,在固定 2000 TPS 的條件下測試 sysbench oltp_update_index ,開啟 Async Commit 後,平均延時降低了 42%,p99 延時降低了 32%。

如果事務只涉及一個 Region,一階段提交的優化能夠更加顯著地降低事務提交的延遲。由於減少了 TiKV 的寫入量,所以也可以提升極限吞吐。

如上圖所示,在固定 2000 TPS 的條件下測試 sysbench oltp_update_non_index 。這是一個一個事務只寫入一個 Region 的場景,開啟一階段提交後,平均延時降低了 46%,p99 延時降低了 35%。

總結

Async Commit 讓 TiDB 事務提交減少了一次寫 TiKV 的延時,是對原先 Percolator 事務模型的一個較大的改進。 新建立的 TiDB 5.0 叢集預設啟用 Async Commit 和一階段提交。從舊版本升級到 5.0 的叢集,則需要使用者手動將全域性系統變數 tidb_enable_async_committidb_enable_1pc 設為 ON 來開啟 Async Commit 和一階段提交特性。

限於篇幅,本文只涉及到了 Async Commit 中的關鍵設計,感興趣的讀者可以閱讀 Async Commit 的設計文件瞭解更多的細節。未來我們也會持續改進 TiDB 事務的效能,改善大家的 TiDB 使用體驗,讓更多人能從 TiDB 中收益。

註釋:

1. 為了保證在 Region Leader 遷移後,新 Leader 的 Max TS 足夠大,在 Region Leader 遷移和 Region Merge 後,TiKV 也會從 PD 獲取最新的時間戳更新 Max TS。

2. 在 prewrite 過程中,為了防止更新的快照讀破壞這個約束,TiKV 會對 prewrite 的 key 加上記憶體鎖,短暫地阻塞住 Start TS ≥ Min Commit TS 的讀請求。

3. 如果 T1 和 T2 的提交過程在時間上有重疊,那麼它們邏輯上的提交的先後順序則是無法確定的。

4. 準確地說,一階段提交只應用於通過單次寫 TiKV 請求就能完成事務的情況。為了提升提交效率,較大的事務會被切分成很多個請求,此時就算它們涉及的都是同一個 Region,目前也不會使用一階段提交。

5. 如果我們允許認為 T1 在邏輯上的提交時間早於 T2 開始的時間(因為不滿足線性一致性),那麼這種情況依然可以認為是滿足快照隔離的。