最近分析型数据库在资本市场和技术社区都非常的火热,各种创业公司的创新型产品如雨后春笋般出现。这一方面是因为当前阶段企业日益依赖从数据中寻找增长潜力带来需求的增长,另一方面云原生技术的发展带来现有技术体系的进化和变革,诸如Snowflakes这类产品的成功证明,使用云原生技术再造分析型数据库技术体系是必要的且存在很大的市场机会。
PolarDB MySQL是因云而生的一个数据库系统, 除了云上OLTP场景,大量客户也对PolarDB提出了实时数据分析的性能需求。对此PolarDB技术团队提出了In-Memory Column Index(IMCI)的技术方案,在复杂分析查询场景获得的数百倍的加速效果。
本文阐述了IMCI背后技术路线的思考和具体方案的取舍。PolarDB MySQL 列存分析功能即将在阿里云上线,敬请期待。
MySQL是一款主要面向OLTP型场景设计的开源数据库,开源社区的研发方向侧重于加强其事务处理的能力,如提升单核性能/多核扩展性/增强集群能力以提升可用性等。在处理大数据量下复杂查询所需要的能力方面,如优化器处理子查询的能力,高性能算子HashJoin, SQL并行执行等,社区一直将其放在比较低优先级上,因此MySQL的数据分析能力提升进展缓慢。
随着MySQL发展为世界上最为流行的开源数据库系统,用户在其中存储了大量的数据,并且运行着关键的业务逻辑,对这些数据进行实时分析成为一个日益增长的需求。当单机MySQL不能满足需求时,用户寻求一个更好的解决方案。
1 MySQL + 专用AP数据库的搭积木方案
专用分析型数据库产品选项众多,一个可选方案是使用两套系统来分别满足的OLTP和OLAP型需求,在两套系统中间通过数据同步工具等进行数据的实时同步。更进一步,用户甚至可以增加一层proxy,自动将TP型负载路由到MySQL上,而将分析型负载路由到OLAP数据库上,对应用层屏蔽底层数据库的部署拓扑。
这样的架构有其灵活之处,例如对于TP数据库和AP数据库都可以各自选择最好的方案,而且实现了TP/AP负载的完全隔离。但是其缺点也是显而易见的。首先,在技术上需要维护两套不同技术体系的数据库系统,其次由于两套系统处理机制的差异,维护上下游的数据实时一致性也非常具有挑战。而且由于同步延迟的存在,下游AP系统存储的经常是过时的数据,导致无法满足实时分析的需求。
2 基于多副本的Divergent Design方法
随着互联网而兴起的新兴数据库产品很多都兼容了MySQL协议,因此成为替代MySQL的一个可选项。而这些分布式数据库产品大部分采用了分布式Share Nothing的方案,其一个核心特点是使用分布式一致性协议来保障单个partition多副本之间的数据一致性。由于一份数据在多个副本之间上完全独立,因此在不同副本上使用不同格式进行存储,来服务不同的查询负载是一个易于实施的方案。典型的如TiDB,其从TiDB4.0开始,在一个Raft Group中的其中一个副本上,使用列式存储(TiFlash)来响应AP型负载, 并通过TiDB的智能路由功能来自动选取数据来源。这样实现了一套数据库系统同时服务OLTP型负载和OLAP型负载。
该方法在诸多Research及Industry领域的工作中都被借鉴并使用,并日益成为分布式数据领域一体化HTAP的事实标准方案。但是应用这个方案的前提是用户需要迁移到对应的NewSQL数据库系统,而这往往带来各种兼容性适配问题。
3 一体化的行列混合存储方案
比多副本Divergent Design方法更进一步的,是在同一个数据库实例中采用行列混合存储的方案,同时响应TP型和AP型负载。这是传统商用数据库Oracle/SQL Server/DB2等不约而同采用的方案。
三家领先的商用数据库厂商,均同时采用了行列混合存储结合内存计算的技术路线,这是有其底层技术逻辑的:列式存储由于有更好的IO效率(压缩,DataSkipping,列裁剪)以及CPU计算效率(Cache Friendly), 因此要达到最极致的分析性能必须使用列式存储,而列式存储中索引稀疏导致的索引精准度问题决定它不可能成为TP场景的存储格式,如此行列混合存储成为一个必选方案。但在行列混合存储架构中,行存索引和列存索引在处理随机更新时存在性能鸿沟, 必须借助DRAM的低读写延时来弥补列式存储更新效率低的问题。因此在低延时在线事务处理和高性能实时数据分析两大前提下,行列混合存储结合内存计算是唯一方案。
对比上述三种方法,从组合搭积木的方法到Divergent Design方法再到一体化的行列混合存储,其集成度越来越高,用户的使用体验也越来越好。但是其对内核工程实现上的挑战也一个比一个大。基础软件的作用就是把复杂留给自己把简单留给用户,因此一体化的方法是符合技术发展趋势的。
PolarDB MySQL能力栈与开源MySQL类似,长于TP但AP能力较弱。由于PolarDB提供了最大单实例100TB的存储能力,同时其事务处理能力远超用户自建MySQL。因此PolarDB用户倾向于在单实例上存储更多的数据,同时会在这些数据上运行一些复杂聚合查询。借助于PolarDB一写多读的架构,用户可以增加只读的RO节点以运行复杂只读查询,从而避免分析型查询对TP负载的干扰。
1 MySQL的架构在AP场景的缺陷
MySQL的实现架构在执行复杂查询时性能差有多个方面的原因,对比专用的OLAP系统,其性能瓶颈体现多个方面:
2 PolarDB 并行查询突破CPU瓶颈
PolarDB团队开发的并行查询框架(Parallel Query), 可以在当查询数据量到达一定阈值时,自动启动并行执行,在存储层将数据分片到不同的线程上,多个线程并行计算,将结果流水线汇总到总线程,最后总线程做些简单归并返回给用户,提高查询效率。
并行查询的加入使得PolarDB突破了单核执行性能的限制,利用多核CPU的并行处理能力,在PolarDB上部分SQL查询耗时成指数级下降。
3 Why We Need Column-Store
并行执行框架突破了CPU扩展能力的限制,带来了显著的性能提升。然而受限于行式存储及行式执行器的效率限制,单核执行性能存在天花板,其峰值性能依然与专用的OLAP系统存在差距。要更进一步的提升PolarDB MySQL的分析性能,我们需要引入列式存储:
PolarDB In-Memory Column Index功能为PolarDB带来列式存储以及内存计算能力,让用户可以在一套PolarDB数据库上同时运行TP和AP型混合负载,在保证现有PolarDB优异的OLTP性能的同时,大幅提升PolarDB在大数据量上运行复杂查询的性能。
In-Memory Column Index使用行列混合存储技术,同时结合了PolarDB基于共享存储一写多读的架构特征,其包含如下几个关键的技术创新点:
几个关键关键技术结合使得PolarDB成为了一个真正的HTAP数据库系统,其在大数据量上运行复杂查询的性能可以与Oracle/SQL Server等业界最顶尖的商用数据库系统处在同一水平。
1 行列混合的优化器
PolarDB原生有一套面向行存的优化器组件,在引擎层增加对列存功能支持之后,此部分需要进行功能增强,优化器需要能够判断一个查询应该被调度到行存执行还是列存执行。我们通过一套白名单机制和执行代价计算框架来完成此项任务。系统保证对支持的SQL进行性加速,同时兼容运行不支持的SQL.
如何实现100%的MySQL兼容性
我们通过一套白名单机制来实现兼容性目标。使用白名单机制是基于如下几点考量。第一点考虑到系统可用资源(主要是内存)的限制,一般不会在所有的表的所有上都创建列索引,当一个查询语句需要使用到列不在列存中存在时,其不能在列存上执行。第二点,基于性能的的考量,我们完全重写了一套面向列存的SQL执行引擎,包括其中所有的物理执行算子和表达式计算,其所覆盖的场景相对MySQL原生行存能够支持的范围有欠缺。当下发的SQL中包含一些IMCI执行引擎不能支持的算子片段或者列类型时,需要能能够识别拦截并切换回行存执行。
查询计划转换
Plan转换的目的是将MySQL的原生逻辑执行计划表示方式AST转换为IMCI的Logical Plan。在生成IMCI的Logical Plan之后,会经过一轮Optimize过程,生成Physical Plan。Plan转换的方法简单直接,只需要遍历这个执行计划树,将 mysql 优化后的 AST 转换成IMCI 以 relation operator 位节点的树状结构即可,是一个比较直接的翻译过程。不过在这个过程中,也会做一部分额外的事情,如进行类型的隐式转换,以兼容MySQL灵活的类型系统。
兼顾行列混合执行的优化器
有行存和列存两套执行引擎的存在,优化器在选择执行计划时有了更多的选择,其可以对比行存执行计划的Cost和列存执行计划的Cost,并使用代价最低的那个执行计划.
在PolarDB中除了有原生MySQL的行存串行执行,还有能够发挥多核计算能力的基于行存的Paralle Query功能。因此实际优化器会在1)行存串行执行,2)行存Paralle Query 3)IMCI 三个选项之中选择。在目前的迭代阶段,优化器按如下的流程操作:
上述策略是基于这样一个判断,从执行性能对比,行存串行执行 < 行存并行执行 < IMCI。从SQL兼容性上看,IMCI < 行存并行执行 < 行存串行执行。但是实际情况会更复杂,例如某些情况下,基于行存有序索引覆盖的并行Index Join会比基于列存的Sort Merge join有更低的Cost. 目前的策略下可能就选择了IMCI 列存执行。
2 面向列式存储的执行引擎
IMCI执行引擎是一套面向列存优化,并完全独立于现有MySQL行式执行器的一个实现,重写执行器的目的是为了消除现有行存执行引擎在执行分析型SQL时效率低两个关键瓶颈点:按行访问导致的虚函数访问开销以及无法并行执行。
支持BATCH并行的算子
IMCI执行器引擎使用经典的火山模型,但是借助了列存存储以及向量执行来提升执行性能。
火山模型里,SQL生成的语法树所对应的关系代数中,每一种操作会抽象为一个 Operator,执行引擎会将整个 SQL 构建成一个 Operator 树,查询树自顶向下的调用Next()接口,数据则自底向上的被拉取处理。该方法的优点是其计算模型简单直接,通过把不同物理算子抽象成一个个迭代器。每一个算子只关心自己内部的逻辑即可,让各个算子之间的耦合性降低,从而比较容易写出一个逻辑正确的执行引擎。
向量化执行解决了单核执行效率的问题,而并行执行突破了单核的计算瓶颈。二者结合使得IMCI执行速度相比传统MySQL行式执行有了数量级的速度提升。
SIMD向量化计算加速
AP型场景,SQL中经常会包含很多涉及到一个或者多个值/运算符/函数组成的计算过程,这都是属于表达式计算的范畴。表达式的求值是一个计算密集型的任务,因此表达式的计算效率是影响整体性能的一个关键的因素。
传统MySQL的表达式计算体系以一行为一个单位的逐行运算,一般称其为迭代器模型实现。由于迭代器对整张表进行了抽象,整个表达式实现为一个树形结构,其实现代码易于理解,整个处理的过程非常清晰。
但这种抽象会同时带来性能上的损耗,因为在迭代器进行迭代的过程中,每一行数据的获取都会引发多层的函数调用,同时逐行地获取数据会带来过多的 I/O,对缓存也不友好。MySQL采用树形迭代器模型,是受到存储引擎访问方法的限制,这导致其很难对复杂的逻辑计算进行优化。
在列存格式下,由于每一列的数据都单独顺序存储,涉及到某一个特定列上的表达式计算过程都可以批量进行。对每一个计算表达式,其输入和输出都以Batch为单位,在Batch的处理模式下,计算过程可以使用SIMD指令进行加速。新表达式系统有两项关键优化:
3 支持行列混合存储的存储引擎
事务型应用和分析型应用对存储引擎有着截然不同的要求,前者要求索引可以精确定位到每一行并支持高效的增删改,而后者则需要支持高效批量扫描处理,这两个场景对存储引擎的设计要求完全不同,有时甚至是矛盾的。
因此设计一个一体化的存储引擎能同时服务OLTP型和OLAP型负载非常具有挑战性。目前市场上HTAP存储引擎做的比较好的只有几家有几十年研发积累的大厂,如Oracle (In-Memory Column Store)/Sql Server(In Memory Column index)/DB2(BLU)等。如TiDB等只能通过将多副本集群中的一个副本调整为列存来支持HTAP需求。
一体化的HTAP存储引擎一般使用行列混合的存储方案,即引擎中同时存在行存和列存,行存服务于TP,列存服务于AP。相比于部署独立一套OLTP数据库 加一套OLAP数据库来满足业务需求,单一HTAP引擎具有如下的优势:
PolarDB 采用了和Oracle/Sql Server等商用数据库类似的行列混合存储技术,我们称之为In-Memory Column Index:
实现一个行列混合的存储引擎技术上非常困难,但是在InnoDB这样一个成熟的面向OLTP负载优化的存储引擎中增加列存支持,又面临不同的情况:
上述条件可谓有利有弊,这也影响了对PolarDB整个行列混合存储的方案设计。
表现为Index的列存
在MySQL插件式的存储引擎框架的架构下,增加列存支持最简单方案是实现一个单独的存储引擎,如Inforbright以及MarinaDB的ColumnStore都采用了这种方案。而PolarDB采用了将列存实现为InnoDB的二级索引的方案,主要基于如下几点考量:
如上图所示,在PolarDB中所有Primary Index和Seconary Index都实现为一个B+Tree。而列索引在定义上是一个Index,但其实是一个虚拟的索引,用于捕获对该索引覆盖列的增删改操作。
对于上面的表其主表(Primary Index)包含(C1,C2,C3,C4,C5) 5列数据, Seconary Index索引包含(C2,C1) 两列数据, 在普通二级索引中,C2与C1编码成一行保存在B+tree中。而其中的列存索引包含(C2,C3,C4)三列数据. 在实际物理存储时,会对三列进行拆分独立存储,每一列都会按写入顺序转成列存格式。
列存实现为二级索引的另一个好处是执行器的工程实现非常简单,在MySQL中已经存在覆盖索引的概念,即一个查询所需要的列都在一个二级索引中存储,则可以直接利用这个二级索引中的数据满足查询需求,使用二级索引相对于使用Primary Index可以极大减少读取的数据量进而提升查询性能。当一个查询所需要的列都被列索引覆盖时,借助列存的加速作用,可以数十倍甚至数百倍的提升查询性能。
列存数据组织
对ColumnIndex中每一列,其存储都使用了无序且追加写的格式,结合标记删除及后台异步compaction实现空间回收。其具体实现上有如下几个关键点:
采用这种数据组织方式一方面满足了分析型查询按列进行批量扫描过滤的要求。另一方面对于TP型事务操作影响非常小,写入操作只需要按列追加写到内存即可,删除操作只需要设置一个删除标记位。而更新操作则是一个标记删除附加一个追加写。列存可以做到支持事务级别的更新同时,做到几乎不影响OLTP的性能。
全量及增量行转列
行转列操作在两种情况下会发生,第一种情况是使用DDL语句对部分列创建列索引(一般是业务对一个已有的表有新增分析型需求),此时需要扫描全表数据以创建列索引。另一种情况是在事务操作过程中对于涉及到的列实时行专列。
对于全表行转列的情形,我们使用并行扫描的方式对InnoDB的Primary Key进行扫描,并依次将所有涉及到的列转换为列存形式,这一操作的速度非常快,其基本只受限于服务器可用的IO吞吐速度和可用CPU资源。该操作是一个online-DDL过程,不会阻塞在线业务的运行。
在一个表上建立列索引之后,所有的更新事务将会同步更新行存和列存数据,以保证二者的事务一致性。下图演示了在IMCI功能关闭和开启之间的差异性。在未开启IMCI功能时,事务对所有行的更新都会先加锁,然后再对数据页进行修改,在事务提交之前会对所有加锁的记录一次性方所。在开启IMCI功能之后,事务系统会创建一个列存更新缓存,在所有数据页被修改的同时,会记录所涉及到的列存的修改操作,在事务提交结束前,该更新缓存会应用到列存系统。
在此实现下,列存存储提供了与行存一样的事务隔离级别。对于每个写操作, RowGroup中的每一行都会记录修改该行的事务编号,而对于每个标记删除操作也会记录该设置动作的事务编号。借助写入事务号和删除事务号,AP型查询可以用非常轻量级的方式获得一个全局一致性的快照。
列索引粗糙索引
由前述列的存储格式可以看出, IMCI中所有的Datapack都采用无序且追加写的方式, 因此无法像InnoDB的普通有序索引那样的可以精准的过滤掉不符合要求的数据。在IMCI中,我们借助统计信息来进行数据块过滤,以此来达到降低数据访问单价的目的。
采用基于统计信息的粗糙索引方案对于一些需要精准定位部分数据的查询并不是很友好。但是在一个行列混合存储引擎中,列索引只需要辅助加速那些会涉及到大量数据扫描的查询,在这个场景下使用列会具有显著的优势。而对于那些只会访问到少量数据的SQL,优化器通常会基于代价模型计算得出基于行存会得到一个成本更低的方案。
行列混合存储下的TP和AP资源隔离
PolarDB行列混合存储可以支持在一个实例中同时支持AP型查询和TP型查询。但很多业务有很高的OLTP型负载,而突发性的OLAP性负载可能干扰到TP型业务的响应时延。因此支持负载隔离在HTAP数据库中是一个必须支持的功能。借助PolarDB一写多读的架构,我们可以非常方便对AP型负载和TP型负载进行隔离。在PolarDB的技术架构下,我们有如下几个部署方式:
除了上述部署架构上不同可以支持的资源局隔离之外。在PolarDB内部对于一些需要使用并行执行的大查询支持动态并行度调整(Auto DOP),这个机制会综合考虑当前系统的负载以及可用的CPU和内存资源,对单个查询所用的资源进行限制,以避免单个查询消耗的资源太多,影响其他请求的处理。
为了验证IMCI技术的效果, 我们对PolarDB MySQL IMCI的进行了TPC-H场景的测试。同时在相同的场景下将其与原生MySQL的行存执行引擎以及当前OLAP引擎单机性能最强的ClickHouse进行了对比。测试参数简要介绍如下:
1 PolarDB IMCI VS MySQL串行
在TPC-H场景下,所有22条Query ,IMCI处理延时相对比原生MySQL都有数十倍到数百倍不等的加速效果。其中Q6的的效果将近400倍。体现出了IMCI的巨大优势。
2 PolarDB IMCI VS ClickHouse
而在对比当前社区最火热的分析型数据库ClickHouse时, IMCI在TPC-H场景下的性能也与其基本在同一水平。部分SQL的处理延时各有优劣。用户完全可以使用IMCI替代ClickHouse使用,同时其数据管理也更加方便。
FutureWork
IMCI是PolarDB迈向数据分析市场的第一步,它迭代脚步不会停止,接下里我们会在如下几个方向进一步研究和探索,给客户带来更好的使用体验:
作者 | 北楼
原文链接
本文为阿里云原创内容,未经允许不得转载。