天天看點

TiDB 的正确使用姿勢

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

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

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

按照位元組序的順序掃描的效率是比較高的;

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

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

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

表的資料在 TiDB 内部會被底層存儲 TiKV 切分成很多 64M 的 Region(對應 Spanner 的 Splits 的概念),每個 Region 裡面存儲的都是連續的行,Region 是 TiDB 進行資料排程的機關,随着一個 Region 的資料量越來越大和時間的推移,Region 會分裂/合并,或者移動到叢集中不同的實體機上,使得整個叢集能夠水準擴充。

建議:

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

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

對于高并發且通路頻繁的資料,盡可能一次通路隻命中一個 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 的算法:即選擇把較小的

QQ靓号購買

表裝入記憶體中。 對于多種方案,利用動态規劃算法選擇最優者,例如:

(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。