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

作者:周昱行

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

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

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

  2. 如何建立和維護和 tikv-server 之間的連接?

  3. 如何發送 RPC 請求?

  4. 如何處理各種錯誤?

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

  6. 如何實現 2PC 事務?

我們接下來就對以上幾個問題逐一解答,其中 5、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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章