本文檔用于總結在使用 TiDB 時候的一些最佳實踐,主要涉及 SQL 使用、OLAP/OLTP 優化技巧,特别是一些 TiDB 專有的優化開關。
建議先閱讀講解 TiDB 原理的三篇文章(講存儲,說計算,談排程),再來看這篇文章。
資料庫是一個通用的基礎元件,在開發過程中會考慮到多種目标場景,在具體的業務場景中,需要根據業務的實際情況對資料的參數或者使用方式進行調整。
TiDB 是一個相容 MySQL 協定和文法的分布式資料庫,但是由于其内部實作,特别是支援分布式存儲以及分布式事務,使得一些使用方法和 MySQL 有所差別。
TiDB 的最佳實踐與其實作原理密切相關,建議讀者先了解一些基本的實作機制,包括 Raft、分布式事務、資料分片、負載均衡、SQL 到 KV 的映射方案、二級索引的實作方法、分布式執行引擎。下面會做一點簡單的介紹,更詳細的資訊可以參考 PingCAP 公衆号以及知乎專欄的一些文章。
Raft 是一種一緻性協定,能提供強一緻的資料複制保證,TiDB 最底層用 Raft 來同步資料。每次寫入都要寫入多數副本,才能對外傳回成功,這樣即使丢掉少數副本,也能保證系統中還有最新的資料。比如最大 3 副本的話,每次寫入 2 副本才算成功,任何時候,隻丢失一個副本的情況下,存活的兩個副本中至少有一個具有最新的資料。
相比 Master-Slave 方式的同步,同樣是儲存三副本,Raft 的方式更為高效,寫入的延遲取決于最快的兩個副本,而不是最慢的那個副本。是以使用 Raft 同步的情況下,異地多活成為可能。在典型的兩地三中心場景下,每次寫入隻需要本資料中心以及離得近的一個資料中心寫入成功就能保證資料的一緻性,而并不需要三個資料中心都寫成功。但是這并不意味着在任何場景都能建構跨機房部署的業務,當寫入量比較大時候,機房之間的帶寬和延遲成為關鍵因素,如果寫入速度超過機房之間的帶寬,或者是機房之間延遲過大,整個 Raft 同步機制依然無法很好的運轉。
TiDB 提供完整的分布式事務,事務模型是在 Google Percolator 的基礎上做了一些優化。具體的實作大家可以參考這篇文章。這裡隻說兩點:
樂觀鎖
TiDB 的事務模型采用樂觀鎖,隻有在真正送出的時候,才會做沖突檢測,如果有沖突,則需要重試。這種模型在沖突嚴重的場景下,會比較低效,因為重試之前的操作都是無效的,需要重複做。舉一個比較極端的例子,就是把資料庫當做計數器用,如果通路的并發度比較高,那麼一定會有嚴重的沖突,導緻大量的重試甚至是逾時。但是如果通路沖突并不十分嚴重,那麼樂觀鎖模型具備較高的效率。是以在沖突嚴重的場景下,推薦在系統架構層面解決問題,比如将計數器放在 Redis 中。
事務大小限制
由于分布式事務要做兩階段送出,并且底層還需要做 Raft 複制,如果一個事務非常大,會使得送出過程非常慢,并且會卡住下面的 Raft 複制流程。為了避免系統出現被卡住的情況,我們對事務的大小做了限制:
單條 KV entry 不超過 6MB
KV entry 的總條數不超過 30w
KV entry 的總大小不超過 100MB
在 Google 的 Cloud Spanner 上面,也有類似的限制。
TiKV 自動将底層資料按照 Key 的 Range 進行分片。每個 Region 是一個 Key 的範圍,從 StartKey 到 EndKey 的左閉右開區間。Region 中的 Key-Value 總量超過一定值,就會自動分裂。這部分使用者不需要擔心。
PD 會根據整個 TiKV 叢集的狀态,對叢集的負載進行排程。排程是以 Region 為機關,以 PD 配置的政策為排程邏輯,自動完成。
TiDB 自動将 SQL 結構映射為 KV 結構。具體的可以參考這篇文檔。簡單來說,TiDB 做了兩件事:
一行資料映射為一個 KV,Key 以 <code>TableID</code> 構造字首,以行 ID 為字尾
一條索引映射為一個 KV,Key 以 <code>TableID+IndexID</code> 構造字首,以索引值構造字尾
可以看到,對于一個表中的資料或者索引,會具有相同的字首,這樣在 TiKV 的 Key 空間内,這些 Key-Value 會在相鄰的位置。那麼當寫入量很大,并且集中在一個表上面時,就會造成寫入的熱點,特别是連續寫入的資料中某些索引值也是連續的(比如 update time 這種按時間遞增的字段),會再很少的幾個 Region 上形成寫入熱點,成為整個系統的瓶頸。同樣,如果所有的資料讀取操作也都集中在很小的一個範圍内 (比如在連續的幾萬或者十幾萬行資料上),那麼可能造成資料的通路熱點。
TiDB 支援完整的二級索引,并且是全局索引,很多查詢可以通過索引來優化。如果利用好二級索引,對業務非常重要,很多 MySQL 上的經驗在 TiDB 這裡依然适用,不過 TiDB 還有一些自己的特點,需要注意,這一節主要讨論在 TiDB 上使用二級索引的一些注意事項。
二級索引是否有多越好
二級索引能加速查詢,但是要注意新增一個索引是有副作用的,在上一節中我們介紹了索引的存儲模型,那麼每增加一個索引,在插入一條資料的時候,就要新增一個 Key-Value,是以索引越多,寫入越慢,并且空間占用越大。另外過多的索引也會影響優化器運作時間,并且不合适的索引會誤導優化器。是以索引并不是越多越好。
對哪些列建索引比較合适
上面提到,索引很重要但不是越多越好,我們需要根據具體的業務特點建立合适的索引。原則上我們需要對查詢中需要用到的列建立索引,目的是提高性能。下面幾種情況适合建立索引:
區分度比較大的列,通過索引能顯著地減少過濾後的行數
有多個查詢條件時,可以選擇組合索引,注意需要把等值條件的列放在組合索引的前面
這裡舉一個例子,假設常用的查詢是 <code>select * from t where c1 = 10 and c2 = 100 and c3 > 10</code>, 那麼可以考慮建立組合索引 <code>Index cidx (c1, c2, c3)</code>,這樣可以用查詢條件構造出一個索引字首進行 Scan。
通過索引查詢和直接掃描 Table 的差別
TiDB 實作了全局索引,是以索引和 Table 中的資料并不一定在一個資料分片上,通過索引查詢的時候,需要先掃描索引,得到對應的行 ID,然後通過行 ID 去取資料,是以可能會涉及到兩次網絡請求,會有一定的性能開銷。
如果查詢涉及到大量的行,那麼掃描索引是并發進行,隻要第一批結果已經傳回,就可以開始去取 Table 的資料,是以這裡是一個并行 + Pipeline 的模式,雖然有兩次通路的開銷,但是延遲并不會很大。
有兩種情況不會涉及到兩次通路的問題:
索引中的列已經滿足了查詢需求。比如 Table t 上面的列 c 有索引,查詢是 <code>select c from t where c > 10</code>; 這個時候,隻需要通路索引,就可以拿到所需要的全部資料。這種情況我們稱之為覆寫索引(Covering Index)。是以如果很關注查詢性能,可以将部分不需要過濾但是需要再查詢結果中傳回的列放入索引中,構造成組合索引,比如這個例子: <code>select c1, c2 from t where c1 > 10</code>; 要優化這個查詢可以建立組合索引 <code>Index c12 (c1, c2)</code>。
表的 Primary Key 是整數類型。在這種情況下,TiDB 會将 Primary Key 的值當做行 ID,是以如果查詢條件是在 PK 上面,那麼可以直接構造出行 ID 的範圍,直接掃描 Table 資料,擷取結果。
查詢并發度
資料分散在很多 Region 上,是以 TiDB 在做查詢的時候會并發進行,預設的并發度比較保守,因為過高的并發度會消耗大量的系統資源,且對于 OLTP 類型的查詢,往往不會涉及到大量的資料,較低的并發度已經可以滿足需求。對于 OLAP 類型的 Query,往往需要較高的并發度。是以 TiDB 支援通過 System Variable 來調整查詢并發度。
tidb_distsql_scan_concurrency
在進行掃描資料的時候的并發度,這裡包括掃描 Table 以及索引資料。
tidb_index_lookup_size
如果是需要通路索引擷取行 ID 之後再通路 Table 資料,那麼每次會把一批行 ID 作為一次請求去通路 Table 資料,這個參數可以設定 Batch 的大小,較大的 Batch 會使得延遲增加,較小的 Batch 可能會造成更多的查詢次數。這個參數的合适大小與查詢涉及的資料量有關。一般不需要調整。
tidb_index_lookup_concurrency
如果是需要通路索引擷取行 ID 之後再通路 Table 資料,每次通過行 ID 擷取資料時候的并發度通過這個參數調節。
通過索引保證結果順序
索引除了可以用來過濾資料之外,還能用來對資料排序,首先按照索引的順序擷取行 ID,然後再按照行 ID 的傳回順序傳回行的内容,這樣可以保證傳回結果按照索引列有序。前面提到了掃索引和擷取 Row 之間是并行 + Pipeline 模式,如果要求按照索引的順序傳回 Row,那麼這兩次查詢之間的并發度設定的太高并不會降低延遲,是以預設的并發度比較保守。可以通過 tidb_index_serial_scan_concurrency 變量進行并發度調整。
逆序索引
目前 TiDB 支援對索引進行逆序 Scan,但是速度要比順序 Scan 慢 5 倍左右,是以盡量避免對索引的逆序 Scan。
上一節我們讨論了一些 TiDB 基本的實作機制及其對使用帶來的影響,本節我們從具體的使用場景出發,談一些更為具體的操作實踐。我們以從部署到支撐業務這條鍊路為序,進行讨論。
在部署之前請務必閱讀 TiDB 部署建議以及對硬體的需求。
推薦通過 TiDB-Ansible
部署 TiDB 叢集,這個工具可以部署、停止、銷毀、更新整個叢集,非常友善易用。
具體的使用文檔在這裡。非常不推薦手動部署,後期的維護和更新會很麻煩。
如果有 Unique Key 并且業務端可以保證資料中沒有沖突,可以在 Session 内打開這個開關: <code>SET @@session.tidb_skip_constraint_check=1;</code>
另外為了提高寫入性能,可以對 TiKV 的參數進行調優,具體的文檔在這裡。
請特别注意這個參數:
上面提到了 TiDB 對單個事務的大小有限制,這層限制是在 KV 層面,反映在 SQL 層面的話,簡單來說一行資料會映射為一個 KV entry,每多一個索引,也會增加一個 KV entry,是以這個限制反映在 SQL 層面是:
單行資料不大于 6MB
總的行數*(1 + 索引個數) < 30w
一次送出的全部資料小于 100MB
另外注意,無論是大小限制還是行數限制,還要考慮 TiDB 做編碼以及事務額外 Key 的開銷,在使用的時候,建議每個事務的行數不要超過 1w 行,否則有可能會超過限制,或者是性能不佳。
建議無論是 Insert,Update 還是 Delete 語句,都通過分 Batch 或者是加 Limit 的方式限制。
在删除大量資料的時候,建議使用 <code>Delete * from t where xx limit 5000;</code> 這樣的方案,通過循環來删除,用 <code>Affected Rows == 0</code> 作為循環結束條件,這樣避免遇到事務大小的限制。
如果一次删除的資料量非常大,這種循環的方式會越來越慢,因為每次删除都是從前向後周遊,前面的删除之後,短時間内會殘留不少删除标記(後續會被 gc 掉),影響後面的 Delete 語句。如果有可能,建議把 Where 條件細化。舉個例子,假設要删除 2017-05-26 當天的所有資料,那麼可以這樣做:
上面是一段僞代碼,意思就是要把大塊的資料拆成小塊删除,以避免删除過程中前面的 Delete 語句影響後面的 Delete 語句。
看業務的查詢需求以及具體的語句,可以參考這篇文檔
可以通過 SET 語句控制 SQL 執行的并發度,另外通過 Hint 控制 Join 實體算子選擇。
另外 MySQL 标準的索引選擇 Hint 文法,也可以用,通過 <code>Use Index/Ignore Index hint</code> 控制優化器選擇索引。
如果是個 OLTP 和 OLAP 混合類型的業務,可以把 TP 請求和 AP 請求發送到不同的 tidb-server 上,這樣能夠減小 AP 業務對于 TP 業務的影響。 承載 AP 業務的 tidb-server 推薦使用高配的機器,比如 CPU 核數比較多,記憶體比較大。
Metrics 系統是了解系統狀态的最佳方法,建議所有的使用者都部署監控系統。TiDB 使用 Grafana+Prometheus 監控系統狀态,如果使用 TiDB-Ansible 部署叢集,那麼會自動部署和配置監控系統。
監控系統中的監控項很多,大部分是給 TiDB 開發者檢視的内容,如果沒有對源代碼比較深入的了解,并沒有必要了解這些監控項。我們會精簡出一些和業務相關或者是系統關鍵元件狀态相關的監控項,放在一個獨立的面闆中,供使用者使用。
除了監控之外,檢視日志也是了解系統狀态的常用方法。TiDB 的三個元件 tidb-server/tikv-server/pd-server 都有一個 <code>--log-file</code> 的參數,如果啟動的時候設定了這個參數,那麼日志會儲存着參數所設定的檔案的位置,另外會自動的按天對 Log 檔案做歸檔。如果沒有設定 <code>--log-file</code> 參數,日志會輸出在 stderr 中。
了解一個系統或者解決使用中的問題最好的方法是閱讀文檔,明白實作原理,TiDB 有大量的官方文檔,希望大家在遇到問題的時候能先嘗試通過文檔或者搜尋 Issue list 尋找解決方案。官方文檔在這裡。如果希望閱讀英文文檔,可以看這裡。
其中的 FAQ
和故障診斷章節建議大家仔細閱讀。另外 TiDB 還有一些不錯的工具,也有配套的文檔,具體的見各項工具的 GitHub 頁面。
除了文檔之外,還有很多不錯的文章介紹 TiDB 的各項技術細節内幕,大家可以關注下面這些文章釋出管道:
公衆号:微信搜尋 PingCAP
知乎專欄:TiDB 的後花園
官方部落格
簡單來說,TiDB 适合具備下面這些特點的場景:
資料量大,單機儲存不下
不希望做 Sharding 或者懶得做 Sharding
通路模式上沒有明顯的熱點
需要事務、需要強一緻、需要災備
作者簡介:申礫,TiDB Tech Lead,前網易有道詞典伺服器端核心開發,前奇虎 360 新聞推薦系統 / 地圖基礎資料與檢索系統 Tech Lead。 TiDB 源碼位址:https://github.com/pingcap/tidb