作者:黄海升,TiFlash 研发工程师
TiFlash 自开源以来得到了社区的广泛关注,很多小伙伴通过源码阅读的活动学习 TiFlash 背后的设计原理,也有许多小伙伴跃跃欲试,希望能参与到 TiFlash 的贡献中来,十分钟成为 TiFlash Contributor 系列应运而生,我们将从原理到实践,与大家分享关于 TiFlash 的一切!
前言
在前篇TiFlash 函数下推必知必会里我们简述了 TiDB 下推函数到 TiFlash 的开发过程,讲述了在开发过程中必知必会的一些知识。
在本篇,我们会沿着用户旅程,手把手教你具体怎么在 TiFlash 里实现一个向量化函数的~
TiDB 侧修改
步骤1:打开下推
在 TiDB repo 中把要下推到 TiFlash 的函数补充到expression/expression.go中的scalarExprSupportedByFlash里。
TiDB planner 在执行算子下推到 TiFlash 的逻辑时,会依赖这个方法来判断当前函数是否能下推到 TiFlash。
step2: UT 验证下推
- expression/expr_to_pb_test.go 中的 TestExprPushDownToFlash
在 TiDB repo,expression/expr_to_pb_test.go中的TestExprPushDownToFlash补充新函数的 UT。
go test $BUILD/expression/expr_to_pb_test.go
即可在本地把单测跑起来。
- planner/core/integration_test.go
在 TiDB repo 中的/planner/core/integration_test.go中补充对应的 UT。
可以参考planner/core/integration_test.go中的TestRightShiftPushDownToTiFlash。
test case 的名字可以形如Test${func_name}PushDownToTiFlash
,形式大致如下
func Test${func_name}PushDownToTiFlash(t *testing.T) { store, clean := testkit.CreateMockStore(t) defer clean() tk := testkit.NewTestKit(t, store) tk.MustExec("use test") tk.MustExec("drop table if exists t") tk.MustExec("create table t (id int, value decimal(6,3), name char(128))") tk.MustExec("set @@tidb_allow_mpp=1; set @@tidb_enforce_mpp=1;") tk.MustExec("set @@tidb_isolation_read_engines = 'tiflash'") //Createvirtual tiflashreplicainfo. dom :=domain.GetDomain(tk.Session())is:= dom.InfoSchema() db,exists:=is.SchemaByName(model.NewCIStr("test")) require.True(t,exists)for_, tblInfo := range db.Tables{iftblInfo.Name.L == "t" { tblInfo.TiFlashReplica = &model.TiFlashReplicaInfo{ Count:1, Available:true, } } } tk.MustQuery("explain select ${func}(a) from t;").Check(testkit.Rows(${plan})) }
验证${plan}
中${func}
是否在下推到 TiFlash 的算子中。
go test $BUILD/planner/core/integration_test.go
即可在本地把单测跑起来。
TiFlash 侧修改
step1: 了解前置知识
了解 TiFlash 向量化计算
TiFlash 作为一个向量化分析计算引擎,不仅仅在存储层按列存储压缩,在计算层也会按列将数据保存在内存中,并且按列对数据做计算。
如上图所示
- TiFlash 在内存中以 Block 的形式来保存一批数据。Block 中以 Column 来保存每一列数据。
- TiFlash 计算过程中,以 Block 中的 Column 为计算单位,每次获取一个 Column 完成计算后,再获取下一个 Column。
了解IFunction接口
目前 TiFlash 所有的函数实现代码都放在dbms/src/Functions下面。我们以dbms/src/Functions/FunctionsString.cpp中的来电显示nLength为例,来简单介绍一个向量化函数的工作过程。
向量化函数通常继承dbms/src/Functions/IFunction.h中的IFunction接口,接口定义如下(省去注释和部分成员函数)
classIFunction{public:virtual字符串getName()const=0;virtualsize_tgetNumberOfArguments()const=0;virtualDataTypePtrgetReturnTypeImpl(constDataTypes &/*arguments*/)const;virtualvoidexecuteImpl(Block & block,constColumnNumbers & arguments,size_tresult)const; };
getName
返回 Function 的 name,name 是作为 TiFlash 向量化函数的唯一标识来使用。getNumberOfArguments
记录向量化函数的参数有多少个。getReturnTypeImpl
负责做向量化函数的类型推导,因为输入参数数据类型的变化可能会导致输出数据类型变化。- 来电显示nLength::getReturnTypeImpl会固定返回
Int64
,属于比较简单的情况。
- 来电显示nLength::getReturnTypeImpl会固定返回
executeImpl
负责向量化函数的执行逻辑,这也是一个向量化函数的主体部分。一个 TiFlash 向量化函数够不够"向量化",够不够快也就看这里了。- 来电显示nLength::executeImpl的行为如下图所示,简单来说:
- 从 Block 中获取 str_column
- 创建同等大小的 len_column
- foreach str_column,获取每一个行的 str,调用 str.length(),将结果插入 len_column 中的对应行。
- 将 len_column 插入到 Block 中,完成单次计算。
- 来电显示nLength::executeImpl的行为如下图所示,简单来说:
voidexecuteImpl(Block & block,constColumnNumbers & arguments,size_tresult)constoverride{// 1.read str_column from blockconstIColumn * str_column = block.getByPosition(arguments[0]).column.get();// 2.create len_columnintval_num = str_column->size();autolen_column = ColumnInt64::create(); len_column->reserve(val_num);// 3.foreach str_column and computeField str_field;for(inti =0; i < val_num; ++i) { str_column->get(i, str_field); len_column->insert(static_cast(str_field.get().size())); }// 4.insert len_column to Blockblock.getByPosition(result).column = std::move(col_res); }
向量化计算本身并不神秘,精髓就是 foreach column。:)
了解 DataType 体系
TiFlash 数据类型的代码放在dbms/src/DataTypes下面。
classIDataType:privateboost::noncopyable {public:virtual字符串getName()const;virtualTypeIndexgetTypeId();virtualMutableColumnPtrcreateColumn()const;ColumnPtrcreateColumnConst(size_tsize,constField & field)const; }
DataType 用于处理数据类型相关的逻辑,例如类型推导,Column 创建等等。
每一种数据类型都会有一个对应的实现class DataType${Type} final : public IDataType
。
值得注意的是,Nullable 本身并不是作为 DataType 的一个属性,而是独立一个 DataType 实现:dbms/src/DataTypes/DataTypeNullable.h中的DataTypeNullable。
所以你会发现DataTypeNullable(DataTypeString).isString() == false
。
对于DataTypeNullable
,我们通常用DataTypePtr data_type = removeNullable(nullable_data_type);
来获取实际的数据类型。
了解 Column 体系
TiFlash 关于 Column 的主要代码放在dbms/src/Columns下面。
classIColumn:publicCOWPtr {public:virtualsize_tsize()const=0;boolempty()const{returnsize() ==0; }virtualFieldoperator[](size_tn)const=0;virtualvoidget(size_tn, Field & res)const=0; }
Column 是计算过程中列数据存放的容器。
获取 Column 中数据的一种常用手法是
for(size_ti =0; i < column.size(); ++i) T data = column[i].get();
Column 有两种类型
- 常量 column:dbms/src/Columns/ColumnConst.h中的ColumnConst
- 向量 column:dbms/src/Columns/ColumnVector.h中的ColumnVector
之所以要区分出这两类 Column 是为了在具体函数实现时可以做特殊优化提速。
比如dbms/src/Functions/modulo.cpp中的ModuloByConstantImpl,modulo(vector, const)
可以将a % b
转换 为a - a / b * b
,这样会提速。
详情可见faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/。
ColumnVector 和 ColumnConst 使用姿势通常为
if(constColumnVector * col = checkAndGetColumn>(column.get())) {// ...}elseif(constColumnConst * col = checkAndGetColumn>(column.get())) {// ...}
我们通常使用DataType::CreateColumn
和DataType::CreateColumnConst
来创建 ColumnVector 和 ColumnConst。
除此之外 ColumnVector 对 string 和 decimal 分别有特殊优化实现:
大家可以去看看实现代码和相关的使用代码,这里就不展开了。
用 C++ 模板做类型体操
向量化函数里输入参数的类型可能会有很多种,比如 add 函数的输入数据类型可以是UInt8, ..., UInt64, Int8, ..., Int64, Float32, Float64, Decimal32, ..., Decimal256
,多达 14 种,如果要为每一种数据类型实现一遍执行逻辑是非常繁琐的。
用 C++ 模板做类型体操,简化函数开发逻辑是一种很常见的做法。
- 首先脱离具体的数据类型,将向量化函数的执行逻辑抽象成一个模板函数
template<typenameType1,typenameType2>voidexecuteImpl(Column arg1, Column arg2, ...) ;
在
IFunction::executeImpl
将不同数据类型的参数转发给模板函数,在 TiFlash 里有几种转发做法- 用 DataType->getTypeId(),获取每一个 type 的标识,做 switch case 调用模板函数,例如dbms/src/Functions/FunctionsString.cpp中的PadImpl::executePad。
TypeIndex type_index = block.getByPosition(arguments[0]).type->getTypeId();switch(type_index) {caseTypeIndex:: UInt8:executeImpl
(block, arguments);break;caseTypeIndex::UInt16:executeImpl (block, arguments);break;caseTypeIndex::UInt32:executeImpl (block, arguments);break;caseTypeIndex::UInt64:executeImpl (block, arguments);break;caseTypeIndex::Int8:executeImpl (block, arguments);break;caseTypeIndex::Int16:executeImpl (block, arguments);break;caseTypeIndex::Int32:executeImpl (block, arguments);break;caseTypeIndex::Int64:executeImpl (block, arguments);break;default:throwException(fmt::format("the argument type of {} is invalid, expect integer, got {}",getName(), type_index), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT); }; - 用
castTypeToEither
获取参数数据类型,调用模板函数,例如dbms/src/Functions/FunctionsString.cpp中的FormatImpl::executeImpl。
voidexecuteImpl(Block & block,constColumnNumbers & arguments,size_tresult)constoverride{boolis_type_valid =getType(block.getByPosition(arguments[0]).type, [&](constauto& type,bool) {usingType = std::decay_t<decltype(type)>;usingFieldType =typenameType::FieldType;executeImpl
(block, arguments);returntrue; });if(!is_type_valid)throwException(fmt::format("argument of function {} is invalid.",getName())); }template<typenameF>staticboolgetType(DataTypePtr type, F && f){returncastTypeToEither< DataTypeDecimal32, DataTypeDecimal64, DataTypeDecimal128, DataTypeDecimal256, DataTypeFloat32, DataTypeFloat64, DataTypeInt8, DataTypeInt16, DataTypeInt32, DataTypeInt64, DataTypeUInt8, DataTypeUInt16, DataTypeUInt32, DataTypeUInt64>(type.get(), std::forward (f)); }
个人喜好选择哪一种都可以。当然,如果有 C++ 老司机们有自己喜欢的做法,请尽情施展,没必要局限在 TiFlash 已有的做法里。
step2: 实现下推
在这里我们对前篇TiFlash 函数下推必知必会所述开发流程做一个简单回顾。
1.首先在函数映射表里添加 TiDB Function 到 TiFlash Function 的映射。
根据函数的类型,映射表分别为
- 窗口函数dbms/src/Flash/Coprocessor/DAGUtils.cpp中的window_func_map
- 聚合函数dbms/src/Flash/Coprocessor/DAGUtils.cpp中的agg_func_map
- distinct 聚合函数dbms/src/Flash/Coprocessor/DAGUtils.cpp中的distinct_agg_func_map
- 标量函数dbms/src/Flash/Coprocessor/DAGUtils.cpp中的scalar_func_map
2.然后根据函数的实现逻辑,我们可以选择
- 复用原有 TiFlash 函数的逻辑,
- 对类似
ifNull(arg1, arg2) = if(isNull(arg1), arg2, arg1)
这种情况,我们可以考虑复用原有 TiFlash 函数的逻辑。 - 我们把 TiFlash 函数复用的代码实现放在dbms/src/Flash/Coprocessor/DAGExpressionAnalyzerHelper.cpp中的DAGExpressionAnalyzerHelper::function_builder_map里。
- 对类似
- 从头开始实现一个 TiFlash 函数
- 编写一个
来电显示nClass
,实现IFunction
这个 interface 的四个接口。 - 然后调用
factory.registerFunction
注册函数。(); factory.registerFunction
通常会和函数实现放在一起,比如 String 函数都会放在dbms/src/Functions/FunctionsString.cpp中的registerFunctionsString。();
- 编写一个
step3: UT 验证函数功能
在前篇TiFlash 函数下推必知必会里提到了关于 Unit Test 如何写。
这里补充一下大家比较关心的,怎么在本地把测试跑起来~
见 TiFlash repo 中README.md中所述。
To run unit tests, you need to build with-DCMAKE_BUILD_TYPE=DEBUG
:
cd $BUILD
cmake $WORKSPACE/tiflash -GNinja -DCMAKE_BUILD_TYPE=DEBUG
ninja gtests_dbms # Most TiFlash unit tests
ninja gtests_libdaemon # Settings related tests
ninja gtests_libcommon
And the unit-test executables are at$BUILD/dbms/gtests_dbms
,$BUILD/libs/libdaemon/src/tests/gtests_libdaemon
and$BUILD/libs/libcommon/src/tests/gtests_libcommon
.
集成测试
在前篇TiFlash 函数下推必知必会里提到了关于 Integration Test 如何写。
这里补充一下大家比较关心的,怎么在本地把测试跑起来~
测试的相关脚本在/tests目录下。
- 首先如TiFlash 函数下推必知必会中所述,起一个带有自己 build 好的 TiDB 和 TiFlash 的集群。
- 然后修改/tests/_env.sh里的 TiFlash 和 TiDB 的相关端口配置。
- 最后调用/tests/run-test.sh把测试跑起来,如
./run_test.sh $Build/tests/fullstack-test/expr/format.test
。
How To Contribute
- 首先在https://github.com/pingcap/tiflash/issues/5092中认领一个你感兴趣的函数,并告诉大家你将会完成这个函数,避免同一个函数被重复认领。
- 然后就可以按照前面所述的内容,在本地完成开发测试。
- 在本地验证函数下推到 TiFlash 且执行结果无误,并且代码本身也觉得 ok 后,就可以提 pr 到 github 上。TiDB 和 TiFlash 各自需要提一个 pr,对应 TiDB 和 TiFlash 侧的修改。
- TiDB 和 TiFlash 两边的 pr merge 顺序并没有要求,大家可以放心提 pr~
- TiDB 和 TiFlash 的 pr 描述里都贴上对应 TiFlash/TiDB 的 pr 链接
- TiDB 和 TiFlash 的 pr 都需要补充 release note,例如
Support to pushdown ${function} to TiFlash
- 待两边 pr 都被充分 review,获得 LGT2 后,就可以由 committer merge 到 master。