本文作者是来自 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 诞生的优秀项目能够在社区中延续下去,感兴趣的小伙伴们可以加入进来哦。