最近這幾個月,特別是 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 的演算法:即選擇把較小的表裝入記憶體中。
對於多種方案,利用動態規劃演算法選擇最優者,例如:
(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。