其他
400倍加速, PolarDB HTAP实时数据分析技术解密
前言
一 MySQL生态HTAP数据库解决方案
1 MySQL + 专用AP数据库的搭积木方案
2 基于多副本的Divergent Design方法
3 一体化的行列混合存储方案
Oracle公司在在2013年发表的Oracle 12C上,发布了Database In-Memory套件,其最核心的功能即为In-Memory Column Store,通过提供行列混合存储/高级查询优化(物化表达式,JoinGroup)等技术提升OLAP性能。
微软在SQL Server 2016 SP1上,开始提供Column Store Indexs功能,用户可以根据负载特征,灵活的使用纯行存表,纯列存表,行列混合表,列存表+行存索引等多种模式。
IBM在2013年发布的10.5版本(Kepler)中,增加了DB2 BLU Acceleration组件,通过列式数据存储配合内存计算以及DataSkipping技术,大幅提升分析场景的性能。
二 PolarDB MySQL AP能力的演进
1 MySQL的架构在AP场景的缺陷
MySQL的SQL执行引擎基于流式迭代器模型(Volcano Iterator)实现, 这个架构在工程实现上依赖大量深层次的函数嵌套及虚函数调用,在处理海量数据时,这种架构会影响现代CPU流水线的pipline效率,导致CPU Cache效率低下。同时Iterator执行模型也无法充分发挥现代CPU提供的SIMD指令来做执行加速。 执行引擎只能串行执行,无法发挥现代多核CPU的并行话能力。官方从MySQL 8.0开始,在一些count(*)等基本查询上增加并行执行的能力,但是复杂SQL的并行执行能力构建依然任重道远。 MySQL最常用的存储引擎都是按行存储,在按列进行海量数据分析时,按行从磁盘读取数据存在非常大的IO带宽浪费。其次行式存储格式在处理大量数据时大量拷贝不必要的列数据,对内存读写效率也存在冲击。
2 PolarDB 并行查询突破CPU瓶颈
3 Why We Need Column-Store
在分析场景经常需要访问某个列的大量记录,而列存按列拆分存储的方式会避免读取不需要的列。其次列存由于把相同属性的列连续保存,其压缩效率也远超行存,通常可以达到10倍以上。最后列存中大块存储的结构,结合MIN/MAX等粗糙索引信息可以实现大范围的数据过滤。所有这些行为都极大的提升了IO的效率。在现今存储计算分离的架构下,减少通过网络读取的数据量可以对查询处理的响应时间带来立竿见影的提升。 列式存储同样能提高CPU在处理数据时的执行效率,首先列存的紧凑排列方式可提升CPU访问内存效率,减少L1/L2 Cache miss导致的执行停顿。其次在列式存储上可以使用应用SIMD技术进一步提升单核吞吐能力,而这是现代高性能分析执行引擎的通用技术路线(Oracle/SQL Server/ClickHouse).
三 PolarDB In-Memory Column Index
在PolarDB的存储引擎(InnoDB)上新增对列式索引(Columnar Index)的支持,用户可以选择通过DDL将一张表的全部列或者部分列创建为列索引,列索引采用列压缩存储,其存储空间消耗会远小于行存格式。默认列索引会全部常驻内存以实现最大化分析性能,但是当内存不够时也支持将其持久化到共享存储上。 在PolarDB的SQL执行器层,我们重写了一套面向列存的执行器引擎框架(Column-oriented), 该执行器框架充分利用列式存储的优势,如以4096行的一个Batch为单位访问存储层的数据,使用SIMD指令提升CPU单核心处理数据的吞吐,所有关键算子均支持并行执行。在列式存储上,新的执行器对比MySQL原有的行存执行器性有几个数量级的性能提升。 支持行列混合执行的优化器框架,该优化器框架会根据下发的SQL是否能在列索引上执行覆盖查询,并且其所依赖的的函数及算子能被列式执行器所支持来决定是否启动列式执行。优化器会同时对行存执行计划和列存执行计划做代价估算,并选中代价交代的执行计划。 用户可以使用PolarDB集群中的一个RO节点作为分析型节点,在该RO节点上配置生成列存索引,复杂查询运行在列存索引上并使用所有可用CPU的计算能力,在获得最大执行性能的同时不影响该集群上的TP型负载的可用内存和CPU资源。
四 In-Memory Column Index的技术架构
1 行列混合的优化器
如何实现100%的MySQL兼容性
查询计划转换
兼顾行列混合执行的优化器
1.执行SQL的Parse过程并生成LogicalPlan,然后调用MySQL原生优化器按照执行一定优化操作,如join order调整等。同时该阶段获得的逻辑执行计划会转给IMCI的执行计划编译模块,尝试生成一个列存的执行计划(此处可能会被白名单拦截并fallback回行存)。
2.PolarDB的Optimizer会根据行存的Plan,计算得出一个面向行存的执行Cost。如果此Cost超过一定阈值,则会尝试下推到IMCI执行器使用IMCI_Plan进行执行。
3.如果IMCI无法执行此SQL,则PolarDB会尝试编译出一个Parallel Query的执行计划并执行。如果无法生成PQ的执行计划,则说明IMCI和PQ均无法支持此SQL,fallback回行存执行。
2 面向列式存储的执行引擎
支持BATCH并行的算子
在IMCI的执行引擎中,每个Operator也使用迭代器函数来访问数据,但不同的是每次调用迭代器会返回一批的数据,而不是一行,可以认为这是一个支持batch处理的火山模型。
串行执行受制于单核计算效率,访存延时,IO延迟等限制,执行能力有限。而IMCI执行器在几个关键物理算子(Scan/Join/Agg等)上均支持并行执行。除物理算子需要支持并行外,IMCI的优化器需要支持生成并行执行计划,优化器在确定一个表的访问方式时,会根据需要访问的数据量来决定是否启用并行执行,如果确定启用并行执行,则会参考一系列状态数据决定并行度:包括当前系统可用的CPU/Memory/IO资源, 目前已经调度和在排队的任务信息, 统计信息, query 的复杂程度, 用户可配置的参数等。根据这些数据计算出一个推荐的DOP值给算子, 而一个算子内部会使用相同的DOP。同时DOP也支持用户使用Hint的方式进行设定。
SIMD向量化计算加速
充分利用列式存储的优势,使用分批处理的模型代替迭代器模型,我们使用SIMD指令重写了大部分常用数据类型的表达式内核实现,例如所有数字类型(int, decimal, double)的基本数学运算(+, -, *, /, abs),全部都有对应的SIMD指令实现。在AVX512指令集的加持下, 单核运算性能获得会数倍的提升。
采用了与Postgres类似表达式实现方法:在SQL编译及优化阶段,IMCI的表达式以一个树形结构来存储(与现有行式迭代器模型的表现方法类似),但是在执行之前会对该表达式树进行一个后序遍历,将其转换为一维数组来存储,在后续计算时只需要遍历该一维数组结构即可以完成运算。由于消除了树形迭代器模型中的递归过程,计算效率更高。同时该方法对计算过程提供简洁的抽象,将数据和计算过程分离,天然适合并行计算。
3 支持行列混合存储的存储引擎
行存数据和列存数据具有实时一致性,能满足很多苛刻的业务需求,所有数据写入即可见于分析型查询。
更低的成本,用户可以非常方便的指定哪些列甚至一张表哪个范围的存储为列存格式用于分析。全量数据继续以行存存储。
管理运维方便,用户无需关注数据在两套系统之间同步及数据一致性问题。
建表时可以指定部分表或者列为列存格式,或者对已有的表可以使用Alter table语句为其增加列存属性,分析型查询会自动使用列存格式来进行查询加速。
列存数据默认压缩格式存储在磁盘上,并可以使用In-Memory Column Store Area来做缓存加速并加速查询,传统的行格式依然保存在BufferPool中供OLTP性负载使用。
所有事务的增删改操作都会实时反应到列存存储上,保证事务级别的数据一致性。
满足OLTP业务的需求是第一优先的,因此增加列存支持不能对TP性能太大影响。这要求我们维护列存必须足够轻量,必要时需要牺牲AP性能保TP性能。
列存的设计无需考虑事务并发对数据的修改, 数据的unique check等问题,这些问题在行存系统中已经被解决,而这些问题对ClickHouse等单独的列存引擎是非常难以处理的。
由于有一个久经考验的行存系统的存在,列存系统出现任何问题,都可以切换回行存系统响应查询请求。
表现为Index的列存
InnoDB原生是支持多索引的,Insert/Update/Delete操作都会以行粒度apply到Primary Index和所有的Secondary Index上,并且保证事务。将列存实现为一个二级索引可以复用这套事务处理框架。
在数据编码格式上,实现为二级索引的列存可以和其他行存索引使用完全一样的内格式,直接内存拷贝即可,不需要考虑charset和collation等信息,这对上层执行器也是完全透明的。
二级索引操作非常灵活,可以在建表时即指定索引所包含的列,也可以后续通过DDL语句对一个二级索引中包含的列进行增加或者删除操作。例如用户可以将需要分析的int/float/Double列加入列索引,而对于一般只需要点查但是又占用大量空间的text/blob字段,则可以保留在行存中。
崩溃恢复过程可以复用InnoDB的Redo事务日志模块, 与现有实现无缝兼容。同时也方便支持PolarDB的物理复制过程,支持在独立RO节点或者Standby节点上生成列存索引提供分析服务。
同时二级索引与主表有一样的生命周期,方便管理。
列存数据组织
列索引中记录按RowGroup进行组织,每个RowGroup中不同的列会各自打包形成DataPack。
每个RowGroup都采用追加写,分属每个列的DataPack也是采用追加写模式。对于一个列索引,只有个Active RowGroup负责接受新的写入。当该RowGroup写满之后即冻结,其包含的所有Datapack会转为压缩格保存到磁盘上,同时记录每个数据块的统计信息便于过滤。
列存RowGroup中每新写入一行都会分配一个RowID用作定位,属于一行的所有列都可以用该RowID计算定位,同时系统维护PK到RowID的映射索引,以支持后续的删除和修改操作。
更新操作采用标记删除的方式来支持,对于更新操作,首先根据RowID计算出其原始位置并设置删除标记,然后在ActiveRowGroup中写入新的数据版本。
当一个RowGroup中的无效记录超过一定阈值,则会触发后台异步compaction操作,其作用一方面是回收空间,另一方面可以让有效数据存储更加紧凑,提升分析型查询单的效率。
全量及增量行转列
列索引粗糙索引
在每个Active Datapack终结写入的时候,会预先进行计算,并生成Datapack所包含数据的最小值/最大值/数值的总和/空值的个数/记录总条数等信息。所有这些信息会维护在DataPacks Meta元信息区域并常驻内存。由于冻结的Datapack中还会存在数据的删除操作,因此统计信息的更新维护会放到后台完成。
对于查询请求,会根据查询条件将Datapacks分为相关、不相关、可能相关三大类,从而减少实际的数据块访问。而对于一些聚合查询操作,如count/sum等,可以通过预先计算好的统计值进行简单的运算得出,这些数据块甚至都不需要进行解压。
行列混合存储下的TP和AP资源隔离
第一种方式,RW上开启行列混合存储,此种模式部署可以支持轻量级的AP查询,在主要为TP负载,且AP型请求比较少时可以采用。或者使用PolarDB进行报表查询,但是数据来自批量数据导入的场景。
第二种方式,RW支持OLTP型负载,并启动一个AP型RO开启行列混合存储以支持查询,此种部署模式下CPU资源可以实现100%隔离,同时该AP型RO节点上的内存可以100%分配给列存存储和执行器。但是由于使用的相同的共享存储,因此在IO上会相互产生一定影响,对于这个问题我们在未来会支持将列存数据写入到外部存储如OSS等,实现IO的资源隔离,同时提升AP型RO上的IO吞吐速率。
第三种方式,RW/RO支持OLTP型负载,在单独的Standby节点开启行列混合存储以支持AP型查询,由于standby是使用独立的共享存储集群,这种方案在第二种方案支持CPU和内存资源隔离的基础上,还可以实现IO资源的隔离。
五 PolarDB IMCI的OLAP性能
数据量TPC-H 100GB, 22条Query
CPU Intel(R) Xeon(R) CPU E5-2682 2 socket 内存 512G, 启动后数据都灌进内存。
1 PolarDB IMCI VS MySQL串行
2 PolarDB IMCI VS ClickHouse
FutureWork
自动化的索引推荐系统,目前列存的创建和删除需要用户手动指定,这增加了DBA的工作量,目前我们正在研究引入自动化推荐技术,根据用户的SQL请求特征,自动创建列存索引,降低维护负担。
单独的列存表以及OSS存储,目前IMCI只是一个索引,对纯分析型场景,去除行存可以更进一步的降低存储大小,而IMCI执行器支持读写OSS对象存储能将存储成本降到最低。
行列混合执行,即一个SQl的执行计划部分片段在行存执行,部分片段在列存执行。以获得最大化的执行加速效果。