TiDB 的正確使用姿勢

PingCAP

  •  1.3k

TiDB 的正確使用姿勢 mysqlsqlnosqlrustgithub

發佈於 2017-03-10   約 10 分鐘

最近這幾個月,特別是 TiDB RC1 發佈後,越來越多的用戶已經開始測試起來,也有很多朋友已經在生產環境中使用,我們這邊也陸續的收到了很多用戶的測試和使用反饋。非常感謝各位小夥伴和早期用戶的厚愛,而且看了這麼多場景後,也總結出了一些 TiDB 的使用實踐 (其實 Spanner 的最佳實踐大部分在 TiDB 中也是適用的,MySQL 最佳實踐也是),也是藉着 Google Cloud Spanner 發佈的東風,看了一下 Spanner 官方的一些最佳實踐文檔,寫篇文章講講 TiDB 以及分佈式關係型數據庫的一些正確的使用姿勢,當然,時代也在一直髮展,TiDB 也在不停的進化,這篇文章基本上只代表近期的一些觀察。

首先談談 Schema 設計的一些比較好的經驗。由於 TiDB 是一個分佈式的數據庫,可能在表結構設計的時候需要考慮的事情和傳統的單機數據庫不太一樣,需要開發者能夠帶着「這個表的數據會分散在不同的機器上」這個前提,才能做更好的設計。

和 Spanner 一樣,TiDB 中的一張表的行(Rows)是按照主鍵的字節序排序的(整數類型的主鍵我們會使用特定的編碼使其字節序和按大小排序一致),即使在 CREATE TABLE 語句中不顯式的創建主鍵,TiDB 也會分配一個隱式的。
有四點需要記住:

  1. 按照字節序的順序掃描的效率是比較高的;

  2. 連續的行大概率會存儲在同一臺機器的鄰近位置,每次批量的讀取和寫入的效率會高;

  3. 索引是有序的(主鍵也是一種索引),一行的每一列的索引都會佔用一個 KV Pair,比如,某個表除了主鍵有 3 個索引,那麼在這個表中插入一行,對應在底層存儲就是 4 個 KV Pairs 的寫入:數據行以及 3 個索引行。

  4. 一行的數據都是存在一個 KV Pair 中,不會被切分,這點和類 BigTable 的列式存儲很不一樣。

表的數據在 TiDB 內部會被底層存儲 TiKV 切分成很多 64M 的 Region(對應 Spanner 的 Splits 的概念),每個 Region 裏面存儲的都是連續的行,Region 是 TiDB 進行數據調度的單位,隨着一個 Region 的數據量越來越大和時間的推移,Region 會分裂/合併,或者移動到集羣中不同的物理機上,使得整個集羣能夠水平擴展。

  • 建議:

    1. 儘可能批量寫入,但是一次寫入總大小不要超過 Region 的分裂閾值(64M),另外 TiDB 也對單個事務有大小的限制。

    2. 存儲超寬表是比較不合適的,特別是一行的列非常多,同時不是太稀疏,一個經驗是最好單行的總數據大小不要超過 64K,越小越好。大的數據最好拆到多張表中。

    3. 對於高併發且訪問頻繁的數據,儘可能一次訪問只命中一個 Region,這個也很好理解,比如一個模糊查詢或者一個沒有索引的表掃描操作,可能會發生在多個物理節點上,一來會有更大的網絡開銷,二來訪問的 Region 越多,遇到 stale region 然後重試的概率也越大(可以理解爲 TiDB 會經常做 Region 的移動,客戶端的路由信息可能更新不那麼及時),這些可能會影響 .99 延遲;另一方面,小事務(在一個 Region 的範圍內)的寫入的延遲會更低,TiDB 針對同一個 Region 內的跨行事務是有優化的。另外 TiDB 對通過主鍵精準的點查詢(結果集只有一條)效率更高。

關於索引

除了使用主鍵查詢外,TiDB 允許用戶創建二級索引以加速訪問,就像上面提到過的,在 TiKV 的層面,TiDB 這邊的表裏面的行數據和索引的數據看起來都是 TiKV 中的 KV Pair,所以很多適用於表數據的原則也適用於索引。和 Spanner 有點不一樣的是,TiDB 只支持全局索引,也就是 Spanner 中默認的 Non-interleaved indexes。全局索引的好處是對使用者沒有限制,可以 scale 到任意大小,不過這意味着,索引信息不一定和實際的數據在一個 Region 內。

  • 建議:
    對於大海撈針式的查詢來說 (海量數據中精準定位某條或者某幾條),務必通過索引。

當然也不要盲目的創建索引,創建太多索引會影響寫入的性能。

反模式 (最好別這麼幹!)

其實 Spanner 的白皮書已經寫得很清楚了,我再贅述一下:

第一種,過度依賴單調遞增的主鍵,AUTO INCREMENT ID
在傳統的關係型數據庫中,開發者經常會依賴自增 ID 來作爲 PRIMARY KEY,但是其實大多數場景大家想要的只是一個不重複的 ID 而已,至於是不是自增其實無所謂,但是這個對於分佈式數據庫來說是不推薦的,隨着插入的壓力增大,會在這張表的尾部 Region 形成熱點,而且這個熱點並沒有辦法分散到多臺機器。TiDB 在 GA 的版本中會對非自增 ID 主鍵進行優化,讓 insert workload 儘可能分散。

  • 建議:
    如果業務沒有必要使用單調遞增 ID 作爲主鍵,就別用,使用真正有意義的列作爲主鍵(一般來說,例如:郵箱、用戶名等)

使用隨機的 UUID 或者對單調遞增的 ID 進行 bit-reverse (位反轉)

第二種,單調遞增的索引 (比如時間戳)
很多日誌類型的業務,因爲經常需要按照時間的維度查詢,所以很自然需要對 timestamp 創建索引,但是這類索引的問題本質上和單調遞增主鍵是一樣的,因爲在 TiDB 的內部實現裏,索引也是一堆連續的 KV Pairs,不斷的插入單調遞增的時間戳會造成索引尾部的 Region 形成熱點,導致寫入的吞吐受到影響。

  • 建議:
    因爲不可避免的,很多用戶在使用 TiDB 存儲日誌,畢竟 TiDB 的彈性伸縮能力和 MySQL 兼容的查詢特性是很適合這類業務的。另一方面,如果發現寫入的壓力實在扛不住,但是又非常想用 TiDB 來存儲這種類型的數據,可以像 Spanner 建議的那樣做 Application 層面的 Sharding,以存儲日誌爲例,原來的可能在 TiDB 上創建一個 log 表,更好的模式是可以創建多個 log 表,如:log_1, log_2 … log_N,然後業務層插入的時候根據時間戳進行 hash ,隨機分配到 1..N 這幾個分片表中的一個。

相應的,查詢的時候需要將查詢請求分發到各個分片上,最後在業務層彙總結果。

查詢優化

TiDB 的優化分爲基於規則的優化(Rule Based Optimization)和基於代價的優化(Cost Based Optimization), 本質上 TiDB 的 SQL 引擎更像是一個分佈式計算框架,對於大表的數據因爲本身 TiDB 會將數據分散到多個存儲節點上,能將查詢邏輯下推,會大大的提升查詢的效率。

TiDB 基於規則的優化有:
謂詞下推

謂詞下推會將 where/on/having 條件推到離數據表儘可能近的地方,比如:

select * from t join s on t.id = s.id where t.c1 < 10

可以被 TiDB 自動改寫成

select * from (select * from t where t.c1 < 10) as t join s on t.id = s.id

關聯子查詢消除

關聯子查詢可能被 TiDB 改寫成 Join,例如:

select * from t where t.id in (select id from s where s.c1 < 10 and s.name = t.name)

可以被改寫成:

select * from t semi join s on t.id = s.id and s.name = t.name and s.c1 < 10

聚合下推
聚合函數可以被推過 Join,所以類似帶等值連接的 Join 的效率會比較高,例如:

select count(s.id) from t join s on t.id = s.t_id

可以被改寫成:

select sum(agg0) from t join (select count(id) as agg0, t_id from s group by t_id) as s on t.id = s.t_id

基於規則的優化有時可以組合以產生意想不到的效果,例如:

select s.c2 from s where 0 = (select count(id) from t where t.s_id = s.id)

在TiDB中,這個語句會先通過關聯子查詢消除的優化,變成:

select s.c2 from s left outer join t on t.s_id = s.id group by s.id where 0 = count(t.id)

然後這個語句會通過聚合下推的優化,變成:

select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id group by s.id where 0 = sum(agg0)

再經過聚合消除的判斷,語句可以優化成:

select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id where 0 = agg0

基於代價的優化有:

讀取表時,如果有多條索引可以選擇,我們可以通過統計信息選擇最優的索引。例如:

select * from t where age = 30 and name in ( ‘小明’, ‘小強’)
對於包含 Join 的操作,我們可以區分大小表,TiDB 的對於一個大表和一個小表的 Join 會有特殊的優化。
例如 
select * from t join s on s.id = t.id
優化器會通過對錶大小的估計來選擇 Join 的算法:即選擇把較小的表裝入內存中。
對於多種方案,利用動態規劃算法選擇最優者,例如:

(select * from t where c1 < 10) union all (select * from s where c2 < 10) order by c3 limit 10

t 和 s 可以根據索引的數據分佈來確定選擇索引 c3 還是 c2。

總之正確使用 TiDB 的姿勢,或者說 TiDB 的典型的應用場景是:

大數據量下,MySQL 複雜查詢很慢;

大數據量下,數據增長很快,接近單機處理的極限,不想分庫分表或者使用數據庫中間件等對業務侵入性較大,架構反過來約束業務的 Sharding 方案;

大數據量下,有高併發實時寫入、實時查詢、實時統計分析的需求;

有分佈式事務、多數據中心的數據 100% 強一致性、auto-failover 的高可用的需求。

如果整篇文章你只想記住一句話,那就是數據條數少於 5000w 的場景下通常用不到 TiDB,TiDB 是爲大規模的數據場景設計的。如果還想記住一句話,那就是單機 MySQL 能滿足的場景也用不到 TiDB。

閱讀 29.9k發佈於 2017-03-10

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