從接觸TiDB以來(lái),就看到過(guò)TiDB官方文檔上的提示竣况,gc_life_time設(shè)置過(guò)大吮螺,會(huì)因?yàn)闅v史版本過(guò)多,影響查詢效率帕翻,但是為什么SQL非要去掃描歷史版本呢鸠补?下面列舉一些知識(shí)點(diǎn)一步一步來(lái)解析這個(gè)問題
1. TiDB key的編碼方式
TiDB 會(huì)對(duì)每個(gè)表分配一個(gè)全局唯一的table_id,每一個(gè)索引都會(huì)分配一個(gè)表內(nèi)唯一的 index_id嘀掸,每一行分配一個(gè) row_id(如果表有整數(shù)型的 Primary Key紫岩,那么會(huì)用 Primary Key 的值當(dāng)做 row_id,如果沒有睬塌,那么TiDB會(huì)自動(dòng)生成一個(gè)隱式主鍵_tidb_rowid)
數(shù)據(jù)編碼方式:
t{table_id}_r{row_id}-->[col1,col2,col3,...]
索引編碼方式:
unique index
t{table_id}_i{index_id}_{index_column_value}-->[row_id]
非unique index
t{table_id}_i{index_id}_{index_column_value}_{row_id}-->null
舉個(gè)栗子:
CREATE TABLE `test_table` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`column1` varchar(10) DEFAULT NULL,
`column2` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_column1` (`column1`)
id | column1 | column2 |
---|---|---|
1 | "a" | "b" |
2 | "c" | "d" |
那么主鍵編碼可以抽象為
t10_r1-->[1,"a","b"]
t10_r2-->[2,"c","d"]
索引idx_column1
編碼可以抽象為
t10_i1_a_1-->null
t10_i1_b_2-->null
2. MVCC多版本信息是如何保存的泉蝌?
TiDB使用基于Percolator的事務(wù)模型,將一行數(shù)據(jù)抽象為default揩晴、write 和 lock 3 個(gè) CF(column family)存儲(chǔ)勋陪,其中:
- default CF存儲(chǔ)的真正數(shù)據(jù)
${key}_${start_ts} --> ${value}
- write CF存儲(chǔ)數(shù)據(jù)的版本信息,commit_ts代表一行記錄的真正版本
${key}_${commit_ts}-->${start_ts}
- lock CF存放鎖信息,提交中的事務(wù)會(huì)加lock硫兰,包含primary lock的位置
${key}-->${start_ts,primary_key,..etc}
一個(gè)讀取操作的過(guò)程如下:
- 事務(wù)begin,從PD獲取start_ts
- 讀取key诅愚,先判斷l(xiāng)ock CF有沒有鎖,如果
a. 有鎖
判斷primary key狀態(tài)是否超時(shí)
- 若鎖未超時(shí)劫映,等待
- 若鎖已超時(shí)违孝,根據(jù)primary key的狀態(tài)rollback或commit殘留事務(wù)
b. 無(wú)鎖
根據(jù)當(dāng)前事務(wù)獲取的start_ts對(duì)比數(shù)據(jù)的commit_ts(write CF)
- start_ts大于commit_ts,返回這行數(shù)據(jù)
- start_ts小于commit_ts泳赋,繼續(xù)查找當(dāng)前行的下一個(gè)(更早的)版本 - 根據(jù)步驟2得到的commit_ts雌桑,從default CF中獲取真正的數(shù)據(jù)
TiDB將一行記錄的多個(gè)版本按照從新到老的順序排列,這樣方便我們獲得滿足查詢條件的最新記錄祖今,TiKV默認(rèn)存儲(chǔ)引擎是RocksDB校坑,RocksDB一個(gè)seek操作要比next操作昂貴很多拣技,如下這個(gè)例子
假設(shè)key1,key2,key3都有多次更新,生成的mvcc版本從老到新分別為key1_v1,key1_v2,key1_v3,key1_v4...
幾個(gè)相鄰key的存放方式抽象為下圖:
Rocksdb沒辦法精確去定位每一個(gè)key耍目,如果掃描每一個(gè)key都走seek接口过咬,這樣代價(jià)太大。所以如圖制妄,假設(shè)一個(gè)范圍查詢seek到第一個(gè)key掸绞,key1之后,就開始調(diào)用next函數(shù)獲取后面的key2耕捞、key3值衔掸,這樣需要遍歷key1甚至key2的所有歷史版本。如此俺抽,就能解釋為什么過(guò)多的歷史版本會(huì)讓查詢效率急劇下降了敞映。
我們?nèi)粘9ぷ鹘?jīng)常碰見的幾個(gè)問題:
一、SQL執(zhí)行時(shí)間不穩(wěn)定
慢日志中會(huì)發(fā)現(xiàn)這些SQL語(yǔ)句的total keys比process keys大很多磷斧,這就是典型的歷史版本過(guò)多振愿,導(dǎo)致掃描了大量歷史數(shù)據(jù)。
解決方法
- 減小gc_life_time弛饭,或者讓業(yè)務(wù)縮小查詢范圍冕末。
- 升級(jí)TiDB3.0版本
TiDB3.0之前的版本,全局GC效率不高侣颂,容易積壓大量歷史版本數(shù)據(jù)档桃。3.0之后改成了分布式GC,能夠快速釋放大量已刪除的歷史版本憔晒,再加上更完善的region merge功能藻肄,會(huì)讓整個(gè)集群的性能提升一個(gè)很大的臺(tái)階。
二拒担、刪除&歸檔特定日期以前的記錄
while True:
delete table where {$condition} limit n
if affectrows==0:
break
這個(gè)場(chǎng)景的現(xiàn)象是delete語(yǔ)句會(huì)越來(lái)越慢嘹屯。
因?yàn)閽呙璺秶鷞$condition}是固定不變的,delete刪除語(yǔ)句在TiDB處理方式是標(biāo)記刪除从撼,刪除本身實(shí)際上也是插入一條kv記錄州弟,只不過(guò)value變成了delete。所以谋逻,循環(huán)執(zhí)行delete 語(yǔ)句呆馁,每次刪除n條記錄,下一次delete語(yǔ)句要掃描的key就會(huì)+n毁兆,執(zhí)行時(shí)間越來(lái)越長(zhǎng)(大家可以去做個(gè)實(shí)驗(yàn),觀察慢日志文件阴挣,同樣的delete語(yǔ)句total keys會(huì)不斷增加)气堕。
那么,怎樣去刪除&歸檔特定日期前的記錄比較高效呢?
首先茎芭,我們知道TiDB對(duì)事務(wù)大小是有限制的
- 單個(gè)事務(wù)包含的SQL語(yǔ)句不超過(guò)5000條
- 操作的單條記錄不超過(guò) 6MB
- 事務(wù)操作的總keys不超過(guò) 30w
- 事務(wù)操作的所有記錄總大小不超過(guò) 100MB
由于TiDB的事務(wù)限制和TiDB mvcc的實(shí)現(xiàn)原理揖膜,想要?jiǎng)h除&歸檔一個(gè)特定范圍的數(shù)據(jù),目前沒有太好的方法梅桩,個(gè)人整理一些心得供大家參考:
第一種方式:
盡量縮小范圍刪除的粒度壹粟,比如提前按分鐘將數(shù)據(jù)分段,打開tidb_batch_delete宿百,提高并發(fā)去刪除趁仙。注意使用開閉區(qū)間,分段之間不要出現(xiàn)沖突垦页,TiDB解決事務(wù)沖突的代價(jià)比較大雀费。
set @@session.tidb_batch_delete=1;
delete from table where create_time > '$start_step' and create_time <= '$end_step';
如果分段內(nèi)的數(shù)據(jù)超出事務(wù)大小限制,TiDB會(huì)自動(dòng)將delete操作拆分成多個(gè)batch痊焊。
個(gè)人親測(cè)盏袄,這種方式刪除數(shù)據(jù)的速度還是比較快的。
第二種方式:
按照日期分表薄啥,刪除過(guò)期的表即可辕羽。TiDB刪表是秒級(jí)的,后續(xù)空間回收也比較快垄惧,缺點(diǎn)是侵入業(yè)務(wù)逛漫。
兩種方式各有利弊,大家各取所需吧赘艳。