本文作者是來自 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 誕生的優秀專案能夠在社群中延續下去,感興趣的小夥伴們可以加入進來哦。