天天看點

你踩過這些坑嗎?謹慎在時間類型列上建立索引

作者: Zeratulll ​

MySQL中,一般情況下我們不需要關注有序資料的寫入在Innodb的Btree上是否存在熱點,因為它能承擔的吞吐量是比較大的,在單機的範疇内不太容易達到瓶頸。

但是在TiDB中,寫入有序資料很容易導緻熱點,這個熱點與單機資料庫不同。如果一個節點成為了熱點(隻有它在工作,或者所有請求都需要通路它),那整個叢集無論增加多少台機器,都對提升資料庫的性能容量毫無幫助,純純的浪費錢了。這是分布式相對單機額外産生的問題。

一個表包含時間字段(例如訂單表、日志表、使用者表等等),并且在時間字段上建立一個索引是我們使用MySQL時一種很常見的做法。這些時間字段很多會使用插入或者修改的時間(例如DEFAULT值設為CURRENT_TIMESTAMP或者SQL中使用NOW函數來作為值)。

時間是一種典型的有序資料,那麼在使用TiDB時,我們是否可以保持像在MySQL中一樣的做法來使用時間字段呢?時間字段是否會産生熱點,又該如何避免?

本文将從TiDB的原理來解答上述問題。如果你是核心開發者,也有助于幫助讀者進一步了解分布式資料庫中資料的編碼與分布。

問題

一個有趣的問題,考慮下面四張表(結構上的主要差異在于主鍵是AUTO_INCREMENT或者AUTO_RANDOM,gmt_create列是date類型或者datetime類型):

CREATE TABLE orders1 (
id bigint(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime,
PRIMARY KEY (id) ,
KEY idx_gmt_create (gmt_create)
);      
CREATE TABLE orders2 (
id bigint(11) NOT NULL AUTO_INCREMENT,
gmt_create date,
PRIMARY KEY (id) ,
KEY idx_gmt_create (gmt_create)
);      
CREATE TABLE orders3 (
id bigint(11)  NOT NULL  AUTO_RANDOM,
gmt_create datetime,
PRIMARY KEY (id) ,
KEY idx_gmt_create (gmt_create)
);      
CREATE TABLE orders4 (
id bigint(11) NOT NULL AUTO_RANDOM,
gmt_create date,
PRIMARY KEY (id) ,
KEY idx_gmt_create (gmt_create)
);      

并使用​

​insert into orders (id,gmt_create) values (null,now())​

​進行進行連續的寫入操作。

問題是:這四張表存在哪幾個熱點?

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

答案是:一共存在5個熱點(你答對了嗎?)

orders1中存在的熱點:gmt_create索引、主鍵; orders2中存在的熱點:gmt_create索引、主鍵; orders3中存在的熱點:gmt_create索引; orders4不存在熱點。

如圖所示:

你踩過這些坑嗎?謹慎在時間類型列上建立索引

解讀

AUTO_INCREMENT的熱點

orders1和orders2的主鍵上存在熱點。這個的原因大家都知道的,因為TiDB的資料是按照有序的range進行劃分的,主鍵自增,會導緻寫入都發生在做最後的range上,是以最後的range會是熱點。這個在TiDB的文檔中也有描述,這裡就不再贅述了:

從 TiDB 編碼規則可知,同一個表的資料會在以表 ID 開頭為字首的一個 range 中,資料的順序按照 RowID 的值順序排列。在表 insert 的過程中如果 RowID 的值是遞增的,則插入的行隻能在末端追加。當 Region 達到一定的大小之後會進行分裂,分裂之後還是隻能在 range 範圍的末端追加,永遠隻能在一個 Region 上進行 insert 操作,形成熱點。

常見的 increment 類型自增主鍵就是順序遞增的,預設情況下,在主鍵為整數型時,會用主鍵值當做 RowID ,此時 RowID 為順序遞增,在大量 insert 時形成表的寫入熱點。

同時,TiDB 中 RowID 預設也按照自增的方式順序遞增,主鍵不為整數類型時,同樣會遇到寫入熱點的問題。

order3和orders4的主鍵不存在熱點,因為使用AUTO_RANDOM來生成主鍵,将主鍵做了随機化。這樣的代價也是有的,主鍵失去了宏觀上的有序性(因為TiDB的AUTO_INCREMENT是按TiDB Server分段的,是以不能說是“有序”)。

DATE與DATETIME

再來看idx_gmt_create。

回顧TiDB中索引的編碼方式:

Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
Value: null      

對于上述表結構,簡化下就是:

Key: {gmt_create}_{id}      

這種編碼格式,在比較大小的時候,簡單說就是gmt_create不同則按gmt_create進行比較,gmt_create相同則按照id來比較。

對于orders1與orders3,gmt_create是DATETIME類型,包含了日期與時分秒(微秒)資訊。按時間不停寫入的資料,其gmt_create就是不停的在增長的有序資料,與AUTO_INCREMENT的主鍵類似,它也會不停的往最後一個range進行寫入,是以最後一個range會成為熱點。這裡orders1與orders3的行為是一緻的,因為gmt_create作為字首已經是有序的了,編碼出來的key基本就是有序的。後面的id作為字尾,無論是有序的還是随機的,都無法影響這個結果。

對于orders2與orders4,gmt_create是DATE類型,隻包含了日期。對于一天内寫入的資料,其gmt_create的值實際上都是同一個。也就是說,在決定這個資料寫到哪個range的時候,起到比較作用的是id。

由于orders2的id是AUTO_INCREMENT的,是以編碼出來的key也是有序的,是以産生了熱點。

而orders4的id是随機的,是亂序的,是以編碼出來的key也不具備有序性,寫入就會分散到很多range中,是以沒有熱點。

注意:實際上,當日期發生切換的時候(例如每天的0點0分0秒),orders4會在短時間内出現熱點(這個時間長短取決于你的流量多久能寫滿幾百兆,将這一天資料分裂到多個range内),這個熱點将表現成系統在0點的劇烈抖動,想象下雙十一零點出現這種抖動吧!

優化的可能性

TiDB可以考慮修改DATETIME/TIMESTAMP類型的編碼方式(或者提供一些額外的選項)。例如對于Key的部分,截斷到小時,後面使用随機數進行補齊(充當了上文中随機主鍵的作用),将未截斷的資料儲存在value中或者key的結尾。

這樣能很好的将連續寫入的時間資料進行打散,相應的代價是,查詢代價會變大(無論查詢條件多麼精确,都需要查出至少一小時的資料),需要過濾一些無用的資料。

結論

  1. 使用DATE類型,并且主鍵使用AUTO_RANDOM。缺點是無法存儲時分秒,主鍵也失去了宏觀上的自增性;
  2. 使用DATE類型,并且和另一個不自增的離散列建立組合索引。例如idx_gmt_create;
  3. 使用DATE類型,并且主鍵使用SHARD_ROW_ID_BITS。缺點是無法存儲時分秒,主鍵失去了宏觀上的自增性,并且SHARD_ROW_ID_BITS與主鍵使用聚簇相沖突,這會造成寫入的放大以及主鍵查詢需要做回表;
  4. 注意DATE類型即使在平時沒有熱點,在0點時刻也可能帶來劇烈抖動
  5. 使用分區表,這樣時間索引成為了分區内的Local索引,等于按分區做了打散。這是目前能想到的DATETIME類型上使用索引又避免熱點的唯一方法,但代價也很大,TiDB目前不支援在分區表上建立全局索引,不帶分區鍵的查詢性能上也容易有問題,這對業務代碼有很強的侵入性。