TiFlash 函式下推必知必會丨十分鐘成為 TiFlash Contributor

語言: CN / TW / HK

作者: 黃海升,TiFlash 研發工程師

TiFlash 自開源以來得到了社群的廣泛關注,很多小夥伴通過原始碼閱讀的活動學習 TiFlash 背後的設計原理,也有許多小夥伴躍躍欲試,希望能參與到 TiFlash 的貢獻中來。這次,我們特別篩選了 TiFlash 中一些入門級別的 issue,幫助大家無門檻地參與到大型開源專案中來。 Issue 列表:http://github.com/pingcap/tiflash/issues/5092 按照慣例,我們也給 TiFlash New Contributor 準備了限量馬克杯,獲取流程見文末。

背景知識

TiFlash 作為 TiDB HTAP 體系的重要一環,會接收並執行 TiDB 下推下來的運算元。而有時 Projection, Selection 等等運算元裡會帶有函式,這就意味要下推這些運算元就必須支援在 TiFlash 裡執行運算元包含的函式。

1.PNG

如上圖所示,如果某個運算元帶有 TiFlash 不支援的函式,就會導致一連串的運算元都無法下推到 TiFlash 裡執行。為了最大化地發揮 TiFlash MPP 平行計算的能力,我們需要讓 TiFlash 支援 TiDB 的所有函式。看似無關緊要的函式支援,卻是 TiDB HTAP 的重要一環!

手把手教你下推函式

1. 確認要下推的函式的行為

函式是由 TiDB 下推給 TiFlash 執行的,所以必須保證函式在 TiFlash 執行的邏輯和 TiDB 保持一致,包括: - 主要邏輯 - 返回值型別 - 異常處理 - etc

以返回值型別為例,sqrt在 TiDB 一定會返回 float64,即便引數是Decimal型別的,也會在函式內部對引數先evalReal;而 floorceil 則會根據引數的型別和大小決定返回值是普通的整型,還是Decimal 型別。

一般情況下,TiFlash 要與 TiDB 保持一致是比較簡單的。但是對於一些特別的輸入,在實現的時候需要特別關注,如 sqrt 一個負數,是返回 NaN,還是返回 Null,還是丟擲異常呢?

所以在實際開發之前,要去好好地看一下 TiDB 是如何實現這個函式的。

2. 將 TiDB function 對映到 TiFlash function

TiDB 對函式的標識是 tipb::ScalarFuncSig,而 TiFlash 使用 func_name 作為函式的標識。

在 TiFlash 的程式碼裡,我們會用對映表的形式將 tipb::ScalarFuncSig 對映成 func_name

所以下推新函式的第二步,是給你要下推的函式,在 TiFlash 起個 func_name,然後在對應的對映表裡加一個 tipb::ScalarFuncSigfunc_name 的對映。

2.PNG

通常 SQL 函式會分為 window functionaggregate functiondistinct aggregation functionscalar function在 TiFlash 側會為每一類函式維護一個對映表,對映表和函式的對應如下: - window_func_map - 用於 window function - agg_func_map - 用於普通的聚合函式 - distinct_agg_func_map - 用於 distinct 的聚合函式 - scalar_func_map - 用於一般的標量函式

3. 註冊 TiFlash 函式

在映射了 tipb::ScalarFuncSigfunc_name 後,TiDB 下推的函式會根據 func_name 找到 TiFlash 函式對應的 builder,build 出 TiFlash Function 後,由 TiFlash Function 在實際執行流中執行函式邏輯。

目前在 TiFlash 有兩種 Function Builder 的實現方法,一種是 reuse function,一種是 create function directly。

3.PNG

reuse function

reuse function 用於可以複用其他函式的情況。比如 ifNull(arg1, arg2) -> if(isNull(arg1), arg2, arg1),如果自己直接寫一個 ifNull 的實現就會相當耗費時間,通過這種方式就可以直接複用其他函式的邏輯。

在 TiFlash 中是用 DAGExpressionAnalyzerHelper::function_builder_map 來記錄哪些是複用函式以及如何複用的邏輯。 新增一個對應的 DAGExpressionAnalyzerHelper::FunctionBuilder,在 DAGExpressionAnalyzerHelper::function_builder_map 新增對應的對映 <func_name, FunctionBuilder>。 具體的實現可以參考 DAGExpressionAnalyzerHelper 裡其他 FunctionBuilder 的實現。

create function directly

create function directly 用於不能複用其他函式的情況。需要在 dbms/src/Functions 下面寫對應的函式實現程式碼。通常會有一定的分類,比如 String 相關的會在 FunctionString 裡面。

然後呼叫 factory.registerFunction 將函式實現類註冊在到 FunctionFactory 即可。factory.registerFunction 通常都會放在一起,簡單找找即可。

4. TiFlash 側開發函式

接下來要進行 TiFlash 側函式主體的開發。如果不能複用 TiFlash 已經開發好的函式,那我們就得繼承 IFunction 介面開發一個函式。不過好在 clickhouse 本身已經有很多現成的函式,不過因為不一定與 TiDB/MySQL 相容,我們不能直接使用,所以留在了 Functions 下面,以待後來者利用。

所以當真的需要繼承 IFunction 實現一個函式時,可以先檢索 Functions 下面有沒有現成的語意相同的 clickhouse 函式,在那個函式上修修改改,滿足與 TiDB/Mysql 的相容性後,納入 TiFlash Function 體系裡。

如果不巧,沒有現成的 clickhouse 函式利用,那就得從 0 開始開發一個向量化函式,不過也不必擔憂,雖然向量化函式開發相對困難一點,但是還是可以從別的函式上找到一些脈絡,模仿一些開發正規化。

TiFlash vs. TiDB

TiFlash 和 TiDB 的向量化函式實現上存在不同點,參與過 TiDB 貢獻的 Contributor 需要關注下: - C++ 與 Golang 的區別 - TiFlash 裡重度使用 C++ 模板去寫函式,尤其是涉及資料型別的程式碼; - TiFlash 的向量化函式體系和 TiDB 的函式體系(行式/向量化)的不同 - 表示式相關類的設計、使用與TiDB 差別很大 - IDataType - IColumn - 引數的 Column 型別(vector 和 const)組合會爆炸式增長。比如兩個引數的 function 會有四種組合 - vector, const - vector, vector - const, vector - const, const

以上兩點讓 TiFlash 的函式開發有一定的難度,和 TiDB 的函式開發差別會相當大。可以參考下 Function 目錄下其他函式的實現,比如 FunctionSubStringIndex在開發函式的時候大家應該會有很多體會 :)

可以參考的函式實現

  • TiDBConcat
  • FunctionSubStringIndex
  • Format

5. TiDB 側下推函式

下推函式是從 TiDB 側發起的,所以 TiDB 也要做一些修改,讓函式下推。在 expression/expression.go 裡的 scalarExprSupportedByFlash 會判斷哪些函式可以被下推到 TiFlash 裡執行,TiDB planner 會根據 scalarExprSupportedByFlash 來決定運算元是否可以下推到 TiFlash。

比如要下推 sqrt 函式到 tiflash,在 tidb 的 expression/expression.go 中找到函式 scalarExprSupportedByFlash,會發現所有可以下推的函式的名字都被 hard-code 進了各種 switch case,將需要下推的函式 aqrt 加進 switch case 中即可。

6. 驗證函式真的下推了

在 TiDB 和 TiFlash 側的開發都完成後,我們需要先在本地驗證一下整個下推流程是不是真的 work 了。 1. 首先我們需要現在本地啟動一個 TiDB,TiKV,TiFlash 和 PD 的叢集。這裡推薦用 TiUP,按照 官方文件 安裝 TiUP,用 playground 啟動即可。

比如: tiup playground nightly --tiflash 1 --db 1 --pd 1 --kv 1

  1. 然後用自己 build 好的 TiDB 和 TiFlash 替換掉原有叢集的 TiDB 和 TiFlash
  2. TiFlash

執行 ps -ef | grep tiflash,找到 tiflash 程序,形式應該像這樣:

xzx 11238 11028 52 20:20 pts/0 00:00:05 /home/xzx/.tiup/components/tiflash/v5.0.0-nightly-20210706/tiflash/tiflash server --config-file=/home/xzx/.tiup/data/ScRdWJM/tiflash-0/tiflash.toml

記下程序號 11238,記下 tiflash 後面跟的引數 server --config-file=/home/xzx/.tiup/data/ScRdWJM/tiflash-0/tiflash.toml

然後 kill 11238,用 server --config-file=/home/xzx/.tiup/data/ScRdWJM/tiflash-0/tiflash.toml 啟動自己 build 好的 TiFlash。

  • TiDB

與 TiFlash 類似,找到 tiup TiDB 程序,kill 掉原程序,用對應引數啟動 TiDB 替換即可。

4.png

  1. 驗證下推流程

用類似 explain select sum(sqrt(x)) from test 的查詢來看函式是否被下推到 tiflash 計算。 建立 tiflash 副本: sql create table test.t (xxx); -- 因為通常本地起一個節點, 所以 tiflash 副本數只能設 1 alter table test.t set tiflash replica 1; 測試的 SQL 可以像這樣: sql -- 儘量使用 MPP set tidb_enforce_mpp=1; -- 強制只能走 TiFlash set tidb_isolation_read_engines='tiflash'; explain select xxxfunc(a) from t; 如果函式被下推到了 TiFlash,那 explain 的結果可以看到包含該函式的 Projection 運算元在 TiFlash 側。explain sql 可以反覆執行多幾次,因為 TiFlash 副本建立需要一些時間,但是不會太長。如果很長一段時間都看不到函式下推了,那麼應該就是真的有問題。:)

explain sql 執行成功之後,可以把 explain 去掉,實際執行下 sql 看效果。

7. 測試

提交 pr 後,在 TiFlash 的 GitHub CI 裡,會啟動實際的 TiDB, TiFlash, PD, TiKV 叢集,自動執行單元測試和整合測試。需要貢獻者提前準備測試的程式碼。

整合測試

對於函式下推,通常會在 integration-test 增加一組測試。在 tests/fullstack-test/expr 下面,為新的下推函式建一個 func.test,測試內容參照同目錄下其他函式的測試即可,如 substring_index.test

單元測試

形式

TiFlash 的函式單測放在 dbms/src/Functions/test 下面。通常命名格式為 gtest_${func_name}.cpp

單測模板如下: ```C++

include

include

namespace DB::tests { class {gtest_name} : public DB::tests::FunctionTest { };

TEST_F({gtest_name}, {gtest_unit_name}) try { const String & func_name = {function_name};

// case1
ASSERT_COLUMN_EQ(
    {ouput_result},
    executeFunction(
        func_name,
        {input_1},
        {input_2},
        ...,
        {input_n},);
// case2
...
// case3
...

} CATCH

TEST_F({gtest_name}, {gtest_unit_name2})... TEST_F({gtest_name}, {gtest_unit_name3})... ...

} // namespace DB::tests ``` 可以參考該目錄下其他函式單測的寫法, 做適當調整。

FunctionTestUtils 是用於函式測試的公共類,裡面提供了各類常用的方法,如 createColumn 等等。如果在寫 gtest 時發現有其他可以共用方法,也可以補充在這裡。

內容

以 function(arg_1, arg_2, arg_3, … arg_n) 為例,一個 TiFlash 函式單元測試的內容應該至少包含以下幾個部分:

資料型別

對於每個 arg_i 的所有支援型別 Type,需要測試 Type 與 Nullable(Type)。此外理論上所有 arg_i 都應該支援 DataTypeNullable(DataTypeNothing),但是 TiDB 很少會用到 DataTypeNullable(DataTypeNothing),所以碰到相關的 bug 可以先記下來。

列型別

對於 arg_i 的每種 Type: 1. 如果該 Type 不為 nullable,需要測試兩種形式的列: - ColumnVector - ColumnConst 2. 如果該 Type 為 nullable,需要測試三種形式的列: - ColumnVector - ColumnConst(ColumnNullable(non-null value)) - ColumnConst(ColumnNullable(null value)) 3. 如果該 Type 為 DataTypeNullable(DataTypeNothing), 需要測試兩種形式的列: - ColumnVector - ColumnConst(ColumnNullable(null value))

邊界值

一些通用的邊界值例子如下: 1. 數值型別(int,double,decimal 等):最大/最小值,0 值,null 值 2. 字串型別:空字串,中文等非 ascii 字元,null 值,有 collation/無 collation 3. 日期型別:zero date,早於 1970-01-01 的某個時間,夏令時時間,null 值

此外,對於具體的函式,可以根據其具體實現,有針對性地構造邊界值。

返回值型別

根據 MySQL 相關文件,確保 TiFlash 函式返回值型別與 MySQL/TiDB 一致

注意: 1. Decimal 型別在 TiFlash 的內部表示有四種:Decimal32,Decimal64,Decimal128 和 Decimal256,對於所有 Decimal 型別,這四種內部表示都需要測試到。 2. 函式的每個 arg_i 可能的型別實際上應該以 TiDB 可能下推的型別為準,考慮到獲取 TiDB 可能下推的型別比較麻煩,當前測試可以根據 TiFlash 目前支援的型別來寫 3. 有一部分 TiDB 下推的函式中,其下推的函式簽名中包含了型別資訊,例如對於 a = b ,TiDB 下推的函式簽名包括:EQInt,EQReal,EQString,EQDecimal,EQTime,EQDuration,EQJson,雖然 a 和 b 各自都可以是 int/real/string/decimal/time/duration/json 類別,但是 TiDB 下推的時候保證了 a 和 b 的類別是一致的,從工作量角度考慮,當前測試只需要保證相同類別之間的 equal 函式被測試到即可,int = decimal 這種的可以先不測。 4. 對於輸入引數可以無窮多的函式(例如 case when),需要確保其最小迴圈單元被測試到。 5. 預期測試過程中會發現很多 bug,對於一些比較容易 fix 的 bug,可以在測試的同時順便 fix,對於一些比較難或者不確定需不需要 fix 的 bug,可以先開 issue,再將相應的測試註釋掉。

常見的問題

  1. 函式即使返回 null,也需要給其對應的 nestedColumn 賦一個有意義的值

TiFlash 中的函式實現中,有一個可以過載的函式:useDefaultImplementationForNulls,對於大多數函式來說,如果不需要對 null 做特殊處理的話,可以返回 true,這樣的話,在實現這個函式的時候就不需要有任何 null 值相關的考慮,其原理是在 IExecutableFunction::defaultImplementationForNulls 中會將 nullable column 的 nestedColumn 取出來傳給該函式,而 nestedColumn 始終都是 not null 的型別。

當然對於一些需要對 null 值特殊處理的函式,比如 concat_ws,因為要達到 “輸出引數如果是 null 則忽略該引數” 的目的,concat_ws 需要自己處理 null 值邏輯,這樣的話就必須過載 useDefaultImplementationForNulls 讓其返回 false。對於需要自己實現 null 值處理邏輯的函式,如果結果為 null,必須給這個 nullable column 的 nestedColumn 設上一個有意義的值,所有 Function 都假設 nullable column 對應的 nestedColumn 中每一行都是一個有意義的值,即使是 null。之前出現過因為 nestedColumn 裡面值不合法導致的bug,具體可以參照:#3875, #2268 推薦預設值如下: - 數值型別:零值 - Date相關型別:zerodate - 字串型別:空字串

  1. 使用 useDefaultImplementationForConstants() 簡化函式開發

TiFlash 中的函式實現中,有一個可以過載的函式:useDefaultImplementationForConstants,如果過載這個方法返回 true,那麼在函式開發的時候,可以不考慮 const, const, ..., const 的列組合。

IExecutableFunction::defaultImplementationForConstantArguments 中會將 const, const, ..., const 轉為 vector, vector, .., vector 來處理。

  1. 使用 getArgumentsThatAreAlwaysConstant 簡化函式開發 (不推薦)

在函式開發中,可能發現某個引數通常為常量,並且如果假設該引數一直為常量的話,開發函式會簡單很多,這時候可以考慮強制該引數為常量,不為常量就報錯。這時過載 getArgumentsThatAreAlwaysConstant,返回指定的常量引數的下標(從 0 開始)即可。

但是通常情況下不要這麼做,除非是開發週期要求很緊的時候,在後面也最好找時間補回去。

如果你在貢獻的過程中遇到其他問題,請來這裡提問:http://internals.tidb.io/c/sqlengine

限量馬克杯獲取流程

5.jpg 任何一個新加入集體的小夥伴都將收到我們充滿誠意的禮物,成為 New TiFlash Contributor 即可獲贈限量版馬克杯,很榮幸能夠認識你,也很高興能和你一起堅定地走得更遠。獲取流程如下: 1. 認領 issue,issue 列表:Issue 列表:http://github.com/pingcap/tiflash/issues/5092; 2. 提交 PR; 3. PR 提交之後,請耐心等待維護者進行 Review; - 程式碼提交後 CI 會執行測試,需要保證所有的單元測試是可以通過的。期間可能有其它的提交會與當前 PR 衝突,這時需要修復衝突; - 維護者在 Review 過程中可能會提出一些修改意見。修改完成之後如果 reviewer 認為沒問題了,你會收到 LGTM(looks good to me) 的回覆。當收到兩個及以上的 LGTM 後,該 PR 將會被合併; 4. 合併 PR 後自動成為 Contributor,就可以填表單領取你的專屬馬克杯啦,表單地址:http://forms.pingcap.com/f/tidb-contribution-swag 5. 後臺 AI 核查 GitHub ID 及資料資訊,確認無誤後會快遞寄出屬於你的限量版馬克杯。