TiDB 簡介
TiDB 是 PingCAP 公司設計的開源分散式 HTAP (Hybrid Transactional and Analytical Processing) 資料庫,結合了傳統的 RDBMS 和 NoSQL 的最佳特性。TiDB 相容 MySQL,支援無限的水平擴充套件,具備強一致性和高可用性。TiDB 的目標是為 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 場景提供一站式的解決方案。
TiDB 整體架構
TiDB 叢集主要包括三個核心元件:TiDB Server,PD Server 和 TiKV Server。此外,還有用於解決使用者複雜 OLAP 需求的 TiSpark 元件。
TiDB Server
TiDB Server 負責接收 SQL 請求,處理 SQL 相關的邏輯,並通過 PD 找到儲存計算所需資料的 TiKV 地址,與 TiKV 互動獲取資料,最終返回結果。TiDB Server 是無狀態的,其本身並不儲存資料,只負責計算,可以無限水平擴充套件,可以通過負載均衡元件(如LVS、HAProxy 或 F5)對外提供統一的接入地址。
PD Server
Placement Driver (簡稱 PD) 是整個叢集的管理模組,其主要工作有三個:一是儲存叢集的元資訊(某個 Key 儲存在哪個 TiKV 節點);二是對 TiKV 叢集進行排程和負載均衡(如資料的遷移、Raft group leader 的遷移等);三是分配全域性唯一且遞增的事務 ID。
PD 是一個叢集,需要部署奇數個節點,一般線上推薦至少部署 3 個節點。
TiKV Server
TiKV Server 負責儲存資料,從外部看 TiKV 是一個分散式的提供事務的 Key-Value 儲存引擎。儲存資料的基本單位是 Region,每個 Region 負責儲存一個 Key Range(從 StartKey 到 EndKey 的左閉右開區間)的資料,每個 TiKV 節點會負責多個 Region。TiKV 使用 Raft 協議做複製,保持資料的一致性和容災。副本以 Region 為單位進行管理,不同節點上的多個 Region 構成一個 Raft Group,互為副本。資料在多個 TiKV 之間的負載均衡由 PD 排程,這裡也是以 Region 為單位進行排程。
TiSpark
TiSpark 作為 TiDB 中解決使用者複雜 OLAP 需求的主要元件,將 Spark SQL 直接執行在 TiDB 儲存層上,同時融合 TiKV 分散式叢集的優勢,並融入大資料社群生態。至此,TiDB 可以通過一套系統,同時支援 OLTP 與 OLAP,免除使用者資料同步的煩惱。
TiDB 核心特性
TiDB 的兩大核心特性:水平擴充套件與高可用。
水平擴充套件
水平擴充套件包括兩方面:計算能力和儲存能力。
TiDB Server 負責處理 SQL 請求,隨著業務的增長,可以簡單的新增 TiDB Server 節點,提高整體的處理能力,提供更高的吞吐。
TiKV 負責儲存資料,隨著資料量的增長,可以部署更多的 TiKV Server 節點解決資料 Scale 的問題。
PD 會在 TiKV 節點之間以 Region 為單位做排程,將部分資料遷移到新加的節點上。
所以在業務的早期,可以只部署少量的服務例項(推薦至少部署 3 個 TiKV, 3 個 PD,2 個 TiDB),隨著業務量的增長,按照需求新增 TiKV 或者 TiDB 例項。
高可用
TiDB/TiKV/PD 這三個元件都能容忍部分例項失效,不影響整個叢集的可用性。
-
TiDB 是無狀態的,通過負載均衡元件對外提供服務。當單個例項失效時,會影響正在這個例項上進行的 Session,從應用的角度看,會出現單次請求失敗的情況,重新連線後即可繼續獲得服務。單個例項失效後,可以重啟這個例項或者部署一個新的例項。
-
PD 是一個叢集,通過 Raft 協議保持資料的一致性,單個例項失效時,如果這個例項不是 Raft 的 leader,那麼服務完全不受影響;如果這個例項是 Raft 的 leader,會重新選出新的 Raft leader,自動恢復服務。PD 在選舉的過程中無法對外提供服務,這個時間大約是3秒鐘。
-
TiKV 是一個叢集,通過 Raft 協議保持資料的一致性(副本數量可配置,預設儲存三副本),並通過 PD 做負載均衡排程。單個節點失效時,會影響這個節點上儲存的所有 Region。對於 Region 中的 Leader 結點,會中斷服務,等待重新選舉;對於 Region 中的 Follower 節點,不會影響服務。當某個 TiKV 節點失效,並且在一段時間內(預設 30 分鐘)無法恢復,PD 會將其上的資料遷移到其他的 TiKV 節點上。
TiDB 技術內幕
儲存
資料庫最根本的功能是能把資料存下來,所以我們從這裡開始。
Key-Value
作為儲存資料的系統,首先要決定的是資料的儲存模型,也就是資料以什麼樣的形式儲存下來。
TiKV 的選擇是 Key-Value 模型,並且提供有序遍歷方法。簡單來講,可以將 TiKV 看做一個巨大的 Map,其中 Key 和 Value 都是原始的 Byte 陣列,在這個 Map 中,Key 按照 Byte 陣列總的原始二進位制位元位比較順序排列。
對 TiKV 記住兩點:
-
這是一個巨大的 Map,也就是儲存的是 Key-Value pair
-
這個 Map 中的 Key-Value pair 按照 Key 的二進位制順序有序,也就是我們可以 Seek 到某一個 Key 的位置,然後不斷的呼叫 Next 方法以遞增的順序獲取比這個 Key 大的 Key-Value
這裡的儲存模型和 SQL 中的 Table 無關!
RocksDB
任何持久化的儲存引擎,資料終歸要儲存在磁碟上,TiKV 也不例外。
TiKV 沒有選擇直接向磁碟上寫資料,而是把資料儲存在 RocksDB 中,具體的資料落地由 RocksDB 負責。
RocksDB 是一個非常優秀的開源的單機儲存引擎,而且還有 Facebook 的團隊在做持續的優化。這裡可以簡單的認為 RocksDB 是一個單機的 Key-Value Map。
Raft
如何保證單機失效的情況下,資料不丟失,不出錯?簡單來說,我們需要想辦法把資料複製到多臺機器上,這樣一臺機器掛了,我們還有其他的機器上的副本;複雜來說,我們還需要這個複製方案是可靠、高效並且能處理副本失效的情況。
Raft 是一個一致性演算法,它和 Paxos 等價,但是更加易於理解。
Raft 是一個一致性協議,提供幾個重要的功能:
-
Leader 選舉
-
成員變更
-
日誌複製
TiKV 利用 Raft 來做資料複製,每個資料變更都會落地為一條 Raft 日誌,通過 Raft 的日誌複製功能,將資料安全可靠地同步到 Group 的多數節點中。
通過單機的 RocksDB,我們可以將資料快速地儲存在磁碟上;通過 Raft,我們可以將資料複製到多臺機器上,以防單機失效。資料的寫入是通過 Raft 這一層的介面寫入,而不是直接寫 RocksDB。通過實現 Raft,我們擁有了一個分散式的 KV,現在再也不用擔心某臺機器掛掉了。
Region
對於一個 KV 系統,將資料分散在多臺機器上有兩種比較典型的方案:一種是按照 Key 做 Hash,根據 Hash 值選擇對應的儲存節點;另一種是分 Range,某一段連續的 Key 都儲存在一個儲存節點上。TiKV 選擇了第二種方式,將整個 Key-Value 空間分成很多段,每一段是一系列連續的 Key,我們將每一段叫做一個 Region,並且我們會盡量保持每個 Region 中儲存的資料不超過一定的大小(這個大小可以配置,目前預設是 64mb)。每一個 Region 都可以用 StartKey 到 EndKey 這樣一個左閉右開區間來描述。
將資料劃分成 Region 後,我們將會做兩件重要的事情:
-
以 Region 為單位,將資料分散在叢集中所有的節點上,並且儘量保證每個節點上服務的 Region 數量差不多
-
以 Region 為單位做 Raft 的複製和成員管理
第一點,資料按照 Key 切分成很多 Region,每個 Region 的資料只會儲存在一個節點上面。
第二點,TiKV 是以 Region 為單位做資料的複製,也就是一個 Region 的資料會儲存多個副本,我們將每一個副本叫做一個 Replica。Replica 之間是通過 Raft 來保持資料的一致(終於提到了 Raft),一個 Region 的多個 Replica 會儲存在不同的節點上,構成一個 Raft Group。其中一個 Replica 會作為這個 Group 的 Leader,其他的 Replica 作為 Follower。所有的讀和寫都是通過 Leader 進行,再由 Leader 複製給 Follower。
Region 為單位做資料的分散和複製,就有了一個分散式的具備一定容災能力的 KeyValue 系統。
MVCC
很多資料庫都會實現多版本控制(MVCC),TiKV 也不例外。
設想這樣的場景,兩個 Client 同時去修改一個 Key 的 Value,如果沒有 MVCC,就需要對資料上鎖,在分散式場景下,可能會帶來效能以及死鎖問題。 TiKV 的 MVCC 實現是通過在 Key 後面新增 Version 來實現,簡單來說,沒有 MVCC 之前,可以把 TiKV 看做這樣的:
Key1 -> Value Key2 -> Value …… KeyN -> Value
有了 MVCC 之後,TiKV 的 Key 排列是這樣的:
Key1-Version3 -> Value Key1-Version2 -> Value Key1-Version1 -> Value …… Key2-Version4 -> Value Key2-Version3 -> Value Key2-Version2 -> Value Key2-Version1 -> Value …… KeyN-Version2 -> Value KeyN-Version1 -> Value ……
注意,對於同一個 Key 的多個版本,我們把版本號較大的放在前面,版本號小的放在後面,這樣當用戶通過一個 Key + Version 來獲取 Value 的時候,可以將 Key 和 Version 構造出 MVCC 的 Key,也就是 Key-Version。然後可以直接 Seek(Key-Version),定位到第一個大於等於這個 Key-Version 的位置。
事務
TiKV 的事務採用的是 Percolator 模型,並且做了大量的優化。
TiKV 的事務採用樂觀鎖,事務的執行過程中,不會檢測寫寫衝突,只有在提交過程中,才會做衝突檢測,衝突的雙方中比較早完成提交的會寫入成功,另一方會嘗試重新執行整個事務。
計算
關係模型到 Key-Value 模型的對映
如何在 KV 結構上儲存 Table 以及如何在 KV 結構上執行 SQL 語句。
CREATE TABLE User { ID int, Name varchar(20), Role varchar(20), Age int, PRIMARY KEY (ID), Key idxAge (age) };
對於一個 Table 來說,需要儲存的資料包括三部分:
-
表的元資訊
-
Table 中的 Row
-
索引資料
對於 Index,TiDB 不止需要支援 Primary Index,還需要支援 Secondary Index。
查詢的時候有兩種模式,一種是點查,比如通過 Primary Key 或者 Unique Key 的等值條件進行查詢,如 select name from user where id=1;
,這種需要通過索引快速定位到某一行資料;另一種是 Range 查詢,如 select name from user where age > 30 and age < 35;
,這個時候需要通過idxAge索引查詢 age 在 30 和 35 之間的那些資料。Index 還分為 Unique Index 和 非 Unique Index,這兩種都需要支援。
-
對於 Insert 語句,需要將 Row 寫入 KV,並且建立好索引資料。
-
對於 Update 語句,需要將 Row 更新的同時,更新索引資料(如果有必要)。
-
對於 Delete 語句,需要在刪除 Row 的同時,將索引也刪除。
-
對於 Select 語句,首先我們需要能夠簡單快速地讀取一行資料,所以每個 Row 需要有一個 ID (顯示或隱式的 ID)。其次可能會讀取連續多行資料,比如
Select * from user;
。最後還有通過索引讀取資料的需求,對索引的使用可能是點查或者是範圍查詢。
一個全域性有序的分散式 Key-Value 引擎。
TiDB 對每個表分配一個 TableID,每一個索引都會分配一個 IndexID,每一行分配一個 RowID(如果表有整數型的 Primary Key,那麼會用 Primary Key 的值當做 RowID),其中 TableID 在整個叢集內唯一,IndexID/RowID 在表內唯一,這些 ID 都是 int64 型別。 每行資料按照如下規則進行編碼成 Key-Value pair:
Key: tablePrefix_rowPrefix_tableID_rowID Value: [col1, col2, col3, col4]
對於 Index 資料,會按照如下規則編碼成 Key-Value pair:
Key: tablePrefix_idxPrefix_tableID_indexID_indexColumnsValue Value: rowID
Index 資料還需要考慮 Unique Index 和非 Unique Index 兩種情況,對於 Unique Index,可以按照上述編碼規則。但是對於非 Unique Index,通過這種編碼並不能構造出唯一的 Key,因為同一個 Index 的 tablePrefix_idxPrefix_tableID_indexID_ 都一樣,可能有多行資料的 ColumnsValue 是一樣的,所以對於非 Unique Index 的編碼做了一點調整:
Key: tablePrefix_idxPrefix_tableID_indexID_ColumnsValue_rowID Value:null
無論是 Row 還是 Index 的 Key 編碼方案,一個 Table 內部所有的 Row 都有相同的字首,一個 Index 的資料也都有相同的字首。
採用這種編碼後,一個表的所有 Row 資料就會按照 RowID 的順序排列在 TiKV 的 Key 空間中,某一個 Index 的資料也會按照 Index 的 ColumnValue 順序排列在 Key 空間內。
通過這個對映方案,將 Row 和 Index 資料都轉換為 Key-Value 資料,且每一行、每一條索引資料都是有唯一的 Key。
假設表中有 3 行資料:
“TiDB”, “SQL Layer”, 10 “TiKV”, “KV Engine”, 20 “PD”, “Manager”, 30
每行資料都會對映為一個 Key-Value pair,假設這個表的 Table ID 為 10,其 Row 的資料為:
t_r_10_1 --> ["TiDB", "SQL Layer", 10] t_r_10_2 --> ["TiKV", "KV Engine", 20] t_r_10_3 --> ["PD", "Manager", 30]
這個表還有一個 Index,假設這個 Index 的 ID 為 1,則其資料為:
t_i_10_1_10_1 --> null t_i_10_1_20_2 --> null t_i_10_1_30_3 --> null
元資訊管理
Database/Table 都有元資訊,也就是其定義以及各項屬性,這些資訊也需要持久化,我們也將這些資訊儲存在 TiKV 中。
每個 Database/Table 都被分配了一個唯一的 ID,這個 ID 作為唯一標識,並且在編碼為 Key-Value 時,這個 ID 都會編碼到 Key 中,再加上 m_ 字首。
SQL on KV 架構
TiDB 的整體架構如下圖所示:
TiKV Cluster 主要作用是作為 KV 引擎儲存資料。TiDB Servers 這一層的節點都是無狀態的節點,本身並不儲存資料,節點之間完全對等。TiDB Server 這一層最重要的工作是處理使用者請求,執行 SQL 運算邏輯。
SQL 運算
理解了 SQL 到 KV 的對映方案之後,接下來我們要理解如何使用這些資料來滿足使用者的查詢需求。
將 SQL 查詢對映為對 KV 的查詢,再通過 KV 介面獲取對應的資料,最後執行各種計算。這樣一個操作流程轉換為 KV 操作流程:
-
構造出 Key Range:一個表中所有的 RowID 都在 [0, MaxInt64) 這個範圍內,那麼我們用 0 和 MaxInt64 根據 Row 的 Key 編碼規則,就能構造出一個 [StartKey, EndKey) 的左閉右開區間
-
掃描 Key Range:根據上面構造出的 Key Range,讀取 TiKV 中的資料
-
過濾資料:對於讀到的每一行資料,計算 name="TiDB" 這個表示式,如果為真,則向上返回這一行,否則丟棄這一行資料
-
計算 Count:對符合要求的每一行,累計到 Count 值上面
這個方案肯定是可以 Work 的,但是並不能 Work 的很好,原因是顯而易見的:
-
1、在掃描資料的時候,每一行都要通過 KV 操作同 TiKV 中讀取出來,至少有一次 RPC 開銷,如果需要掃描的資料很多,那麼這個開銷會非常大
-
2、並不是所有的行都有用,如果不滿足條件,其實可以不讀取出來
-
3、符合要求的行的值並沒有什麼意義,實際上這裡只需要有幾行資料這個資訊就行
分散式 SQL 運算
如何避免上述缺陷,首先我們需要將計算儘量靠近儲存節點,以避免大量的 RPC 呼叫。其次,我們需要將 Filter 也下推到儲存節點進行計算,這樣只需要返回有效的行,避免無意義的網路傳輸。最後,我們可以將聚合函式、GroupBy 也下推到儲存節點,進行預聚合,每個節點只需要返回一個 Count 值即可,再由 tidb-server 將 Count 值 Sum 起來。
這裡有一個數據逐層返回的示意圖:
SQL 層架構
使用者的 SQL 請求會直接或者通過 Load Balancer 傳送到 tidb-server,tidb-server 會解析 MySQL Protocol Packet,獲取請求內容,然後做語法解析、查詢計劃制定和優化、執行查詢計劃獲取和處理資料。資料全部儲存在 TiKV 叢集中,所以在這個過程中 tidb-server 需要和 tikv-server 互動,獲取資料。最後 tidb-server 需要將查詢結果返回給使用者。
排程
為什麼要進行排程
TiKV 叢集是 TiDB 資料庫的分散式 KV 儲存引擎,資料以 Region 為單位進行復制和管理,每個 Region 會有多個 Replica(副本),這些 Replica 會分佈在不同的 TiKV 節點上,其中 Leader 負責讀/寫,Follower 負責同步 Leader 發來的 raft log。
排程的需求
作為一個分散式高可用儲存系統,必須滿足的需求,包括四種:
-
副本數量不能多也不能少
-
副本需要分佈在不同的機器上
-
新加節點後,可以將其他節點上的副本遷移過來
-
節點下線後,需要將該節點的資料遷移走
作為一個良好的分散式系統,需要優化的地方,包括:
-
維持整個叢集的 Leader 分佈均勻
-
維持每個節點的儲存容量均勻
-
維持訪問熱點分佈均勻
-
控制 Balance 的速度,避免影響線上服務
-
管理節點狀態,包括手動上線/下線節點,以及自動下線失效節點
排程的基本操作
排程需求,整理下來最終落地的無非是下面三件事:
-
增加一個 Replica
-
刪除一個 Replica
-
將 Leader 角色在一個 Raft Group 的不同 Replica 之間 transfer
資訊收集
排程依賴於整個叢集資訊的收集,簡單來說,我們需要知道每個 TiKV 節點的狀態以及每個 Region 的狀態。TiKV 叢集會向 PD 彙報兩類訊息:
1、每個 TiKV 節點會定期向 PD 彙報節點的整體資訊
TiKV 節點(Store)與 PD 之間存在心跳包,一方面 PD 通過心跳包檢測每個 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也會攜帶這個 Store 的狀態資訊,主要包括:
-
總磁碟容量
-
可用磁碟容量
-
承載的 Region 數量
-
資料寫入速度
-
傳送/接受的 Snapshot 數量(Replica 之間可能會通過 Snapshot 同步資料)
-
是否過載
-
標籤資訊(標籤是具備層級關係的一系列 Tag)
2、每個 Raft Group 的 Leader 會定期向 PD 彙報資訊
每個 Raft Group 的 Leader 和 PD 之間存在心跳包,用於彙報這個 Region 的狀態,主要包括下面幾點資訊:
-
Leader 的位置
-
Followers 的位置
-
掉線 Replica 的個數
-
資料寫入/讀取的速度
PD 不斷的通過這兩類心跳訊息收集整個叢集的資訊,再以這些資訊作為決策的依據。
排程的策略
PD 收集了這些資訊後,還需要一些策略來制定具體的排程計劃。
1、一個 Region 的 Replica 數量正確
當 PD 通過某個 Region Leader 的心跳包發現這個 Region 的 Replica 數量不滿足要求時,需要通過 Add/Remove Replica 操作調整 Replica 數量。
2、一個 Raft Group 中的多個 Replica 不在同一個位置
在一般情況下,PD 只會保證多個 Replica 不落在一個節點上,以避免單個節點失效導致多個 Replica 丟失。在實際部署中,還可能出現下面這些需求:
-
多個節點部署在同一臺物理機器上
-
TiKV 節點分佈在多個機架上,希望單個機架掉電時,也能保證系統可用性
-
TiKV 節點分佈在多個 IDC 中,希望單個機房掉電時,也能保證系統可用
這些需求本質上都是某一個節點具備共同的位置屬性,構成一個最小的容錯單元,我們希望這個單元內部不會存在一個 Region 的多個 Replica。
3、副本在 Store 之間的分佈均勻分配
4、Leader 數量在 Store 之間均勻分配
Raft 協議要讀取和寫入都通過 Leader 進行,所以計算的負載主要在 Leader 上面,PD 會盡可能將 Leader 在節點間分散開。
5、訪問熱點數量在 Store 之間均勻分配
6、各個 Store 的儲存空間佔用大致相等
7、控制排程速度,避免影響線上服務
8、支援手動下線節點
排程的實現
PD 不斷的通過 Store 或者 Leader 的心跳包收集資訊,獲得整個叢集的詳細資料,並且根據這些資訊以及排程策略生成排程操作序列,每次收到 Region Leader 發來的心跳包時,PD 都會檢查是否有對這個 Region 待進行的操作,通過心跳包的回覆訊息,將需要進行的操作返回給 Region Leader,並在後面的心跳包中監測執行結果。注意這裡的操作只是給 Region Leader 的建議,並不保證一定能得到執行,具體是否會執行以及什麼時候執行,由 Region Leader 自己根據當前自身狀態來定。
Reference:
https://www.pingcap.com/docs-cn/overview/