記憶體悲觀鎖

語言: CN / TW / HK

作者:jiyf

原文來源:https://tidb.net/blog/b29eb6fd

背景

在 v6.0.0 版本,針對悲觀事務引入了記憶體悲觀鎖的優化(in-memory lock),從壓測資料來看,帶來的效能提升非常明顯。

TiDB 事務模型從最初的樂觀事務到悲觀事務;在悲觀事務上,又針對悲觀鎖進行的 ”pipelined 寫入“ 和 ”in-memory lock“ 優化,從功能特性上可以看出演進過程。

樂觀事務

樂觀事務在提交時,可能因為併發寫造成寫寫衝突,不同設定會出現以下兩種不同的現象:

  • 關閉樂觀事務重試,事務提交失敗:也就是執行 DML 成功(不會被阻塞),但是在執行 commit 時候失敗,表現出與 MySQL 等資料庫不一致的行為。
  • 開啟樂觀事務重試,自動重試後返回成功,但是因為重試 DML 使用的 start_ts 是重新獲取的,不是事務開始的 start_ts,也就是說實際執行結果相當於同一個事務中讀和寫是使用不同的 start_ts,執行結果可能跟預期不一致。

悲觀事務

針對樂觀事務存在的問題,悲觀事務通過在執行 DML 過程中加悲觀鎖,來達到與傳統資料庫的行為:

  • 併發執行 DML,對同一行資料進行更改,先執行者會加悲觀鎖,後執行者被鎖阻塞
  • 悲觀鎖避免了寫衝突的問題,使得 commit 順利完成

悲觀事務寫入悲觀鎖,相對樂觀事務帶來以下開銷:

  • 悲觀鎖寫入 TiKV,增加了 RPC 呼叫流程並同步等待悲觀鎖寫入成功,導致 DML 時延增加
  • 悲觀鎖資訊會通過 raft 寫入多個副本,給 TiKV raftstore、磁碟等帶來處理壓力

pipelined

針對悲觀鎖帶來的時延增加問題,在 TiKV 層增加了 pipelined 加鎖流程優化,優化前後邏輯對比:

  • 優化前:滿足加鎖條件,等待 lock 資訊通過 raft 寫入多副本成功,通知 TiDB 加鎖成功
  • pipelined :滿足加鎖條件,通知 TiDB 加鎖成功、非同步 lock 資訊 raft 寫入多副本(兩者併發執行)

非同步 lock 資訊 raft 寫入流程後,從使用者角度看,悲觀鎖流程的時延降低了。

in-memory

pipelined 優化只是減少了 DML 時延,lock 資訊跟優化前一樣需要經過 raft 寫入多個 region 副本,這個過程會給 raftstore、磁碟帶來負載壓力。

記憶體悲觀鎖針對 lock 資訊 raft 寫入多副本,做了更進一步優化,總結如下:

  • lock 資訊只儲存在記憶體中,不用寫入磁碟
  • lock 資訊不用通過 raft 寫入多個副本,只要存在於 region leader
  • lock 資訊寫記憶體,延遲相對於通過 raft 寫多副本,延遲極小

從優化邏輯上看,帶來的效能提升會有以下幾點:

  • 減小 DML 時延
  • 降低磁碟的使用頻寬
  • 降低 raftstore CPU 消耗

實現原理

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、Region split,會先將 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 悲觀鎖只在 region leader 上維護,所以這裡的鎖丟失是指新的 region leader 沒有獲取到變更前 region 上的悲觀鎖資訊,事務在 region leader 變更前上的鎖,不能阻塞後來事務的更改(悲觀鎖失效了)。

在 pipelined 加鎖流程,同樣會有悲觀鎖失效的現象,因為非同步寫入可能失敗,悲觀鎖沒有寫成功,但是卻通知了上鎖成功。

in-memory 悲觀鎖丟失產生的原因主要是 TiKV 網路隔離或者節點宕機,畢竟對於 region 變更,正常會先通過 raft 將當前 region 的悲觀鎖同步給其他 region peer。感覺 in-memory 悲觀鎖比 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 被解除阻塞,並提交成功。

所以如果業務中依賴這種加鎖機制,可能導致正確性受影響。如下使用場景:

mysql> begin;
mysql> insert into tb values(...) 或者 select 1 from tb where id=1 for update;
...加鎖後,達到序列執行業務邏輯的目的...
mysql> commit;

開啟 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 的寫入情況:

in-memory-success.png

記憶體限制

每個 region 的 in-memory 鎖記憶體限制為 512 KiB,如果當前 region 悲觀鎖記憶體達到限制,新的悲觀鎖寫入將回退到 pipelined 加鎖流程。

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

in-memory-full.png

由於大量悲觀鎖寫入,悲觀鎖記憶體達到限制值,監控中 full 值大量出現。

rocks-locks.png

回退到 pipelined 寫入流程,通過 raft 寫入多副本,Rockdb 的 lock CF 出現 lock 資訊。

效能測試

對樂觀鎖、悲觀鎖、pipelined 寫入、in-memory lock 進行壓力測試。

TiDB、TiKV 獨立部署,均為高配置伺服器,其中 TiDB 節點足夠能使壓測效能瓶頸集中在 TiKV 上。

測試工具 sysbench,壓測指令碼 oltp_write_only,64 張表,1000w 資料;不列出具體機器配置資訊,只直觀比較各種模式下效能差異。

壓測結果 TPS:

oltp_write_only_TPS.png

壓測結果 Latency:

oltp_write_only_LATENCY.png

從壓測結果上來看:

  • 效能排行從高到底:in-memory > optimistic > pipelined > pessimistic
  • 在壓測執行緒較小時,in-memory 和 optimistic 效能接近,等到併發增大,可能是 optimistic 事務衝突重試的原因導致 in-memory 後來居上
  • 隨著併發數增大,TiKV 磁碟 iops、頻寬很快增長,pessimistic 和 pipelined 磁碟負載較早出現壓力,後面時延增加較快,對應 TPS 增長相對緩慢
  • 當接近 TiKV 磁碟效能瓶頸時,in-memory 和 optimistic 能支撐叢集更大的 TPS。

悲觀鎖優化

對比下 in-memory、pipelined 兩個特性,對於悲觀鎖的效能提升。

oltp_write_only_tps_promotion.png

TPS 提升:

  • in-memory 提升明顯,始終維持在一個較高值 35% 以上

    • 同併發下 Latency 減少,對應 TPS 增長
    • 高併發下,減少磁碟 io 壓力、減少了 raftstore 壓力
  • pipelined 提升在 10% 左右,在較小併發時非同步寫入 Latency 減少,支撐了較大的 TPS 提升;當磁碟壓力增大,慢慢出現效能瓶頸,提升越來越小。

oltp_write_only_latency_reduce.png

減少 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