写点什么

揭秘 TDSQL-A:兼容 Oracle 的同时支持海量数据交互

发布于: 4 小时前

6 月 5 日在“国产数据库硬核技术沙龙-TDSQL-A 技术揭秘”系列分享中,5 位腾讯云技术大咖分别从整体技术架构、列式存储及相关执行优化、集群数据交互总线、Fragment 执行框架/查询分片策略/子查询框架以及向量化执行引擎等多个方面对 TDSQL-A 进行了深入解读。


其中腾讯云数据库高级工程师-陈再妮,对于“TDSQL-A 海量数据交互之道及企业级数据库能力”,进行议题分享。没有观看直播的小伙伴,可不要错过本次分享内容的文字实录。


以下内容为现场分享实录:


TDSQL-A 是腾讯基于开源数据库 PG 自主研发的分布式分析型数据库系统,最早可追溯到 08 年,到现在已经经历 10 余年打磨,团队拥有数十篇核心研发专利。TDSQL-A 全面兼容 PostgreSQL,高度兼容 Oracle 语法,采用无共享架构,支持行列混合存储,在具备业界领先的数据分析能力的同时还具有完整支持分布式事务 ACID 的能力。


下面是 TDSQL-A 的总体架构,中间是数据交互总线,它将整个分布式集群的各个节点有机地联合起来,负责整个集群中所有节点数据的交互。这个数据交互总线,我们称它为 FN(forward node),FN 通过业界领先的集群内通讯技术,可以支持上千台节点的超大规模集群,非常适用于 PB 级的海量 OLAP 场景。



1

FN 设计与部署方式

1.1 原分布式执行框架


对分布式系统来说,数据重分布是无法避免的,就如下图中所示的,当某个表的 hash join 的字段不是分布键时,我们就需要进行数据重分布。在数据重分布的过程中,我们需要创建相应的连接和进程。


假定我们系统现在有 N 个 DN,DN 即数据节点,在数据重分布过程中每个 DN 都要跟其他 DN 之间建立连接,那么这个系统中的连接数就是 NN。如果有 M 层 Join,那就是 MNN 个连接,再假定我们系统的并发数是 X,在这个基础上再乘以 X,那就是 XMNN 个连接。


这样的话,当整个集群规模达到千百台、并发为上百个时,整个集群的连接数将会有上亿个。这个连接数是非常庞大的,此外,如果连接太多创建回收 socket 也会成为瓶颈。为了解决这个问题,我们引入了数据交互总线节点 FN(forward node)。


1.2 优化后的分布式执行框


我们在每台服务器上都部署了一个 FN,用于跟其他节点之间的数据通信,图中用红色标记的节点就是 FN。FN 通过 FID,src_node_id, dest_node_id 来进行网络数据路由。而 FID 则标识了查询的 RemotSubplan。整个业务逻辑归结起来就是,我是谁,我从哪里来,要到哪里去。


通过这种路由方式,在系统中有 N 个 DN 数据节点的情况下,不管整个系统的查询有多复杂,并发量有多大,整个集群之间最多有服务器个数 N*(N-1)个 socket 连接,有效降低了整个集群内的连接数量。此外,本机节点通过共享内存进行数据交互可以不走网络,这样就可以支持超大规模的集群部署。


1.3 部署结构


为了将服务器资源充分利用起来,一般情况下我们会在服务器上部署多个 CN 或者 DN。比如下图中这个服务器上就部署了两个 DN,而 FN 我们只需要在每台服务上部署一个即可,也就是说,一个 FN 可以服务于这个机器上的多个 CN/DN 节点的通讯。


FN 实际上也是一个 postgres 进程,与 CN、DN 或者 GTM 属于平级的关系。FN 进程在启动时,可以采用参数-Z forwardnode 来标识该进程为 FN。把 FN 设计为 postgres 进程的优点在于可以复用 PG 的元数据管理、参数配置等。加入 FN 的主要目的,在于减少 DN 之间、CN 和 DN 之间数据交换时创建的连接,从而保证大规模集群下网络连接不成为瓶颈。


1.4 通讯消息


在分布式系统中,节点之间的消息一般分为两种类型;一种是控制消息,走的是控制流;一种是数据消息,走的是数据流。


在 TDSQL-A 中,控制消息一般用于元数据的分发管理和命令的传递。传递方式跟 PG 是一致的,采用的是 Libpq 协议。数据的传输通过 FN 来进行,采用的是 TCP/IP 协议,FN 之间建立 Socket 连接,来进行数据的传送。


控制消息的应用场景可以归为以下三类:


CN 向 DN 下发 plan 或者 sql,commit/rollback 等执行命令 CN/DN 向本机 FN 发起启动时节点的注册/退出时节点注销请求不同机器上的 FN 之间相互注册/注销请求(FN 跟 FN 之间,也是通过注册、注销进行交互的)


对于数据消息,它需要通过 FN 来进行转发。这里我们又可以把它细分为不同服务器之间、同一服务器内部这两种应用场景。


在不同服务器之间,CN、DN 只需要与本机的 FN 进行交互,不再需要与其他 CN/DN 之间进行连接。FN 根据目的节点的 ID,去和目的节点的 FN 进行连接,这是服务器之间的转发。


在同一服务器内部,CN/DN 与 FN 通过共享内存来进行通信。比如说,DN 通过共享内存将数据传递给 FN,FN 再通过共享内存将数据传输到本机器上的其他 DN,这样就不需要进行网络通信。


不管是在服务器之间,还是同一个服务器内部,都是由 FN 进行处理,这样的话,可以做到数据通信的统一管理。我们也不用担心 FN 会成为瓶颈,因为在 FN 内部也可以配置为多线程进行运行。


1.5 集群初始化


由于引入了 FN,我们需要对整个集群的初始化进行调整,调整后的流程如下图所示。


初始化过程先是 GTM,再到 FN,最后是 CN、DN。需要注意的是,我们要保证 FN 启动后,本机上的 CN、DN 再启动,这是因为本机上的 CN、DN 启动后需要向本机的 FN 进行注册,只有注册成功才能够对外提供服务。关闭顺序恰好与启动顺序相反,也就是说,关闭的时候先关闭 DN、CN,再关闭 FN,最后是 GTM。


2

FN 数据发送与接收过程

2.1 执行计划


我们举一个例子。有两张表,一个是 A 表,一个是 B 表,它们都有两列,f1 列作为分布列,f2 不是分布列,我们要进行一个 join 的查询:B 表用的是 f2,它不是一个分布列,这样的话就需要进行数据重分布,就发生了数据的交互。


前面提到 FN 是通过 FID、原节点 ID、目的节点 ID 进行数据路由,原节点 ID、目的节点 ID 很清楚不赘述,先重点了解 FID。


FID 在生成执行计划的时候已经确定,整体执行计划如下:如右下角的方框所示,先对 B 表进行数据扫描,扫描完之后把数据重分布发送到其他机器节点上,其他机器节点收到这部分数据后进行一个 join,join 结束之后再进行投影。


扫描之后再发送出去这一部分,对应的 FID 是 FID2;join 结果发送数据这一部分,对应的是 FID1;最后收到数据做投影的是 FID0——只要牵扯到数据交互,就会有 FID 来进行标识。这个 FID 是 GTM 来进行统一分配管理,可以保证任意时刻都是全局唯一,这也就能标识”我是谁”这个逻辑。


2.2 数据传输


针对上面的执行计划,我们来详细看一下数据的传输过程。在执行 FID2 的时候,DN1 上的数据需要传输到 DN2 上,DN2 上的数据也需要传输到 DN1 上。我们先看 DN1 怎么到 DN2 这个链路。首先是 FN1 通过共享内存,拿到 DN1 上的数据后,发现要去的是 DN2,而且现在执行的是 fid2 这个执行计划,它就把这部分数据通过 fid2 发送了出去。服务器 3 上的 FN,就会在自己的 fid2 的接收队列中收到这部分数据,收到这部分数据后,再通过共享内存传递给这个服务器上的 DN2,这样数据就传送过来了。同样的,DN2 上的数据也是这样传送过去 DN1 的。在 DN1 和 DN2 完成数据传输后,需要把 Join 结果数据传送到 CN 上去做投影,此部分对应的是 fid1,CN 上的 FN 作为一个目的节点,它会从 fid1 的接收队列中收到来自于 FN1、FN2 的数据,然后通过共享内存传递到 CN,CN 再返回到客户端,这样就完成了整个数据传输的路由过程。


2.3 FN 内部架构


下图是 FN 内部的详细架构。FN 内部大概可分为四类:


元数据管理,管理与其他节点通信的所需的信息,比如节点 ID、注册进程 ID、FID 使用情况等;针对每个 DN 节点要进行数据传送的共享内存,一般共享内存中存储的是一些 DN 节点的统计信息、拥塞控制信息;FN 作为发送者,这里会有一些发送的共享内存、发送队列、发送线程;FN 作为接收者,会有一些接收的共享内存、接收线程和接收队列。


2.4 数据收发流程


模块是怎么相互合作来进行发送、接收数据的。


首先我们来看一下数据发送过程。前面提到,CN/DN 启动的时候需要向本机的 FN 进行注册,注册时 FN 就会为注册的节点分配好内存空间,放在 buffer pool 里面,留待以后备用。这样的话,以后注册节点的这些 DProcess 进程(就是真正去处理用户请求的进程)它调用接口向 FN 申请共享内存,如果要发送,就把自己那部分的数据放到申请的内存里面去,放完之后,将这部分数据放到发送端 FN 的 Fragment 的消息队列里去,它就返回了。FN 内部会有调度线程,会根据数据的 FID 从 Fragment 队列中读出来,看这个消息要去的目的节点是哪里,把它调度到对应的目的节点的发送消息队列里面去。FN 内部有一个发送线程,发送线程就会真正地进行发送,发送完之后将那部分内存释放回 buffer pool,这是整个发送过程。


接收端这边,收到数据也会向对应的 buffer pool 请求一块内存来存放这部分数据。然后把这部分存放接收数据的内存挂载到接收队列里面,接收端的 FN 也有自己的调度线程,会检查接收队列里面的消息的 FID,根据 FID 再把它调度到对应的 Fragment 队列里去。最后 DN2 端这边的 DProcess 消费进程(用于处理用户请求),它会从 Fragment 消费队列中把对应的数据读出来,读出来后把这块内存再释放回去,这样就完成了整个 FN 内部数据的收发流程。


3

企业级 Oracle 兼容能力解读

3.1 分区表能力


首先是最常用的分区表能力。TDSQL-A 支持 range、list、hash、高性能等间隔分区,并且可以支持多级分区级联,在分区表的访问方法上,也全面兼容了 oracle 的语法,除了可以直接访问子表外,还可以关联父表名字来进行访问。还可以支持 Update 分区字段的值。


就像下图中的例子所示:0-30 是一个子表,30-60 是一个子表,我们可以把这个表里面的 id,即分区键,把它改成不属于它以前这个分区范围内的数据,改了之后 TDSQL-A 内部会自动修改这条数据,将它由以前的分区挪到新的分区里面。以前的分区就查不到这条数据了。


除此之外,我们还支持分区子表的合并拆分能力、新加分区时 default 分区自动移动的能力。


我们先来看分区子表的拆分与合并。随着时间的推移,在使用过程中,系统的分区会越来越多,为了方便管理,很多用户就会想将早期的分区进行合并,TDSQL-A 也像 oracle 一样提供了这样的能力。像图中左边所示,它可以通过 merge partitions 将 1 月份和 2 月份的分区进行合并,形成一个稍大的分区,这样就可以有效地去减少分区的数量。


分区的拆分刚好与合并相反,对于用户经常访问的热点数据,如果这些热点数据所处的分区内数据太多的话,每次就会扫到很多不必要的数据,我们可以通过拆分,将热点的分区拆分掉,拆分之后,在后续进行热点访问的时候,比如说我要访问“50”这个数据,我就只扫描 30-60 的子分区,0-30 的分区就不需要扫描,这就能有效减少数据扫描,提高查询效率。


分区表一般会有默认的 default 分区,用来存储不属于其它分区的数据。在下图这个例子中,比如说 2019 年 12 月份的数据,还有 2020 年 3 月份的数据,它都不属于前面已创建的这两个子分区,但如果用户在之后创建了 2020 年 3 月份这个新分区的话,我们数据库就会自动把这部分属于这个分区的数据从 default 这个默认分区,移动到对应的分区里。之后 default 分区中就不包含这部分数据了,只有剩下的其他数据。这类似于 oracle 创建了分区之后 default 分区会自动移动的功能。


3.2 数据治理能力


TDSQL-A 支持不同子分区存储到不同的节点组,不同的节点组关联着不同机器,通过这样的方案可以将热数据存储在配置好的机器上,冷数据存储在稍微差点的机器上,以此来实现冷热数据的分级存储,降低用户数据的存储成本。


3.3 存储过程能力


另一个重要的 Oracle 兼容能力就是存储过程,TDSQL-A 中也是支持的。比如说,存储过程中可以指定,在 i 是偶数的时候,对这个事物进行提交,它是奇数的时候,对它进行回滚。这样的话,最终执行完成之后,这个数据表中就只有偶数的数据。


3.4 函数扩展语法能力


此外,为了全面兼容 oracle,TDSQL-A 的函数在创建调用语法上也进行了适配。比如说创建的时候,可以不像 PG 那样指定用 $$进行包围,可以做到像 oracle 一样以斜杠来进行结尾。如果有空参数的话,也可以不需要括号。


我们还可以在任意 statement 前或者是 block 前去添加 label,然后通过 goto 去跳转到指定 label 里去。如果是原生 PG 的话,一般只有循环前面才能添加 label。这些都是属于 TDSQL-A 自己开发的新功能。


在函数上面,我们还添加了对 WITH FUNCTION 的语法支持。比如说这个例子中 SELECT 调用的这个 add_fnc 函数,就只对这个 SELECT 有用,对于其他任何查询都是没有用的。而且如果这个 WITH FUNCTION 函数的函数名跟系统中其他函数名重名的话,这个 SELECT 中的函数优先级是高于其他函数的。


3.5 PACKAGE 支持


和存储函数相关的 package,我们也是支持的。通过创建这样的一些包,用户可以将一些常用的函数分装到一个包里去,之后可以指定一个包来进行相应的调用。这里需要注意的是,创建的话,应该是先创建包再去创建一个包体,删除的话刚好是相反的。


3.6 ROWID & ROWNUM 支持


很多用户经常使用的 oracle 中典型的 ROWID 和 ROWNUM,TDSQL-A 也是支持的。如果用户在建表的时候,指定了 WITH ROWID 这样的参数,这样建出来的表在后面进行查询时,就可以指定要查的这个 ROWID,就可以看到它唯一标识了这一行的数据,并且在进行数据变更之后,仍然可以保证这个 ROWID 不变。跟 ROWID 相近的还有 ROWNUM,但实际上 ROWNUM 跟它有很大区别,它不是真正存储的,它只是用户在进行查询之后,对返回的记录进行编号。


3.7 MERGE INTO 语法支持


TDSQL-A 还支持 MERGE INTO 语法。我们添加了对 MergeStmt 子句的解析,也增加了 MERGE 命令,可以做到将两个表进行 MERGE 合并。像这个例子中所示,将 MERGE INTO 到 test1 里,使用参考 test2,如果匹配上的话,就对 test1 的数据进行更新,如果不匹配,就可以通过这个语法,将 test2 里面的数据全部添加到 test1 里去,达到合并的目的。、


3.8 Start with connect by 支持


Oracle 里的 start with connect by 层次查询,TDSQL-A 也是支持的。它实际上是进行了树的中序遍历。我们是通过一个递归 CTE 来进行实现的,最终实现层次查询的结果。


3.9 PIVOT & UNPIVOT 支持


TDSQL-A 还支持 PIVOT 和 UNPIVOT 函数。PIVOT 是一个把行数据转成列属性的函数,UNPIVOT 刚好相反,是将列属性转成行数据。大概的实现方法是:对于 PIVOT,针对不在这个 in 中的这些列,把它转换为一个 grop by 里面的字段,来进行实现。对于 UNPIVO,把这些数据转换成一个 lateral join 来进行实现。这些比较有特点的 Oracle 常用函数,TDSQ-A 都是支持的。


3.10 其他兼容能力


此外我们还支持 Oracle 中 List AGG、SQL hints、同义词、Dual 表、各种日期、时间、字符串、表达式等常用函数,可以做到 Oracle 常用语法的 90%以上兼容。

用户头像

还未添加个人签名 2018.12.08 加入

还未添加个人简介

评论

发布
暂无评论
揭秘TDSQL-A:兼容Oracle的同时支持海量数据交互