TiDB 6.0 實戰分享丨記憶體悲觀鎖原理淺析與實踐
TiDB 6.0 版本針對悲觀事務引入了記憶體悲觀鎖的優化,帶來了明顯的效能提升。本文將從最初的樂觀事務到悲觀事務入手;介紹 6.0 版本針對悲觀鎖進行優化的原理,並結合壓測資料驗證其帶來的效能提升。
作者簡介:jiyf,開源 NewSQL 愛好者,目前就職於天翼雲,後端開發工程師,TiDB 社群資深使用者。
背景
在 v6.0.0 版本,針對悲觀事務引入了記憶體悲觀鎖的優化(In-memory lock),從壓測資料來看,帶來的效能提升非常明顯(Sysbench 工具壓測 oltp_write_only 指令碼)。
- Tps 提升 30% 左右
- 減少 Latency 在 15% 左右
TiDB 事務模型從最初的樂觀事務到悲觀事務;在悲觀事務上,又針對悲觀鎖進行的 ”Pipelined 寫入“ 和 ”In-memory lock“ 優化,從功能特性上可以看出演進過程(參考TiDB 事務概覽)。
樂觀事務
樂觀事務在提交時,可能因為併發寫造成寫寫衝突,不同設定會出現以下兩種不同的現象:
- 關閉樂觀事務重試,事務提交失敗:也就是執行 DML 成功(不會被阻塞),但是在執行 commit 時候失敗,表現出與 MySQL 等資料庫不相容的行為。
| T1 | T2 | 說明 | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | | mysql> set session tidb_disable_txn_auto_retry = 1(或者 set session tidb_retry_limit=0;);Query OK, 0 rows affected (0.00 sec) | 關閉重試 | | mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin optimistic;Query OK, 0 rows affected (0.00 sec) | T2開啟樂觀事務 | | mysql> delete from t where id = 1;Query OK, 1 row affected (0.00 sec) | | | | | mysql> delete from t where id = 1;Query OK, 1 row affected (0.00 sec) | 語句執行成功,沒有被 T1 阻塞,跟 MySQL 行為不相容 | | mysql> commit;Query OK, 0 rows affected (0.00 sec) | | T1 提交成功 | | | mysql> commit;ERROR 9007 (HY000): ...... | T2 提交失敗 |
T2 事務提交失敗,具體的報錯資訊如下:
mysql> commit;
ERROR 9007 (HY000): Write conflict, txnStartTS=433599424403603460, conflictStartTS=433599425871872005, conflictCommitTS=433599429279744001, key={tableID=5623, handle=1} primary={tableID=5623, handle=1} [try again later]
- 開啟樂觀事務重試,自動重試後返回成功,但是因為重試 DML 使用的事務 id(start_ts) 是重新獲取的,不是事務開始的事務 id(start_ts),也就是說實際執行結果相當於同一個事務中讀和寫是使用不同的事務 id(start_ts),執行結果可能跟預期不一致。
| T1 | T2 | 說明 | | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------- | | | mysql> set session tidb_disable_txn_auto_retry = 0;Query OK, 0 rows affected (0.00 sec) | 開啟重試 | | | mysql> set session tidb_retry_limit = 10;Query OK, 0 rows affected (0.00 sec) | 設定最大重試次數 | | mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin optimistic;Query OK, 0 rows affected (0.00 sec) | T2開啟樂觀事務 | | mysql> delete from t where id = 1;Query OK, 1 row affected (0.01 sec) | | T1 刪除 id = 1的記錄 | | | mysql> delete from t where id in (1, 2, 3); Query OK, 1 row affected (0.00 sec) | T2 沒有被 T1 阻塞,同樣刪除了 id = 1 的記錄,affected rows 顯示為 1. | | mysql> insert into t (name, age) values("lihua", 9), ("humei", 8); Query OK, 2 rows affected (0.00 sec)Records: 2 Duplicates: 0 Warnings: 0 | | T1 插入兩條記錄,由於自增 id,兩條新紀錄的 id 分別為 2 和 3. | | mysql> commit;Query OK, 0 rows affected (0.00 sec) | | T1 提交,表 t 中只有 id 為 2 和 3 的記錄 | | | mysql> commit;Query OK, 0 rows affected (0.01 sec) | T2 提交成功 | | mysql> select id, name, age from t;Empty set (0.01 sec) | mysql> select id, name, age from t;Empty set (0.01 sec) | 表 t 中, id 為 2 和 3 的記錄也被刪除。 |
這裡事務 T2 就涉及到樂觀事務重試情況下的兩個侷限性:
- T2 提示 affected rows 顯示為 1 行,刪除的是僅有的 id = 1 的記錄,但是實際提交時候,刪除的是 id 為 2 和 3 的兩條記錄,實際的 affected rows 是 2 行,參考部落格TiDB 新特性漫談:悲觀事務。
- 破壞可重複讀的隔離級別,參考下重試的侷限性的說明,在使用重試時,要判斷好是否會影響業務的正確性。
悲觀事務
針對樂觀事務存在的問題,悲觀事務通過在執行 DML 過程中加悲觀鎖,來達到與傳統資料庫的行為:
- 併發執行 DML,對同一行資料進行更改,先執行者會加悲觀鎖,後執行者被鎖阻塞
- 讓寫衝突按順序執行,這樣可以避免樂觀事務在 commit 時遇到衝突後多次重試的問題,使得 commit 順利完成
悲觀事務寫入悲觀鎖,相對樂觀事務帶來以下開銷:
- 悲觀鎖寫入 TiKV,增加了 RPC 呼叫流程並同步等待悲觀鎖寫入成功,導致 DML 時延增加
- 悲觀鎖資訊會通過 raft 寫入多個副本,給 TiKV raftstore、磁碟等帶來處理壓力
pipelined
針對悲觀鎖帶來的時延增加問題,在 TiKV 層增加了 pipelined 加鎖流程優化,優化前後邏輯對比:
- 優化前:滿足加鎖條件,等待 lock 資訊通過 raft 寫入多副本成功,通知 TiDB 加鎖成功
- pipelined :滿足加鎖條件,通知 TiDB 加鎖成功、非同步 lock 資訊 raft 寫入多副本(兩者併發執行)
非同步 lock 資訊 raft 寫入流程後,從使用者角度看,悲觀鎖流程的時延降低了;但是從 TiKV 負載的角度,並沒有節省開銷。
in-memory
pipelined 優化只是減少了 DML 時延,lock 資訊跟優化前一樣需要經過 raft 寫入多個 region 副本,這個過程會給 raftstore、磁碟帶來負載壓力。
記憶體悲觀鎖針對 lock 資訊 raft 寫入多副本,做了更進一步優化,總結如下:
- lock 資訊只儲存在記憶體中,不用寫入磁碟
- lock 資訊不用通過 raft 寫入多個副本,只要存在於 region leader
- lock 資訊寫記憶體,延遲相對於通過 raft 寫多副本,延遲極小
從優化邏輯上看,帶來的效能提升會有以下幾點:
- 減小 DML 時延
- 降低磁碟的使用頻寬
- 降低 raftstore CPU 消耗
實現原理
引用下記憶體悲觀鎖 RFC In-memory Pessimistic Locks 的介紹:
Here is the general idea:
- Pessimistic locks are written into a region-level lock table.
- Pessimistic locks are sent to other peers before a voluntary leader transfer.
- Pessimistic locks in the source region are sent to the target region before a region merge.
- On splitting a region, pessimistic locks are moved to the corresponding new regions.
- Each region has limited space for in-memory pessimistic locks.
簡單理解就是為每個 region 單獨維護(只在 leader peer 維護)一個記憶體 lock table,當出現 region 變動時候例如 Leader transfer、Region merge 會先將 lock table 中的悲觀鎖通過 raft 同步到其他節點,這個 lock table 有大小限制。
in-memory lock 跟非優化前相比,不會破壞資料一致性,具體的實現細節挺複雜,但是可以簡單理解下:
- in-memory 悲觀鎖正常存在 region leader lock table 情況下
- 對於讀寫 leader,跟普通悲觀鎖讀寫一致
- 對於 follow read,基於 snapshot 讀,只有 prewrite lock 會影響讀取結果,而 prewrite 的資料是會同步到 follower 的,所以仍然沒問題
- in-memory 悲觀鎖丟失
- 對於 write 操作,事務提交 prewrite 階段會檢查版本衝突,有衝突會因為衝突提交失敗,沒衝突正常提交
- 對於 read 操作,同上面 follower read,悲觀鎖不會影響讀
鎖丟失
in-memory 悲觀鎖的設計初衷是在收益與付出之間做的權衡:
- 相對於樂觀事務,悲觀事務加鎖,讓寫衝突按順序執行,衝突場景下事務提交成功率更高。
- 相對於同步持久化的悲觀鎖,減少了 TiKV 負載的開銷,但是同時會有鎖丟失。
鎖丟失的原因:in-memory 悲觀鎖只在 region leader 上維護,這裡的鎖丟失是指新的 region leader 沒有獲取到變更前 region 上的悲觀鎖資訊。原因主要是 TiKV 網路隔離或者節點宕機,畢竟對於 region 變更,正常會先通過 raft 將當前 region 的悲觀鎖同步給其他 region peer。感覺 in-memory 悲觀鎖比 pipelined 加鎖,宕機後鎖丟失會更多。
鎖丟失的影響(參考Pipelined 加鎖流程):
- 事務在 region leader 變更前上的鎖,無法阻塞修改相同資料的其他事務。如果業務邏輯依賴加鎖或等鎖機制,業務邏輯的正確性將受到影響。
- 有較低概率導致事務提交失敗,但不會影響事務正確性。
在 pipelined 加鎖流程,同樣會有悲觀鎖失效的現象,因為非同步寫入可能失敗,悲觀鎖沒有寫成功,但是卻通知了上鎖成功。
| T1 | T2 | OS CLi | | ------------------------------------------------------------------- | -------------------------------------------------- | ------------ | | mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin;Query OK, 0 rows affected (0.00 sec) | | | | | | | mysql> delete from t where id=1;Query OK, 1 row affected (0.00 sec) | | | | | mysql> delete from t where id=1; | | | | ...... | kill -9 tikv | | | Query OK, 1 row affected (19.20 sec) | | | | mysql> commit;Query OK, 0 rows affected (0.00 sec) | | | mysql> commit;ERROR 1105 (HY000) | | |
事務 T1 提交失敗,詳細報錯資訊如下:
mysql> commit;
ERROR 1105 (HY000): tikv aborts txn: Error(Txn(Error(Mvcc(Error(PessimisticLockNotFound { start_ts: TimeStamp(433149465930498050), key: [116, 128, 0, 0, 0, 0, 0, 1, 202, 95, 114, 128, 0, 0, 0, 0, 0, 0, 1] })))))
這裡事務 T1 先加鎖成功,事務 T2 被阻塞,kill tikv 導致 leader transfer,新的 leader 沒有事務 T1 的悲觀鎖資訊,然後事務 T2 被解除阻塞,並提交成功。事務 T1 提交失敗,但不會影響資料的一致性。
所以如果業務中依賴這種加鎖機制,可能導致業務正確性受影響。如下使用場景:
mysql> begin;
mysql> insert into tb values(...) 或者 select 1 from tb where id=1 for update;
...加鎖成功...
...業務依賴以上加鎖成功做業務選擇...
...在鎖丟失場景可能多個事務都能加鎖成功導致出現不符合業務預期的行為...
mysql> commit;
如果對於成功率和事務過程中執行返回結果有強需求或者依賴的業務,可選擇關閉記憶體鎖(以及 pipelined 寫入)模式。
開啟 in-memory
TiKV 配置檔案:
[pessimistic-txn]
pipelined = true
in-memory = true
只有 pipelined 和 in-memory 同時開啟才能開啟記憶體悲觀鎖。
可以線上動態開啟、關閉:
```
set config tikv pessimistic-txn.pipelined='true'; set config tikv pessimistic-txn.in-memory='true'; ```
Grafana 檢視 in-memory lock 的寫入情況,在 {clusterName}-TiKV-Details->Pessimistic Locking 標籤下:
記憶體限制
每個 region 的 in-memory 鎖記憶體固定限制為 512 KiB,如果當前 region 悲觀鎖記憶體達到限制,新的悲觀鎖寫入將回退到 pipelined 加鎖流程(在典型 TP 場景下,很少會超過這個限制)。
``` mysql> begin; Query OK, 0 rows affected (0.00 sec)
mysql> update sbtest1 set k=k+1 limit 10000000; Query OK, 10000000 rows affected (3.26 sec) Rows matched: 10000000 Changed: 10000000 Warnings: 0 ```
由於大量悲觀鎖寫入,悲觀鎖記憶體達到限制值,監控中 full 值大量出現。
回退到 pipelined 寫入流程,通過 raft 寫入多副本,Rockdb 的 lock CF 出現 lock 資訊,在 {clusterName}-TiKV-Details->RocksDB - kv 標籤下。
效能測試
對樂觀鎖、悲觀鎖、pipelined 寫入、in-memory lock 進行壓力測試。
| 型別 | cpu | 記憶體 | 磁碟 | 網絡卡 | NUMA | 節點數 | 機器數 | | ---- | ---- | ---- | -------------- | --------- | ------ | --- | --- | | TiKV | 96執行緒 | 384G | 2 * 2.9T NVME | 40000Mb/s | 2 node | 6 | 3 |
TiKV 部署:在每塊 NEME 盤上部署一個 TIKV 節點,分別繫結一個 NUMA node,單臺機器 2 個 TiKV 節點,配置引數如下(變動的引數只跟壓測的事務型別有關)。
server_configs:
tikv:
pessimistic-txn.in-memory: true
pessimistic-txn.pipelined: true
raftdb.max-background-jobs: 6
raftstore.apply-pool-size: 6
raftstore.store-pool-size: 6
readpool.coprocessor.use-unified-pool: true
readpool.storage.normal-concurrency: 16
readpool.storage.use-unified-pool: true
readpool.unified.max-thread-count: 38
readpool.unified.min-thread-count: 5
rocksdb.max-background-jobs: 8
server.grpc-concurrency: 10
storage.block-cache.capacity: 90G
storage.scheduler-worker-pool-size: 12
TiDB、pd 獨立部署,均為高配置伺服器,其中 TiDB 節點足夠多,能使壓測效能瓶頸集中在 TiKV 上,使用 LVS DR 模式做負載均衡。
測試工具 sysbench,壓測指令碼 oltp_write_only,64 張表,1000w 資料,直觀比較各種模式下效能差異。
壓測結果 TPS:
壓測結果 Latency:
從壓測結果上來看:
- 效能排行從高到底:in-memory > optimistic > pipelined > pessimistic
- 在壓測執行緒較小時,in-memory 和 optimistic 效能接近,等到併發增大,可能是 optimistic 事務衝突重試的原因導致 in-memory 後來居上
- 隨著併發數增大,TiKV 磁碟 iops、頻寬很快增長,pessimistic 和 pipelined 磁碟負載較早出現壓力,後面時延增加較快,對應 TPS 增長相對緩慢
- 當接近 TiKV 磁碟效能瓶頸時,in-memory 和 optimistic 能支撐叢集更大的 TPS。
悲觀鎖優化
對比下 in-memory、pipelined 兩個特性,對於悲觀鎖的效能提升。
TPS 提升:
- in-memory 提升明顯,始終維持在一個較高值 35% 以上
- 同併發下 Latency 減少,對應 TPS 增長
- 高併發下,減少磁碟 io 壓力、減少了 raftstore 壓力
- pipelined 提升在 10% 左右,在較小併發時非同步寫入 Latency 減少,支撐了較大的 TPS 提升;當磁碟壓力增大,慢慢出現效能瓶頸,提升越來越小。
減少 Latency:
- 在併發小時,時延提升明顯,分別能到到 20%、10% 的提升。
- 在併發增大後,磁碟出現較大壓力,由於時延增加太大,總提升越來越不明顯
- in-memory 維持在 10% 以上
- pipelined 降到 5% 以下
總結
從壓測資料來看,v6.0.0 版本的記憶體悲觀鎖是非常有吸引力的新特性。
通過減少 DML 時延、避免悲觀鎖 raft 寫入多副本、減少 raftstore 處理壓力以及磁碟頻寬,能達到可觀的寫入效能提升:
- Tps 提升 30% 上下
- 減少 Latency 在 15% 上下
在記憶體悲觀鎖的使用中,要注意鎖丟失問題,如果影響業務的正確性邏輯,應關閉 in-memory lock 與 pipelined 寫入這兩個悲觀事務特性。
參考
官方文件:記憶體悲觀鎖
記憶體悲觀鎖 RFC In-memory Pessimistic Locks
Tracking Issue: In-memory Pessimistic Locks
- TiDB 6.5 新特性解析丨過去一年,我們是如何讓 TiFlash 高效又穩定地榨乾 CPU?
- TiDB 在安信證券資產中心與極速交易場景的實踐
- 微眾銀行 TiDB HTAP 和自動化運維實踐
- PingCAP 副總裁劉鬆 :“ Serverless 化” 即將成為資料庫的下一個變革性技術
- TiCDC 原始碼閱讀(四)TiCDC Scheduler 工作原理解析
- Hackathon 實用指南丨快速給 TiDB 新增一個功能
- Hackathon idea 清單出爐,總有一款適合你
- TiDB Hackathon 2022丨總獎金池超 35 萬!邀你喚醒程式碼世界的更多可能性!
- 劉奇:能否掌控複雜性,決定著分散式資料庫的生死存亡
- TiFlash 原始碼閱讀(九)TiFlash 中常用運算元的設計與實現
- TiFlash 原始碼閱讀(八)TiFlash 表示式的實現與設計
- 如何在 TiDB Cloud 上使用 Databricks 進行資料分析 | TiDB Cloud 使用指南
- TiFlash 原始碼閱讀(五) DeltaTree 儲存引擎設計及實現分析 - Part 2
- 深入解析 TiFlash丨面向編譯器的自動向量化加速
- TiFlash 原始碼閱讀(四)TiFlash DDL 模組設計及實現分析
- TiDB v6.0.0 (DMR) :快取表初試丨TiDB Book Rush
- TiFlash 函式下推必知必會丨十分鐘成為 TiFlash Contributor
- TiDB 6.0 實戰分享丨記憶體悲觀鎖原理淺析與實踐
- TiDB 6.1 發版:LTS 版本來了
- TiDB 6.0 實戰分享丨冷熱儲存分離解決方案