三篇文章了解 TiDB 技術內幕——說計算
在上一篇文章中,我們介紹了 TiDB 如何存儲數據,也就是 TiKV 的一些基本概念。本篇將介紹 TiDB 如何利用底層的 KV 存儲,將關係模型映射為 Key-Value 模型,以及如何進行 SQL 計算。
關係模型到 Key-Value 模型的映射
在這我們將關係模型簡單理解為 Table 和 SQL 語句,那麼問題變為如何在 KV 結構上保存 Table 以及如何在 KV 結構上運行 SQL 語句。
假設我們有這樣一個表的定義:
CREATE TABLE User { ID int, Name varchar(20), Role varchar(20), Age int, PRIMARY KEY (ID), Key idxAge (age)};
SQL 和 KV 結構之間存在巨大的區別,那麼如何能夠方便高效地進行映射,就成為一個很重要的問題。一個好的映射方案必須有利於對數據操作的需求。那麼我們先看一下對數據的操作有哪些需求,分別有哪些特點。
對於一個 Table 來說,需要存儲的數據包括三部分:
1. 表的元信息
2. Table 中的 Row
3. 索引數據
表的元信息我們暫時不討論,會有專門的章節來介紹。
對於 Row,可以選擇行存或者列存,這兩種各有優缺點。TiDB 面向的首要目標是 OLTP 業務,這類業務需要支持快速地讀取、保存、修改、刪除一行數據,所以採用行存是比較合適的。
對於 Index,TiDB 不只需要支持 Primary Index,還需要支持 Secondary Index。Index 的作用的輔助查詢,提升查詢性能,以及保證某些 Constraint。查詢的時候有兩種模式,一種是點查,比如通過 Primary Key 或者 Unique Key 的等值條件進行查詢,

分析完需要存儲的數據的特點,我們再看看對這些數據的操作需求,主要考慮 Insert/Update/Delete/Select 這四種語句。
對於 Insert 語句,需要將 Row 寫入 KV,並且建立好索引數據。
對於 Update 語句,需要將 Row 更新的同時,更新索引數據(如果有必要)。對於 Delete 語句,需要在刪除 Row 的同時,將索引也刪除。上面三個語句處理起來都很簡單。對於 Select 語句,情況會複雜一些。首先我們需要能夠簡單快速地讀取一行數據,所以每個 Row 需要有一個 ID (顯示或隱式的 ID)。其次可能會讀取連續多行數據,TiDB 對每個表分配一個 TableID,每一個索引都會分配一個 IndexID,每一行分配一個 RowID(如果表有整數型的 Primary Key,那麼會用 Primary Key 的值當做 RowID),其中 TableID 在整個集群內唯一,IndexID/RowID 在表內唯一,這些 ID 都是 int64 類型。
每行數據按照如下規則進行編碼成 Key-Value pair:Key: tablePrefix_rowPrefix_tableID_rowIDValue: [col1, col2, col3, col4]
Key: tablePrefix_idxPrefix_tableID_indexID_indexColumnsValueValue: rowID
Key: tablePrefix_idxPrefix_tableID_indexID_ColumnsValue_rowIDValue:null
var( tablePrefix = []byte{t} recordPrefixSep = []byte("_r") indexPrefixSep = []byte("_i"))
1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 203, "PD", "Manager", 30那麼首先每行數據都會映射為一個 Key-Value pair,注意這個表有一個 Int 類型的 Primary Key,所以 RowID 的值即為這個 Primary Key 的值。假設這個表的 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]
t_i_10_1_10_1 —> nullt_i_10_1_20_2 --> nullt_i_10_1_30_3 --> null
大家可以結合上述編碼規則來理解上面這個例子,希望大家能理解我們為什麼選擇了這個映射方案,這樣做的目的是什麼。
元信息管理
上節介紹了表中的數據和索引是如何映射為 KV,本節介紹一下元信息的存儲。Database/Table 都有元信息,也就是其定義以及各項屬性,這些信息也需要持久化,我們也將這些信息存儲在 TiKV 中。每個 Database/Table 都被分配了一個唯一的 ID,這個 ID 作為唯一標識,並且在編碼為 Key-Value 時,這個 ID 都會編碼到 Key 中,再加上 m_ 前綴。這樣可以構造出一個 Key,對應的 Value 中存儲的是序列化後的元信息。?除此之外,還有一個專門的 Key-Value 存儲當前 Schema 信息的版本。TiDB 使用 Google F1 的 Online Schema 變更演算法,有一個後台線程在不斷的檢查 TiKV 上面存儲的 Schema 版本是否發生變化,並且保證在一定時間內一定能夠獲取版本的變化(如果確實發生了變化)。這部分的具體實現參見文章:TiDB 的非同步 schema 變更實現。SQL on KV 架構
TiDB 的整體架構如下圖所示:
TiKV Cluster 主要作用是作為 KV 引擎存儲數據,上篇文章已經介紹過了細節,這裡不再敷述。本篇文章主要介紹 SQL 層,也就是 TiDB Servers 這一層,這一層的節點都是無狀態的節點,本身並不存儲數據,節點之間完全對等。TiDB Server 這一層最重要的工作是處理用戶請求,執行 SQL 運算邏輯,接下來我們做一些簡單的介紹。
SQL 運算
理解了 SQL 到 KV 的映射方案之後,我們可以理解關係數據是如何保存的,接下來我們要理解如何使用這些數據來滿足用戶的查詢需求,也就是一個查詢語句是如何操作底層存儲的數據。能想到的最簡單的方案就是通過上一節所述的映射方案,將 SQL 查詢映射為對 KV 的查詢,再通過 KV 介面獲取對應的數據,最後執行各種計算。這個方案肯定是可以 Work 的,但是並不能 Work 的很好,原因是顯而易見的:
1. 在掃描數據的時候,每一行都要通過 KV 操作同 TiKV 中讀取出來,至少有一次 RPC 開銷,如果需要掃描的數據很多,那麼這個開銷會非常大;2. 並不是所有的行都有用,如果不滿足條件,其實可以不讀取出來;3. 符合要求的行的值並沒有什麼意義,實際上這裡只需要有幾行數據這個信息就行。分散式 SQL 運算
如何避免上述缺陷也是顯而易見的,首先我們需要將計算盡量靠近存儲節點,以避免大量的 RPC 調用。其次,我們需要將 Filter 也下推到存儲節點進行計算,這樣只需要返回有效的行,避免無意義的網路傳輸。最後,我們可以將聚合函數、GroupBy 也下推到存儲節點,進行預聚合,每個節點只需要返回一個 Count 值即可,再由 tidb-server 將 Count 值 Sum 起來。這裡有一個數據逐層返回的示意圖:
SQL 層架構
上面幾節簡要介紹了 SQL 層的一些功能,希望大家對 SQL 語句的處理有一個基本的了解。實際上 TiDB 的 SQL 層要複雜的多,模塊以及層次非常多,下面這個圖列出了重要的模塊以及調用關係:
小結
到這裡,我們已經從 SQL 的角度了解了數據是如何存儲,如何用於計算。SQL 層更詳細的介紹會在今後的文章中給出,比如優化器的工作原理,分散式執行框架的細節。下一篇文章我們將會介紹一些關於 PD 的信息,這部分會比較有意思,裡面的很多東西是在使用 TiDB 過程中看不到,但是對整體集群又非常重要。主要會涉及到集群的管理和調度。(文/申礫)推薦閱讀:
※How we Hunted a Data Corruption bug in RocksDB
※黃東旭DTCC2017演講實錄:When TiDB Meets Kubernetes
※TiDB 源碼初探
※【開源訪談】黃東旭:「無人區」的探索者,TiDB 的前行之路
※TiDB 1.1 Alpha Release
TAG:TiDB |



