TiDB MVCC 版本堆積相關原理及排查手段

導讀

本文介紹了 TiDB 中 MVCC(多版本併發控制)機制的原理和相關排查手段。 TiDB 使用 MVCC 機制實現事務,在寫入新數據時不會直接替換舊數據,而是保留舊數據的同時以時間戳區分版本。 當歷史版本堆積過多時,會導致讀寫性能下降。 爲了解決這個問題,TiDB 使用 Garbage Collection(GC)定期清理不再需要的舊數據。 文章從 TiDB 中 MVCC 版本的生成原理、數據寫入過程和 TiDB 版本堆積常見排查手段等方面進行了詳細介紹 。


TiDB 的事務的實現採用了 MVCC(多版本併發控制)機制,當新寫入的數據覆蓋舊的數據時,舊的數據不會被替換掉,而是與新寫入的數據同時保留,並以時間戳來區分版本。 Garbage Collection(GC)的任務便是清理不再需要的舊數據。

如上所述,TiDB 底層使用的是單機存儲引擎 rocksdb, 爲了實現分佈式事務接口,TiDB 又採用 MVCC 機制,基於 rocksdb 實現了高可用分佈式存儲引擎 TiKV。也就是當新寫入(增刪改)的數據覆蓋到舊數據時,舊數據不會被替換掉,而是與新寫入的數據同時保留,並以時間戳來區分版本。當這些歷史版本堆積越來越多時,就會引出一系列問題,最常見的便是讀寫變慢。TIDB 爲了降低歷史版本對性能的影響,會定期發起 Garbage Collection(GC) ( https://docs-archive.pingcap.com/zh/tidb/v7.2/garbage-collection-overview ) 清理不再需要的舊數據。

本文作爲 TiDB GC 的前序文章,我們將詳細介紹一下這些舊版本數據是如何堆積起來的,以及如何排查確認當前版本數據的堆積已經對集羣性能構成了影響。

TiDB 中的 MVCC 版本的生成原理

  • 在 TIDB 層,我們最初收到的是一個關係型表的數據,TiDB 會將這個關係型表數據轉化成 key-value,同時調用分佈式事務接口,將 key-value 數據寫入到 TiKV。
  • 在 TIKV 層,我們採用 MVCC 機制提供了分佈式事務接口,最終所有的寫入都會轉化成一條 MVCC key-value 格式寫入到 raftstore. 說到 MVCC 格式的 key-value, 無非就是每一個 key 上都有一個版本號,代表其提交的先後順序。後面我們將這類格式的數據統一稱爲 MVCC key-value 對。
  • 在 raftstore 層,則最終將數據以 key-value 的形式,寫入到 rocksdb 中。(注意,rocksdb( https://docs.pingcap.com/tidb/stable/rocksdb-overview ) 本身基於 LSM 架構實現,所以它也有 MVCC 的概念,本文不做詳細介紹,只對 TiDB 相關的內容點到爲止)

數據寫入過程

下面我們舉個例子來詳細講講在 TiDB 集羣中,一個具體的寫入過程。

當我們在 TiDB 中,執行以下 SQL 時:

insert into students set name="Bob",age=12,score=99 

1.1 TiDB  SQL table 轉爲 Key-Value

在 TiDB 層,我們有以上關係型表,上面這一行數據最終會變成三對 key-value(詳細原理 https://book.tidb.io/session1/chapter3/tidb-kv-to-relation.html ),分別對應:

  • 主鍵對需要保證 key 唯一性:主鍵值 => 本行所有列數據
  • 唯一索引按 key 有序排列加速查詢速度:name 列:唯一索引 => 主鍵
  • 非唯一索引按 key 有序排列提升查詢性能:age 列:索引+主鍵 => 空值

1.2 TiKV 側 MVCC 版本寫入

在 TiKV 層,分佈式事務接口在收到對應的 key-value 對後,會轉成對應的 MVCC key-value 寫入到 raftstore. 這裏我們不展開分佈式事務的具體實現邏輯,只用最簡單的樂觀鎖模型( https://zhuanlan.zhihu.com/p/87608202 )來舉例。

Prewrite 接口完畢後:

其中鎖 Lock CF(無版本號)如下:

  • t{table-id}r1=> start_ts,primary_key,ttl,PUT
  • t{table-id}i{indexID}_Bob=>start_ts,primary_key,ttl,PUT
  • t{table-id}i{indexID}_12_1=>start_ts,primary_key,ttl,PUT

數據 Default CF(mvcc 版本號 start_ts 在 key 的後綴裏)如下:

  • t{table-id}r1_{start_ts}=> {Bob,12,99}
  • t{table-id}i{indexID}Bob{start_ts}=>1
  • t{table-id}i{indexID}_12_1{start_ts}=>null

具體實現中,會有一個優化,即當 value 值不是很大時,不會將數據單獨放在 Default CF 裏面(這裏不展開具體介紹)。

Commit 接口調用完畢之後:

以主鍵對爲例子,數據會發生如下變化:

Write CF 裏面寫入:

  • t{table_id}{commit_ts}=>start_ts

Lock CF 中對應 key 被刪除(注意這裏是 rocksdb 的一次刪除,rocksdb 底層 LSM 也是 mvcc, 即刪除對 rocksdb 也是寫入一個新版本):

  • t{table-id}r1=> start_ts,primary_key,ttl,PUT

綜上,我們 以 主鍵所在 key 爲例 ,展開 講講這個 key 隨着增刪改 mvcc 版本的變遷。

transaction 1: insert set id=1

transaction 2: update where id=1

update 之後,在 raftstore 裏面留下的 mvcc 信息如下:

也就是說,update 並沒有直接去更新上一次寫入的內容,而是重新寫了一份數據到底層。

transaction 3: delete id=1

那如果我們 delete id=1 的這一行數據呢?從下面我們可以看到,delete 也是通過寫入一個新版本到底層。

綜上,當我們對 id=1 依次做了 insert/update/delete 之後,對於 TiDB 客戶端來說,這一行數據已經刪除,但是對於存儲底層來說,此時在 raftstore 層留下了以下多個 mvcc 版本。

可以看到,同一行數據會隨着增刪改的次數,積累越來越多的版本,這裏歷史的 mvcc 版本如果不及時清理,不光物理磁盤空間無法釋放,更會對讀寫產生性能影響,所以我們需要 GC 來對這些舊版本數據進行回收。

TiDB 版本堆積常見排查手段

如前文所說,當 MVCC 版本出現堆積時,會對讀寫造成性能影響,此時,我們就需要對 GC 參數及狀態進行判斷,加速舊版本數據的回收,提升集羣讀寫性能。

那麼,在實際的業務場景中,如何判斷我們的 MVCC 數據版本是否出現堆積,並對當前集羣讀寫性能造成了影響呢?

2.1 Slow log 視角(具體慢 SQL 視角)

如前文所說,MVCC 版本堆積最直接的影響是讀寫變慢,所以我們從 slow log( https://docs-archive.pingcap.com/zh/tidb/v7.2/identify-slow-queries ) 可以來排查 SQL 執行慢的原因是否是 mvcc 歷史版本是否堆積過多。

tidb slow log: scan_detail: {total_process_keys: 1139428, total_process_keys_size: 433849330, total_keys: 1139434, rocksdb: {delete_skipped_count: 0, key_skipped_count: 2278852,....

上面摘取的一段日誌是 slow log 裏面,與 TiDB mvcc 版本數量有關的幾個字段:

  • total_process_keys: 本次查詢掃描的有用的用戶 key 個數。不包含已刪除的版本及 rocksdb 裏面 tombstone 的版本
  • total_keys :本次查詢總共掃的 mvcc 版本個數

 total_keys > total_process_keys*6 時,代表着查詢範圍內的平均每個 key 的 mvcc 版本是 6 以上,需要注意 GC 的相關參數是否合理,檢查 GC 的狀態是否正常。

Rocksdb 相關指標 (rocksdb 裏面的 mvcc):

下面我們舉個例子來加深理解 slow log 裏面的這些字段。(注意後續所有的例子 SQL 中,查詢語句需要加上“explain analyze( https://docs.pingcap.com/tidb/stable/sql-statement-explain-analyze )” 才能看到具體的 mvcc 掃描詳情)

Step 1:創建表結構

  • total_process_keys 是 0,因爲它是一張空表。
  • total_keys =1 因爲我們在查詢之前並不知道這張表是否爲空,需要拿出符合條件的第一個 MVCC 版本才能確認這條 mvcc 不是本表數據。

Step 2:插入一條 ID=1 的新數據

可以看到,插入完成後再查詢時:

  • total_process_keys =1, 表中當前一共有一行數據(id=1)
  • total_keys =2, 掃完 id=1 的 key 後,還要往後掃一個 key 才能確認此表中已經沒有數據

Step 3:更新 ID=1 的這行數據

更新完後再查詢時:

  • total_process_keys =1 因爲確實這張表中只有一行數據
  • total_keys = 3, 因爲 id=1 這行數據有兩個版本, 也就是本次更新增加了一個版本

Step 4:刪除 ID=1 所在行

刪除後執行查詢時:

  • total_process_keys =0:刪除了 id=1 這行數據後,表裏面沒有數據了
  • total_keys = 3+1:而刪除 id=1 給這行數據增加了一個版本,所以 total_keys 比上一次多了 1 個

Step 5:插入一條 ID=2 的新數據

請嘗試自行分析。

2.2 Grafana (集羣)視角

因爲 slow log 默認只記錄 300 ms 以上的 SQL 讀取細節,怎麼看整個集羣 mvcc 讀取狀態呢?這就需要我們從 grafana 級別來宏觀分析了。

分佈式事務 mvcc

監控地址:tikv-details->coprocessor-details-> Total Ops Details(TableScan/IndexScan)

如圖所說:

Ops 具體分兩種:

  • Table scan:代表着按 table 主鍵查詢
  • Index scan:代表着按索引查詢

具體監控值分兩類:

  • processed_keys:代表查詢後實際用戶可見的 key 個數,與 slow-log 中的 total_processed_keys 概念一致
  • next/seek/..:代表本次查詢在 TiKV 迭代器中每個指令的調用次數,一般 next 居多。所有指令總調用次數接近於 slow log 裏面 total_keys

同樣的,如果從上圖中看到 processed_keys 所在的線如果遠遠小於 next, 則說明 mvcc 版本冗餘對當前的讀取已經構成性能影響。

Rocksdb 層看 MVCC

tikv-details->coprocessor->total rocksdb perf statistics:

這裏 delete_skipped 主要是指 rocksdb 裏面的 tombstone, 對應於 slow log 裏面的 delete_skipped_count。

2.3 Region 視角(熱點更新表視角)

在實際業務中,我們往往對某些 table 或者 table 中的某些行更新比較頻繁,從集羣角度看,就只有這些 table 涉及到的 region 的數據版本堆積比較嚴重。

同時 TiDB 在設計時,要求同一個 key 所在的所有 mvcc 版本數據只能落在一個 region 裏面,所以如果 TiDB 中某一行數據更新過於頻繁,會導致版本堆積過多而出現大 region 的情況(大於 1 G)。那麼在遇到大 region 時,我們如何判斷是否出現了這種情況呢?

tikv-ctl( https://docs.pingcap.com/tidb/stable/tikv-control#print-some-properties-about-region )工具提供了命令來查看具體 region 內 mvcc 數據的分佈:

tiup ctl:v6.5.0 tikv --host 127.0.0.1:20160 region-properties -r  6493
Starting component `ctl`: /home/tidb/.tiup/components/ctl/v6.5.0/ctl tikv --host 127.0.0.1:20160 region-properties -r 6493
mvcc.min_ts: 442383585748713474
mvcc.max_ts: 442383589195644931
mvcc.num_rows: 410870
mvcc.num_puts: 410870
mvcc.num_deletes: 0
mvcc.num_versions: 410870
mvcc.max_row_versions: 1
writecf.num_entries: 410870
writecf.num_deletes: 0
writecf.num_files: 1
writecf.sst_files: 053983.sst
defaultcf.num_entries: 0
defaultcf.num_files: 0
defaultcf.sst_files: 
region.start_key: 7480000000000000ffe75f728000000000ff3f028e0000000000fa
region.end_key: 7480000000000000ffe75f728000000000ff454f410000000000fa
region.middle_key_by_approximate_size: 7480000000000000ffe75f728000000000ff42250e0000000000faf9dc567895dbfffe

其中我們重點關注 mvcc 爲前綴的爲 mvcc 相關數據:

  • mvcc.min_ts:這個 region 裏面的所有版本中最小(最老)的 tso
  • mvcc.min_ts:本 region 數據中最新的 mvcc 版本 的 tso
  • mvcc.num_rows:用戶可見的 key 個數(包含已刪除的)= mvcc.num_put+mvcc.num_delete
  • mvcc.num_put:用戶可見的 key 個數(不包含已刪除的)
  • mvcc.num_delete:用戶可見的已刪除的 key 數
  • mvcc.num_version:用戶可見的 mvcc 版本個數
  • mvcc.max_row_versions:本 region 中版本數最多的那個 key 擁有的版本數量

Rocksdb 的相關指標不詳細展開,只需要關注到 *cf.num_deletes 比較高時,可以通過 手動 compaction ( https://docs.pingcap.com/tidb/stable/tikv-control#compact-data-of-each-tikv-manually )指定 CF 來解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章