本文作者是來自CC 組的蘭海同學,他們的項目《讓 TiDB 訪問多種數據源》在本屆 TiDB Hackathon 2018 中獲得了二等獎。該項目可以讓 TiDB 支持多種外部數據源的訪問,針對不同數據源的特點會不同的下推工作,使 TiDB 成為一個更加通用的資料庫查詢優化和計算平台。

我們隊伍是由武漢大學在校學生組成。我們選擇的課題是讓 TiDB 接入若干外部的數據源,使得 TiDB 成為一個更加通用的查詢優化和計算平台。

為什麼選這個課題

剛開始我們選擇課題是 TiDB 執行計劃的實時動態可視化。但是填了報名單後,TiDB Robot 回復我們說做可視化的人太多了。我們擔心和別人太多衝突,所以諮詢了導師的意見,改成了 TiDB 外部數據源訪問。這期間也閱讀了 F1 Query 和 Calcite 論文,看了東旭哥(PingCAP CTO)在 PingCAP 內部的論文閱讀的分享視頻。感覺寫一個簡單 Demo,還是可行的。

系統架構和效果展示

如上圖所示,TiDB 通過 RPC 接入多個不同的數據源。TiDB 利用 RPC 發送請求給遠端數據源,遠端數據源收到請求後,進行查詢處理,返回結果。TiDB 拿到返回結果進一步的進行計算處理。

我們通過定義一張系統表 foreign_register(table_name,source_type,rpc_info) 記錄一個表上的數據具體來自哪種數據源類型,以及對應的 RPC 連接信息。對於來自 TiKV 的我們不用在這個表中寫入,默認的數據就是來自 TiKV。

我們想訪問一張 PostgreSQL(後面簡稱為 PG)上的表:首先,我們在 TiDB 上定義一個表(記為表 a),然後利用我們 register_foreign(a,postgresql,ip#port#table_name) 註冊相關信息。之後我們就可以通過 select * from a 來讀取在 PG 上名為 table_name 的表。

我們在設計各個數據源上數據訪問時,充分考慮各個數據源自身的特點。將合適的操作下推到具體的數據源來做。例如,PG 本身就是一個完整的資料庫系統,我們支持投影、條件、連接下推給 PG 來做。Redis 是一個內存鍵值資料庫,我們考慮到其 Get 以及用正則來匹配鍵值很快,我們將在 Key 值列的點查詢以及模糊匹配查詢都推給了 Redis 來做,其他條件查詢我們就沒有進行下推。

具體的運行效果如下:

如圖所示,我們在遠程開了 3 個 RPC Server,負責接收 TiDB 執行過程中的外部表請求,並在內部的系統表中註冊三張表,並在 TiDB 本地進行了模式的創建——分別是remotecsv,remoteredis,remotepg,還有一張本地 KV Store 上的 localkv 表。我們對 4 張表進行 Join 操作,效果如圖所示,說明如下。

  1. 遠程 csv 文件我們不做選擇下推,所以可以發現 csv 上的條件還是在 root(即本地)上做。
  2. 遠程的 PG 表,我們會進行選擇下推,所以可以發現 PG 表的 selection 被推到了 PG 上。
  3. 遠程的 Redis 表,我們也會進行選擇下推,同時還可以包括模型查詢條件(Like)的下推。

P.S. 此外,對於 PostgreSQL 源上兩個表的 Join 操作,我們也做了Join 的下推,Join 節點也被推送到了 PostgreSQL 來做,具體的圖示如下:

如何做的

由於項目偏硬核的,需要充分理解 TiDB 的優化器,執行器等代碼細節。所以在比賽前期,我們花了兩三天去研讀 TiDB 的優化器,執行器代碼,弄清楚一個簡單的 Select 語句扔進 TiDB 是如何進行邏輯優化,物理優化,以及生成執行器。之前我們對 TiDB 這些細節都不了解,硬著去啃。發現 TiDB 生成完執行器,會調用一個 Open 函數,這個函數還是一個遞歸調用,最終到 TableReader 才發出數據讀取請求,並且已經開始拿返回結果。這個和以前分析的資料庫系統還有些不同。前期為了檢驗我們自己對 TiDB 的執行流程理解的是否清楚,我們嘗試著去讓 TiDB 讀取本地 csv 文件。

比賽正式開始,我們一方面完善 csv,不讓其進行條件下推,因為我們遠端 RPC 沒有處理條件的能力,我們修改了邏輯計劃的條件下推規則,遇到數據源是 csv 的,我們拒絕條件下推。另一方面,首先得啃下硬骨頭 PostgreSQL。我們考慮了兩種方案,第一種是拿到 TiDB 的物理計劃後,我們將其轉換為 SQL,然後發給 PG;第二種方案我們直接將 TiDB 的物理計劃序列化為 PG 的物理計劃,發給 PG。我們考慮到第二種方案需要給 PG 本身加接受物理計劃的鉤子,就果斷放棄,不然可能兩天都費在改 PG 代碼上了。我們首先實現了 select * from pgtable。主要修改了增加 pgSelectResult 結構體實現對應的結構體。通過看該結構體以及其對應介面函數,大家就知道如何去讀取一個數據源上的數據,以及是如何做投影下推。修改 Datasource 數據結構增加對數據源類型,RPC 信息,以及條件字元串,在部分物理計劃內,我們也增加相關信息。同時根據數據源信息,在 (e*TableReaderExecutor)buildResp 增加對來源是 PG 的表處理。

接著我們開始嘗試條件下推:select * from pgtable where … 將 where 推下去。我們發現第一問題:由於我們的註冊表裡面沒有記錄外部源數據表的模式信息導致,下推去構建 SQL 的時候根本拿不到外部數據源 PG 上正確的屬性名。所以我們暫時保證 TiDB 創建的表模式與 PG 創建的表模式完全一樣來解決這個問題。條件下推,我們對條件的轉換為字元串在函數 ExpressionToString 中,看該函數調用即可明白是如何轉換的。當前我們支持等於、大於、小於三種操作符的下推。

很快就到了 1 號下午了,我們主要工作就是進行 Join下推 的工作。Join 下推主要是當我們發現兩個 Join 的表都來來自於同一個 PG 實例時,我們就將該 Join 下推給 PG。我們增加一種 Join 執行器:PushDownJoinExec 。弄完 Join 已經是晚上了。而且中間還遇到幾個 Bug,首先,PG 等數據源沒有一條結果滿足時的邊界條件沒有進行檢查,其次是,在 Join 下推時,某些情況下 Join 條件未必都是在 On 子句,這個時候需要考慮 Where 子句的信息。最後使得連接和條件同時下推沒有問題。因為不同表的相同屬性需要進行區分。主要難點就是對各個物理計劃的結構體中的解析工作。

到了晚上,我們準備開始著手接入 Redis。考慮到 Redis 本身是 KV 型,對於給定 Key 的 Get 以及給定 Key 模式的匹配是很快。我們直接想到對於 Redis,我們允許 Key 值列上的條件下推,讓 Redis 來做過濾。因為 Redis 是 API 形式,我們單獨定義一個簡單請求協議,來區別單值,模糊,以及全庫查詢三種基本情況,見 RequestRedis 定義。Redis 整體也像是 PG 一樣的處理,主要沒有 Join 下推這一個比較複雜的點。

我們之後又對 Explain 部分進行修改,使得能夠列印能夠反映我們現在加入外部數據源後運算元真實的執行情況,可以見 explainPlanInRowFormat 部分代碼。之後我們開始進行測試每個數據源上的,以及多個數據源融合起來進行測試。

不足之處

  1. 我們很多物理計劃都是復用 TiDB 本身的,給物理計劃加上很多附屬屬性。其實最好是將這些物理計劃單獨抽取出來形成一個,不去復用。
  2. Cost 沒有進行細緻考慮,例如對於 Join 下推,其實兩張 100 萬的表進行 Join 可能使得結果成為 1000 萬,那麼網路傳輸的代價反而更大了。這些具體運算元下推的代價還需要細緻的考慮。

比較痛苦的經歷

  1. TiDB 不支持 Create Funtion,我們就只好寫內置函數,就把另外一個 Parser 模塊拖下來,自己修改加上語法,然後在加上自己設計的內置函數。
  2. 最痛苦還是兩個方面,首先 Golang 語言,我們之前沒有用得很多,經常遇到些小問題,例如 interface 的靈活使用等。其次就是涉及的 TiDB 的源碼模塊很多,從優化器、執行器、內置函數以及各種各樣的結構。雖然思路很簡單,但是改動的地方都很細節。

收穫

比賽過程中,看到了非常多優秀選手,以及他們酷炫的作業,感覺還是有很長的路要走。Hackathon 的選手都好厲害,聽到大家噼里啪啦敲鍵盤的聲音,似乎自己也不覺得有多累了。人啊,逼一下自己,會感受到自己無窮的力量。通過這次活動,我們最終能夠靈活使用 Golang 語言,對 TiDB 整體也有了更深入的認識,希望自己以後能夠稱為 TiDB 的代碼貢獻者。

最後非常感謝 PingCAP 這次組織的 Hackathon 活動,感謝導師團、志願者,以及還有特別感謝導師張建的指導。

TiDB Hackathon 2018 共評選出六個優秀項目,本系列文章將由這六個項目成員主筆,分享他們的參賽經驗和成果。我們非常希望本屆 Hackathon 誕生的優秀項目能夠在社區中延續下去,感興趣的小夥伴們可以加入進來哦。

推薦閱讀:

相关文章