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

語言: CN / TW / HK

作者: Zeratulll 原文來源:https://tidb.net/blog/9468d259

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不存在熱點。

如圖所示:

no-alt

解讀

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的結尾。

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

結論

相容性其實包含功能相容性與效能相容性,TiDB雖然功能上與MySQL的相容性做的不錯,但效能上的差異點還是比較多的。

就本例而言,我們可以得出的結論是,使用TiDB時,在時間型別上建立索引需要慎重,如果按照使用單機MySQL的習慣進行建立,很容易出現熱點,導致雖然使用了分散式,但毫無擴充套件性可言。

如需建立,有以下幾個方法(每種方法都不完美,只能做取捨):

  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目前不支援在分割槽表上建立全域性索引,不帶分割槽鍵的查詢效能上也容易有問題,這對業務程式碼有很強的侵入性。