做者:姚維python
本文經過闡述一個高併發批量寫入數據到 TiDB 的典型場景中,TiDB 中常見的問題,給出一個業務的最佳實踐,避免業務在開發的時候陷入 TiDB 使用的 「反模式」。ios
本文主要面向對 TiDB 有必定了解的讀者,讀者在閱讀本文以前,推薦先閱讀講解 TiDB 原理的三篇文章(講存儲,說計算,談調度),以及 TiDB Best Practice。git
高併發批量插入場景,一般存在於業務系統中的批量任務中,例如清算以及結算等業務。它存在如下顯著的特色:github
數據量大sql
須要短期內將歷史數據入庫數據庫
須要短期內讀取大量數據bash
這就對 TiDB 提出了一些挑戰:架構
寫入/讀取能力是否能夠線性水平擴展併發
數據在持續大併發寫入,性能是否穩定不衰減分佈式
對於分佈式數據庫來講,除了自己的基礎性能以外,最重要的就是充分利用全部節點能力,避免出現單個節點成爲瓶頸。
若是要解決以上挑戰,須要從 TiDB 數據切分以及調度的原理開始講起。這裏只是做簡單的說明,詳細請你們參見:說調度。
TiDB 對於數據的切分,按 Region 爲單位,一個 Region 有大小限制(默認 96M)。 Region 的切分方式是範圍切分。每一個 Region 會有多副本,每一組副本,稱爲一個 Raft-Group。由 Leader 負責執行這塊數據的讀 & 寫(固然 TiDB 即將支持 Follower-Read)。Leader 會自動地被 PD 組件均勻調度在不一樣的物理節點上,用以均分讀寫壓力。
只要業務的寫入沒有 AUTO_INCREMENT 的主鍵或者單調遞增的索引(也即沒有業務上的寫入熱點,更多細節參見 TiDB 正確使用方式)。從原理上來講,TiDB 依靠這個架構,是能夠線性擴展讀寫能力,而且能夠充分利用分佈式的資源的。這一點上 TiDB 尤爲適合高併發批量寫入場景的業務。
可是軟件世界裏,沒有銀彈。具體的事情還須要具體分析。咱們接下來就經過一些簡單的負載來探討 TiDB 在這種場景下,須要如何被正確的使用,才能達到此場景理論上的最佳性能。
有一張簡單的表:
CREATE TABLE IF NOT EXISTS TEST_HOTSPOT(
id BIGINT PRIMARY KEY,
age INT,
user_name VARCHAR(32),
email VARCHAR(128)
)
複製代碼
這個表結構很是簡單,除了 id 爲主鍵之外,沒有額外的二級索引。寫入的語句以下,id 經過隨機數離散生成:
INSERT INTO TEST_HOTSPOT(id, age, user_name, email) values(%v, %v, '%v', '%v');
複製代碼
負載是短期內密集地執行以上寫入語句。
到目前爲止,彷佛已經符合了咱們上述提到的 TiDB 最佳實踐了,業務上沒有熱點產生,只要咱們有足夠的機器,就能夠充分利用 TiDB 的分佈式能力了。要驗證這一點,咱們能夠在實驗環境中試一試(實驗環境部署拓撲是 2 個 TiDB 節點,3 個 PD 節點,6 個 TiKV 節點,請你們忽略 QPS,這裏的測試只是爲了闡述原理,並不是 benchmark):
客戶端在短期內發起了 「密集」 的寫入,TiDB 收到的請求是 3K QPS。若是沒有意外的話,壓力應該均攤給 6 個 TiKV 節點。可是從 TiKV 節點的 CPU 使用狀況上看,存在明顯的寫入傾斜(tikv - 3 節點是寫入熱點):
Raft store CPU 表明 raftstore 線程的 CPU 使用率,一般表明着寫入的負載,在這個場景下 tikv-3 是 raft 的 leader,tikv-0 跟 tikv-1 是 raft 的 follower,其餘的 tikv 節點的負載幾乎爲空。
從 PD 的監控中也能夠印證這一點:
上面這個現象是有一些違反直覺的,形成這個現象的緣由是:剛建立表的時候,這個表在 TiKV 只會對應爲一個 Region,範圍是:
[CommonPrefix + TableID, CommonPrefix + TableID + 1)
複製代碼
對於在短期內的大量寫入,它會持續寫入到同一個 Region。
上圖簡單描述了這個過程,持續寫入,TiKV 會將 Region 切分。可是因爲是由原 Leader 所在的 Store 首先發起選舉,因此大機率下舊的 Store 會成爲新切分好的兩個 Region 的 Leader。對於新切分好的 Region 2,3。也會重複以前發生在 Region 1 上的事情。也就是壓力會密集地集中在 TiKV-Node 1 中。
在持續寫入的過程當中, PD 能發現 Node 1 中產生了熱點,它就會將 Leader 均分到其餘的 Node 上。若是 TiKV 的節點數能多於副本數的話,還會發生 Region 的遷移,儘可能往空閒的 Node 上遷移,這兩個操做在插入過程,在 PD 監控中也能夠印證:
在持續寫入一段時間之後,整個集羣會被 PD 自動地調度成一個壓力均勻的狀態,到那個時候纔會真正利用整個集羣的能力。對於大多數狀況來講,這個是沒有問題的,這個階段屬於表 Region 的預熱階段。
可是對於高併發批量密集寫入場景來講,這個倒是應該避免的。
那麼咱們可否跳過這個預熱的過程,直接將 Region 切分爲預期的數量,提早調度到集羣的各個節點中呢?
TiDB 在 v3.0.x 版本以及 v2.1.13 之後的版本支持了一個新特性叫作 Split Region。這個特性提供了新的語法:
SPLIT TABLE table_name [INDEX index_name] BETWEEN (lower_value) AND (upper_value) REGIONS region_num
SPLIT TABLE table_name [INDEX index_name] BY (value_list) [, (value_list)]
複製代碼
讀者可能會有疑問,爲什麼 TiDB 不自動將這個切分動做提早完成?你們先看一下下圖:
從圖 8 能夠知道,Table 行數據 key 的編碼之中,行數據惟一可變的是行 ID (rowID)。在 TiDB 中 rowID 是一個 Int64 整形。那麼是否咱們將 Int64 整形範圍均勻切分紅咱們要的份數,而後均勻分佈在不一樣的節點就能夠解決問題呢?
答案是不必定,須要看狀況,若是行 id 的寫入是徹底離散的,那麼上述方式是可行的。可是若是行 id 或者索引是有固定的範圍或者前綴的。例如,我只在 [2000w, 5000w) 的範圍內離散插入,這種寫入依然是在業務上沒有熱點的,可是若是按上面的方式切分,那麼就有可能在開始也仍是隻寫入到某個 Region。
做爲通用的數據庫,TiDB 並不對數據的分佈做假設,因此開始只用一個 Region 來表達一個表,等到真實數據插入進來之後,TiDB 自動地根據這個數據的分佈來做切分。這種方式是較通用的。
因此 TiDB 提供了 Split Region 語法,來專門針對短時批量寫入場景做優化,下面咱們嘗試在上面的例子中用如下語句提早切散 Region,再看看負載狀況。
因爲測試的寫入是在正數範圍內徹底離散,因此咱們能夠用如下語句,在 Int64 空間內提早將表切散爲 128 個 Region:
SPLIT TABLE TEST_HOTSPOT BETWEEN (0) AND (9223372036854775807) REGIONS 128;
複製代碼
切分完成之後,能夠經過 SHOW TABLE test_hotspot REGIONS;
語句查看打散的狀況,若是 SCATTERING 列值所有爲 0,表明調度成功。
也能夠經過 table-regions.py 腳本,查看 Region 的分佈,已經比較均勻了:
[root@172.16.4.4 scripts]# python table-regions.py --host 172.16.4.3 --port 31453 test test_hotspot
[RECORD - test.test_hotspot] - Leaders Distribution:
total leader count: 127
store: 1, num_leaders: 21, percentage: 16.54%
store: 4, num_leaders: 20, percentage: 15.75%
store: 6, num_leaders: 21, percentage: 16.54%
store: 46, num_leaders: 21, percentage: 16.54%
store: 82, num_leaders: 23, percentage: 18.11%
store: 62, num_leaders: 21, percentage: 16.54%
複製代碼
咱們再從新運行插入負載:
能夠看到已經消除了明顯的熱點問題了。
固然,這裏只是舉例了一個簡單的表,還有索引熱點的問題。如何預先切散索引相關的 Region?
這個問題能夠留給讀者,經過 Split Region 文檔 能夠得到更多的信息。
若是表沒有主鍵或者主鍵不是 int 類型,用戶也不想本身生成一個隨機分佈的主鍵 ID,TiDB 內部會有一個隱式的 _tidb_rowid 列做爲行 id。在不使用 SHARD_ROW_ID_BITS 的狀況下,_tidb_rowid 列的值基本上也是單調遞增的,此時也會有寫熱點存在。(查看什麼是 SHARD_ROW_ID_BITS)
要避免由 _tidb_rowid 帶來的寫入熱點問題,能夠在建表時,使用 SHARD_ROW_ID_BITS 和 PRE_SPLIT_REGIONS 這兩個建表 option(查看什麼是 PRE_SPLIT_REGIONS)。
SHARD_ROW_ID_BITS 用來把 _tidb_rowid 列生成的行 ID 隨機打散,pre_split_regions 用來在建完表後就預先 split region。注意:pre_split_regions 必須小於等於 shard_row_id_bits。
示例:
create table t (a int, b int) shard_row_id_bits = 4 pre_split_regions=·3;
複製代碼
SHARD_ROW_ID_BITS = 4 表示 tidb_rowid 的值會隨機分佈成 16 (16=2^4) 個範圍區間。
pre_split_regions=3 表示建完表後提早 split 出 8 (2^3) 個 region。
在表 t 開始寫入後,數據寫入到提早 split 好的 8 個 region 中,這樣也避免了剛開始建表完後由於只有一個 region 而存在的寫熱點問題。
TiDB 2.1 版本中在 SQL 層引入了 latch 機制,用於在寫入衝突比較頻繁的場景中提早發現事務衝突,減小 TiDB 跟 TiKV 事務提交時寫寫衝突致使的重試。對於跑批場景,一般是存量數據,因此並不存在事務的寫入衝突。能夠把 TiDB 的 latch 關閉,以減小細小內存對象的分配:
[txn-local-latches]
enabled = false
複製代碼