TiDB 源碼閱讀系列文章(十八)tikv-client(上)

做者:周昱行git

在整個 SQL 執行過程當中,須要通過 Parser,Optimizer,Executor,DistSQL 這幾個主要的步驟,最終數據的讀寫是經過 tikv-client 與 TiKV 集羣通信來完成的。github

爲了完成數據讀寫的任務,tikv-client 須要解決如下幾個具體問題:算法

  1. 如何定位到某一個 key 或 key range 所在的 TiKV 地址?緩存

  2. 如何創建和維護和 tikv-server 之間的鏈接?網絡

  3. 如何發送 RPC 請求?數據結構

  4. 如何處理各類錯誤?併發

  5. 如何實現分佈式讀取多個 TiKV 節點的數據?負載均衡

  6. 如何實現 2PC 事務?框架

咱們接下來就對以上幾個問題逐一解答,其中 五、6 會在下篇中介紹。分佈式

如何定位 key 所在的 tikv-server

咱們須要回顧一下以前 《三篇文章瞭解 TiDB 技術內幕——說存儲》 這篇文章中介紹過的一個重要的概念:Region。

TiDB 的數據分佈是以 Region 爲單位的,一個 Region 包含了一個範圍內的數據,一般是 96MB 的大小,Region 的 meta 信息包含了 StartKey 和 EndKey 這兩個屬性。當某個 key >= StartKey && key < EndKey 的時候,咱們就知道了這個 key 所在的 Region,而後咱們就能夠經過查找該 Region 所在的 TiKV 地址,去這個地址讀取這個 key 的數據。

獲取 key 所在的 Region, 是經過向 PD 發送請求完成的。PD client 實現了這樣一個接口:

GetRegion(ctx context.Context, key []byte) (*metapb.Region, *metapb.Peer, error)

經過調用這個接口,咱們就能夠定位這個 key 所在的 Region 了。

若是須要獲取一個範圍內的多個 Region,咱們會從這個範圍的 StartKey 開始,屢次調用 GetRegion 這個接口,每次返回的 Region 的 EndKey 作爲下次請求的 StartKey,直到返回的 Region 的 EndKey 大於請求範圍的 EndKey。

以上執行過程有一個很明顯的問題,就是咱們每次讀取數據的時候,都須要先去訪問 PD,這樣會給 PD 帶來巨大壓力,同時影響請求的性能。

爲了解決這個問題,tikv-client 實現了一個 RegionCache 的組件,緩存 Region 信息, 當須要定位 key 所在的 Region 的時候,若是 RegionCache 命中,就不須要訪問 PD 了。RegionCache 的內部,有兩種數據結構保存 Region 信息,一個是 map,另外一個是 b-tree,用 map 能夠快速根據 region ID 查找到 Region,用 b-tree 能夠根據一個 key 找到包含該 key 的 Region。

嚴格來講,PD 上保存的 Region 信息,也是一層 cache,真正最新的 Region 信息是存儲在 tikv-server 上的,每一個 tikv-server 會本身決定何時進行 Region 分裂,在 Region 變化的時候,把信息上報給 PD,PD 用上報上來的 Region 信息,知足 tidb-server 的查詢需求。

當咱們從 cache 獲取了 Region 信息,併發送請求之後, tikv-server 會對 Region 信息進行校驗,確保請求的 Region 信息是正確的。

若是由於 Region 分裂,Region 遷移致使了 Region 信息變化,請求的 Region 信息就會過時,這時 tikv-server 就會返回 Region 錯誤。遇到了 Region 錯誤,咱們就須要清理 RegionCache,從新獲取最新的 Region 信息,並從新發送請求。

如何創建和維護和 tikv-server 之間的鏈接

當 TiDB 定位到 key 所在的 tikv-server 之後,就須要創建和 TiKV 之間的鏈接,咱們都知道, TCP 鏈接的創建和關閉有不小的開銷,同時會增大延遲,使用鏈接池能夠節省這部分開銷,TiDB 和 tikv-server 之間也維護了一個鏈接池 connArray

TiDB 和 TiKV 之間經過 gRPC 通訊,而 gPRC 支持在單 TCP 鏈接上多路複用,因此多個併發的請求能夠在單個鏈接上執行而不會相互阻塞。

理論上一個 tidb-server 和一個 tikv-server 之間只須要維護一個鏈接,可是在性能測試的時候發現,單個鏈接在併發-高的時候,會成爲性能瓶頸,因此實際實現的時候,tidb-server 對每個 tikv-server 地址維護了多個鏈接,並以 round-robin 算法選擇鏈接發送請求。鏈接的個數能夠在 config 文件裏配置,默認是 16。

如何發送 RPC 請求

tikv-client 經過 tikvStore 這個類型,實現 kv.Storage 這個接口,咱們能夠把 tikvStore 理解成 tikv-client 的一個包裝。外部調用 kv.Storage 的接口,並不須要關心 RPC 的細節,RPC 請求都是 tikvStore 爲了實現 kv.Storage 接口而發起的。

實現不一樣的 kv.Storage 接口須要發送不一樣的 RPC 請求。好比實現 Snapshot.BatchGet 須要tikvpb.TikvClient.KvBatchGet 方法;實現 Transaction.Commit,須要 tikvpb.TikvClient.KvPrewrite, tikvpb.TikvClient.KvCommit 等多個方法。

在 tikvStore 的實現裏,並無直接調用 RPC 方法,而是經過一個 Client 接口調用,作這一層的抽象的主要目的是爲了讓下層能夠有不一樣的實現。好比用來測試的 mocktikv 就本身實現了 Client 接口,經過本地調用實現,並不須要調用真正的 RPC。

rpcClient 是真正實現 RPC 請求的 Client 實現,經過調用 tikvrpc.CallRPC,發送 RPC 請求。tikvrpc.CallRPC 再往下層走,就是調用具體每一個 RPC  生成的代碼了,到了生成的代碼這一層,就已是 gRPC 框架這一層的內容了,咱們就不繼續深刻解析了,感興趣的同窗能夠研究一下 gRPC 的實現。

如何處理各類錯誤

咱們前面提到 RPC 請求都是經過 Client 接口發送的,但實際上這個接口並無直接被各個 tikvStore 的各個方法調用,而是經過一個 RegionRequestSender 的對象調用的。

RegionRequestSender 主要的工做除了發送 RPC 請求,還要負責處理各類能夠重試的錯誤,好比網絡錯誤和部分 Region 錯誤。

RPC 請求遇到的錯誤主要分爲兩大類:Region 錯誤和網絡錯誤。

Region  錯誤 是由 tikv-server 收到請求後,在 response 裏返回的,常見的有如下幾種:

  1. NotLeader

    這種錯誤的緣由一般是 Region 的調度,PD 爲了負載均衡,可能會把一個熱點 Region 的 leader 調度到空閒的 tikv-server 上,而請求只能由 leader 來處理。遇到這種錯誤就須要 tikv-client 重試,把請求發給新的 leader。

  2. StaleEpoch

    這種錯誤主要是由於 Region 的分裂,當 Region 內的數據量增多之後,會分裂成多個新的 Region。新的 Region 包含的 range 是不一樣的,若是直接執行,返回的結果有多是錯誤的,因此 TiKV 就會拒絕這個請求。tikv-client 須要從 PD 獲取最新的 Region 信息並重試。

  3. ServerIsBusy

    這個錯誤一般是由於 tikv-server 積壓了過多的請求處理不完,tikv-server 若是不拒絕這個請求,隊列會愈來愈長,可能等到客戶端超時了,請求尚未來的及處理。因此作爲一種保護機制,tikv-server 提早返回錯誤,讓客戶端等待一段時間後再重試。

另外一類錯誤是網絡錯誤,錯誤是由 SendRequest 的返回值 返回的 error 的,遇到這種錯誤一般意味着這個 tikv-server 沒有正常返回請求,多是網絡隔離或 tikv-server down 了。tikv-client 遇到這種錯誤,會調用 OnSendFail 方法,處理這個錯誤,會在 RegionCache 裏把這個請求失敗的 tikv-server 上的全部 region 都 drop 掉,避免其餘請求遇到一樣的錯誤。

當遇到能夠重試的錯誤的時候,咱們須要等待一段時間後重試,咱們須要保證每次重試等待時間不能過短也不能太長,過短會形成屢次無謂的請求,增長系統壓力和開銷,太長會增長請求的延遲。咱們用指數退避的算法來計算每一次重試前的等待時間,這部分的邏輯是在 Backoffer 裏實現的。

在上層執行一個 SQL 語句的時候,在 tikv-client 這一層會觸發多個順序的或併發的請求,發向多個 tikv-server,爲了保證上層 SQL 語句的超時時間,咱們須要考慮的不只僅是單個 RPC 請求,還須要考慮一個 query 總體的超時時間。

爲了解決這個問題,Backoffer 實現了 fork 功能, 在發送每個子請求的時候,須要 fork 出一個 child Backofferchild Backoffer 負責單個 RPC 請求的重試,它記錄了 parent Backoffer 已經等待的時間,保證總的等待時間,不會超過 query 超時時間。

對於不一樣錯誤,須要等待的時間是不同的,每一個 Backoffer 在建立時,會根據不一樣類型,建立不一樣的 backoff 函數

以上就是 tikv-client 上篇的內容,咱們在下篇會詳細介紹實現分佈式計算相關的 copIterator 和實現分佈式事務的 twoPCCommiter

相關文章
相關標籤/搜索