Async Commit 原理介紹

本文作者:陳奕霖 (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

引入 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

默認情況下,TiDB 事務滿足快照隔離的隔離級別和線性一致性。我們希望這些性質對於 Async Commit 事務同樣能夠成立,那麼確定合適的 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 的快照保持了一致。

ueae14

🌟 保證線性一致性

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

實時性要求在事務提交成功後,事務的修改立刻就能被新事務讀取到。新事務的快照時間戳是向 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 的事實。所以此時是沒有線性一致性的。

phxjM5

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

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

cUZFDD

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

發生這樣的異常場景的條件比較苛刻,所以我們給用戶提供了省去獲取 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 提升不明顯的場景

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_commit和 tidb_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 開始的時間(因爲不滿足線性一致性),那麼這種情況依然可以認爲是滿足快照隔離的。

歡迎聯繫我們:

Transaction SIG 的主要職責是對 TiKV 分佈式事務的未來發展進行討論和規劃,並組織社區成員進行相關開發和維護。現在你們可以在 TiKV 社區 Slack 的 #sig-transaction channel 找到我們。

從 TiDB 4.0 發佈以來總計有 538 位 Contributor 提交了 12513 個 PR 幫助我們一起完成企業級核心場景的里程碑版本的開發,Async Commit 只是這些 PR 的代表。爲感謝所有參與 5.0 版本的 Contributor 們,TiDB 社區精心準備了一份 5.0 定製周邊。如果你也是 5.0 的 Contributor,請在 5 月 5 日前填寫表單,告訴我們你的地址。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/m-22HHO3-9uyRxCfNNHLjA