Author: gushen
事务处理(OLTP)和分析处理(OLAP)混合的工作负载在当前的业务系统中变得越来越常见。由于实时、易运维等需求,一些业务系统会采用HTAP数据库来代替原有的OLTP-ETL-OLAP架构,所谓的HTAP数据库,可以在一套数据库系统中,同时较为高效地处理TP请求和AP请求。 然而,由于TP请求和AP请求的访问模式截然不同,高效处理两种请求依赖于不同的存储格式和计算模式。因此,一类HTAP数据库会存储两种不同格式的数据,并在处理不同的请求时选择不同的模式对相应数据进行计算。 阿里云瑶池旗下的云原生数据库PolarDB也采用了这样的方式来处理混合型的工作负载,开启内存列存索引(In-Memory Column Index,以下简称IMCI)功能的只读节点(RO)会额外维护一份列式索引,并在处理AP请求时采用向量化的方式对列式数据进行计算(列式计算)。而在处理TP请求时依然采用MySQL原有的one-tuple-at-a-time的方式对行式索引的InnoDB数据进行计算(行式计算),列式索引和行式索引都通过物理复制链路回放读写节点(RW)的写入。在这套系统中,处理两种请求的存储、执行器、优化器都彼此独立,TP请求和AP请求在执行路径上完全分离,一条SQL语句要么选择列式计算,或者选择行式计算。
从用户的工作负载中,我们观察到,对于混合负载中大部分的请求,“行列分离”的计算方式已经能够以最优的计划执行,然而对于少部分请求,列式计算和行式计算都不是最优的:
为了更高效地处理混合负载中的这部分长尾请求,我们在IMCI的优化器和执行器中,基于两个不同索引,设计并实现了“行列融合”执行架构。本文将介绍云原生数据库PolarDB在“行列融合”执行架构的基础组件(优化器的代价模型,执行器的多引擎访问,存储引擎的日志回放和事务处理)设计,以及针对上述第一种类型的请求所设计的HybridProject算子。通过基础组件,我们可以较为容易的实现HybridIndexScan和HybridIndexJoin来处理上述剩下两种类型的请求。
在一个“行列分离”的系统中实现“行列融合”执行,主要的挑战来自三个方面:
上述三种类型的长尾请求在“行列分离”的执行架构下会基于代价被看作AP请求由IMCI执行,因此我们选择基于IMCI的优化器,引入对行式执行片段的代价估计,从而使这些长尾请求能够选择“行列融合”的执行计划。 在引入行式执行代价估计的过程中,我们遵循如下两个原则:
基于上述原则,我们采用如下设计:
根据上述可比较的代价常量,我们就可以基于MySQL和IMCI各自的算法和统计信息,计算出“行列融合”执行计划的可比较代价。
IMCI优化器对长尾请求选择“行列融合”的执行计划后,我们通过在IMCI执行器中引入新的Hybrid算子来计算行式执行片段。 新的Hybrid算子需要在IMCI执行器中访问InnoDB,我们参考MySQL的Server层的实现,通过TABLE对象中的handler来和存储引擎交互。从上图的执行流程可见,虽然Prepare阶段已经Open Table,但相关对象和接口都是面向MySQL单线程执行实现的,因此在IMCI执行器的worker中,需要再次Open Table,并克隆或引用相关对象。这部分主要难度在于兼容MySQL逻辑的工程复杂度。
两个不同索引异步回放的流程如上图黄色部分所示,其中InnoDB在回放完成后会更新latest read view,而列式索引在回放完成后会更新列式索引的last commit seq。回放流程每隔一段redo(包含若干条redo log entry)运行一次,一段redo内InnoDB事务和IMCI事务的提交情况是一致的,因此一段redo在两个不同索引上都回放完成后,基于更新的latest read view和last commit seq在两个不同索引上的数据可见性是一致的,我们称上述read view和commit seq为“对应的”。如果查询使用“对应的”read view和commit seq来判断数据可见性,就能实现强一致读。 然而,行列索引的回放流程是异步的,由于回放速度的差异,任意时刻latest read view和last commit seq可能不是“对应的”,这种情况下我们需要找到最近更新的“对应的”read view和commit seq。 为此,我们在异步回放流程中引入上图蓝色部分所示流程:
引入该流程后,“行列融合”查询会在尚未被purge的read view中,寻找一个“对应的”commit seq已经生效的read view来判断数据可见性。
通过上述基础组件,我们引入了HybridProject来处理第一种长尾case。HybridProject通过primary key从InnoDB的主索引获取完整的行,进行相关表达式的计算后输出。除基础组件提供的能力外,HybridProject需要执行计划中它的孩子算子输出primary key数据,可以通过两种方式实现:
由于目前IMCI优化器还不支持列裁剪,我们采用第二种方式实现。
我们在ClickHouse官方提供的OnTime数据集上,验证了“行列融合”执行架构和HybridProject的效果(https://clickhouse.com/docs/en/getting-started/example-datasets/ontime)。 OnTime数据集的表包含110列,是一个典型的大宽表。我们通过如下SQL模拟第一种类型的长尾请求:
select * from ontime order by ArrTime limit 1000;
三种执行计划均为冷启动查询,实验结果如下:
“行列融合”执行 | 纯列式执行 | 纯行式执行 | |
---|---|---|---|
耗时 | 0.33 sec | 2.56 sec | 232.48 sec |
由实验结果可见,对于混合型工作负载中的长尾请求,通过“行列融合”执行架构和Hybrid算子,PolarDB可以实现最优的性能,相对于纯列式执行或纯行式执行都有数量级的提升。
“行列融合”执行架构可以选择更优的执行计划来处理混合型工作负载中的长尾请求。我们未来将基于此架构引入HybridIndexScan和HybridIndexJoin算子,全面提升长尾请求的性能。