(论文阅读)TiDB:一款基于Raft的HTAP数据库

引言

混合事务分析处理(HTAP)数据库要求隔离处理事务查询和分析查询,以消除它们之间的干扰。要实现这一点,有必要维护为这两种查询类型指定的数据的不同副本。然而,为存储系统中的分布式副本提供一致的视图是一项挑战,在存储系统中,分析请求可以大规模地、高可用性地从事务工作负载中高效地读取一致的新数据。

为了应对这一挑战,我们建议扩展基于复制状态机的一致性算法,为HTAP工作负载提供一致的副本。基于这种新颖的思想,我们提出了一个基于raft的HTAP数据库:TiDB。在数据库中,我们设计了一个由行存储和列存储组成的multi-Raft存储系统。行存储是基于Raft算法构建的。它是可伸缩的,可以实现高可用性事务请求的更新。特别是,它异步地将Raft日志复制到learners中,learners将元组的行格式转换为列格式,形成一个可实时更新的列存储。列存储允许分析查询有效地读取新鲜且一致的数据,并且与行存储上的事务具有很强的隔离性。基于该存储系统,我们构建了一个SQL引擎来处理大规模分布式事务和昂贵的分析查询。SQL引擎最佳地访问行格式和列格式的数据副本。我们还包括一个强大的分析引擎TiSpark,以帮助TiDB连接到Hadoop生态系统。综合实验表明,TiDB在一个专注于HTAP工作负载的基准测试下实现了各自的高性能。

1 介绍

关系数据库管理系统(RDBMS)因其关系模型、强大的事务保证和SQL接口而流行。它们在传统应用程序(如业务系统)中被广泛采用。但是,老的RDBMS不提供可伸缩性和高可用性。因此,在21世纪初,互联网应用程序更喜欢NoSQL系统,如谷歌Bigtable和DynamoDB。NoSQL系统放宽了一致性要求,并提供了高可伸缩性和可替代的数据模型,如键值对、图和文档。然而,许多应用程序还需要强大的事务、数据一致性和SQL接口,因此出现了NewSQL系统。像CockroachDB和谷歌Spanner这样的NewSQL系统为联机事务处理(OLTP)读/写工作负载提供了NoSQL的高可伸缩性,并且仍然为事务保持ACID保证。此外,基于sql的联机分析处理(OLAP)系统正在迅速发展,就像许多SQL-on-Hadoop系统一样。

这些系统遵循“one size does not fit all”范式,为OLAP和OLTP的不同目的使用不同的数据模型和技术。然而,开发、部署和维护多个系统的成本非常高。此外,实时分析最新版本的数据也很有吸引力。这在工业界和学术界产生了混合OLTP和OLAP (HTAP)系统。HTAP系统应该像NewSQL系统一样实现可伸缩性、高可用性和事务一致性。此外,HTAP系统需要有效地读取最新数据,以保证OLTP和OLAP请求在新鲜度和隔离性两个额外要求下的吞吐量和延迟。

新鲜度表示分析查询如何处理最近的数据。实时分析最新数据具有巨大的商业价值。但是,在一些HTAP解决方案中,例如基于提取-转换-加载(ETL)处理的解决方案,不能保证这一点。通过ETL进程,OLTP系统定期刷新一批最新的数据到OLAP系统。ETL耗时数小时或数天,因此无法提供实时分析。ETL阶段可以通过将最新的更新流式传输到OLAP系统来取代,以减少同步时间。然而,由于这两种方法缺乏全局数据治理模型,因此考虑一致性语义更为复杂。与多个系统接口会带来额外的开销。

隔离性指的是为单独的OLTP和OLAP查询保证隔离的性能。一些内存数据库(如HyPer)允许分析查询从同一服务器上的事务处理中读取最新版本的数据。尽管这种方法提供了新的数据,但它不能同时实现OLTP和OLAP的高性能。这是由于数据同步损失和工作负载干扰造成的。通过在HyPer和SAP HANA上运行HTAP基准测试CH-benCHmark来研究这种影响。研究发现,当系统协同运行分析查询时,其可达到的最大OLTP吞吐量会显著降低。SAP HANA吞吐量降低了至少三倍,HyPer降低了至少五倍。在MemSQL中也得到了类似的结果。此外,如果内存数据库仅部署在单个服务器上,则无法提供高可用性和可伸缩性。

为了保证独立的性能,有必要在不同的硬件资源上运行OLTP和OLAP请求。主要的困难是在单个系统中维护来自OLTP工作负载的OLAP请求的最新副本。此外,系统需要在多个副本之间保持数据一致性。注意,维护一致的副本也是可用性所必需的。高可用性可以使用众所周知的共识算法来实现,例如Paxos和Raft。它们基于复制状态机来同步副本。可以扩展这些一致性算法,为HTAP工作负载提供一致的副本。据我们所知,这个想法以前还没有被研究过。

根据这个想法,我们提出了一个基于raft的HTAP数据库:TiDB。它为Raft共识算法引入了专用节点(称为learners)。Learners异步复制来自leader节点的事务日志,为OLAP查询构造新的副本。特别是,learners将日志中的行格式元组转换为列格式,以便副本更适合于分析查询。这种日志复制对运行在领导节点上的事务性查询的开销很小。而且,这种复制的延迟非常短,可以保证OLAP的数据新鲜度。我们使用不同的数据副本分别处理OLAP和OLTP请求,以避免它们之间的干扰。我们还可以基于行格式和列格式的数据副本来优化HTAP请求。TiDB基于Raft协议,提供高可用性、可伸缩性和数据一致性。

TiDB提供了一种创新的解决方案,帮助基于共识算法的NewSQL系统进化为HTAP系统。NewSQL系统通过复制其数据库(如谷歌Spanner和CockroachDB)来确保OLTP请求的高可用性、可伸缩性和数据持久性。它们通过来自共识算法的复制机制在数据副本之间同步数据。基于日志复制,NewSQL系统可以提供专用于OLAP请求的列式副本,这样它们就可以像TiDB一样隔离地支持HTAP请求。我们的贡献总结如下。

  • 我们提出建立一个基于共识算法的HTAP系统,并实现了一个基于raft的HTAP数据库TiDB。它是一个开源项目,为HTAP工作负载提供高可用性、一致性、可伸缩性、数据新鲜度和隔离性。
  • 我们将learner角色引入到Raft算法中,为实时OLAP查询生成列式存储。
  • 我们实现了一个multi-Raft存储系统,并优化了它的读写,以便系统在扩展到更多节点时提供高性能。
  • 我们为大规模HTAP查询定制了一个SQL引擎。引擎可以最佳地选择使用基于行的存储和列式存储。
  • 我们进行了全面的实验,使用HTAP基准测试来评估TiDB在OLTP、OLAP和HTAP方面的性能。

本文的其余部分组织如下。我们在第2节中描述了主要思想——基于raft的HTAP,并在第3节中说明了TiDB的体系结构。TiDB的multi-Raft存储和HTAP引擎将在第4节和第5节详细介绍。实验评估在第6节中提出。我们在第7节中总结了相关工作。最后,我们在第8节对本文进行总结。

2 基于RAFT的HTAP

Raft和Paxos等共识算法是构建一致、可扩展和高可用性分布式系统的基础。它们的优势在于,可以使用复制状态机在服务器之间实时可靠地复制数据。我们调整了这个功能,以便针对不同的HTAP工作负载将数据复制到不同的服务器上。通过这种方式,我们保证OLTP和OLAP工作负载彼此隔离,而且OLAP请求具有最新的一致的数据视图。据我们所知,以前没有使用这些共识算法来构建HTAP数据库的工作。

由于Raft算法被设计为易于理解和实现,因此我们将重点放在实现可用于生产上的HTAP数据库的Raft扩展上。在高层次上,我们的想法如下:使用行格式将数据存储在多个Raft组中,以提供事务性查询。每个小组由一个领导和追随者组成。我们为每个组添加了一个learner角色,异步复制来自leader的数据。这种方法开销低,并且保持数据一致性。复制到learner的数据被转换为基于列的格式。扩展了查询优化器,以探索访问基于行和基于列的副本的物理计划。

在标准的Raft组中,每个follower都可以成为leader来处理和编写请求。因此,简单地增加更多的追随者不会独立资源。此外,添加更多的追随者将影响组的性能,因为leader必须等待来自更大的quorum节点的响应才能响应客户机。因此,我们在Raft共识算法中引入了一个learner角色。learner不参与leader选举,也不是日志复制的法定人数的一部分。从leader到learner的日志复制是异步的;在响应客户端之前,leader不需要等待成功。在读取时,leader和learner之间的强一致性被强制执行。通过设计,leader和learner之间的日志复制滞后很低。

事务性查询需要高效的数据更新,而连接或聚合等分析查询需要读取列的子集,但这些列需要读取大量的行。基于行的格式可以利用索引有效地服务于事务性查询。基于列的格式可以有效地利用数据压缩和矢量化处理。因此,在复制到Raft learner时,数据从基于行的格式转换为基于列的格式。此外,learner可以部署在单独的物理资源中。因此,事务查询和分析查询在隔离的资源中处理。
在这里插入图片描述

图-Raft组中增加列存learners角色

我们的设计还提供了新的优化机会。由于数据在基于行的格式和基于列的格式之间保持一致,因此我们的查询优化器可以生成访问其中一个或两个存储的物理计划。

我们提出了扩展Raft以满足HTAP数据库的新鲜度和隔离性要求的想法。为了使HTAP数据库能用于生产,我们克服了许多工程挑战,主要包括:

  1. 如何构建一个可扩展的Raft存储系统来支持高度并发的读/写?如果数据量超过了Raft算法管理的每个节点上的可用空间,我们需要一个分区策略来在服务器上分发数据。此外,在基本的Raft流程中,请求是顺序处理的,任何请求都必须经过Raft节点的仲裁批准才能响应客户端。此过程涉及网络和磁盘操作,因此非常耗时。这种开销使得leader成为处理请求的瓶颈,特别是在大型数据集上。
  2. 如何以低延迟将日志同步到learner中以保持数据新鲜?正在进行的事务可能会生成一些非常大的日志。这些日志需要在learner中快速重播和具体化,以便可以读取新的数据。将日志数据转换为列格式可能会由于模式不匹配而遇到错误。这可能会延迟日志同步。
  3. 如何在保证性能的情况下高效地处理事务性和分析性查询?大型事务性查询需要读写分布在多个服务器上的大量数据。分析查询也消耗大量资源,不应该影响在线事务。为了减少执行开销,他们还需要在行格式存储和列格式存储上选择最优计划。

在下面的部分中,我们将详细介绍TiDB的设计和实现,以解决这些挑战。

3 架构

在本节中,我们将描述TiDB的高级结构。TiDB支持MySQL协议,可以被MySQL兼容的客户端访问。它有三个核心组件:分布式存储层、Placement Driver(PD)和计算引擎层。
在这里插入图片描述

图-TiDB架构

分布式存储层由行存储(TiKV)和列存储(TiFlash)组成。逻辑上,存储在TiKV中的数据是一个有序的键值映射。每个元组被映射成一个键值对。键由它的表ID和行ID组成,值是实际的行数据,其中表ID和行ID是唯一的整数,行ID来自主键列。例如,包含四列的元组被编码为:

Key:{table{tableID} record{rowID}}
Value: {col0, col1, col2, col3}

为了向外扩展,我们采用范围分区策略,将大的键值映射拆分为许多连续的范围,每个范围称为一个Region。每个区域都有多个副本,以实现高可用性。Raft共识算法用于保持每个Region的副本之间的一致性,形成Raft组。不同Raft组的领导将数据从TiKV异步复制到TiFlash。TiKV和TiFlash可以部署在单独的物理资源中,从而在处理事务查询和分析查询时提供隔离。

Placement Driver(PD)负责管理Regions,包括提供每个键的Region和物理位置,并自动移动Region以平衡工作负载。PD也是我们的时间戳oracle(TSO),提供严格递增和全局唯一的时间戳。这些时间戳也用作我们的事务ID。为了增强鲁棒性和性能,PD可以包含多个PD成员。PD没有持久状态,并且在启动时,PD成员从其他成员和TiKV节点收集所有必要的数据。

计算引擎层是无状态的,是可扩展的。我们定制的SQL引擎有一个基于成本的查询优化器和一个分布式查询执行器。TiDB实现了一个基于Percolator的两阶段提交(2PC)协议,以支持事务处理。查询优化器可以根据查询最优地选择从TiKV和TiFlash中读取。

TiDB的体系结构满足HTAP数据库的要求。TiDB的每个组件都设计为具有高可用性和可伸缩性。存储层使用Raft算法来实现数据副本之间的一致性。TiKV和TiFlash之间的低延迟复制使分析查询可以获得新数据。查询优化器以及TiKV和TiFlash之间的强一致性数据提供了快速的分析查询处理,对事务处理的影响很小。

除上述组件外,TiDB还集成了Spark,有助于将TiDB中的数据与HDFS (Hadoop Distributed File System)进行集成。TiDB有一组丰富的生态系统工具,用于向TiDB导入数据和从TiDB导出数据,以及将数据从其他数据库迁移到TiDB。

在下面几节中,我们将深入研究分布式存储层、SQL引擎和TiSpark,以演示TiDB(一个可用于生产的HTAP数据库)的功能。

4 Multi-Raft 存储

上图显示了TiDB中分布式存储层的体系结构,其中具有相同形状的对象扮演相同的角色。存储层由基于行的存储TiKV和基于列的存储TiFlash组成。存储将一个大的表映射成一个大的键值映射,这个键值映射被分成许多Region,存储在TiKV中。每个Region使用Raft一致性算法来保持副本之间的一致性,以实现高可用性。在将数据复制到TiFlash时,可以将多个Region合并为一个分区,以方便进行表扫描。TiKV和TiFlash之间的数据通过异步日志复制保持一致。由于多个Raft组在分布式存储层中管理数据,因此我们称之为multi-Raft存储。在接下来的部分中,我们将详细描述TiKV和TiFlash,重点介绍使TiDB成为可用于生产的HTAP数据库的优化。
在这里插入图片描述

图-multi-Raft存储架构

4.1 行存储(TiKV)

TiKV部署由许多TiKV服务器组成。使用Raft在TiKV服务器之间复制Regions。每个TiKV服务器都可以是不同Region的Raft leader或follower。在每个TiKV服务器上,数据和元数据被持久化到RocksDB,这是一个可嵌入的、持久化的键值存储。每个Region都有一个可配置的最大大小,默认96 MB。Raft leader的TiKV服务器处理相应Region的读/写请求。

当Raft算法响应读写请求时,基本的Raft过程在leader和follower之间执行:

(1)Region leader接收来自SQL引擎层的请求。

(2)leader将请求追加到它的日志中。

(3)leader将新的日志条目发送给其followers,follower又将这些条目附加到他们的日志中。

(4)leader等待它的followers做出反应。如果仲裁节点成功响应,则leader提交请求并在本地应用它。

(5)leader将结果发送给客户端,并继续处理传入的请求。

这个过程保证了数据的一致性和高可用性。但是,它不能提供高效的性能,因为这些步骤是顺序发生的,并且可能会导致大量的I/O开销(磁盘和网络)。下面的部分描述了我们如何优化这个过程以实现高读/写吞吐量。

4.1.1 Leaders和Followers之间的优化

在上述流程中,第二步和第三步可以并行进行,因为它们之间不存在依赖关系。因此,leader在本地追加日志的同时,将日志发送给followers。如果在leader上追加日志失败,但有一定数量的follower成功追加日志,则仍然可以提交日志。在第三步中,当向followers发送日志时,leader缓冲日志条目并批量发送给其followers。发送日志后,leader不必等待followers的响应。相反,它可以假设成功,并使用预测的日志索引发送进一步的日志。如果出现错误,leader调整日志索引,重新发送复制请求。在第四步中,应用已提交日志条目的leader可以由另一个线程异步处理,因为在这个阶段不存在一致性风险。基于以上优化,Raft流程更新如下:

(1)leader接收来自SQL引擎层的请求。

(2)leader将相应的日志发送给follower,并在本地并行追加日志。

(3)leader继续接收来自客户端的请求并重复步骤(2)。

(4)leader提交日志并将它们发送给另一个线程来应用。

(5)leader应用日志后,将结果返回给客户端。

在这个最佳过程中,来自客户端的任何请求仍然运行所有Raft步骤,但是来自多个客户端的请求是并行运行的,因此总体吞吐量增加了。

4.1.2 加速来自客户端的读请求

从TiKV leaders读取数据具有可线性化的语义。这意味着当一个值在时间t从一个Region leader读取时,leader不能返回t之后读取请求的值的先前版本。这可以通过使用如上所述的Raft来实现:为每个读请求发出一个日志条目,并在返回之前等待该条目被提交。然而,这个过程是昂贵的,因为日志必须在Raft组中的大多数节点上复制,从而导致网络I/O的开销。为了提高性能,我们可以避免日志同步阶段。

Raft保证一旦leader成功写入其数据,leader可以响应任何读请求,而无需跨服务器同步日志。但是,在leader选举之后,leader角色可能会在Raft组中的服务器之间移动。为了实现对leader的读取,TiKV实现了以下读取优化。

第一种方法称为读索引。当leader响应读请求时,它将当前提交索引记录为本地读索引,然后向follower发送心跳消息以确认其leader角色。如果它确实是leader,那么一旦它的应用索引大于或等于读索引,它就可以返回该值。这种方法提高了读性能,尽管它会带来一点网络开销。

另一种方法是租约读取,它减少了由读索引引起的心跳的网络开销。leader和follower约定一个租期,在租期内follower不发出选举请求,这样leader就不会被改变。在租期内,leader可以在不连接follower的情况下响应任何读请求。如果每个节点上的CPU时钟相差不大,这种方法可以很好地工作。

除了leader之外,follower还可以响应来自客户端的读取请求,称为follower read。当follower收到一个读请求后,它会向leader请求最新的读索引。如果本地应用的索引等于或大于读索引,则follower可以将该值返回给客户端;否则,它必须等待应用日志。Follower read可以减轻热点区域leader的压力,从而提高读性能。然后可以通过添加更多的follower来进一步提高读取性能。

4.1.3 管理海量Regions

海量Regions分布在服务器集群上。服务器和数据大小是动态变化的,region可能在一些服务器中聚集,尤其是leader副本。这导致一些服务器的磁盘过度使用,而其他服务器的磁盘是空闲的。此外,服务器可能会被添加到集群或从集群中移出。

为了平衡跨服务器的Regions,Placement Driver (PD)在调度Regions时限制了副本的数量和位置。一个关键的约束是在不同的TiKV实例上放置至少三个Region副本,以确保高可用性。通过心跳从服务器收集特定信息来初始化PD。它还监视每个服务器的工作负载,并在不影响应用程序的情况下将热Regions迁移到不同的服务器。

另一方面,维护大量区域涉及发送心跳和管理元数据,这可能会导致大量的网络和存储开销。但是,如果Raft组没有任何工作负载,则不需要心跳。根据区域工作负载的繁忙程度,我们可以调整发送心跳的频率。这减少了遇到网络延迟或节点过载等问题的可能性。

4.1.4 动态Region拆分与合并

一个大的Region可能会变得太热,在合理的时间内无法读写。热Region或大Region应该分割成更小的Region,以便更好地分配工作负载。
另一方面,有可能许多Region很小,很少有人访问;但是,系统仍然需要维护心跳和元数据。在某些情况下,维护这些小Regions会导致大量的网络和CPU开销。因此,有必要合并较小的Regions。注意,为了保持Regions之间的顺序,我们只合并键空间中相邻的区域。根据观察到的工作负载,PD动态地向TiKV发送分割和合并命令。

拆分操作将一个Region划分为几个新的、更小的Region,每个Region覆盖原Region中连续的键范围。覆盖最右边范围的Region重用原始Region的Raft组。其他Region使用新的Raft组。拆分进程类似于Raft进程中的普通更新请求:

  1. PD向一个Region的leader发出一个split命令。
  2. leader接收到split命令后,将该命令转换为日志,并将该日志复制到所有follower节点。日志中只包含split命令,不修改实际数据。
  3. 一旦仲裁复制了日志,leader就会提交split命令,并将该命令应用于Raft组中的所有节点。应用过程包括更新原始Region的范围和epoch元数据,并创建新的Region以覆盖剩余的范围。请注意,该命令是自动应用并同步到磁盘的。
  4. 对于分割Region的每个副本,将创建一个Raft状态机并开始工作,形成一个新的Raft组。原始Region的leader将拆分结果报告给PD。分割过程完成。

注意,当大多数节点提交分割日志时,分割进程成功。类似于提交其他Raft日志,而不是要求所有节点完成分割Region。在分裂之后,如果对网络进行了分区,那么具有最近epoch的节点组将获胜。Region分割的开销很低,因为只需要更改元数据。在分割命令完成后,由于PD的常规负载平衡,新分割的Regions可能会在服务器之间移动。

合并两个相邻的Regions与拆分一个Region相反。PD移动两个Region的副本,将它们放在不同的服务器上。然后,通过两个阶段的操作在每个服务器上本地合并两个Region的相同副本;即停止一个Region的业务,并与另一个Region合并。这种方法不同于分割一个Region,因为它不能在两个Raft组之间使用日志复制过程来同意合并它们。

4.2 列存储(TiFlash)

尽管我们如上所述优化了TiKV的读取数据,但TiKV中的行格式数据并不适合快速分析。因此,我们将列存储(TiFlash)合并到TiDB中。
TiFlash由learner节点组成,learner节点只接收来自Raft组的Raft日志,并将行格式的元组转换为列数据。它们不参与Raft协议来提交日志或选举leader,因此它们对TiKV的开销很小。

用户可以使用SQL语句为一个表建立一个列格式的副本:

ALTER TABLE x SET TiFLASH REPLICA n;

其中x是表的名称,n是副本的数量。缺省值为1。

添加列副本类似于向表添加异步列索引。TiFlash中的每个表被划分为许多分区,每个分区覆盖连续的元组范围,与TiKV中的几个连续Region一致。较大的分区便于范围扫描。

初始化TiFlash实例时,相关Regions的Raft leader开始将其数据复制给新的learners。如果需要快速同步的数据太多,则leader发送其数据的快照。初始化完成后,TiFlash实例开始监听来自Raft组的更新。learner节点接收到日志包后,将日志应用到本地状态机,包括重放日志、转换数据格式和更新本地存储中的引用值。

在下面的部分中,我们将说明TiFlash如何有效地应用日志并与TiKV保持一致的视图。

4.2.1 日志重放

根据Raft算法,learner节点接收到的日志是线性化的。为了保持已提交数据的线性语义,它们按照先进先出(FIFO)策略进行重放。日志重放有三个步骤:

(1)压缩日志:根据后面描述的事务模型,事务日志分为三种状态:预写、提交或回滚。回滚日志中的数据不需要写入磁盘,因此压缩进程根据回滚日志删除无效的预写日志,并将有效的日志放入缓冲区。

(2)解码元组:缓冲区中的日志被解码成行格式的元组,删除有关事务的冗余信息。然后,将解码的元组放入行缓冲区中。

(3)转换数据格式:如果行缓冲区中的数据大小超过大小限制或其持续时间超过时间间隔限制,则将这些行格式元组转换为列数据并写入本地分区数据池。转换引用本地缓存的模式,这些模式将定期与TiKV同步。

为了说明日志重放过程的细节,请考虑以下示例。我们将每个Raft日志项抽象为事务ID-操作类型[事务状态][@start ts][#commit ts]操作数据。根据典型的DMLs,操作类型包括插入、更新和删除元组。事务状态可以是预写、提交或回滚。操作数据可以是特定插入或更新的元组,也可以是删除的键。

在下表示例中,原始日志包含8个条目,它们试图插入两个元组、更新一个元组和删除一个元组。但是插入k1会回滚,因此只保留八个原始日志项中的六个,从中解码三个元组。最后,将三个解码元组转换为五列:操作类型、提交时间戳、键和两列数据。这些列被附加到DeltaTree中。
在这里插入图片描述

表:日志重放及解码

4.2.2 模式同步

为了将元组实时转换为列存格式,learner节点必须了解最新的模式。这种模式过程与TiKV上的无模式操作不同,后者将元组编码为字节数组。最新的模式信息存储在TiKV中。为了减少TiFlash向TiKV请求最新模式的次数,每个learner节点维护一个模式缓存。缓存通过模式同步器与TiKV的模式同步。如果缓存的模式过期,则解码的数据与本地模式之间存在不匹配,必须重新转换数据。在模式同步的频率和模式不匹配的数量之间存在一种权衡。我们采取两阶段策略:

  • 定期同步:模式同步器定期从TiKV获取最新的模式,并将更改应用到其本地缓存。在大多数情况下,这种常规同步减少了模式同步的频率。
  • 强制同步:如果模式同步器检测到一个不匹配的模式,它会主动从TiKV获取最新的模式。当元组和模式之间的列数不同,或者列值溢出时,就会触发此问题。
4.2.3 列存Delta Tree

为了高效地读写高吞吐量的列式数据,我们设计了一个新的列式存储引擎DeltaTree,它可以立即追加增量更新,然后将它们与每个分区之前的稳定版本合并。增量更新和稳定数据分别存储在DeltaTree中。在稳定空间(Stable space)中,分区数据以块(Chunk)的形式存储,每个块覆盖较小范围的分区元组。此外,这些行格式的元组是逐列存储的。相反,增量是按照TiKV生成它们的顺序直接附加到增量空间(Delta space)中的。TiFlash中列存数据的存储格式类似于Parquet。它还将行组存储到列块中。不同的是,TiFlash将行组的列数据及其元数据存储到不同的文件中以并发更新文件,而不是像Parquet只存储一个文件。TiFlash只是使用常见的LZ4压缩来压缩数据文件,以节省磁盘大小。
在这里插入图片描述

图-列存delta tree

新传入的增量是插入数据或删除范围的原子批处理。这些增量缓存在内存中并物化到磁盘中。它们按顺序存储,因此它们实现了预写日志(write-ahead log, WAL)的功能。这些增量通常存储在许多小文件中,因此在读取时会产生很大的IO开销。为了降低成本,我们定期将这些小的增量压缩成一个较大的增量,然后将较大的增量刷新到磁盘,并替换之前具体化的小增量。传入增量的内存副本有助于读取最新数据,如果旧增量达到有限大小,则将其删除。

当读取某些特定元组的最新数据时,有必要将所有增量文件与其稳定元组合并(即读放大),因为相关增量分布的位置事先不知道。由于要读取大量文件,这样的过程开销很大。此外,许多增量文件可能包含无用的数据(即空间放大),这会浪费存储空间并减慢将它们与稳定元组合并的速度。因此,我们周期性地将这些增量合并到稳定空间中。每个增量文件及其相关块被读入内存并合并。增量中插入的元组被添加到稳定元组中,修改的元组替换原始元组,删除的元组被移动。合并后的块会自动替换磁盘中的原始块。

合并增量是昂贵的,因为相关的键在增量空间中是无序的。这种混乱也减慢了delta与稳定块的集成,从而为读请求返回最新的数据。因此,我们在增量空间的顶部建立一个B+树索引。更新的每个增量项按键和时间戳顺序插入到B+树中。这个顺序优先级有助于在响应读请求时有效地定位一系列键的更新,或者在增量空间中查找单个键。此外,B+树中的有序数据很容易与稳定块合并。

我们进行了一个微观实验,将DeltaTree的性能与TiFlash中的日志结构合并(LSM)树进行比较,在TiFlash中,根据Raft日志更新数据时读取数据。我们设置了三个TiKV节点和一个TiFlash节点,硬件配置在实验部分列出。我们在TiKV上运行唯一的写工作负载Sysbench,并在TiFlash上运行“select count(id),count(k) from sbtest1”。为了避免数据压缩带来的大量写入放大,我们使用通用压缩而不是级别压缩来实现LSM存储引擎。ClickHouse(一个面向列的OLAP数据库)也采用了这种实现。

如下表所示,无论元组是1亿个还是2亿个,以及事务性工作负载如何,从delta tree读取的速度都比LSM树快两倍左右。这是因为在delta tree中,每次读取最多访问B+树中索引的一层增量文件,而它访问LSM树中更多重叠的文件。在不同的写工作负载下,性能几乎保持稳定,因为增量文件的比例几乎相同。虽然DeltaTree(16.11)的写放大比LSM树(4.74)大,但也是可以接受的。
在这里插入图片描述

表-DeltaTree和LSM tree的读性能

4.2.4 读过程

与follower read一样,learner节点提供快照隔离,因此我们可以在特定时间戳从TiFlash读取数据。在收到读取请求后,learner向其leader发送读取索引请求,以获取包含所请求时间戳的最新数据。作为回应,leader将指定的日志发送给learner,learner重放并存储日志。将日志写入DeltaTree后,将从DeltaTree读取特定数据以响应读取请求。

5 HTAP 引擎

为了处理大规模事务和分析查询,我们提供了一个SQL引擎来评估事务和分析查询。SQL引擎采用Percolator模型在分布式集群中实现乐观锁定和悲观锁定。SQL引擎通过使用基于规则和成本的优化器、索引和将计算下推到存储层来加速分析查询。我们还实现了TiSpark与Hadoop生态系统的连接,增强了OLAP能力。HTAP请求可以在独立的存储区和引擎服务器中单独处理。特别是,SQL引擎和TiSpark受益于同时使用行和列存储以获得最佳结果。

5.1 事务处理

TiDB提供具有快照隔离(SI)或可重复读取(RR)语义的ACID事务。SI允许事务中的每个请求读取数据的一致版本。RR意味着事务中的不同语句可能对同一个键读取不同的值,但是重复读取(即使用相同时间戳的两次读取)将始终读取相同的值。我们的实现基于多版本并发控制(MVCC),避免了读写锁定并防止写写冲突。

在TiDB中,事务在SQL引擎、TiKV和PD之间进行协作。每个组件在交易过程中的职责如下:

  • SQL引擎: 协调事务。它接收来自客户端的写和读请求,将数据转换为键值格式,并使用两阶段提交(2PC)将事务写入TiKV。
  • PD: 管理逻辑Regions和物理位置;提供全局严格递增的时间戳。
  • TiKV: 提供分布式事务接口,实现MVCC,并将数据持久化到磁盘。

TiDB实现了乐观锁和悲观锁。它们改编自Percolator模型,该模型选择一个键作为主键,并用它来表示事务的状态,并使用base 2PC来执行事务。乐观事务的流程如下图所示。(为简单起见,该图忽略了异常处理。)
在这里插入图片描述

图-乐观与悲观事务流程

(1)在从客户机接收到“begin”命令后,SQL引擎向PD请求一个时间戳作为事务的开始时间戳(start_ts)。

(2)SQL引擎通过从TiKV读取数据并将其写入本地内存来执行SQL DMLs。TiKV在事务的start_ts之前提供了最近的提交时间戳(commit_ts)。

(3)当SQL引擎从客户机接收到提交命令时,它启动2PC协议。它随机选择一个主键,并行锁定所有键,并向TiKV节点发送预写。

(4)如果所有预写都成功,SQL引擎向PD请求事务的commit_ts,并向TiKV发送提交命令。TiKV提交主键并向SQL引擎发送成功响应。

(5)SQL引擎将成功返回给客户端。

(6)SQL引擎通过向TiKV发送进一步的提交命令,以异步和并行的方式提交辅助键并清除锁。

乐观事务和悲观事务的主要区别在于何时获取锁。在乐观事务中,锁是在预写阶段(上面的第3步)增量获取的。在悲观事务中,锁是在预写之前(步骤2的一部分)执行DML时获得的。这意味着一旦预写开始,事务就不会因为与另一个事务冲突而失败。(它仍然可能由于网络分区或其他问题而失败。)

在悲观事务中锁定键时,SQL引擎获取一个新的时间戳,称为for_update_ts。如果SQL引擎无法获取锁,它可以重试从该锁开始的事务,而不是回滚并重试整个事务。在读取数据时,TiKV使用for_update_ts而不是start_ts来决定可以读取键的哪些值。通过这种方式,悲观事务保持RR隔离级别,即使对事务进行部分重试。

对于悲观事务,用户还可以选择只要求已提交读(RC)隔离级别。这样可以减少事务之间的冲突,从而提高性能,但代价是减少隔离的事务。在实现上的不同之处在于,对于RR,如果读取试图访问被另一个事务锁定的键,TiKV必须报告冲突;对于RC,锁可以在读取时被忽略。
TiDB在没有集中式锁管理器的情况下实现分布式事务。锁存储在TiKV中,具有很高的可伸缩性和可用性。此外,SQL引擎和PD服务器是可伸缩的,可以处理OLTP请求。在服务器上同时运行许多事务可以实现高度的并行性。

从PD请求时间戳。每个时间戳包括物理时间和逻辑时间。物理时间为当前时间,精度为毫秒级,逻辑时间为18位。因此,理论上,PD可以每毫秒分配2^18个时间戳。在实践中,它每秒可以生成大约100万个时间戳,因为分配时间戳只需要几个周期。客户端每批请求一次时间戳来分摊开销,尤其是网络延迟。目前,在我们的实验和许多生产环境中,获取时间戳并不是性能瓶颈。

5.2 分析处理

在本节中,我们将描述针对OLAP查询的优化,包括优化器、索引,以及定制SQL引擎和TiSpark中的下推计算。

5.2.1 SQL引擎中的查询优化

TiDB实现了一个查询优化器,查询优化分为两个阶段:基于规则的优化(RBO)产生逻辑计划,然后是基于成本的优化(CBO),将逻辑计划转换为物理计划。我们的RBO有一组丰富的转换规则,包括裁剪不需要的列、消除投影、下推谓词、派生谓词、常量折叠、消除“group by”或外部连接,以及取消嵌套子查询。我们的CBO根据执行成本从候选计划中选择最便宜的计划。注意,TiDB提供两种数据存储,TiKV和TiFlash,因此扫描表通常有三种选择:在TiKV中扫描行格式的表,在TiKV中扫描带索引的表,在TiFlash中扫描列。

索引对于提高数据库中的查询性能非常重要,通常用于点获取或范围查询,为散列连接和合并连接提供更便宜的数据扫描路径。TiDB实现了可伸缩索引,以便在分布式环境中工作。由于维护索引会消耗大量资源,并可能影响在线事务和分析,因此我们在后台异步构建或删除索引。

索引以与数据相同的方式按Regions划分,并作为键值存储在TiKV中。唯一键索引上的索引项被编码为:

Key: {table{tableID} index{indexID} indexedColValue}
Value: {rowID}

非唯一索引上的索引项被解码为:

Key: {table{tableID} index{indexID} indexedColValue rowID}
Value: {null}

使用索引需要二进制搜索来定位包含索引相关部分的Regions。为了提高索引选择的稳定性和减少物理优化的开销,我们使用天际线剪枝算法来消除无用的候选索引。如果有多个匹配不同查询条件的候选索引,我们将合并部分结果(即一组合格的行IDs)以获得精确的结果集。

物理计划(CBO的结果)由SQL引擎层使用拉迭代器模型(pulling iterator model)执行。通过将一些计算下推到存储层,可以进一步优化执行。在存储层中,执行计算的组件称为协处理器(coprocessor)。协处理器在不同的服务器上并行执行执行计划的子树。这减少了必须从存储层发送到引擎层的元组的数量。例如,通过计算协处理器中的过滤器,被拒绝的元组在存储层中被过滤掉,只有被接受的元组需要发送到引擎层。协处理器可以计算逻辑运算、算术运算和其他常用功能。在某些情况下,它可以执行聚合和TopN。协处理器可以通过向量化操作进一步提高性能:不是对整行进行迭代,而是对行进行批处理,数据按列组织,从而实现更高效的迭代。

5.2.2 TiSpark

为了帮助TiDB连接到Hadoop生态系统,TiDB在multi-Raft存储上添加了TiSpark。除了SQL, TiSpark还支持强大的计算能力,如机器学习库,可以处理来自TiDB外部的数据。

下图显示了TiSpark如何与TiDB集成。在TiSpark中,Spark驱动程序从TiKV读取元数据来构建一个Spark目录,包括表模式和索引信息。Spark驱动程序向PD请求从TiKV读取MVCC数据的时间戳,以确保它获得数据库的一致快照。与SQL引擎一样,Spark Driver可以将计算下推到存储层的协处理器,并使用可用的索引。这是通过修改Spark优化器生成的计划来完成的。我们还定制了一些读取操作来从TiKV和TiFlash中读取数据,并将它们组装成行供Spark worker使用。例如,TiSpark可以同时从多个TiDB Regions读取数据,并且可以从存储层并行获取索引数据。为了减少对特定版本Spark的依赖,这些功能中的大多数都是在附加包中实现的。
在这里插入图片描述

图-TiSpark与TiDB的交互

TiSpark与普通连接器的区别有两个方面。它不仅可以同时读取多个数据Regions,还可以从存储层并行获取索引数据。读取索引可以帮助Spark中的优化器选择最优的计划,从而降低执行成本。另一方面,TiSpark修改从Spark中的原始优化器生成的计划,将部分执行下推到存储层的协处理器,这进一步降低了执行开销。除了从存储层读取数据外,TiSpark还支持在存储层通过事务加载大数据。为了实现这一点,TiSpark采用两阶段提交和锁表。

5.3 隔离与协调

资源隔离是保证事务性查询性能的有效方法。分析查询通常会消耗大量资源,如CPU、内存和I/O带宽。如果这些查询与事务性查询一起运行,事务性查询可能会严重延迟。为了在TiDB中避免这个问题,我们在不同的引擎服务器上调度分析查询和事务性查询,并在单独的服务器上部署TiKV和TiFlash。事务性查询主要访问TiKV,而分析性查询主要访问TiFlash。通过Raft维护TiKV和TiFlash之间的数据一致性的开销很低,因此使用TiFlash运行分析查询对事务处理的性能影响很小。

数据在TiKV和TiFlash之间是一致的,因此查询可以通过从TiKV或TiFlash读取来服务。因此,我们的查询优化器可以从更大的物理计划空间中进行选择,并且最优计划可能同时从TiKV和TiFlash中读取数据。当TiKV访问表时,它提供行扫描和索引扫描,而TiFlash支持列扫描。

这三种访问路径在执行成本和数据顺序属性上各不相同。行扫描和列扫描按主键排序;索引扫描根据键的编码提供了几种排序。不同路径的成本取决于元组/列/索引的平均大小(S_(tuple/col/index))和元组/区域的估计数量(N_(tuple/reg))。我们将数据扫描的I/O开销表示为f_scan,文件查找开销表示为f_seek。查询优化器根据式(1)选择最优访问路径。如式(2)所示,行扫描的开销来自于扫描连续行数据和查找Region文件。列扫描的代价(式(3))是扫描m列的总和。如果被索引的列不满足表扫描所需的列,则索引扫描(式(4))应考虑扫描索引文件的成本和扫描数据文件的成本(即双读)。注意,double read通常随机扫描元组,这涉及到在式(5)中查找更多文件。
在这里插入图片描述

作为查询优化器在同一查询中选择行格式和列格式存储来访问不同表的示例,请考虑“select T.*, S.a from T join S on T.b=S.b where T.a between 1 and 100”。 这是一个典型的连接查询,其中T和S在行存储中的列a以及列副本上都有索引。使用索引从行存储访问T,从列存储访问S是最优的。使用索引从行存储访问T,从列存储访问S是最优的。这是因为查询需要一段来自T的完整元组,并且通过索引按元组访问数据比使用列存储便宜。另一方面,在使用列存储时,获取S的两个完整列的成本更低。

TiKV和TiFlash的协调仍然可以保证隔离性能。对于分析查询,只有小范围扫描或点查询可以通过follower read读取访问TiKV,这对leader的影响很小。我们还将TiKV上用于分析查询的默认访问表大小限制为最多500 MB。事务性查询可以访问TiFlash中的列数据来检查一些约束,比如唯一性。我们为特定的表设置了多个列副本,其中一个表副本专门用于事务性查询。在单独的服务器上处理事务性查询可以避免影响分析查询。

6 实验

在本节中,我们首先分别评估TiDB的OLTP和OLAP能力。对于OLAP,我们研究了SQL引擎选择TiKV和TiFlash的能力,并将TiSpark与其他OLAP系统进行了比较。然后,我们测量了TiDB的HTAP性能,包括TiKV和TiFlash之间的日志复制延迟。最后,我们将TiDB与MemSQL在隔离性方面进行比较。

6.1 实验环境

集群:6台服务器,每台188GB内存、2个 Intel R Xeon R CPU E5-2630 v4处理器,即2个NUMA节点。每个处理有10个物理核(20个线程)和一个25MB的共享L3缓存。操作系统片为CentOS 7.6.1810。万兆网络。

负载:我们的实验是在使用CH-benCHmark的混合OLTP和OLAP工作负载下进行的。源代码在线发布。该基准测试由标准OLTP和OLAP基准测试组成:TPC-C和TPC-H。它是基于未修改版本的TPC-C基准构建的。OLAP部分包含22个受TPC-H启发的分析查询,TPC-H的模式从TPC-H改编为CH-benCHmark模式,另外还有3个缺失的TPC-H关系。在运行时,两个工作负载由多个客户机同时发出;实验中客户端的数量是不同的。吞吐量分别以每秒查询数(QPS)或每秒事务数(TPS)来衡量。CH-benCHmark中的数据单位称为仓库,与TPC-C相同。100个仓库占用大约70GB的内存。

6.2 OLTP性能

在CHbenCHmark的OLTP部分下,我们使用乐观锁定和悲观锁定来评估TiDB的独立OLTP性能;即TPC-C基准。我们将TiDB的性能与另一个分布式NewSQL数据库CockroachDB (CRDB)进行比较。CRDB部署在六台同类服务器上。对于TiDB, SQL引擎和TiKV部署在六台服务器上,它们的实例分别绑定到每台服务器上的两个NUMA节点。PD部署在六台服务器中的三台上。为了平衡请求,TiDB和CRDB都通过HAProxy负载均衡器访问。我们使用不同数量的客户机来测量50、100和200仓库上的吞吐量和平均延迟。
在这里插入图片描述

图-OLTP性能

图(b)和图©中的吞吐量图与图(a)不同。在图(a)中,对于少于256个客户端,TiDB的吞吐量随着乐观锁定和悲观锁定客户机数量的增加而增加。对于超过256个客户端,乐观锁定的吞吐量保持稳定,然后开始下降,而悲观锁定的吞吐量在512个客户端时达到最大值,然后下降。图(b)和图©中TiDB的吞吐量不断增加。这个结果是意料之中的,因为资源争用在高并发性和小数据大小的情况下是最严重的。

一般来说,乐观锁定的性能优于悲观锁定,但数据量较小和并发性较高(在50或100个仓库上有1024个客户端)的情况除外,在这种情况下,资源争用很严重,会导致许多乐观事务被重试。由于200个仓库的资源争用比较少,乐观锁定仍然可以产生更好的性能。

在大多数情况下,TiDB的吞吐量高于CRDB,特别是在大型仓库上使用乐观锁定时。即使采用悲观锁定进行公平比较(CRDB总是使用悲观锁定),TiDB的性能仍然更高。我们认为TiDB的性能优势是由于事务处理和Raft算法的优化。

图(d)显示,更多的客户端导致更多的延迟,特别是在达到最大吞吐量之后,因为更多的请求必须等待更长的时间。这也解释了较少仓库的较高延迟。对于某些客户端,更高的吞吐量可以减少TiDB和CRDB的延迟。50和100个仓库也存在类似的结果。

我们评估从PD请求时间戳的性能,因为这可能是一个潜在的瓶颈。我们使用1200个客户端来连续请求时间戳。客户端位于集群中的不同服务器上。模拟TiDB,每个客户端批量向PD发送时间戳请求。如下表所示,六台服务器中的每台每秒都可以接收602594个时间戳,这是运行TPC-C基准测试时所需速率的100多倍。当运行TPC-C时,TiDB每台服务器每秒最多请求6000个时间戳。当服务器数量增加时,每个服务器上接收到的时间戳数量减少,但时间戳总数几乎相同。这个比率远远超过了现实生活中的需求。关于延迟,只有一小部分请求花费1毫秒或2毫秒。我们得出结论,从PD获取时间戳目前不是TiDB的性能瓶颈。
在这里插入图片描述

表-获取timestamp性能

6.3 OLAP性能

我们从两个角度评估TiDB的OLAP性能。首先,我们评估SQL引擎在CH-benCHmark的OLAP部分下使用100个仓库最佳选择行存储或列存储的能力。我们设置了三种类型的存储:仅TiKV,仅TiFlash,以及同时使用TiKV和TiFlash。我们将每个查询运行五次,并计算平均执行时间。如下图所示,仅从一种存储中获取数据,两种存储都没有优势。从TiKV和TiFlash请求数据总是表现更好。
在这里插入图片描述

图-选择TiKV或TiFlash用于分析查询

Q8、Q12和Q22产生有趣的结果。在Q8和Q12中,仅TiKV的案例花费的时间比仅TiFlash的案例要少,但在Q22中花费的时间更多。TiKV和TiFlash的情况下,表现优于仅TiKV和TiFlash的情况下。

Q12主要包含一个两表连接,但是它在每种存储类型中采用不同的物理实现。在仅使用TiKV的情况下,它使用索引连接,它扫描表ORDER LINE中的几个符合条件的元组,并使用索引查找表OORDER。索引读取器的成本非常低,因此在仅使用TiFlash的情况下,它优于使用散列连接,后者扫描两个表中所需的列。当同时使用TiKV和TiFlash时,成本会进一步降低,因为它使用更便宜的索引连接,从TiFlash中扫描ORDER LINE,并使用TiKV中的索引查找OORDER。在TiKV和TiFlash的情况下,读取列存储可以将只使用TiKV的情况的执行时间减少一半。

在Q22中,它的exists()子查询被转换为反半连接。它在仅TiKV的情况下使用索引连接,在仅TiFlash的情况下使用散列连接。但与Q12中的执行不同的是,使用索引连接比使用散列连接更昂贵。当从TiFlash获取内部表并使用来自TiKV的索引查找外部表时,降低了索引连接的成本。因此,TiKV和TiFlash的情况同样需要最少的时间。

Q8更为复杂。它包含一个包含9个表的连接。在仅使用tikv的情况下,它使用两个索引合并连接和六个散列连接,并使用索引查找两个表(CUSTOMER和OORDER)。该计划耗时1.13秒,优于仅使用tiflash的情况下的8个散列连接,后者耗时1.64秒。在TiKV和TiFlash的情况下,它的开销进一步减少,除了在六个散列连接中扫描来自TiFlash的数据外,物理计划几乎没有变化。这一改进将执行时间减少到0.55秒。在这三种查询中,仅使用TiKV或TiFlash会获得不同的性能,将它们结合使用可以获得最佳结果。

对于Q1、Q4、Q6、Q11、Q13、Q14和Q19,仅TiFlash的情况比仅TiKV的情况表现更好,并且TiKV和TiFlash的情况与仅TiFlash的情况获得相同的性能。这七个查询的原因各不相同。Q1和Q6主要由单个表上的聚合组成,因此在TiFlash中的列存储上运行花费的时间更少,是一个最佳选择。这些结果突出了先前工作中描述的列式存储的优点。Q4和Q11分别在每种情况下使用相同的物理计划执行。但是,从TiFlash中扫描数据比从TiKV中扫描数据便宜,因此在仅使用TiFlash的情况下,执行时间更少,也是一种最佳选择。Q13、Q14和Q19每个都包含一个两表连接,它被实现为散列连接。虽然仅TiKV的情况在探测哈希表时采用索引读取器,但它也比从TiFlash扫描数据更昂贵。

Q9是一个多连接查询。在仅使用TiKV的情况下,它使用索引对某些表进行索引合并连接。它比在TiFlash上进行散列连接更便宜,因此它成为最佳选择。Q7, Q20和Q21产生类似的结果,但是由于篇幅有限,它们被省略了。22个TPC-H查询中的其余8个在三种存储设置中具有相当的性能。

此外,我们使用CH-benCHmark对500个仓库的22个分析查询将TiSpark与SparkSQL、PrestoDB和Greenplum进行了比较。每个数据库安装在六台服务器上。对于SparkSQL和PrestoDB,数据在Hive中以列式parquet文件的形式存储。下图比较了这些系统的性能。TiSpark的性能与SparkSQL相当,因为它们使用相同的引擎。性能差距相当小,主要来自访问不同的存储系统:扫描压缩的parquet文件更便宜,因此SparkSQL通常优于TiSpark。然而,在某些情况下,TiSpark可以将更多的计算推到存储层,从而抵消了这种优势。将TiSpark与PrestoDB和Greenplum进行比较,是将TiSpark的底层引擎SparkSQL与其他两个引擎进行比较。然而,这超出了本文的范围,我们将不详细讨论它。
在这里插入图片描述

图-分析查询性能对比

6.4 HTAP性能

除了调查事务处理(TP)和分析处理(AP)性能外,我们还使用基于整个CH-benCHmark的混合工作负载,使用单独的事务客户机(TC)和分析客户机(AC)来评估TiDB。这些实验在100个仓库中进行。数据被加载到TiKV中,同时被复制到TiFlash中。TiKV部署在三台服务器上,由一个TiDB SQL引擎实例访问。TiFlash部署在另外三台服务器上,并与TiSpark实例并置。该配置分别服务于分析查询和事务性查询。每次跑10分钟,热身3分钟。我们测量了TP和AP工作负载的吞吐量和平均延迟。

图(a)和图(b)分别显示了具有不同数量的TP客户机和AP客户机的事务的吞吐量和平均延迟。吞吐量随着TP客户机的增加而增加,但在略低于512客户机时达到最大值。在TP客户端数量相同的情况下,与没有AP客户端相比,更多的分析处理客户端最多会使TP吞吐量降低10%。这证实了TiKV和TiFlash之间的日志复制实现了高度隔离。

事务的平均延迟增加没有上限。这是因为即使更多的客户机发出更多的请求,它们也不能立即完成,而必须等待。等待时间导致了延迟的增加。

图©和图(d)中显示的类似吞吐量和延迟结果显示了TP对AP请求的影响。在16个AP客户机下,AP吞吐量很快达到最大值,因为AP查询非常昂贵,并且会竞争资源。这种争用降低了AP客户机数量越多的吞吐量。对于相同数量的AP客户机,吞吐量几乎保持不变,最多只下降5%。这表明TP不会显著影响AP的执行。分析查询的平均延迟增加是由于客户端数量增加导致等待时间增加。
在这里插入图片描述

图-TiDB的HTAP性能

6.5 日志复制延迟

为了实现实时分析处理,事务更新应该立即对TiFlash可见。这样的数据新鲜度是由TiKV和TiFlash之间的日志复制延迟决定的。我们在使用不同数量的事务客户机和分析客户机运行CH-benCHmark时测量日志复制时间。我们在运行CH-benCHmark的10分钟内记录每个复制的延迟,并每10秒计算一次平均延迟。我们还计算了10分钟内日志复制延迟的分布。
在这里插入图片描述

表-可见度延迟的计数分布

如下图(a)所示,在10个仓库上,日志复制延迟始终小于300 ms,大多数延迟小于100 ms。图(b)显示了100个仓库的延迟增加;大多数都小于1000ms。上表给出了更精确的细节。对于10个仓库,无论客户端设置如何,几乎99%的查询花费都小于500毫秒。对于100个仓库,分别有2个和32个分析客户端,大约99%和85%的查询用时不到1000毫秒。这些指标突出表明,TiDB可以保证在HTAP工作负载上数据保持大约一秒的新鲜度。

在这里插入图片描述

图-日志复制的可见性延迟

对比图(a)和图(b),我们发现延迟时间与数据大小有关。仓库越多,延迟就越大,因为数据越多,需要同步的日志就越多。此外,延迟还取决于分析请求的数量,但由于事务性客户机的数量,延迟的影响较小。32个ACs的时延大于2个ACs的时延。但是,对于相同数量的分析客户端,延迟没有太大差异。对于100个仓库和2个ACs,超过80%的查询花费的时间少于100毫秒,但是对于32个AC,不到50%的查询花费的时间少于100毫秒。这是因为更多的分析查询以更高的频率诱导日志复制。

6.6 对比MemSQL

我们使用CH-benCHmark对TiDB和MemSQL 7.0进行了比较。本实验旨在突出最先进的HTAP系统的隔离问题,而不是OLTP和OLAP性能。MemSQL是一种分布式关系数据库,可以处理大规模的事务和实时分析。MemSQL部署在六台服务器上:一台主服务器、一台聚合器服务器和四台叶子服务器。我们将100个仓库加载到MemSQL中,并使用不同数量的AP和TP客户机运行基准测试。基准测试运行了10分钟,热身时间为5分钟。下图说明了工作负载干扰对MemSQL的性能有显著影响。特别是,随着AP客户机数量的增加,事务吞吐量显著减慢,下降了五倍以上。AP吞吐量也会随着TP客户机的增加而降低,但这种影响并不明显,因为事务查询不需要分析查询的大量资源。
在这里插入图片描述

图-MemSQL的HTAP性能

7 相关工作

构建HTAP系统的常用方法有:从现有数据库发展、扩展开源分析系统或从头开始构建。TiDB是从头构建的,在体系结构、数据来源、计算引擎和一致性保证方面与其他系统不同。

从现有数据库发展而来。成熟的数据库可以提供基于现有产品的HTAP解决方案,它们特别关注于加速分析查询。它们采用自定义方法分别实现数据一致性和高可用性。相反,TiDB自然受益于Raft中的日志复制,从而实现数据一致性和高可用性。

Oracle在2014年推出了数据库内存选项,作为业界第一个双格式内存RDBMS。此选项旨在打破分析查询工作负载中的性能障碍,而不会影响(甚至提高)常规事务性工作负载的性能。列式存储是只读快照,在某个时间点上是一致的,并且使用完全在线的重新填充机制进行更新。Oracle的后期工作展示了其分布式架构的高可用性方面,并提供了容错的分析查询执行。

SQL Server在其核心中集成了两个专用存储引擎:用于分析工作负载的Apollo列存储引擎和用于事务性工作负载的Hekaton内存引擎。数据迁移任务定期将数据从Hekaton表的尾部复制到压缩的列存储中。SQL Server使用列存储索引和批处理来高效地处理数据
访问分析查询,利用SIMD进行数据扫描。

SAP HANA支持有效地评估单独的OLAP和OLTP查询,并为每个查询使用不同的数据组织。为了扩展OLAP性能,它异步地将行存储数据复制到分布在服务器集群上的列存储。这种方法提供了亚秒可见性的MVCC数据。但是,处理错误和保持数据一致需要付出很多努力。重要的是,事务引擎缺乏高可用性,因为它只部署在单个节点上。

改造开源系统。Apache Spark是一个用于数据分析的开源框架。它需要一个事务模块来实现HTAP。下面列出的许多系统都遵循这个想法。TiDB并不依赖Spark,因为TiSpark是一个扩展。TiDB是一个独立的HTAP数据库,没有TiSpark。

Wildfire构建了一个基于Spark的HTAP引擎。它处理同一列数据组织(即Parquet)上的分析请求和事务请求。它对并发更新采用last-write-wins语义,对读采用快照隔离。为了实现高可用性,分片日志被复制到多个节点,而不需要一致性算法的帮助。分析查询和事务查询可以在单独的节点上处理;然而,在处理最新的更新时有明显的延迟。Wildfire为大规模HTAP工作负载使用了统一的多版本、多区域索引方法。

SnappyData提供了OLTP、OLAP和流分析的统一平台。它集成了一个用于高吞吐量分析的计算引擎(Spark)和一个向外扩展的内存事务存储(GemFire)。最近的更新以行格式存储,然后保存为列格式用于分析查询。交易遵循2PC协议,使用GemFire的Paxos实现,以确保整个集群的共识和一致的视图。

从零开始。许多新的HTAP系统已经研究了混合工作负载的不同方面,其中包括利用内存计算来提高性能、优化数据存储和可用性。与TiDB不同,它们不能同时提供高可用性、数据一致性、可伸缩性、数据新鲜度和隔离性。

MemSQL有一个可扩展的内存OLTP和快速分析查询引擎。MemSQL可以以行或列格式存储数据库表。它可以将部分数据保持行格式,并将其转换为列格式,以便在将数据写入磁盘时进行快速分析。它将重复查询编译成低级机器代码以加速分析查询,并且使用许多无锁结构来帮助事务处理。但是,在运行HTAP工作负载时,它不能为OLAP和OLTP提供独立的性能。

HyPer使用操作系统的fork系统调用为分析工作负载提供快照隔离。它的新版本采用MVCC实现来提供序列化性、快速事务处理和快速扫描。ScyPer扩展了HyPer,通过使用逻辑或物理重做日志传播更新,在远程副本上大规模评估分析查询。

BatchDB是为HTAP工作负载设计的内存数据库引擎。它依赖于具有专用副本的主从复制,每个副本都针对特定的工作负载类型(即OLTP或OLAP)进行了优化。它最大限度地减少了事务引擎和分析引擎之间的负载交互,从而支持在严格的SLA下对HTAP工作负载的新数据进行实时分析。注意,它对行格式的副本执行分析查询,并且不保证高可用性。

基于Lineage的数据存储(L-Store)通过引入更新友好的、基于lineage的存储体系结构,在单个统一引擎中结合了实时分析和事务性查询处理。存储支持本地多版本列式存储模型上的无争用更新机制,以便将稳定的数据从写优化的列式格式惰性地独立地过渡到读优化的列式布局。

Peloton是一个自驱动SQL数据库管理系统。它尝试在运行时为HTAP工作负载调整数据生成。它使用无锁、多版本并发控制来支持实时分析。然而,它是一个单节点的内存数据库。

CockroachDB是一个分布式SQL数据库,它提供高可用性、数据一致性、可伸缩性和隔离性。像TiDB一样,它建立在Raft算法之上,并支持分布式事务。它提供了更强的隔离属性:序列化性,而不是快照隔离。但是,它不支持专用的OLAP或HTAP功能。

8 结论

我们已经介绍了一个用于生产的HTAP数据库:TiDB。TiDB建立在TiKV之上,TiKV是一个分布式的、基于行的存储,使用Raft算法。我们引入了用于实时分析的列式学习器,它异步复制TiKV的日志,并将行格式的数据转换为列格式。TiKV和TiFlash之间的这种日志复制以很小的开销提供了实时数据一致性。TiKV和TiFlash可以部署在单独的物理资源上,以有效地处理事务性和分析性查询。在为事务性查询和分析性查询扫描表时,TiDB可以最佳地选择访问它们。实验结果表明,TiDB在HTAP基准CH-benCHmark下具有良好的性能。TiDB提供了将NewSQL系统发展为HTAP系统的通用解决方案。