TDSQL-A 技术架构演进及创新实践

TDSQL-A 发展历程
【TDSQL-A 技术架构演进及创新实践】TDSQL-A 是一款基于 PostgreSQL 自主研发的分布式在线关系型数据库。是一个面向海量数据实时在线分析产品,采用无共享 MPP 构架。面向分析型场景的极致性能优化,我们自研了列式存储,同时也支持行列混合存储模式。在数据转发层面上,针对大规模集群面临的连接风暴问题对集群执行/转发框架做了更深入优化,来保证可以支持超过千台规模的集群能力。同时为加速用户在数据挖掘或分析场景上的时延,通过多种计算能力优化来达到给用户提供更好效果。
在多年的发展过程中 TDSQL-A 依托腾讯内部业务进行充分打磨,在内部业务及外部企业级用户场景下都有良好表现,并在 2021 年 5 月 18 日上线腾讯云。
TDSQL-A 整体构架
首先整体介绍 TDSQL-A 架构。TDSQL-A 是一个多 CN 入口的 MPP 分布式集群设计,CN 节点作为业务访问入口,每个节点是对等的,对外提供一致的用户元数据和视图访问,同时也可以通过多入口分担用户高并发压力场景下的连接处理。
因为是一个多 CN 入口,需要一个全局事务管理器 GTM 节点,进行全局事务管理以及 Sequence 等全局一致能力的处理节点。早期 GTM 在高并发情况下获取全局事务快照会有性能瓶颈,TDSQL PG 版以及 TDSQL-A 都针对分布式提交协议做了基于 timestamp 的改造,解决了全局事务快照的单点瓶颈问题。TDSQL-A 整体不管是行存和列存事务提交,整体的提交协议都基于 timestamp(GTS)协议,提供业界领先的高并发能力支持。
数据存储和计算节点我们称为 Datenode,Datenode 节点经过 TDSQL-A 构架优化,支持超过 1000 个节点以上的集群部署,支持 10PB 级别以上的用户数据量。同时在计算时,会尽可能把所有计算都通过智能的优化器规划推到 DN 节点上做计算。
TDSQL-A 整体构建演进。由于用户数据量持续增大,需要面临最大挑战是在大规模集群下大数据访问量和复杂查询场景。例如 TPC-DS 这类复杂的用户场景,它的 query 是带有复杂的子查询场景及 with 语句的。在这种情况下多表关联会比较多,在分布式系统下会有多层重分布。
按照之前早期构架,在执行时碰到 RemoteSubplan 算子的时候才会往下发整体的下一步查询计划,如果查询中重分布的层次比较多,每一层 DN 都会认为自己是一个发起者,会导致大量多层进程连接和网络连接消耗。
做一个比较简单的计算,如果 200 个 DN 节点有 100 个并发查询,每个查询是 5 个数据重分布,计算将会有超过 10 万个连接数。这个问题在集群规模达到上千个节点后会更加严重,这也是整个 MPP 在大规模情况下的通用问题。
而 TDSQL-A 针对性地做了比较大的改造,首先整体执行框架进行了重构,在 TDSQL-A 里查询计划是统一在 CN 上去做规划。当查询计划生成后,会根据 Remote Subplan 或需要做数据重分布这些节点,对查询计划做一个划分。不同层次会统一由 CN 节点到 DN 节点去建立相应 DProcess 进程,相当于有一个统一的 CN 协调者来管理所有进程和连接数,这样就会比较可控地去建立所需最小进程数和相应连接。同时不同进程间也可以去进行异步启动,加速复杂查询的直接效率。
实际上这里还不够,虽然进程数比较可控,但同时连接数还是一个问题,例如集群规模非常大,超过 1000 个节点以后,连接数膨胀还是很严重。而对于超大规模集群我们是引入了数据转发节点。数据转发节点会在每台物理机进行部署,如果有混布场景也是一个数据转发节点,会负责这台机器上所有 DN 或 CN 之前的数据交互。这样对于一个大规模计算集群,实际上网络连接数就会比较可控,因为网络会走到数据转发节点上,而机器上的 DN 节点或者 CN 节点会通过共享内存和数据转发节点进行交互。这里还有一个额外优化,如果在同一台机器有混布的情况下,相同机器上的 DN 交互可以不走网络,直接走共享内存做一个直接转发。
通过数据转发节点的引入整个集群规模就可以有一个比较线性和扩展能力,按照 N 个节点和 M 层 Join 来计算,不管你的产品多复杂它只有 N*(N-1)个网络连接数,整体由 FN 节点去规划。很好地去解决 MPP 场景下,超大规模集群如何保持高并发和复杂查询场景下网络连接问题。
上面介绍改造之后整个查询计划分片也会比较明确。包括重分布代价在内,优化器会考虑到分布式场景中数据转发的网络开销,基于代价模型去做自动查询优化。在 CN 生成查询计划后会递归遍历整个执行计划树,把整个查询计划分成多个 Fragment。从上面开始向下看,上面是更靠近 CN 节点,就是 Fragmentid 1,这里缩写是 FID 1,这样每次碰到 Remote subplan 节点时相当于需要拆分成一个新的 Fragment。同一个 Fragment 会在每一个参与计算的 DN 统一去建立这样一层进程。中间是通过 FN 节点去进行网络传输。右边是一个比较简单的标准查询计划两个 Hash Join,通过不同 Fragment 去分层的进行异步计算。
我们的自研列式存储,例如用户有一些星型数据模型或者一些表列数较多而实际参与计算的列比较少,这种情况很多都需要列裁剪去做执行优化,如果没有列存整体效果会比较差。通过列存尽可能减少磁盘 IO 扫描和相关的计算层计算裁剪。这样整体在海量数据下计算量消耗降低会比较明显。其实做优化最高效方法还是通过优化执行计划去做计算裁剪,第二步才是在必要相同计算量前提下去进行执行优化,不管是你的算子优化,还是机器资源物理层优化。最开始都要从执行计划角度去做,所以列存是非常重要的。
前面有提到我们的列存表和行存表一样,都使用了基于 timestamp 的分布式提交协议,所以整体行列之间可以保持混合查询事务一致性。同时用户也可以在同一个库或同一个实例里,去根据业务场景针对不同特征建立行存表和列存表,可以自动在查询计划中选择更好的 access path。
这是自研列式存储格式的简单介绍,每一张列式存储表,都有一张对应的元数据 registry 表,去记录它存储状态和更新的状态信息。
我们的物理文件结构最小单元叫 Silo,就是一个谷仓的概念,一个 Silo 是一个数据块列式分布的紧凑排列。这样一个 Silo 里面展开,会有相应的右边这些信息,除了头部信息,最上面还会有一个 checksum 保证数据校验的正确性,后面有标记位去加速数据访问和 filter 效果以及 null bitmap,最后是具体的数据。
介绍一下列存储延迟扫描优化,例如有一个查询,在同一张表上有多个 Predicate 条件,比如 10 列有 3 列带有 Predicate。按照常见的做法,这些虽然是列存储,但需要的这些列还是会提前扫描去做一个整体物化,再做一个 Predicate。这种延迟扫描其实可以做更优,因为它可能对两个或三个 Predicate 中间层级选择率比较明显。可以先扫第一列,第一列扫完后它可能已经通过 Predicate 过滤掉很多数据,这时再去扫第二列或第三列时,或后面其它数据列,都可以通过 ctid 扫后面需要的一些数据。如果列比较多或过滤效果比较好,它会减少扫描的数据量。这是基于列存储不同列的物理文件隔离去做一个前提,因为这种情况下才能减少真正扫描量,而不是增加 reaccess 的问题。
上面介绍了每一个 Silo 的格式,我们会尽量放更多的数据在一个 Silo 里,增加它的数据压缩能力。另外要引入相关压缩能力算法提高整体存储效能,降低用户存储成本。
这里有两层,首先是通用的透明压缩,透明压缩会使用 LZ4 或 Zstd 算法,针对特定数据类型会加轻量级压缩能力。同时对于不同类型我们也有不同压缩最优推荐,这是具象化到产品里的能力,用户只需要选择 low、middle,或者是 high,例如你希望压缩 low,我们会自动替你选择相应的压缩算法。
比如整数类型,如果是 low 我们用 Delta+RLE,middle 和 high 就会加上 Lz4 或 Zstd 类似透明压缩。而针对 Numeric 也有深度优化,这里是列存压缩存储,如果你已经选择压缩,实际上它会自动转成 int 类型。这样不仅是存储空间节省,在你计算同时也能很快的做向量化计算能力。
介绍一下我们基于列存储和执行框架优势去深入挖掘执行引擎上的能力。首先是一个多层级并行能力,在这里分为几个层面,一个是分布式多节点和多进程执行能力,这里由 FN 转发能力或优化器自动规划能力去决定,当然也是由 MPP 整体构架来决定的。中间一层,因为现在代码整体是基于 PG10 来做的,但实际上我们合入了很多更新,例如 PG12、PG13 里的能力或并行能力,包括优化器里针对这些场景,比如说 partitoin-wise Join 的能力都有引入。
在中间这一层算子的并行计算能力情况下也会有比较好的效果,同时我们自己针对多种场景,比如 FN 能力在并行过程中遇到的一些问题,做了深入的处理。整体在基于 MPP 框架,超大规模 MPP 框架下同时对算子级进程做了深度优化。另外一个最底层的在 SIMD 并行指令层面进行深入的优化。
前面介绍了基于列存我们做了很多深入优化,比如前面提到的 LateRead 延迟扫描能力,实际上在计算层我们也有基于列存延迟物化能力,可以理解为统一把列存的特性在计算层优化到极致。
延迟物化这里介绍下,比如一个 query 里面有 hashjoin,一般的做法是,下面 Scan 层会把所有的列或数据都扫进来,再去做 Join 计算,这是一个通用性场景。实际上如果在 Join 选择率比较好的情况下,对于不参与 Join condition 的这些列,物理扫描的那些数据列可以通过 Join 之后再去扫描,因为是列存储,可以 Join 之后再把列进行补全,这样 Join 在选择率很好的情况下可以减少大量的磁盘 IO 和网络消耗。
这里有一个简单计算,一个有 20 亿条数据选择率百分之一的 join 场景,可能会减少 7.4G 的无效数据传输和无效数据扫描,这个效果非常明显。类似场景下我们做了延迟物化的整体优化,在最开始扫描的时候只需要扫 Join condition 需要的列去做 Join,Join 结束后再把剩下的列数据再补全。
TDSQL-A 执行引擎优化
在这里我们深入研究,一个是执行引擎框架,另外是基于优化器 CBO 里自动形成延迟物化相关的执行计划。如果大家对优化器比较熟会知道在这里 PG 的代价模型是很先进的,目前是自底向上的动态规划过程,相比于一些新的优化器使用 cascade model,通用优化效果其实各有优劣。前面提到并行算子在我们合入了 PG12、PG13 以后,整个优化器里也引入了并行执行 CBO 能力。延迟物化也是持续在上面做一个优化,也就是 path 生成的过程中它是可以通过 restriction 去算出最开始扫描时只需要扫的那些列。这样生成 path 时只需要去构架一个辅助信息,去标记一下哪些列是需要提前物化,哪些列是可以进行延迟物化的。
这里实际有很多细化问题,例如延迟物化常见问题,如果有更多算子导致 reaccess 的场景,效果可能会下降,这在 CBO 里都有考虑。例如 Hashjoin 的落盘情况下以及 RemoteSubplan 都可能会有乱序问题,在这里我们都有相应的考虑在里面,所以整体会是一个基于 CBO 比较智能的延迟物化能力。
前面多个点提到了向量化执行引擎整体设计,向量化和 SIMD 是一个更核心方向。在这里我们自研了整套向量化执行引擎,可以支持 TPC-DS 及更复杂的查询场景,让复杂查询全都执行在向量化执行引擎上面。
在 Hash Agg 或者表达式计算等场景下,我们会去做针对列存储和向量化技术做联合优化,比如 numeric 转换成定长类型。同时还去针对向量化内存结构 做了深入优化,比如说 SIMD 和向量化效果到底能有多少,其实和数据编排有非常大关联性。更好的数据编排以及算子实现可以减少 CPU Cache miss。在这里我们花了比较多的精力在内存编排上。这些都是原生在内核里去实现。同时在算子上也是自己去单独拉出一套向量化执行引擎算子,在 SIMD 场景下针对算子细节和其他典型场景都有 SIMD 指令引入,保证在多个层次上,从数据编排的基础到算子核心,再到 SIMD 整体都进行了深入优化。
同时做为分析型产品,可能更多在交易系统后端链路上,需要去接入不同数据源保证可以有更多的适应性场景,如果沿用原有的 Copy 模式性能就会比较差。
所以我们针对分布式 MPP 场景去做了高速数据交互工具 TDSQL-TDX,这是借助一个数据服务器,让 TDX 统一去处理 DN 的数据请求,DN 去访问 TDX 取到切分的数据分片,就可以达到基于 DN 个数并行的进行数据交互。
另外这个工具也支持数据导出,相比传统用的 Copy 模式有数十倍的提升。另外我们也将持续对 TDX 工具进行优化,支持更多生态。
未来规划
前面介绍了很多构架升级包括一些细化能力,当然我们还有更多的点可以继续深入细化。例如在 SIMD 覆盖场景上增强,深入对列存储格式编排和向量化执行引擎做深度优化还有更进一步的空间。同时也希望继续可以和 PG 生态做一个持续融合,比如并行或其它的算子能力,都将持续融合 PG 社区能力,同时也会考虑整体把 code base 去进行持续升级。
最后一个点是 Oracle 兼容能力,实际内核能力上 PostgresSQL 整体 Oracle 兼容能力是非常强的,我们也会持续在相关能力融合和能力进行提升。对于国产 MPP 或类似 Oracle 替代场景,因为 Oracle 不仅是做为交易型,可能很多厂商都是混合场景,而我们做为一个 MPP 也可以支持 Oracle 兼容能力,这个可以打开更多的适应性场景。

    推荐阅读