作者:李坤、高振嬌
背景
TiDB 的 GC 相關的問題比如 GC 的流程、參數設定、監控以及日志解析,GC 設定多大比較合适,設定過大對叢集會産生什麼樣的影響,GC 卡住了應該從哪裡排查等等一系列的問題,是筆者在使用 TiDB 過程中經常遇到的問題。
故筆者将 GC 相關的内容進行了相關的整理,一共分為 3 篇,第一篇為 『GC 原理淺析』,第二篇為『GC 監控及日志解讀』,而最後一篇則為『GC 處理案例 & FAQ』。
本文從 GC Leader 的選舉,流程等次元簡要分析了 GC 相關的原理,希望能夠幫助大家了解 GC 的流程,表示不嚴謹或者錯誤的地方,歡迎大家批評指正 ~~~
GC
TiDB 的事務的實作采用了 MVCC(多版本并發控制)機制,當新寫入的資料覆寫舊的資料時,舊的資料不會被替換掉,而是與新寫入的資料同時保留,并以時間戳來區分版本,GC 的任務便是清理不再需要的舊資料。
整體流程
GC Leader 的選舉
一個 TiDB 叢集中會有一個 TiDB 執行個體被選舉為 GC leader,GC 的運作由 GC leader 來控制。GC Leader 的選舉是從 TiDB Server 中選出一個作為 GC Leader ,GC Worker 是 TiDB Server 上的一個子產品,隻有 GC Leader 會處理 GC 的工作,其他 TiDB Server 上的 GC Worker 是不工作的。選舉 GC Leader 的方式很簡單,GC Worker 每分鐘 Tick 時,如果發現沒有 Leader 或 Leader 失效,就把自己寫進去,成為 GC Leader。
Safepoint
每次 GC 時,首先 TiDB 會計算一個稱為 Safepoint 的時間戳,接下來 TiDB 會在保證 Safepoint 之後的快照全部擁有正确資料的前提下,删除更早的過期資料。
GC 流程
每一輪 GC 分為以下三個步驟,這三個步驟在整個 GC 的流程中是串行執行。如果一輪 GC 運作時間太久,上次 GC 還在前兩個階段,下輪 GC 又開始了,下一輪 GC 會忽略,GC Leader 會報 “there’s already a gc job running,skipped”:
- Resolve Locks:該階段會對所有 Region 掃描 Safepoint 之前的鎖,并清理這些鎖。
- Delete Ranges:該階段快速地删除由于 DROP TABLE/DROP INDEX 等操作産生的整區間的廢棄資料。
- Do GC:該階段每個 TiKV 節點将會各自掃描該節點上的資料,并對每一個 key 删除其不再需要的舊版本。
預設配置下,GC 每 10 分鐘觸發一次,每次 GC 會保留最近 10 分鐘内的資料( 即預設 GC Life Time 為 10 分鐘,Safepoint 的計算方式為目前時間減去 GC Life Time )。
為了使持續時間較長的事務能在超過 GC Life Time 之後仍然可以正常運作,Safepoint 不會超過正在執行中的事務的開始時間 (start_ts)。
實作細節
Resolve Locks(清理鎖)
TiDB 的事務是基于 Google Percolator 模型實作的,事務的送出是一個兩階段送出的過程。第一階段完成時,所有涉及的 key 都會上鎖,其中一個鎖會被選為 Primary,其餘的鎖 ( Secondary ) 則會存儲一個指向 Primary 的指針;第二階段會将 Primary 鎖所在的 key 加上一個 Write 記錄,并去除鎖。
如果因為某些原因(如發生故障等),這些 Secondary 鎖沒有完成替換、殘留了下來,那麼也可以根據鎖中的資訊找到 Primary,并根據 Primary 是否送出來判斷整個事務是否送出。但是,如果 Primary 的資訊在 GC 中被删除了,而該事務又存在未成功送出的 Secondary 鎖,那麼就永遠無法得知該鎖是否可以送出。這樣,資料的正确性就無法保證。
Resolve Locks 這一步的任務即對 Safepoint 之前的鎖進行清理。即如果一個鎖對應的 Primary 已經送出,那麼該鎖也應該被送出;反之,則應該復原。而如果 Primary 仍然是上鎖的狀态(沒有送出也沒有復原),則應當将該事務視為逾時失敗而復原。
Resolve Locks 的執行方式是由 GC leader 對所有的 Region 發送請求掃描過期的鎖,并對掃到的鎖查詢 Primary 的狀态,再發送請求對其進行送出或復原。
從 3.0 版本開始,Resolve Locks 實作了并行,把所有 Region 配置設定給各個線程,所有線程并行的向各個 Region 的 Leader 發送請求:
-
并發線程數
若
tikv_gc_auto_concurrency = 1
,則每個 TiKV 自動一個線程。
若
,則由tikv_gc_auto_concurrency = 0
決定總線程數,但每個 tikv 最多一個線程。tikv_gc_concurrency
- 實際清鎖的操作,是調用了 RocksDB 的 Delete ,RocksDB 的内部實作原理是寫一個删除标記,需要等 RocksDB 執行 Compaction 回收空間,通常這步驟涉及的資料非常少。
Delete Ranges(删除區間)
在執行 Drop/Truncate Table ,Drop Index 等操作時,會有大量連續的資料被删除。如果對每個 key 都進行删除操作、再對每個 key 進行 GC 的話,那麼執行效率和空間回收速度都可能非常的低下。事實上,這種時候 TiDB 并不會對每個 key 進行删除操作,而是将這些待删除的區間及删除操作的時間戳記錄下來。Delete Ranges 會将這些時間戳在 Safepoint 之前的區間進行快速的實體删除,而普通 DML 的多版本不在這個階段回收。
- TiKV 預設使用 RocksDB 的 UnsafeDestroyRange 接口
- Drop/Truncate Table ,Drop Index 會先把 Ranges 寫進 TiDB 系統表(
),TiDB 的 GC worker 定期檢視是否過了 Safepoint,然後拿出這些 Ranges,并發的給 TiKV 去删除 sst 檔案,并發數和 concurrency 無關,而是直接發給各個 TiKV。删除是直接删除,不需要等 compact 。完成 Delete Ranges 後,會記錄在 TiDB 系統表mysql.gc_delete_range
,表中的内容過 24 小時後會清除:mysql.gc_delete_range_done
mysql> select * from gc_delete_range_done;
+--------+------------+--------------------+--------------------+--------------------+
| job_id | element_id | start_key | end_key | ts |
+--------+------------+--------------------+--------------------+--------------------+
| 283 | 171 | 7480000000000000ab | 7480000000000000ac | 422048703668289538 |
| 283 | 172 | 7480000000000000ac | 7480000000000000ad | 422048703668289538 |
+--------+------------+--------------------+--------------------+--------------------+
2 rows in set (0.01 sec)
Do GC(進行 GC 清理)
這一步主要是針對 DML 操作産生的 key 的過期版本進行删除。為了保證 Safepoint 之後的任何時間戳都具有一緻的快照,這一步删除 Safepoint 之前送出的資料,但是會對每個 key 保留 Safepoint 前的最後一次寫入(除非最後一次寫入是删除)。
在進行這一步時,TiDB 隻需将 Safepoint 發送給 PD,即可結束整輪 GC。TiKV 會每 1 分鐘自行檢測是否 Safepoint 發生了更新,然後會對目前節點上所有 Region Leader 進行 GC。與此同時,GC Leader 可以繼續觸發下一輪 GC。
詳細的流程如下:
- 調用 RocksDB 的 Delete 接口,打一個删除标記。
- 每一輪 GC 都會掃所有的 Region,但會根據 sst 上的元資訊初步判斷是否有較多的曆史資料,進而來判斷是否可以跳過。如果增量資料比較大,表示要打标記的老版本較多,會大幅增加耗時。
- GC 打完标記後,不會立即釋放空間,最終通過 RocksDB Compaction 來真正回收空間。
- 如果這時 TiKV 程序挂掉了,重新開機後,需要等下一輪 GC 開始繼續。
- 并行度
- 3.0 開始,預設設定
,無需 TiDB 通過對 TiKV 發送請求的方式來驅動,而是 TiDB 隻需在每個 GC 周期發送 safepoint 到 PD 就可以結束整輪 GC,每台 TiKV 會自行去 PD 擷取 safepoint 後分布式處理。tikv_gc_mode = distributed
- 由于通常 Do GC 比較慢,下一輪 interval 到來時,上一輪 GC 還沒有跑完:
- 3.0 開始,如果沒有執行完,下一輪 GC 将新的 safepoint 更新到 PD 後,TiKV 每隔 1 分鐘到 PD 擷取新的 safepoint,擷取後會使用新的 safepoint 将剩餘的 Region 完成掃描,并盡量回頭完成 100% 的 Region ( TiKV 會将執行到的 Region 位置在記憶體中,并按 Region 順序掃描所有 Region )。
配置
TiDB 的 GC 相關的配置存儲于 mysql.tidb 系統表中,可以通過 SQL 語句對這些參數進行查詢和更改:
select VARIABLE_NAME, VARIABLE_VALUE from mysql.tidb where VARIABLE_NAME like "tikv_gc%";
+--------------------------+----------------------------------------------------------------------------------------------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+--------------------------+----------------------------------------------------------------------------------------------------+
| tikv_gc_leader_uuid | 5afd54a0ea40005 |
| tikv_gc_leader_desc | host:tidb-cluster-tidb-0, pid:215, start at 2019-07-15 11:09:14.029668932 +0000 UTC m=+0.463731223 |
| tikv_gc_leader_lease | 20190715-12:12:14 +0000 |
| tikv_gc_enable | true |
| tikv_gc_run_interval | 10m0s |
| tikv_gc_life_time | 10m0s |
| tikv_gc_last_run_time | 20190715-12:09:14 +0000 |
| tikv_gc_safe_point | 20190715-11:59:14 +0000 |
| tikv_gc_auto_concurrency | true |
| tikv_gc_mode | distributed |
+--------------------------+----------------------------------------------------------------------------------------------------+
13 rows in set (0.00 sec)
例如,如果需要将 GC 調整為保留最近一天以内的資料,隻需執行下列語句即可:
update mysql.tidb set VARIABLE_VALUE="24h" where VARIABLE_NAME="tikv_gc_life_time";
注意:
mysql.tidb 系統表中除了下文列出的 GC 的配置以外,還包含一些 TiDB 用于儲存部分叢集狀态(包括 GC 狀态)的記錄。請勿手動更改這些記錄。其中,與 GC 有關的記錄如下:
- tikv_gc_leader_uuid,tikv_gc_leader_desc 和 tikv_gc_leader_lease 用于記錄 GC Leader 的狀态
- tikv_gc_last_run_time:最近一次 GC 運作的時間(每輪 GC 開始時更新)
- tikv_gc_safe_point:目前的 Safepoint (每輪 GC 開始時更新)
tikv_gc_enable
控制是否啟用 GC。
預設值:true
tikv_gc_run_interval
指定 GC 運作時間間隔。Duration 類型,使用 Go 的 Duration 字元串格式,如 “1h30m”,“15m” 等。
預設值:“10m0s”
tikv_gc_life_time
每次 GC 時,保留資料的時限。Duration 類型。每次 GC 時将以目前時間減去該配置的值作為 Safepoint。
預設值:“10m0s”
注意:
- 在資料更新頻繁的場景下,如果将
設定得比較大(如數天甚至數月),可能會有一些潛在的問題,如:
tikv_gc_life_time
- 磁盤空間占用較多。
- 大量的曆史版本會在一定程度上影響性能,尤其是範圍查詢(如 select count(*) from t)。
- 如果存在運作時間很長、超過了
的事務,那麼在 GC 時,會保留自該事務的開始時間 (start_ts) 以來的資料,以允許該事務繼續運作。例如,如果
tikv_gc_life_time
配置為 10 分鐘,而某次 GC 時,叢集中正在運作的事務中開始時間最早的一個事務已經運作了 15 分鐘,那麼本次 GC 便會保留最近 15 分鐘的資料。
tikv_gc_life_time
tikv_gc_mode
指定 GC 模式。可選值如下:
- “distributed”(預設):分布式 GC 模式。在此模式下,Do GC 階段由 TiDB 上的 GC leader 向 PD 發送 Safepoint,每個 TiKV 節點各自擷取該 Safepoint 并對所有目前節點上作為 leader 的 Region 進行 GC。此模式于 TiDB 3.0 引入。
- “central”:集中 GC 模式。在此模式下,Do GC 階段由 GC leader 向所有的 Region 發送 GC 請求。TiDB 2.1 及更早版本采用此 GC 模式。
tikv_gc_auto_concurrency
控制是否由 TiDB 自動決定 GC concurrency,即同時進行 GC 的線程數。
當 tikv_gc_mode 設為 “distributed”,GC concurrency 将應用于 Resolve Locks 階段。當 tikv_gc_mode 設為 “central” 時,GC concurrency 将應用于 Resolve Locks 以及 Do GC 兩個階段。
- true(預設):自動以 TiKV 節點的個數作為 GC concurrency
- false:使用 tikv_gc_concurrency 的值作為 GC 并發數
tikv_gc_concurrency
手動設定 GC concurrency。要使用該參數,必須将 tikv_gc_auto_concurrency 設為 false 。
預設值:2

- 串行:由 TiDB 逐個向 Region 發送請求。
- 并行:使用 tikv_gc_concurrency 選項所指定的線程數,并行地向每個 Region 發送請求。
- 自動并行:使用 TiKV 節點的個數作為線程數,并行地向每個 Region 發送請求。
- 分布式:無需 TiDB 通過對 TiKV 發送請求的方式來驅動,而是每台 TiKV 自行工作。
流控
tikv-ctl --host=ip:port modify-tikv-config -m server -n gc.max_write_bytes_per_sec -v 10MB