PolarDB-PG是云原生数据库,具有存储计算分离的架构。可以根据用户需要弹性扩充存储节点,也可以根据用户的计算需求弹性扩充用户的计算节点。但是如果使用原生的PolarDB-PG处理HTAP场景,在处理AP场景时会遇到两个挑战。
第一,单机的PG只支持单机的串行与单机的并行,不支持多机查询和跨机查询,无法发挥多个计算节点的特性,CPU和memory无法横向的scale out,只能单机scale up,即必须增加CPU和memory的实例规格。
第二,原生的PG直接套用到PolarDB上,无法充分发挥共享存储池的大吞吐能力,因为只能利用单机计算节点上的RO能力。
而根据TP和AP在存储和计算上是否共享与分离的维度,可以分为三种:
第一,TP和AP在存储计算上都分离,即分为TP与AP两套独立的系统。TP的数据需要导入到AP系统中,存在延迟、时效性不高的问题。同时两份存储也增加了冗余、存储成本以及运维难度。
第二,TP和AP在存储和计算上都共享。该模式对TP和AP查询时或多或少都会造成一些影响。同时,受限于TP查询,AP比重增大时,无法弹性scale out,同样也只能在单机上调整自己的CPU与memory。
第三,TP和AP在存储上共享,在计算上分离,即PolarDB云原生HTAP的方案。
PolarDB云原生HTAP的整体架构如上图所示。底层为共享存储池,上层为多个计算节点,每个计算节点内包含了一个读写节点和多个RO节点。
由于TP和AP共享一套存储,减少了存储成本,可以提高查询的时效性,能提供秒毫秒级的数据新鲜度。
其次,TP查询受限于RO节点与RW节点,而AP查询仅受限于部分RO节点,因此可以实现TP与AP的物理隔离,并杜绝了CPU与memory的相互影响。
另外,该架构具备Serverless的弹性扩展能力,可以在任何RO级联上发起分布式MPP查询,可以弹性调整MPP执行节点的范围,可以弹性调整单机MPP的单机并行度。
最后,该架构消除了数据的存储倾斜和计算倾斜,在执行过程中也可充分考虑到PG Buffer Pool的亲和性。
由于PolarDB底层存储在不同节点上是共享的,因此不能像传统MPP一样扫表。我们在原先的单机执行引擎上支持了MPP分布式执行引擎(跨机执行引擎)。同时对Shared-Storage做了优化,基于Shared-Storage的MPP是业界首创。其基本原理为借助Shuffle算子屏蔽数据的分布,借助ParallelScan算子屏蔽共享存储。
上图是典型的火山模型,扫描A表,再扫描B表进行HashJoin,最后做聚合输出。
而PolarDB的MPP场景下,对A表和B表进行了虚拟分区。两个只读节点上默配置了一个worker。左侧只读节点上的worker会扫描A表的Virtual Partition-1,右侧节点上的worker会扫描A表的Virtual Partition-2。B表同理。
通过对Virtual Partition-1和Virtual Partition-2虚拟表的共同扫描,屏蔽了共享存储。通过Shuffle算子的分发之后,分发到上层进行HashJoin的时,已经屏蔽了数据的分布。
而上述执行计划必然需要有优化器的支持。我们基于社区的ORCA优化器扩展了能感知共享存储特性的Transformation Rules。使得能够探索共享存储下特有的Plan空间,比如:一张表在PolarDB中既可以全量扫描,也可以分区域扫描,这是与传统MPP的本质区别。
上图中的A表按照分片扫描,但是如果B是小表,则可以做全表扫描。每个子节点都会扫描B的全量数据,构建一张哈希表。扫描B表的数据不需要下发到各个节点上。
上图灰色部分是PolarDB内核PORCA优化器的适配部分。下半部分是ORCA内核,灰色模块是我们在ORCA内核中对共享存储特性所做的扩展。
PolarDB中有4类算子需要并行化,其中Seqscan的算子的并行化极具代表性。
为了最大限度地利用存储的大IO带宽,在顺序扫描时,按照4MB为单位做逻辑切分,尽量将IO打散到不同的盘上,达到所有盘同时提供读服务的效果。该方案还有一个优势在于每个只读节点只扫描部分表文件,最终能缓存的表大小是所有只读节点的BufferPool总和。
上图可见,通过增加只读节点,扫描性能线性提升30倍。
打开buffer后,扫描时间从37min降至3.75s,提升了600倍。这也是数据亲和性的优势所在。
倾斜是传统MPP固有的问题,主要包含两方面:一方面是存储的倾斜,大对象通过heap内部表关联toast表时,因为无法确切地知道实际存储的数据量有多大,无论怎么切分,数据存储都有可能不均衡;另一方面是执行时的倾斜。不同只读节点上的事务、buffer、网络等会抖动,因此也会存在执行计算倾斜。
为了解决倾斜问题,我们支持了动态扫描。将协调节点内部分成DataThread和ControlThread,其中DataThread负责收集汇总元组,ControlThread负责控制每个扫描算子的扫描进度。
每个算子控制每个节点上scan算子的扫描进度,每个节点上scan算子再扫描下一个块的数据时会向QC节点进行请求查询,从而获得下一个扫描的目标块,使得扫描快的工作进程能多扫描逻辑的数据切片。
此外,尽管是冬天分配,过程中我们也尽量考虑了buffer数据亲和性。另外,每个算子的上下文均存储在各个worker的私有内存中,协调节点不存储表的相关信息。
上图可见,出现大对象时,静态扫描会出现数据倾斜,而使用动态扫描并没有因为 RO节点增多导致数据倾斜严重。
我们利用数据共享的特点,还可支持云原生下极致弹性的要求:将Coordinator全链路上各个模块所需要的外部依赖存在共享存储上,每个节点都可以看到相同的数据。同时worker全链路需要的运行时参数通过控制链路从Coordinator同步,使Coordinator和worker无状态化。任何节点都可以作为协调节点,确定了协调节点之后,控制节点再从协调节点获取相关的控制信息。
以上方式带来的好处在于:SQL的任何只读节点都可以称为协调节点,解决了协调节点单点的问题。其次,SQL可以在任何节点上起任意数量的worker,使算力达到SQL级别的弹性扩展,使得业务有更多的调度策略。
比如四个只读节点,可以让业务域1的SQL只利用只读节点1和只读节点2,业务域2的SQL利用节点3和节点4,为用户提供更多选择。
多个计算节点通过等待回放和globalsnapshot机制完成。等待回放能够保证所有需要的数据版本已经同步完成,globalsnapshot能够保证选取统一的可读版本。
主要流程如下:用户SQL发送后,生成计划并确定协调节点,协调节点会广播ReadLSN,每个worker节点等待回放到ReadLSN。结束之后获取各自的snapshot,通过序列化发送给协调节点。协调节点汇总所有worker,选出最小的snapshot并通过广播发给各个节点,再由广播执行计划树,从而可以保证每个worker能看到相同的数据、相同的快照和相同的plan,最终开始执行。
上图为使用1TB的TPCH进行的测试。
Demo演示:利用PolarDB HTAP加速TPC-H
首先,更新镜像,启动Docker。
确认实例已经拉起,进行连接。
生成TPC-H测试数据集,使用tpch-dbgen工具生成任意大小的数据集。
导入建表语句。
导入数据。
对表的最大并行度进行限制。默认情况下,不支持对PX查询的表设置最大并行度,PX即分布式执行引擎。
开启每张表的最大worker数,大于1则表示可以支持MPP查询。
执行单机的单机并行的执行引擎。并行度设置为2,上图为使用单机并行执行tpch查询q18的计划。
执行q18.sql,结果显示花费12秒。
接下来执行PolarDB的MPP执行引擎。
打开px开关,将单机并行设置为1,查看执行计划。
查看执行效果,耗时5s。
将并度设置为2,查看执行计划,如上图。计划中显示,有4个worker在工作,数据汇总到一个节点上。
查看执行效果,执行时间为3s,与并行度为1时相比,有了将近一倍的提升。
Demo演示:利用PolarDB HTAP加速TPC-H
首先,更新镜像,启动Docker。
确认实例已经拉起,进行连接。
生成TPC-H测试数据集,使用tpch-dbgen工具生成任意大小的数据集。
导入建表语句。
导入数据。
对表的最大并行度进行限制。默认情况下,不支持对PX查询的表设置最大并行度,PX即分布式执行引擎。
开启每张表的最大worker数,大于1则表示可以支持MPP查询。
执行单机的单机并行的执行引擎。并行度设置为2,上图为使用单机并行执行tpch查询q18的计划。
执行q18.sql,结果显示花费12秒。
接下来执行PolarDB的MPP执行引擎。
打开px开关,将单机并行设置为1,查看执行计划。
查看执行效果,耗时5s。
将并度设置为2,查看执行计划,如上图。计划中显示,有4个worker在工作,数据汇总到一个节点上。
查看执行效果,执行时间为3s,与并行度为1时相比,有了将近一倍的提升。