翻译:王学姣
审校:李浩、宇亭
责编:宇亭
设计:Yeekin
导 语
本篇是StoneDB学术分享会专栏的第七篇,在上一期里,我们分享了 SAP 在 2012 年发表的《The SAP HANA Database – An Architecture Overview》论文,主要是介绍了 SAP HANA 列式存储引擎的架构设计,该列存引擎利用现代硬件(多 CPU 内核、大容量主内存和缓存),支持数据压缩、数据库内核并行最大化,提供层次结构(hierarchy)专用的数据结构、用于特定领域的语言支持等企业应用所需的数据库扩展。
本期,我们将带大家读一读这篇《Efficient Transaction Processing in SAP HANA Database – The End of a Column Store Myth》,瞧瞧,这个标题就起得很有火药味,SAP 提出列式存储引擎的时候,市面上敢用行列混存做 HTAP 数据库的基本没几个,但是人家竟然敢直接跟你说,SAP HANA 数据库,就是为了结束人们对列式存储的偏见而生的,谁说列式存储不能做事务型工作(OLTP)的?我们 SAP HANA 证明给你看~(当然了,现在列式存储的数据库其实已经很多了,我们的 StoneDB 团队自研的 Tianmu 引擎就是列式存储引擎)
是不是还挺横的?一众数据库界的大佬也坐不住了:你们 SAP 到底哪来的底气?别急,今天就带大家看看这篇经典论文,揭开 SAP HANA 数据库的神秘面纱。
摘要
SAP HANA 数据库是 SAP 新推出的数据管理平台的核心。SAP HANA 数据库的总体目标是为事务型查询和分析型查询场景提供一个通用的、强大的系统,且在高度可扩展的执行环境中为不同的查询场景提供相同的数据表示。本文重点介绍了 SAP HANA 数据库区别于传统关系型数据库引擎的主要特性。因此,在本文中,我们首先概述了 SAP HANA 的总体架构和设计标准。其次,SAP HANA 挑战了人们关于列存数据结构的偏见,即列存储数据结构只在分析型工作负载中表现优异,并不适合事务型工作负载。 我们概述了如何通过管理记录(record)生命周期来使用不同的格式存储不同阶段的同一记录。除了提供概念的解读外,我们还深入探讨了如何在记录的生命周期中对其进行高效同步,以及如何将数据库条目从写优化的存储格式转移到读优化的存储格式的一些细节。总之,本文旨在说明 SAP HANA 数据库如何实现同时在分析型和事务型工作负载环境中进行高效工作。
简介
现代商业应用中的数据管理是当今软件行业面临的最具挑战性的课题之一。现如今,数据不仅推动着业务发展,还为新业务理念或业务案例的发展提供了基础。各种各样的数据管理已经成为企业的核心资产。此外,数据管理作为推动和发展当前业务的主要工具,已经引起了高层管理者的极大关注。在系统方面,数据管理场景变得极其复杂,难以管理。一个高效、灵活、稳健、低成本的数据管理层成为了大量应用场景的核心,而这些应用场景则是当今商业环境中不可或缺的部分。
起初,传统 ERP 系统被用为处理这些应用场景的信息处理中枢。从数据库系统的角度来看,ERP 系统的 OLTP 工作负载通常需要处理成千上万的并发用户和事务,而这些事务常常伴随着高更新负载和选择性极强的点查询(point query)。另一方面,数据仓库系统(对应于 OLTP 系统)要么对基于大量数据进行聚合查询,要么通过计算统计模型来分析存储在数据库中的内容。 然而,新应用的出现,例如用于识别数据流或ETL/信息集成任务中的异常的实时分析,对数据管理层提出了新的需求和挑战。这些需求和挑战要求一个能够处理现代业务应用的数据管理层。
Stonebraker 等人为上述问题提供了一个答案。他们在 2007 年【13】提出了“The End of an Architectural Era (It's Time for a Complete Rewrite)”的观点,表明传统的数据库管理系统不再能够满足变化多样的市场需求。市场需要不同的数据库管理系统用于解决不同的问题。从本质上来说,此观点证实了如下现状:大型数据管理解决方案是一个包含各种系统的集合。这些系统适用于不同的应用场景。例如,行存储仍然主要应用于 OLTP 领域。使用基于实体(entity-based)的交互模型能够在记录中维护逻辑实体和物理表示之间的 1:1 关系。按列组织的数据结构在分析领域获得了越来越多的关注。这种数据结构可以避免查询列的投影(projection),并可以提供更好的压缩效率。键值存储(key-value store)正在逐渐成为商业数据管理解决方案的一部分,用于处理海量数据以及为程序性的代码提供并行执行的平台。此外,分布式文件系统提供了一种经济的存储机制以及云弹性般灵活的并行度,使键值存储成为了数据管理领域的“头等公民”。行存储、列存储以及键值存储完善了大型数据库管理方案,使之能够处理各种模式(schema)的数据和支持基于图形的数据组织形式。由于模式是伴随数据而出现的,系统通常会提供高效的方法来显式地利用实体之间的模型化关系(modeled relationship),运行分析型图算法(analytical graph algorithm),并展示弱实体的仓库(repository)。在市场进行性能尝试初期,专用系统(specialized system)可能被认为是明智之举,但包含各种专用系统的大型数据库解决方案需要解决各个系统之间的连接、数据复制和同步任务问题,以及在多个系统间编排查询场景,使得此类解决方案变得及其复杂。此外,为这类解决方案进行环境配置和维护不仅操作复杂且极易出错,而且总拥有成本(TCO)也十分高昂。
概括来讲,我们对当前形势进行了如下几方面的观察:
使用角度:我们认为 SQL 不再是现代业务应用唯一适用的交互模型。用户要么被应用层这个屏障完全隔开,要么希望直接与其数据库进行交互。在第一种情况下,我们发现需要用紧耦合机制来实现对应用层的最佳支持。在第二种情况下,我们发现需要针对特定领域使用具有内置数据库特性的脚本语言,如用于统计处理的 R(【4】)或用于 Hadoop 安装的 Pig。我们还观察到对特定领域和专有查询语言的全面支持的需求,例如 SAP 提供的用于金融规划场景的 FOX (【7】)。最后,我们还发现了一个巨大的市场需求:从编程的角度考虑并行性,市场需要直接使能用户的机制。
成本意识 : 我们观察到一个很明确的需求,市场需要一个从硬件到安装成本,再到运营和维护成本,为整个数据管理体系提供更低TCO 的解决方案,一个针对不同工作负载类型和使用模式(usage pattern)的融合解决方案。
性能、性能、还是性能(【16】):我们认为性能仍然是使用专用系统的主要原因。当前所面临的挑战是提供一个在任何时候都能够根据需求灵活地应用专门的运算符或数据结构的解决方案。需要指出的是,不同的工作负载特征并不能完全证明包含各类专用系统的大型数据管理解决方案的合理性。我们过去处理业务应用的经验让我们成为了特定算子集合需求假设的支持者。我们对具有独立生命周期和管理设置的单独系统(individual system)存在偏见。我们的目标并不是提供一个单一的封闭系统,而是提供一个使用通用服务原语的、灵活的数据管理平台。当前SAP HANA 数据库的可用功能是 SAP HANA 设备的核心部分(【2】),可被视为这种工具包的一个具体体现。
贡献和概述
本文旨在描述 SAP HANA 数据库平台的全貌,并深入解释一些为处理事务型工作负载而做的工作细节。我们首先概述了 SAP HANA 的整体架构和设计准则,强调了其与传统关系数据库管理系统的不同之处,尤其是SAP HANA 数据库在典型业务应用范围内的以下显著特征:
SAP HANA 数据库包含一个多引擎查询处理环境。 该环境提供支持不同结构化程度的数据的数据抽象,包括高度结构化的关系型数据、到不规则的结构化数据图(irregularly structured data graphs)、再到非结构化的文本数据。处理引擎的整个使用范围基于一个公共表抽象,作为底层物理数据表示,从而保证互操作性以及允许不同类型之间的数据组合。 SAP HANA 数据库支持在数据库引擎中直接表示特定于应用的业务对象(如 OLAP cube)和逻辑(特定领域的函数库) 。这允许通过底层数据管理平台实现应用语义(application semantics)交换,从而提高查询效率,减少单个应用到数据库的往返次数以及数据库和应用之间传输的数据量。 SAP HANA 数据库针对数据管理层和应用层之间的通信进行了性能优化。例如,SAP HANA 数据库原生支持 SAP 应用服务器的数据类型。此外,还计划将新的应用服务器技术直接集成到 SAP HANA 数据库集群的基础架构中,以支持应用逻辑和数据库管理功能的交叉执行。 SAP HANA 数据库通过利用实现高度优化的面向列的数据表示,支持在同一物理数据库上对事务性和分析性工作负载进行高效处理。 该功能的实现依赖于复杂的多步骤记录生命周期管理(multi-step record lifecycle management)。前三个特征我们已经在【2】中进行了探讨,最后一个特征将在本文的第二部分进行讨论。此处我们概述了与统一表结构(unified table structure)和传播机制相关的一些细节,以便通过可控的生命周期管理流程在系统内移动记录(record)。具体来讲,我们首先使用不同数据结构来保存处于生命周期中不同阶段的数据。然后我们重点关注如何高效地实现不同数据结构之间的传播步骤,从而能够将数据从写优化存储中移动到主内存结构(main memory structure)中。写优化存储非常适合处理插入、更新和删除操作,而主内存结构中的数据是高度压缩的数据,适合处理 OLAP 查询。顺便一提,SAP HANA 数据库还提供了运行 SAP ERP 系统等企业关键型应用所需的技术。例如,SAP HANA 数据库展示了 SAP HANA 集群中各个节点的容错能力,即如果一个节点出现故障,其他节点会自动接管负载。在这种情况下,查询处理既不会被阻塞也不会停止,而是由分布式查询执行框架以一种对用户透明的方式重新进行路由。SAP HANA 数据库也支持备份和恢复等功能。从事务行为来看,SAP HANA 数据库使用多版本并发控制(multi-version concurrency control, MVCC)来实现不同级别的事务隔离。SAP HANA 数据库支持事务级和语句级快照隔离。不够由于篇幅限制,我们不会就该点进行展开说明。
SAP HANA 数据库的分层架构
如【2】中所述,SAP HANA 产品已经上市,由一个带有不同组件的设备模型组成,可为数据分析场景提供现成的解决方案。这是SAP HANA 迈出的第一步,旨在在数据集市场景中让客户接受并习惯以列为导向、以主内存为中心的 SAP HANA 数据库的全新理念。截至目前,SAP 正在为 SAP Business Warehouse 产品提供原生支持,以显著加快查询和转换场景,同时允许完全跳过单个物料化步骤。为了提供这种能力,SAP HANA 拥有数据加载和转换工具以及建模工作室,以创建和维护进出 SAP HANA 的复杂数据流。SAP HANA 数据库是SAP HANA 产品的核心组件,负责高效和灵活的数据存储和数据查询场景(【11,12】)。
图1: SAP HANA 设备概述
SAP HANA 数据库本身遵循严格的分层架构,如图 2 所示。与传统系统类似,SAP HANA 数据库区分数据库请求的编译时(compile time)和运行时(runtime)。此图并未包含 SAP HANA 数据库的所有组件,例如交易管理器(transaction manager)、授权管理器(authorization manager)、元数据管理器(metadata manager)等均未体现。
图2: HANA 数据库分层架构概述
除了纯粹的数据处理性能之外,我们还发现应用层和数据管理层之间缺乏恰当的耦合机制。这是当前最先进系统的主要缺陷之一。这成为了驱动我们将 SAP HANA 数据库设计为可针对不同查询语言进行扩展的平台的一个主要因素。如图 2 所示,不同查询语言可以通过普通连接和会话管理层进入系统,实现同外部的交流。在第一步中,查询字符串被翻译成优化之后的内部表示(类似于抽象语法树),这对于每一种领域特定(domain-specific)的语言而言都是本地实现的。第二步,查询表达式被映射到一个“计算图(calculation graph)”,从而构成了逻辑查询处理框架(logical query processing framework)的核心。
2.1 计算图模型
“计算图模型”遵循经典数据流图(data flow graph) 的原则。源节点(source node)是持久表结构或其他计算图的结果表示。内部节点(inner node)表示消耗一个或多个传入数据流并产生任意数量的传出数据流的逻辑运算符。此外,计算图操作符集可以分成两组操作符类型。一方面,计算图模型定义了一组内置运算符(intrinsic operator),包括聚合(aggregation)、投影(projection)、连接(join)、联合(union)等。SQL 可以完全映射到这类操作符。另一方面,计算图模型提供了实现核心业务算法(例如货币转换和日历功能)的操作符。此外,计算图模型支持以下类型的运算符:
动态 SQL 节点(dynamic SQL node) :计算图模型运算符可以对传入的数据流执行完整的 SQL 语句。该语句可以是参数,并且在计算图运行时被编译和执行,从而产生了一种“嵌套计算”模型的形式。
自定义节点(custom node) :出于性能原因,可以使用自定义节点在 C++ 中实现特定领域的运算符。例如,使用 SAP 专有语言 Fox【6】的规划场景可以利用特殊的“分解(disaggregate)”运算符来原生支持财务规划情况【7】。另一个例子是通过专有的 WIPE graph 语言在数据图中进行图遍历和分析优化。
R 节点(R node) :R 节点(【4】)可用于将传入的数据集转发到 R 执行环境。作为参数给出的 R 脚本将在 SAP HANA 数据库之外执行,并将结果移回到计算图中进行进一步处理。
L 节点(L node) :语言 L 代表 SAP HANA 数据库的内部运行时语言。语言 L 被设计成 C 语言的安全子集。通常情况下,终端用户或应用设计者不能直接访问。语言 L 是所有的不能直接映射到数据流图的领域专用语言的结构(constructs)的合集。换言之,语言L 即各种命令式控制逻辑。除了一组功能操作符之外,计算图模型还提供了“拆分(split)”和“组合(combine)”的运算符,以动态定义和重新分配数据流分区,作为支持应用定义(application-defined)的数据并行化的基础构造。
各领域专用语言的编译器都试图优化从查询脚本到计算图的映射。对于 SQL,映射的基础是有明确定义的、查询表达式的逻辑表示。在一般情况下,映射可以使用启发式或者基于代价的策略,这取决于输入数据的预估大小等因素。例如,编译器可能会决定将循环展开成常规数据流图,或者为特定表达式生成 L 代码【6】。在常规 SQL 的情况下,这是迄今为止最大和最复杂的部分,取自 SAP P*Time1 系统(【1】),内部表示直接映射到关系运算符以捕获SQL 语句的意图。
图 3 描绘了一个计算图模型示例。计算图模型可以通过特定领域语言的编译器间接创建,也可以使用 SAP HANA Studio进行可视化建模,并在 SAP HANA 数据库的应用级内容仓库中注册为计算视图(calculation view)。 这一过程背后的总体思想是定制复杂业务逻辑场景的特定段(segment),这些段可以在多个数据库场景中进行微调和重用,与实际的查询语言无关。换言之,计算图模型可以以虚拟表的形式在任何领域特定语言堆栈中被消费。计算图模型的集合也称为 SAP HANA content,并拥有独立的产品生命周期流程。图3 所示的计算模型概述了 SAP HANA 数据库与标准关系型数据库系统在常规查询计划方面的差异。例如,从应用的角度来看,一个操作符的结果可能会有多个消费者(consumer)需要优化共享的公共子表达式。其次,标记为“script”的节点包含了来自计算模型设计器或由特定领域查询编译器生成的命令性语言片段。最后,节点“conv”显示了使用内置业务函数来执行特定应用的转换例程,例如货币转换或单位转换。
图3: SAP HANA 计算模型图示例
2.2 计算图的编译和执行
一旦用户定义的查询表达式或查询脚本被映射到计算图模型中的数据流图,优化器就会运行经典的基于规则或代价的优化步骤,将逻辑计划重构并转换为物理计划,然后由分布式执行框架执行。执行框架继承自之前的 SAP 产品 SAP BWA(业务仓库加速器)和SAP Enterprise Search,用于处理实际数据流和物理操作符的分布式执行。在优化过程中,逻辑数据流图的片段被映射到“引擎层”提供的物理运算符。引擎层本身由一组不同的物理操作符组成。这些物理操作符有着一些局部优化逻辑,从而使全局计划的片段能够适应实际物理操作符的特性。值得说道的是,SAP HANA 数据库提供了以下一套运算符:
关系运算符:关系运算符用于典型的关系查询图处理。如第3.1小节所述,关系运算符拥有不同的特征,例如,equi-join(【5】)等运算符直接利用统一表(unified table)的现有字典。
OLAP 运算符:OLAP 运算符针对具有事实表和维度表的星形连接(star-join)方案进行了优化。一旦优化器识别出这种类型的场景,与之对应的查询计划片段到 OLAP 运算符的映射就会被枚举为具有相应代价估计的可行物理计划(【8】)。
L 运行时间:内部语言的运行时反映了用于执行在指定计算图的L节点中的表示L代码的构造块(building code)。使用“拆分(split)和组合(combine)”运算符对时,可以在预定义的分区上并行调用L运行时。
文本运算符:文本搜索分析操作符集包括 SAP Enterprise Search 产品中可用的功能集,以提供从相似性度量到实体解析能力的综合文本分析特性(【14】)。
图形运算符:图形运算符最终为基于图形的算法提供支持,以实现复杂的资源规划场景或社会网络分析任务。由于数据流图不仅分布在多个服务器实例(通常运行在不同物理节点上)之间,还分布在不同类型的运算符上,因此 SAP HANA 提供了一个工具箱,用于保证最佳的数据传输和交换格式。尽管所有计算符都需要实施标准的数据传输协议,但不同“集合”内外的个别运算符可能拥有高度专业化的通信协议。例如,关系运算符和 OLAP 运算符以高度压缩的专有格式进行数据交换。此外,R 节点提供了一个到 R内部数据帧格式的映射。
除了“横向”交流之外,不同物理运算符直接可以通过一个通用接口接入到统一表层(unified table layer) 。SAP HANA 数据库提供了一个抽象的表格视图,允许不同的计算符通过多种方法进行访问。这一点我们将在下一部分进行详细介绍。该通用表格结构实现了数据实体的完整生命周期,其主要构成部分为行存储和列存储的组合,用于捕获最近的数据修改效果。由于 SAP HANA 数据库中的表可以标记为“历史(historic)”,因此表层(table layer)实现了用于捕获活跃实体(active entity)的过去值的历史表(history table),并提供了访问时间旅行查询(time travel query)的方式。
最后,SAP HANA 的持久层(persistence layer)保证了系统的可恢复性。即使在主内存中的数据库状态丢失的情况下,系统也依然能够实现快速恢复。虚拟文件(virtual file)是持久层的基础。虚拟文件对可视化页面的可配置大小进行了限制。采用 SAP MaxDB 系统的概念,持久层依靠频繁产生的保存点(savepoint)以非常低的资源开销提供一致快照。更多相关细节,将在下一部分介绍。
2.3 总结
与传统数据库系统相比,SAP HANA 数据库旨在利用平台的灵活性来支持多种(专有)领域特定的语言。灵活的数据流模型(计算图模型)提供了概念上的系统核心:一方面,查询表达式或查询脚本被映射到模型实例。另一方面,所有不同物理运算符都使用相同的表层接口(table layer interface),对单个记录实施完整的生命周期管理。日志和数据区被用于在持久存储中维护主内存数据库的事务一致性副本。
数据库记录的生命周期管理
如图 4 所示,统一表结构(unified table structure)允许所有适用的物理运算符进行数据访问。 统一表结构的内在实质是系统为数据库记录提供了生命周期管理。在我们看来,统一表技术是系统能够高性能地同时处理基于扫描的聚合查询和极具选择性的点查询的关键。这成为 SAP HANA 区别于传统(行存)数据库的一个关键差异点。在就地更新(update-in-place)风格的数据库系统中,记录从概念层面来看,在其整个生命周期中保持在同一位置,而 SAP HANA 通过物理表示的不同阶段对记录实现了概念上的传播。虽然是作为一个一般概念设计的,如图 4 所示,在一个常规表中,记录根据生命周期被划分为以下三个阶段:
图4: 统一表概念概述
L1-delta:L1-delta(L1-增量)结构接受所有数据传入请求,并以写优化的方式存储这些请求,即 L1-delta 保留记录的逻辑行格式。该数据结构针对快速插入和删除、字段更新和记录投影进行了优化。此外,L1-delta 结构不会对数据进行压缩。根据经验,单个节点数据库实例中,L1-delta 结构可以存储10,000到100,000行数据。这一容量因数据库实例的工作负载特征和可用内存大小而异。
L2-delta:L2-delta(L2-增量)结构代表了记录在生命周期中的第二个阶段,以列存格式组织数据。与L1-delta 相比,L2-delta 采用字典编码来优化内存利用。然而,出于性能考虑,字典是未排序的,需要使用二级索引结构来实现对点查询访问模式(access pattern)的最佳支持,例如快速执行唯一约束检查。L2-delta 非常适合存储量超过1000万行的场景。
Main store:最后,main store(主存储)表示采用各种不同压缩方案的最高压缩率的核心数据格式。默认情况下,一列中的所有值都通过排序字典中的位置来表示,并以位打包(bit-packing)的方式存储,以便实现各个值的密集打包(【15】)。虽然字典总是使用各种前缀编码方案进行压缩,不同压缩技术的组合——从简单的游程编码方案到更复杂的压缩技术——也被应用于进一步减少占用的主内存(【9,10】)。由于 SAP HANA 数据库最初是为复杂且高容量加载场景的 OLAP 密集型用例而设计的,因此该系统针对高效批量插入提供了特殊处理,这使得此类操作会绕过 L1-delta 直接进入 L2-delta。与输入位置无关,任何输入记录的行 ID(RowId)都在行(row)输入系统时生成。并且,该行的日志在该行第一次出现时生成,无论是在适合常规更新/插入/删除操作的 L1-delta 中,还是在适合大容量装载操作的 L2-delta 中。
3.1 统一表访问
不同数据结构共享一组通用数据类型。统一表访问通过一个带有行迭代器和列迭代器的公共抽象接口实现。两个迭代器都可以是基于字典(dictionary-based)的迭代器。 此外,在遵循经典的 ONC 协议【3】,支持流水线操作,并尽可能减少中间结果的内存需求的基础上,一些物理计算符可以逐记录或者以矢量化的方式(即逐块)拉取记录。其他物理计算符采用“materialize all”策略,以避免查询执行期间的计算符切换所产生的成本。优化器根据逻辑计算图模型来决定如何对不同种类的计算符进行混合使用,即被采用的不同类型的物理运算符将会被无缝集成到最终的查询执行计划中。
对于使用排序字典的操作符,统一表访问接口还通过全局排序字典公开表内容。 在 L1-delta 结构的字典完成计算和 L2-delta 结构的字典完成排序后,两种 delta 结构的字典会被立刻合并至 main store 的字典中。为了实现唯一性约束的有效性验证,统一表为两种 delta 和 main store 提供了反向索引(inverted index)。
记录的生命周期采用的组织方式将单个记录在系统中进行异步传播,而不干扰当前正在事务控制范围内(transactional sphere of control)运行的数据库操作的组织方式。当前的 SAP HANA 数据库系统提供两次转换,我们称之为 “合并步骤(merge step)” :
L1-to-L2-delta Merge:从 L1-delta 到 L2-delta 的转换意味着数据从按行组织变成了按列组织。L1-delta 中的行被拆分成它们对应的列值,然后被逐列插入到 L2-delta 中。在接收端,系统会执行一次 lookup 来识别出字典结构中可能丢失的值,并且可以选择在字典的末尾插入新条目以避免在字典中进行任何重大的重构操作。在第二步中,使用字典编码(append-only 结构)将相应的列值添加到值向量中。这两个步骤可以并行执行,因为要移动的元组的数量是已知的,使得能够在实际插入它们之前在新字典中保留编码。在第三步中,从 L1-delta 中移除已经传播的条目(propagated entry)。所有运行中的操作要么看到的是完整的 L1-delta 和旧的 end-of-delta 边界,要么看到具有 L2-delta 扩展版本的 L1-delta 结构的截断版本(truncated version)。从设计角度来看,从 L1-delta 到 L2-delta 的转换本质上是增量式的,即记录的转换对目标结构的数据重组没有任何影响。
L2-delta-to-main Merge:一个新的 main store 是基于 L2-delta 和现有的 main store 产生的。虽然 L1-to-L2-delta Merge 对运行事务的影响很小,但 L2-delta-to-main Merge 是资源密集型任务,必须在物理级别上仔细安排并进行高度优化。一旦L2-delta-to-main Merge 开始,当前的L2-delta 就会被关闭用于执行更新操作。与此同时,系统会创建一个新的、空的 L2-delta 结构用来作为 L1-to-L2-delta Merge 的目的端。如果 L2-delta-to-main Merge 失败,系统仍然使用新的 L2-delta 用于L1-to-L2-delta Merge,并继续尝试使用旧版本的 L2-delta 和现有的main 进行合并。第 4 章将详述核心算法以及不同的优化技术,例如 4.2 小节会介绍按列合并(column-wise merge),4.3 小节会介绍部分合并(partial merge)。
上述两个合并步骤都不会对持久存储产生直接影响,并且独立于重启或备份日志重放。
3.2 持续性映射
虽然 SAP HANA 数据库是一个以内存为中心的数据库系统,但其全面的 ACID 支持保证了持久性、原子性,以及系统在定期关闭或系统故障后能够重启恢复。 SAP HANA 数据库考虑了多种持久化观点来构架其持久性。一般来说,持久存储无需细粒度的 UNDO 机制,因为只有像新版本的 main 结构这种量级的批量更改(bulk change)才会传播到持久存储,并且在系统故障后必须回滚。如图 5 所示,SAP HANA 的持久性是基于临时 REDO 日志和短期恢复或长期备份的 save pointing 的组合实现的。
图5:统一表的持久性机制概述
当一条新数据进入系统时,无论是在 L1-delta 还是在 L2-delta (批量插入)进行,都只会为其创建一次 REDO 日志。当一条记录的新版本进入 L1-delta 时,系统会为该新版本创建日志。从 L1-delta 到 L2-delta 的增量传播过程中所发生的数据变化不会被写入至REDO 日志中。反而是字典和 value index(值索引)中的变化会被添加到各个数据页中的数据结构中,这些数据结构最终被移入至下一个保存点的持久存储中。显然,合并事件被写入日志,以确保重启后数据库状态的一致性。
图 6 描绘了合并的细节。字典和值索引这两种结构都是基于分页存储布局(paged storage layout)。 该布局由底层存储子系统进行管理。无论是具有附加条目的现有页或新页,但凡是脏页都会被存储子系统清理掉。该功能由保存点基础架构(save pointing infrastructure)来控制。尽管 L2-delta 中的数据是按列组织的,但是系统可以在单个页面内存储多个 L2-delta 的片段,以便优化存储器消耗。特别是对于小而宽的表,这一设计尤为合理。
图6: L1-to-L2-delta Merge 的细节图
在保存点之后,REDO 日志可以被截断。 在恢复过程中,系统重新加载 L2-delta 的最后一个快照。与之类似,main store 的一个新版本会被持久化至稳定存储中,并可用于重新加载统一表的 main store。总之,因为先前版本的映像仍然存在,无论是 L2-delta 还是main store 的变化都不会写入日志。传统的日志方案仅适用于 L1-delta。
3.3 总结
SAP HANA 数据库中的表的物理表示分为三个层级:
L1-delta:用于高效捕获插入、更新和删除请求的行式存储;
L2-delta:用于将写优化存储和读优化存储进行解耦的中间结构,是一个列式存储;
Main store:该结构不仅非常适合类似于 OLAP 的查询,并且还使用倒排索引对点查询性能进行了针对性调优。一条记录在其“一生”之中,最开始通过异步复制保存至更新效率最高的存储中,然后被复制到读取效率最高的存储中度过“余生”。
合并优化
统一表方法的基本思想是通过将记录从写优化的存储结构以透明的方式传播至使用 L2-delta 索引的读优化的存储结构中,从而实现读写存储之间的解耦。 虽然从 L1-delta 到 L2-delta 的转换不会对现有的数据结构产生太大破坏,但 L2-delta 合并至 main store 的操作需要对表格内容进行重大重组。
4.1 经典合并
在经典合并(classic merge)操作的第一步中,L2-delta 的字典条目会被编译到 main 的字典中,从而实现为特定列生成一个排序后的新的 main 字典。新字典仅包含新的 main 的有效条目,丢弃所有被删除和修改过的记录的条目。按字典顺序排序不仅为最佳压缩提供了先决条件,也为特殊计算符能够直接处理字典编码列打下了基础。
图7显示了一次合并步骤涉及到的主要阶段。在第一阶段,系统基于使用未排序字典的 L2-delta 和使用排序字典的旧 main,生成一个新的排序字典,并保存条目新旧位置的映射。 新位置为条目在新字典中的位置;旧位置则为条目在 main 和 L2-delta 内的位置(非显式存储)。如图所示,一些条目同时存在于 L2-delta 和 main 的字典中,例如“Los Gatos”;还有一些条目仅出现在一个词典中,例如“Campbell”在 L2-delta 中的位置为 4,在 main 的字典映射表中的值为 -1。在第二阶段,系统会参考现有条目和新增条目在新字典中的位置构建新的 main index(主索引)。 如图7 中的示例所示,“Daily City”的条目被转移到新 main 中,位置为 4。“Los Gatos”也从 L2-delta 中的位置 1 和旧 main 中的位置 5 映射到了新位置 6。新 main(字典和值索引)写入磁盘,同时旧的数据结构被释放。在任何情况下,系统都必须在主内存中保存列(字典和main index)的新旧两个版本,直到所有引用该列的旧版本的开放事务的数据库操作都已执行完毕。
图7: L2-delta-to-main Merge 的细节图
由于合并操作的初始版(naive version)非常耗费资源,SAP HANA 数据库对其进行了许多优化。例如,如果 L2-delta 的字典是 main 的字典的子集,那么会直接跳过用于生成字典的第一阶段,从而形成了 main 条目的稳定位置。另一种特殊情况是 L2-delta 字典的值大于 main 字典中的值,例如存在增加的时间戳。在这种情况下,如果对字典值进行编码的位数(number of bits)足以应对扩展基数(extended cardinality),则 L2-delta 字典可以直接添加到 main 字典中。
SAP HANA 数据库还使用了正交技术(orthogonal technique)来实现更为复杂的优化,例如下面即将介绍到的重排序合并(re-sorting merge)和部分合并(partial merge)策略。
4.2 重排序合并
L2-delta 和 main 之间的经典合并(详情请参考 4.1)需要条目的新旧位置之间的映射关系。这些位置用于对位打包值索引(bit-packed value index)内的实值(real value)进行编码,即,用 C 来表示一个列中的值去重后的数量,系统需要使用 ⌈ld(C)⌉ 数量的位(bit)来对该位置进行编码。合并操作会将旧的 main 值映射到新字典中的位置上(具有相同或增加的位数),并在新的 value index 的末尾添加 L2-delta 的条目。
合并操作的扩展版本(extended version)旨在重新组织整个表的内容,将所有列进行重新布局,以提供更高的压缩潜能。 由于 SAP HANA 数据库列存储使用了位置寻址方案,因此第 k 条记录中的值必须位于每一列的第 k 个位置。对表中某列进行重新排序会直接影响表中其他列的压缩潜能。根据【9】中讨论的概念,系统在创建新的 main 之前,会根据 main 和 L2-delta 结构的统计信息计算列的“最佳”排序顺序。
图 8 展示了必要的数据结构(data structure)。除了用于字典的映射表(mapping table)将旧字典位置翻译成新字典中的位置之外,重排序合并(re-sorting merge)的版本还创建了行位置的映射表,从而能够在合并和重新排序某行的各个列之后重构该行。图8 显示了合并过程之前和过程中的同一张表中的列,其中列“City”和“Prod”已经合并,其余列(如“Time”)仍然反映合并前的状态。因此,main 的旧版条目对应于旧字典中的位置,例如“City”列的条目“Los Gatos”在旧字典中用值5 编码,而在合并后的版本中用值6 编码。一般来说,在对“City”列进行合并后,新的 main index 显示新词典的位置以及对行进行重新排序。如图 8 中突出显示的那样,现在可以在第二个位置找到第7 行。“Prod”列也被合并,但没有新建字典,保留了字典位置值。然而“Time”栏还没有合并,仍然是指旧字典和旧的排序顺序。如果需要具有已经合并的列的行构造,则对尚未合并的列的任何访问都需要通过行位置映射表采取额外步骤。在所有列的合并完成之后,可以消除行位置映射表。虽然系统可以通过“堆叠(stacking)”行位置映射表在概念上延迟不经常访问的列的合并,但是系统总是在开始新的合并生成之前完成整个表的合并操作。
因此,是否使用重新排序还需要基于成本考虑,用于平衡在所有列的合并期间用于列访问的额外位置映射的开销,以及由此产生的更高压缩率的可能性。将合并应用于各个列的排序标准还取决于多个因素,例如点对范围访问的比率、压缩潜力的提高等。
4.3 部分合并
经典合并或重排合并的主要缺点在于创建一个新版本的 main 所带来的开销。对于大型表或分区,要计算出一个新的字典并重新生成 main index 的确会占用额外的 CPU 和磁盘资源。而部分合并(partial merge)则通过使用以前的算法来缓和这个问题。部分合并策略体现了在字典中新条目数量较少的情况下,列的最佳压缩潜能。
部分合并的核心思想是将 main 拆分成两个或更多的独立 main:
消极 main(active main) :main store 中的稳定部分,通常不参与合并过程。
积极 main(passive main) :列中可动态伸缩、参与和 L2-delta 的合并的部分。
从概念上来看,部分合并策略中的合并间隔始于一个空的积极 main。 消极 main 通过分类词典和相应的值索引(values index)来反映常规 main 结构。当一个合并操作开始执行时,L2-delta 将合并至仍然为空的积极 main,而消极 main 保持不变。与完全合并(full merge)相比,部分合并有一个细微的不同之处。积极 main 的词典以 n+1 的词典位置值开始,而消极 main 的词典则以 n 结束。尽管系统现在有两个带有本地排序字典的 main 结构,但是各个 main value index 结构的编码并不重叠。显然,积极 main 的字典只保存消极 main 的字典中尚未涵盖的新值。
图10 显示了部分合并后一个消极 main 和一个积极 main 的示例情况。积极 main 的字典编码从 n+1 = 6 开始,从而可以延续消极 main 的编码方案。 虽然消极 main 字典对应的value index 结构仅保存对消极main 字典中条目的引用,但是积极 main 字典的 value index 也可以展示main 字典的编码值,使得积极 main 字典依赖于消极 main 字典。
点访问(point access)在消极字典中被解析。 如果找到了所请求的值,则相应的位置被用作消极和积极 main 的 value index 的编码值。执行并行扫描以找到相应的条目。但是,如果没有找到所请求的值,则查询积极 main 的字典。如果该值存在,则只扫描积极main 的 value index,识别出所需的行的位置。对于范围访问(range access),范围会在积极 main 和消极 main 的字典中解析,并且范围扫描在两个 main 上被执行。对于积极 main 的字典,扫描分为两个部分范围(partial range),一个是消极 main 的字典的编码范围值,另一个是积极 main 的字典的编码范围值。图 10 展示了值在C% 和L% 之间的范围查询的合并过程。为了保证事务的一致性,查询处理还要求与 L1-delta 和 L2-delta 进行类似的合并。
当系统运行时,积极 main 可能会动态地收缩和增长,直到一次完全合并被安排好。 这种做法的主要优点是将完全合并延迟到负载较低的时候执行,并且降低了 L2-delta 到 main 或者积极 main 的合并成本。此外,该优化策略可以通过以下方式部署为经典合并的方案:将积极 main 的最大尺寸设置为 0 来强制在每个步骤中进行(经典)完全合并。显然,该过程可以轻松地扩展到多个消极 main 结构,这些消极 main 结构形成了和本地字典的依赖性相关的逻辑链。这种配置非常适合那些字典变化极少或者字典很稳定的列(例如“Customer”表中的“Country”列)。不过对于大多数列来说,系统将仅维持一个消极 main。
从概念上来讲,部分合并优化策略在 SAP HANA 数据库统一表概念的记录生命周期中增加了一个步骤。越接近传输的末端,对记录的重组就越复杂、越耗费时间和资源,最终形成以传统列存储的高度压缩和读取优化格式。 此外,SAP HANA 数据库提供了历史表(historic table)的概念,可以透明地将记录的历史版本移动到单独的表结构中。因此,在建表时,就必须将表定义为“historic”类型。此外,从应用的角度来看,分区(partitioning)概念的应用可以将最近的数据集与稳定的数据集分隔开来。
4.4 总结
如上所述,SAP HANA 数据库通过对记录进行生命周期管理,为事务型和分析型工作负载提供高效访问。图 11 对比了所讨论的不同存储格式和传播的特征。L1-delta 针对更新密集型工作负载进行了优化,并且可以频繁地将增量数据合并到 L2-delta 结构中。L2-delta 结构已经针对读取操作进行了很好的调优,但是与高度读取优化的 main 结构相比,它产生了更大的内存占用。然而,L2-delta 特别适合作为 L1-delta 行或者批量插入的目标。如前所述,main 结构可以被分为积极 main 和消极 main,表现出极高的压缩率,并且针对基于扫描的查询模式(query pattern)进行了优化。由于资源密集型重组任务,与积极 main 结构的合并,尤其是创建新的 main 结构的完全合并的频率非常低。相比之下,L1-delta 到 L2-delta 的合并可以通过将数据附加到 L2-delta 的字典和 value index 中递增进行。
众所周知,列存储系统可以为 OLAP 型工作负载提供卓越的性能。 通常而言,列式数据布局极其适合那些涉及数百万行却仅仅需要其中几列数据的聚合查询。但是,使用不同的系统处理 OLAP 和 OLTP 查询的方案不再满足现代业务应用的最新需求。 主要原因可归纳为两方面:一方面,运营系统将越来越多的即时业务决策统计操作嵌入到个人业务流程中;另一方面,传统的数据仓库基础设施需要捕获事务流(transactions feed)以进行实时分析。在本文中,我们基于 SAP HANA 数据库的经典列存储架构,概述了涵盖查询转换、计划生成和不同专用引擎交互模型的查询处理环境。此外,我们更详细地解释了通用统一表数据结构。这种由不同状态组成的数据结构为消费查询引擎提供了一个通用接口。本文的总体目标是介绍一些在 SAP HANA 数据库中实施的优化措施,使得列存储适用于大规模事务处理,纠正人们关于列存技术仅用于 OLAP 型工作负载的偏见。
致谢
我们真诚感谢位于沃尔多夫、首尔、柏林和帕洛阿托的 SAP HANA 数据库团队,感谢他们让 HANA 的故事成为现实。
如果您对我们的源码感兴趣,欢迎到我们的 GitHub 代码仓库阅读查看,觉得不错记得点个 Star 哦~
StoneDB 代码仓库:https://github.com/stoneatom/stonedb
StoneDB 社区官网:https://stonedb.io/
END