本文介绍了列存索引的技术背景、简介以及技术架构等内容。
技术背景
MySQL生态HTAP数据库解决方案
MySQL是一款主要面向OLTP型场景设计的开源数据库,开源社区的研发方向侧重于加强其事务处理能力。如提升单核性能、多核扩展性和增强集群能力,以提升可用性等。在处理大数据量下复杂查询所需要的能力方面,如优化器处理子查询的能力、高性能算子HashJoin、SQL并行执行能力等,MySQL社区一直将其放在比较低优先级上,因此,MySQL的数据分析能力提升进展缓慢。
随着MySQL发展为世界上最为流行的开源数据库系统,用户在其中存储了大量的数据,并且运行着关键的业务逻辑,对这些数据进行实时分析成为一个日益增长的需求。当单机MySQL不能满足需求时,用户寻求一个更好的解决方案。如MySQL+专用AP数据库的搭积木方案、基于多副本的Divergent Design方法以及一体化的行列混合存储方案等。
MySQL+专用AP数据库的搭积木方案
该方案由两套系统来分别满足OLTP和OLAP型需求,在两套系统中间通过数据同步工具进行数据的实时同步。用户甚至可以增加一层Proxy,自动将TP型负载路由到MySQL上,将分析性负载路由到OLAP数据库上,对应用层屏蔽底层数据库的部署拓扑。架构图如下:
该架构有其灵活之处。如对于TP数据库和AP数据库都可以各自选择最好的方案,而且实现了TP/AP负载的完全隔离。但是其缺点也是显而易见的。首先,在技术上需要维护两套不同技术体系的数据库系统,其次由于两套系统处理机制的差异,维护上下游的数据实时一致性也非常具有挑战性。而且存在数据同步延迟,下游AP系统存储的经常是过时的数据,从而导致无法满足实时分析的需求。
基于多副本的Divergent Design方法
随着互联网而兴起的新兴数据库产品很多都兼容了MySQL协议,这些分布式数据库产品大部分采用了分布式Share Nothing方案,其一个核心特点是使用分布式一致性协议来保障单个partition多副本之间的数据一致性。由于一份数据在多个副本之间完全独立,因此在不同副本上使用不同格式进行存储,来服务不同的查询负载是一个易于实施的方案。典型的如TiDB,其从TiDB4.0开始,位于一个Raft Group中的其中一个副本上,使用列式存储(TiFlash)来响应AP型负载,并通过TiDB的智能路由功能来自动选取数据来源。实现了一套数据库系统同时服务OLTP型负载和OLAP型负载。
该方法在诸多Research及Industry领域的工作中都被借鉴并使用,并日益成为分布式数据领域一体化HTAP的事实标准方案。 但应用这个方案的前提是用户需要将数据迁移到对应的NewSQL数据库系统中,而这往往会带来各种兼容性问题。
一体化的行列混合存储方案
比多副本的Divergent Design方法更进一步的方案,即在同一个数据库实例中采用行列混合存储的方案,同时响应TP型和AP型负载。这是传统商用数据库Oracle、SQL Server和DB2等不约而同采用的方案。
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技术,大幅提升分析场景的性能。
三家领先的商用数据库厂商,均同时采用了行列混合存储结合内存计算的技术路线。列式存储由于有更好的IO效率(压缩、DataSkipping、列裁剪)以及CPU计算效率(Cache Friendly),因此要达到最极致的分析性能必须使用列式存储,而列式存储中由于索引稀疏导致索引精准度问题决定了它不可能成为TP场景的存储格式。因此,行列混合存储成为一个必选方案。但在行列混合存储架构中,行存索引和列存索引在随机更新数据时存在性能鸿沟,必须借助DRAM的低读写延时来弥补列式存储更新效率低的缺陷。因此,在低延时在线事务处理和高性能实时数据分析两大前提下,行列混合存储结合内存计算成为最优方案。
对比上述三种方案,从组合搭积木的方案到Divergent Design方法,再到一体化的行列混合存储方案。其集成度越来越高,用户的使用体验也越来越好。但是其对内核工程实现上的挑战也越来越大。而基础软件的作用就是将复杂留给自己,将简单留给用户。因此,一体化的行列混合存储方案更为符合技术发展趋势。
PolarDB MySQL AP能力的演进
PolarDB MySQL版能力栈与开源MySQL类似,长于TP但AP能力较弱。由于PolarDB提供了单个集群最大500 TB的存储能力,同时其事务处理能力远超用户自建MySQL。因此,PolarDB用户倾向于在单个集群上存储更多的数据,同时会在这些数据上进行一些复杂的聚合查询。借助于PolarDB一写多读架构,用户可以根据实际需求增加多个RO节点以运行复杂查询,从而避免分析型查询对TP负载的干扰。
MySQL架构在AP场景的缺陷
MySQL架构在执行复杂查询时性能差有多个方面的原因。对比专用的OLAP系统,其性能瓶颈体现在以下方面:
MySQL的SQL执行引擎基于流式迭代器模型(Volcano Iterator)实现。该架构在工程实现上依赖大量深层次的嵌套函数及虚函数,在处理海量数据时,这种架构会影响CPU流水线的Pipeline效率,导致CPU Cache效率低下。同时,Iterator执行模型也无法充分使用CPU提供的SIMD指令来做执行加速。
执行引擎只能串行执行,无法发挥多核CPU的并行能力。官方从MySQL 8.0开始,在
count(*)
等基本查询上增加了并行执行能力,但构建复杂SQL语句的并行执行能力依然任重道远。MySQL最常用的存储引擎都是按行存储,在按列进行海量数据分析时,按行从磁盘读取数据存在非常大的IO带宽浪费。其次,行式存储格式在处理大量数据时会大量拷贝不必要的列数据,对内存读写效率也存在冲击。
PolarDB并行查询突破CPU瓶颈
并行查询框架(Parallel Query)可以在查询数据量到达一定阈值时,自动启动并行执行。在存储层将数据分片到不同的线程上,由多个线程并行计算,并将结果流水线汇总到总线程。最后,总线程做简单归并返回给用户,以提高查询效率。
并行查询突破了单核执行性能的限制,利用多核CPU的并行处理能力,使得部分SQL查询耗时成指数级下降。
PolarDB列式存储
并行执行框架突破了CPU扩展能力的限制,带来了显著的性能提升。然而,受限于行式存储及行式执行器的效率限制,单核执行性能存在天花板,其峰值性能依然与专用的OLAP系统存在差距。要更进一步的提升分析性能,则需要引入列式存储:
在分析场景,经常需要访问某个列的大量记录,而列存按列拆分存储的方式会避免读取不需要的列。其次,列存会将相同属性的列连续保存,其压缩效率也远超行存,通常可以达到10倍以上。列存中大块存储的结构,结合MIN/MAX等粗糙索引信息可以实现大范围的数据过滤。所有这些行为都极大的提升了IO的效率。在存储计算分离架构下,减少通过网络读取的数据量可以对查询处理的响应时间带来立竿见影的提升。
列式存储同样能提高CPU在处理数据时的执行效率。首先,列存的紧凑排列方式可提升CPU访问内存的效率,减少由L1/L2 Cache miss导致的执行停顿时间。其次,在列式存储上可以应用SIMD技术来进一步提升单核吞吐能力。
简介
PolarDB In-Memory Column Index功能提供了列式存储以及内存计算能力,让用户可以在一套数据库上同时运行TP和AP型混合负载,在保证现有PolarDB优异的OLTP性能的同时,大幅提升了在大数据量上运行复杂查询的性能。原理图如下:
列存索引使用行列混合存储技术。同时,结合PolarDB基于共享存储一写多读的架构特征,其包含如下几个关键的技术创新点:
存储引擎(InnoDB)支持存储列式索引(Columnar Index),用户可以通过DDL语句为一张表中的全部列或者部分列创建列索引,列索引采用列压缩存储,其存储空间消耗会远小于行存格式。默认列索引会全部常驻内存以实现最大化分析性能。但是,当内存不够时也支持将其持久化到共享存储中。
在SQL执行器层,重写了一套面向列存的执行器引擎框架(Column-oriented),该执行器框架充分利用列式存储的优势,如以4096行的一个Batch为单位访问存储层的数据,使用SIMD指令提升CPU单核心处理数据的吞吐,所有关键算子均支持并行执行。在列式存储上,新的执行器对比MySQL原有的行存执行器有数量级的性能提升。
支持行列混合执行的优化器框架,该优化器框架会根据下发的SQL语句能否在列索引上执行来覆盖查询,并且其所依赖的函数及算子能否在列式执行器中执行来决定是否启动列式索引。优化器会同时对行存执行计划和列存执行计划做代价估算,并选中代价较低的执行计划。
用户可以使用集群中的一个RO节点作为分析型节点,在该RO节点上配置生成列存索引,复杂查询运行在列存索引上并使用所有可用CPU的计算能力,在获得最大执行性能的同时不影响该集群上的TP型负载的可用内存和CPU资源。
以上几个关键技术的结合,使得PolarDB成为了一个真正的HTAP数据库系统。
技术架构
行列混合优化器
PolarDB有一套面向行存的优化器组件,引擎层支持列存功能之后,此部分需要进行功能增强。优化器需要能够判断一个查询应该被调度到行存执行还是列存执行。基于此,列存索引通过一套白名单机制和执行代价计算框架来完成此项任务。系统保证对支持的SQL语句进行加速查询,同时兼容运行不支持的SQL。
如何实现100%的MySQL兼容性
通过一套白名单机制来实现兼容性。 使用白名单机制是基于如下几点考量。
系统可用资源(主要是内存)限制。
一般情况下,不会为数据库中全部表的所有列上都创建列索引。当一条查询语句中使用到的列没有在列存中存在时,其不能在列存上执行。
性能。
重写一套面向列存的SQL执行引擎,包括所有的物理执行算子和表达式计算,其所覆盖的场景相对MySQL原生行存能够支持的范围有欠缺。当下发的SQL语句中包含一些列存索引不支持的算子片段或者列类型时,需要能够识别拦截并切换回行存执行。
目前,白名单规则校验的内容包含SQL语句中的数据类型、算子、计算表达式以及其他场景。其他场景如不支持multi statement等。
MySQL已经发展了数十年,其支持的各种列类型和SQL语法非常丰富。在IMCI中,初期重点优化在分析型查询语句中最常见的SQL性能问题。即使适用场景有所限制,IMCI能够运行的SQL语法对MySQL功能的兼容性也远超绝大部分OLAP系统。对于那些不能在列存上执行的SQL,则直接回退到MySQL原生执行引擎,因此实现了100%的MySQL兼容。
查询计划转换
Plan转换的目的是将MySQL的原生逻辑执行计划表示方式AST转换为IMCI的Logical Plan。在生成IMCI的Logical Plan之后,会经过Optimize过程,继而生成Physical Plan。
Plan转换只需要遍历整个执行计划树,将MySQL优化后的AST转换成以relation operator为节点的树状结构即可。在这个过程中,会进行类型的隐式转换,以兼容MySQL灵活的类型系统。
Plan转换生成的是一个等价的Logical Plan,其需要被转换成Physical Plan才能被执行器执行。目前IMCI的Optimizer比较简单,除了一些基本的执行计划优化(如决策是使用HashJoin还是使用NestedLoop Join)外,Optimizer最主要的作用是将目前IMCI执行器不支持的子查询转换为一个等价的Join操作。
兼顾行列混合执行的优化器
由于存在行存和列存两套执行引擎,优化器在选择执行计划时有了更多的选择,其可以对比行存执行计划的Cost和列存执行计划的Cost,并使用代价最低的执行计划。
在PolarDB中,除原生MySQL的行存串行执行外,还有能够发挥多核计算能力的基于行存的Parallel Query功能。因此,实际优化器会在行存串行执行、行存Parallel Query、以及IMCI三个之中选择其一。在目前的迭代阶段,优化器按如下的流程执行:
执行SQL的Parse过程并生成LogicalPlan,然后调用MySQL原生优化器,并执行优化操作(join order等)。同时该阶段获得的逻辑执行计划会转给IMCI的执行计划编译模块,并尝试生成一个列存的执行计划(此处可能会被白名单拦截并回滚回行存)。
PolarDB的Optimizer会根据行存计划,计算得出一个面向行存的执行Cost。如果此Cost超过一定阈值,则会尝试下推到IMCI执行器使用IMCI_Plan执行。
如果IMCI无法执行此SQL,则PolarDB会尝试编译出一个Parallel Query的执行计划并执行。如果无法生成PQ的执行计划,则说明IMCI和PQ均无法执行此SQL,则回滚回行存执行。
上述策略是基于这样一个判断,从执行性能上进行对比:行存串行执行 < 行存并行执行 < IMCI。 对比SQL兼容性,IMCI < 行存并行执行 < 行存串行执行。但是实际情况会更加复杂,例如:某些情况下,基于行存有序索引覆盖的并行Index Join会比基于列存的Sort Merge join有更低的Cost。按照当前策略,则会选择IMCI列存执行。
面向列式存储的执行引擎
IMCI是一套面向列存优化并完全独立于现有MySQL行式执行器的执行引擎。重写执行器的目的是为了消除现有行存执行引擎在执行分析型SQL时效率低的两个关键瓶颈点,即按行访问导致的虚函数访问开销以及无法并行执行。
向量化并行执行器
IMCI执行器引擎使用经典的火山模型,但是借助了列存存储以及向量执行来提升执行性能。
火山模型中,SQL生成的语法树所对应的关系代数中,每一种操作会抽象为一个Operator,执行引擎会将整个SQL构建成一个Operator树,查询树自顶向下调用Next()接口,数据则自底向上被拉取处理。该方法的优点是其计算模型简单直接,通过把不同物理算子抽象成一个个迭代器。每一个算子只关心自己内部的逻辑即可,使得各个算子之间的耦合性降低,从而比较容易写出一个逻辑正确的执行引擎。
在IMCI执行引擎中,每个Operator也使用迭代器函数来访问数据,但不同的是每次调用迭代器会返回一批数据,而不是一行,可以认为这是一个使用了向量化模式的火山模型。
串行执行受制于单核计算效率、访存延时、IO延迟等限制,执行能力有限。而IMCI执行器在几个关键物理算子(Scan/Join/Agg等)上均并行执行。除物理算子需要支持并行外,IMCI的优化器也支持生成并行执行计划,优化器在确定一个表的访问方式时,会根据需要访问的数据量来决定是否启用并行执行,如果确定启用并行执行,则会参考一系列状态数据(包括当前系统可用的CPU/Memory/IO资源、目前已经调度和在排队的任务信息、统计信息、query的复杂程度、用户可配置的参数等)。 根据这些数据会计算出一个推荐的DOP值给一个算子,而一个算子内部会使用相同的DOP。同时,DOP也支持用户使用Hint进行设定。
基于以上两点优化思路,重新实现了所有物理执行算子,包括TableScan、HashJoin、NestedLoopJoin、Groupby等。下面以HashJoin为例展示执行器的并行化及向量化加速效果。在IMCI中,HashJoin按如下流程执行:
向量化执行解决了单核执行效率低的问题,而并行执行突破了单核的计算瓶颈。二者结合使得IMCI执行速度相比传统MySQL行式执行有了数量级的提升。
向量化表达式体系
AP型场景,SQL中经常会包含很多涉及到一个或者多个值、运算符和函数组成的计算过程,这都是属于表达式计算的范畴。表达式的求值是一个计算密集型的任务,因此,表达式的计算效率是影响整体性能的一个关键的因素。
传统MySQL的表达式计算体系以一行为一个单位的逐行运算,一般称其为迭代器模型实现。由于迭代器对整张表进行了抽象,整个表达式实现为一个树形结构。但是,这种抽象会同时带来性能上的损耗,因为在迭代器进行迭代的过程中,每一行数据的获取都会引发多层的函数调用。同时,逐行地获取数据会带来过多的 I/O,对缓存也不友好。MySQL采用树形迭代器模型,是受到存储引擎访问方法的限制,导致其很难对这些计算复杂的逻辑进行优化。
而在列存格式下,由于每一列的数据都单独顺序存储,涉及到某一个特定列上的表达式计算过程都可以批量进行。对每一个计算表达式,其输入和输出都以Batch为单位,在Batch的处理模式下,计算过程可以使用SIMD指令进行加速。
向量化表达式的关键优化点:
充分利用列式存储的优势,使用分批处理的模型代替迭代器模型,使用SIMD指令重写了大部分常用数据类型的表达式内核实现。例如,所有数字类型(int、decimal、double)的基本数学运算(+、 -、*、/、abs),全部都使用对应的SIMD指令实现。在AVX512指令集的加持下,单核运算性能数倍提升。
采用了与PostgreSQL类似表达式实现方法:在SQL编译及优化阶段,IMCI的表达式以一个树形结构来存储(与现有行式迭代器模型的表现方法类似)。但是,在执行之前会对该表达式树进行一个后序遍历,将其转换为一维数组来存储,在后续计算时只需要遍历该一维数组结构即可以完成运算。由于消除了树形迭代器模型中的递归过程,计算效率更高。同时该方法对计算过程提供简洁的抽象,将数据和计算过程分离,适合并行计算。
行列混合存储
事务型应用和分析型应用对存储引擎有着截然不同的要求,前者要求索引可以精确定位到每一行并支持高效的增删改操作,而后者则需要支持高效批量扫描处理。这两个场景对存储引擎的设计要求完全不同,有时甚至互相矛盾。因此,设计一个一体化的存储引擎能同时服务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:
建表时可以指定部分表或者列为列存格式,或者对已有的表可以使用
ALTER TABLE
语句为其增加列存属性,分析型查询会自动使用列存格式来进行查询加速。列存数据默认以压缩格式存储在磁盘上,并可以使用In-Memory Column Store Area来做缓存并加速查询,传统的行格式依然保存在Buffer Pool中供OLTP型负载使用。
所有事务的增删改操作都会实时反馈到列存存储,保证了事务级别的数据一致性。
实现一个行列混合的存储引擎非常困难,但是在InnoDB这样一个成熟的面向OLTP负载优化的存储引擎中增加列存,又面临不同的情况:
满足OLTP业务的需求是第一优先级。因此,增加列存不能对TP性能有太大影响。这要求维护列存必须足够轻量,必要时需要牺牲AP性能来维持TP性能。
列存的设计无需考虑事务并发场景下对数据的影响,以及数据的unique check等问题,这些问题在行存系统中已经被解决,而这些问题对ClickHouse等单独的列存引擎来说,非常难以处理。
由于有一个久经考验的行存系统的存在,列存系统出现任何问题,都可以切换回行存系统响应查询请求。
上述条件可谓有利有弊,这也影响了对整个行列混合存储的方案设计。
表现为Index的列存
在MySQL插件式的存储引擎框架下,增加列存支持最简单的方案是实现一个单独的存储引擎,如Inforbright以及MarinaDB的ColumnStore都采用了这种方案。而PolarDB采用了将列存实现为InnoDB的二级索引方案,主要基于如下几点考量:
InnoDB原生支持多索引,Insert、Update和Delete操作都会以行粒度apply到Primary Index和所有的Secondary Index上,并且保证事务。将列存实现为一个二级索引可以复用这套事务处理框架。
在数据编码格式上,二级索引的列存可以和其他行存索引使用完全一样的格式,直接内存拷贝即可,不需要考虑charset和collation等信息。
二级索引操作非常灵活,可以在建表时指定索引所包含的列,也可以后续通过DDL语句对一个二级索引中包含的列进行增加或者删除操作。例如,用户可以将需要分析的int、float和double列加入列索引,而对于一般只需要点查但是又占用大量空间的text和blob字段,则可以保留在行存中。
崩溃恢复过程可以复用InnoDB的Redo事务日志模块,与现有实现无缝兼容。同时也方便PolarDB的物理复制过程,支持在独立RO节点或者Standby节点上生成列存索引提供分析服务。
二级索引与主表有一样的生命周期,方便管理。
如上图所示,在PolarDB中,所有Primary Index和Secondary Index都实现为一个B+Tree。而列索引在定义上是一个Index,但其实是一个虚拟的索引,用于捕获对该索引覆盖列的增删改操作。
对于上面的表,其主表(Primary Index)包含(C1、C2、C3、C4、C5) 5列数据, Secondary Index索引包含(C2、C1)两列数据,在普通二级索引中,C2与C1编码成一行保存在B+tree中。而其中的列存索引包含(C2、C3、C4)三列数据。在实际物理存储时,会对三列进行拆分独立存储,每一列都会按写入顺序转成列存格式。
列存实现为二级索引的另一个好处是执行器的工程实现非常简单,在MySQL中已经存在覆盖索引的概念,即一个查询所需要的列都在一个二级索引中存储,则这个二级索引中的数据满足查询需求,使用二级索引相对于使用Primary Index可以极大减少读取的数据量进而提升查询性能。当一个查询所需要的列都被列索引覆盖时,借助列存的加速作用,可以数十倍甚至数百倍的提升查询性能。
列存数据组织
对Column Index中的每一列,其存储都使用了无序且追加写的格式。结合标记删除及后台异步compaction实现空间回收。其具体实现上有如下几个关键点:
列索引中记录按RowGroup进行组织(目前每个RowGroup包含64K行),每个RowGroup中不同的列会各自打包形成DataPack。
每个RowGroup都采用追加写,分属每个列的DataPack也是采用追加写模式。对于一个列索引,只有Active RowGroup负责接受新的写入。当该RowGroup写满之后即冻结,其包含的所有Datapack会转为压缩格保存到磁盘上,同时记录每个数据块的统计信息便于过滤。
列存RowGroup中每新写入一行都会分配一个RowID用于定位,属于一行的所有列都可以用该RowID计算定位,同时系统维护PK到RowID的映射索引,以支持后续的删除和修改操作。
更新操作采用标记删除的方式,对于删除操作直接设置BitMap即可。对于更新操作,首先根据RowID计算出其原始位置并设置删除标记,然后在ActiveRowGroup中写入新的数据版本。
当一个RowGroup中的无效记录超过一定阈值,则会触发后台异步compaction操作,其作用一方面是回收空间,另一方面可以让有效数据存储更加紧凑,提升分析型查询单的效率。
采用这种数据组织方式,一方面满足了分析型查询按列进行批量扫描过滤的要求。另一方面对于TP型事务操作影响非常小,写入操作只需要按列追加写到内存即可,删除操作只需要设置一个删除标记位。而更新操作则是一个标记删除附加一个追加写。列存在支持事务级别更新的同时,几乎不影响OLTP的性能。
全量及增量行转列
以下两种情况会执行行转列操作:
第一种情况:使用DDL语句对部分列创建列索引(对一张已存在的表有分析型需求),此时需要扫描全表数据以创建列索引。
第二种情况:在事务操作过程中对涉及到的列执行行转列。
对于全表行转列的情形,使用并行扫描的方式对InnoDB中的Primary Key进行扫描,并依次将所有涉及到的列转换为列存形式,该操作的速度非常快,其基本仅受限于服务器可用的IO吞吐速度和CPU资源。该操作是一个online-DDL过程,不会阻塞在线业务的运行。
在一张表上建立列索引之后,所有的更新事务将会同步更新行存和列存数据,以保证二者的事务一致性。下图演示了开启和关闭IMCI功能的差异性。
未开启IMCI功能时,事务对所有行的更新都会先加锁,然后在对数据页进行修改,在事务提交之前会对所有加锁的记录一次性释放锁。
开启IMCI功能之后,事务系统会创建一个列存更新缓存,在所有数据页被修改的同时,会记录所涉及到的列存的修改操作,在事务结束并释放锁之前,该缓存会应用到列存系统。
对于一般的OLTP型请求,最后的内存数据页更新只占事务操作过程的很少一部分时间,因此此方法对TP型事务时延的影响非常小。对于操作了非常多的行的大事务,则会直接将其对列索引的更新实时应用到列存存储,但是在事务提交之前不对外可见,也保证了大事务的提交延时增加在一个非常小的时间范围。同时为了更进一步的降低对TP性能的影响,当AP型查询对数据的实时性要求不高时, 列索引支持异步应用对列存的更新操作。
列存存储提供了与行存一样的事务隔离级别。对于每个写操作,RowGroup中的每一行都会记录修改该行的事务编号,而对于DeleteBitMap,每个标记删除操作也会记录该标志位的事务编号。借助写入事务号和删除事务号,AP型查询可以用非常轻量级的方式获得一个全局一致性的快照。
列索引粗糙索引
由前述列举的存储格式可以看出,IMCI中所有的Datapack都采用无序且追加写的方式,因此无法像InnoDB的普通有序索引那样,可以精准的过滤掉不符合要求的数据。在IMCI中,借助统计信息来进行数据块过滤,以此来达到降低数据访问单价的目的。
在每个Active Datapack终结写入的时候,会预先进行计算,并生成Datapack所包含数据的最小值、最大值、数值的总和、空值的个数和记录总条数等信息。所有这些信息会维护在DataPacks Meta元信息区域并常驻内存。由于冻结的Datapack中还会存在数据的删除操作,因此统计信息的更新维护会放到后台完成。
对于查询请求,会根据查询条件将Datapacks分为相关、不相关、可能相关三大类,从而减少实际的数据块访问。而对于一些聚合查询操作,如count和sum等,可以通过预先计算好的统计值进行简单的运算得出,这些数据块甚至都不需要进行解压。
采用基于统计信息的粗糙索引方案,对于一些需要精准定位部分数据的查询并不是很友好。但是在一个行列混合存储引擎中,列索引只需要辅助加速那些会涉及到大量数据扫描的查询,在这个场景下使用列存索引会具有显著的优势。而对于那些只会访问到少量数据的SQL,优化器通常会基于代价模型计算得出基于行存而得到的一个成本更低的方案。
行列混合存储下的TP和AP资源隔离
行列混合存储可以在一个集群中同时支持AP型查询和TP型查询。但很多业务有很高的OLTP型负载,而突发性的OLAP型负载可能干扰到TP型业务的响应时延。因此负载隔离在HTAP数据库中是一个必须支持的功能。借助一写多读架构,可以非常方便地对AP型负载和TP型负载进行隔离。在PolarDB技术架构下,有如下几个部署方式:
第一种方式:在RW上开启行列混合存储,此种部署模式可以支持轻量级的AP查询,以TP负载为主,且AP型请求比较少时可以采用。或者使用PolarDB进行报表查询,且数据来自批量数据导入的场景。
第二种方式:RW支持OLTP型负载,并启动一个AP型RO开启行列混合存储以支持查询,此种部署模式下CPU资源可以实现100%隔离,同时该AP型RO节点上的内存可以100%分配给列存存储和执行器。但是,由于使用相同的共享存储,因此在IO上会相互产生一定影响。
第三种方式:RW和RO均支持OLTP型负载,在单独的Standby节点开启行列混合存储以支持AP型查询,由于Standby是使用独立的共享存储集群,这种方案在第二种方案支持CPU和内存资源隔离的基础上,还可以实现IO资源的隔离。
除上述部署架构上的不同,和可以支持的资源隔离不同之外。在PolarDB内部对于一些需要使用并行执行的大查询,支持动态并行度调整(Auto DOP),这个机制会综合考虑当前系统的负载以及可用的CPU和内存资源,对单个查询所用的资源进行限制,以避免单个查询消耗的资源太多,影响其他请求的处理。
OLAP性能
详情请参见列存索引(IMCI)性能。