最近这几年数据库领域有一个十分热门的词汇,就是HTAP。如果你家的数据库不是HTAP数据库,都不好意思拿出来和别人见面。对于HTAP数据库这个概念老白是一头雾水的。在20年前,我在给客户做oracle数据库培训的时候,就会提到HTAP负载,OLTP负载和OLAP负载是天然不同的两种不同的负载。OLTP负载是日常的交易型工作负载,其特点是高并发和小事务,SQL语句一般都相对简单,不过并发访问量很大,数据修改的量很大;OLAP负载是一种低并发,少修改,复杂查询的工作负载,一些SQL可能需要进行大规模的数据扫描,多表关联,统计分析等操作。实际上OLTP模式下和OLAP模式下的数据库访问负载的不同,对于数据存储的结构也有不同的要求。OLTP的高并发数据写入需要使用行存储模式才会比较高效,而OLAP的分析型操作,使用列存储比较高效。几年前一个做MPP OLAP数据库的厂商拿出一些测试结果说秒杀ORACLE的时候我也只能笑笑了,不同的存储引擎,对于不同的工作负载,是无法对等比较的。一般来说在OLTP环境下的ORACLE数据库,顶多可以支撑一些批处理的负载,而很难把一个OLTP数据库当成数据仓库来使用。因此在传统地数据库架构中,OLTP->ODS->ETL->数据仓库->数据集市这种架构比较流行。OLAP的工作还是交给比较擅长的列数据库来处理。想要在行存储的数据库中直接支持复杂的OLAP分析是十分困难的。
支持HTAP负载的数据库的目的是在普通的OLTP数据库基础上支持OLAP工作负载,从而实现实时数据仓库的目标。HTAP这个词汇是2014年Gartner发明的,含义为一种新兴的应用架构,支持混合的OLTP/OLAP负载。
这两年HTAP数据库这个词已经被国内的数据库厂商用滥了,实际上就是我们最为熟悉的Oracle数据库也是支持HTAP负载的,在Oracle 12C开始支持的IN-MEMORY DATABASE技术可以支持在内存中建立某些数据的列式缓冲,从而支持类似OLAP的工作负载。在Gatner的HTAP定义中,提出了在应用负载中实现HTAP的两种主要方法:内存或者异构副本,Oracle的IN-MEMORY DATABASE是采用了第一种方法,在内存中建立异构的数据副本。
而PINGCAP的TIDB似乎采用了第二种方法,在TIDB 4.0开始引入了一个新的存储引擎TiFlash。TiFlash使用了ClickHouse的向量化计算引擎,从而提高分析类应用的性能。
以前的TIDB是采用基于LSM-TREE的TiKV存储引擎的,技术LSM-TREE这个词,这将是最近这几期老白文章中的核心,这是一种目前在分布式数据库中较为流行的存储引擎。作为一种LSM-TREE的存储引擎,TiKV对于高并发的写入支持特别好,但是有个缺点,对于大规模的读操作实际上支持是不够的,无论如何优化TiKV引擎,在支持OLAP场景的时候仍然有些力不从心。TiSpark的引入实际上是一种无奈的选择,使用内存引擎来解决这个问题。不过在TiDB 4.0以后,这个问题得到了比较好的解决。解决方案就是TiFlash的引入。其原理可以用简单的方法来说明一下,就是当主引擎的数据写入TiKV后,TiDB会异步的生成一个TiFlash的副本,这个副本是适合OLAP工作负载的。TiDB和TiSpark计算引擎可以智能化的选择TiKV副本或者TiFlash副本用于不同的工作负载,甚至混合使用这两种副本,以达到最佳的SQL执行效果。
之所以老白今天用了较大的篇幅来谈TiDB的TiKV和TiFlash,是因为这种HTAP工作负载的实现方式,和老白这期要介绍的这篇论文的技术路线是类似的。这篇文章的题目为《Real-Time LSM-Trees for HTAP Workloads》,这篇2021年1月17日发表的论文被收录在arxiv论文数据库里(arXiv:2101.06801v1)。
要想真正理解这篇论文,可能需要读者先了解什么是LSM-TREE,今天的篇幅可能讲不到论文的正文了,所以我们利用最后的篇幅来讲讲LSM-TREE这个预备知识。作为近年来最为热门的数据库存储引擎之一的LSM-TREE,可能大多数搞数据库的都听说过,不过可能不一定会去认真深究LSM-TREE是怎么回事。实际上LSM-TREE是影响我们这个世界的谷歌三大论文中的BigTable中提出的,LSM-TREE是Log Structured Merge Tree的简称,维基百科的描述如下图。
维基百科对LSM-TREE的定义是一种对于高性能写入十分有效的数据结构,在实际实现上,LSM-TREE基于谷歌的著名的五分钟理论,也就是说如果某个数据每5分钟至少被访问一次,那么这个数据最好存放在内存里。
LSM-TREE的数据在C0阶段是存储在内存中的SSTABLE中的,这些SSTABLE按照一定大小的SEGMENT存储在内存中,当一个SEGMENTS写满后进入锁定状态,新数据将被写在新的SEMGENTS中,而锁定状态的SEGMENT经过MERGE后会被写入磁盘进行持久化。
大家可能也看出来了,这种SSTABLE写入的时候是不管老数据的,只管把最新的数据写入SSTABLE,然后当一个SEGMENT写满后整体刷盘,这样的架构对于大并发的数据写入操作比传统的以HEAP/BTREE的模式要快得多。
LSM-TREE和数据库又是什么关系呢?我们借用互联网上的一张图来解释一下。
数据库一般包括sql引擎和存储引擎两部分。存储引擎可能把数据存放在持久化存储设备(比如硬盘)上,或者易失性的内存里(比如内存数据库)。在数据库发展的这些年里,存储引擎一直是B+TREE结构占主导地位。随着这些年分布式数据库的发展,谷歌论文BigTable中的LSM-TREE成为一种存储引擎的新宠。下面的图同样来自于互联网,列出了两大阵营中的主要数据库产品。
我们可以看到,大多数的传统数据库或者非分布式数据库都采用了B-TREE结构的存储,表的数据存储在堆结构的数据块中,索引使用B-TREE结构。而一些新型的分布式数据库都采用LSM TREE存储结构。
基于B-TREE结构的存储引擎具有较好的事务处理能力,但是对于大规模的数据写入性能与LSM-TREE相比有巨大的差距。因为LSM-TREE采用内存缓冲+大批量顺序写入的方式,对于高写入的场景具有极高的性能。特别是在更适合顺序访问的SSD盘。LSM-TREE还很适合于分层存储,其中可以利用具有不同性能/成本特性的不同类型的存储,例如RAM,SSD,HDD和远程文件存储(例如AWS S3)。不过LSM TREE也存在自身的不足,在大吞吐量读的场景下,LSM-TREE需要消耗更多的IO,另外为了保证LSM-TREE的性能,需要进行比较和合并操作,这个操作相当消耗CPU和IO资源。
基于上述的原理,LSM-TREE是一种十分适合于大型写入场景的存储引擎,不过由于SSTABLE的MERGE的需要,会更加消耗CPU和IO资源。不过随着写在计算于IO能力的极大提升,这种存储引擎对于现代的计算机系统来说,算是十分适合的。
为了更好的学习这篇论文,我们今天用了两千多字来介绍LSM-TREE存储引擎,不过上面关于LSM-TREE的介绍仅仅是科普的级别,如果大家有兴趣的话,可以去网上找几篇文章认真学习学习。
根据昨天老白些的内容,大家应该可以看出,老白是倾向于一个真正的实时数据仓库系统或者一个具有HTAP负载能力的数据库必须能够同时支持面向两种不同的应用负载的不同的存储模式:面向OLTP的行存储和面向OLAP的列存储。其中一种实现方式昨天我已经介绍了,就是TiDB 4.0的异步TiFlash副本的实现模式,在一个数据库中,实时数据是TiKV的行存储副本,异步副本是TiFlash的列存储格式的。另外一种实现方式是在数据的生命周期中采用不同的存储格式,最近的经常需要做OLTP处理的,以行存储的格式存储,历史的,主要用于分析的以列存储的方式存储。比如SAP HANA、MemSQL、IBM Wildfire等,这些具有实时OLAP能力的数据库都是这么实现的,这种引擎被称为lifecycle-aware 存储引擎。因为LSM-TREE的compare&merge操作的开销是十分大的,对服务器的IO和CPU资源都有较大的消耗,因此实现这种基于数据生命周期混合存储的算法效率十分重要。
LSM-TREE存储引擎天然具有分层存储的结构以及对高并发的写入操作的支持,因此比较适合作为创建按照数据生命周期的混合模式存储引擎的基础。因此这篇论文设计了一种称为实时LSM-TREE的扩展性的树结构,可以在不同级别上存储行数据或者列数据,并在不同的Design space中使用不同的存储格式,以适应不同的工作负载。对于最近使用的数据,都是具有OLTP特征的访问,包括插入,修改,删除,按键值的查询,这些操作都比较适合行存储格式;而对于老化的数据,其访问操作主要以范围扫描,统计分析为主,这些数据就使用OLAP类型效率比较高的列存储模式。基于上述分析,将所有的数据的工作负载分为插入(Insert)、更新(Update)、删除(Delete)、点查询(Point read)和扫描(scan)这五种。先约定对一些操作的描述符:
- Π = {A,C}:表示使用A\B列作为过滤条件
- insert(key,row):插入一条新纪录
- read(key, Π):根据过滤条件查找某些记录
- scan(keylow ,keyhigh, Π):根据扫描条件扫描某个数据表,包含无条件扫描的情况
- update(key, value,Π):根据条件修改部分数据
- delete(key):根据key删除数据
如果我们假设read和update访问最近插入了密钥具有宽的∏(几乎所有列)的数据,而scan经常只是访问一个范围较窄的∏--一个列或几列(取决于数据的年龄)。那么这种基于生命周期的异构存储引擎就能够针对不同负载提供最佳的性能了。
在继续介绍这篇论文中的算法之前,我们只能先打断一下,来讨论一下基于LSM-TREE的存储引擎如何支持MVCC以及如何支持类似传统数据库的行锁功能的。因为传统的LSM-TREE数据引擎都是使用在NOSQL的数据库中,支持的是最终一致性,而不是强事务一致性。CAP在一个分布式的数据库中是需要做出取舍的,为了提高性能,牺牲强一致性是一种必须的选择。
大家都知道非分布式数据库本质上主要是关系,通过结构化查询语言SQL来访问并支持强一致性和ACID事务。分布式数据库必须遵守CAP定理,在出现故障时必须明确选择一致性或可用性。NoSQL数据库本身是分布式的,第一代NoSQL数据库(包括Apache Cassandra、AWS DynamoDB、Couchbase)选择了可用性而不是一致性。换句话说,它们遵循最终的一致性,不支持ACID事务,并且仅限于基本操作。而新一代数据库,如FoundationDB和ms azurecosmosdb,通过给NoSQL增加强一致性,从而开始支持ACID事务。
如何设计存储引擎才能让遵循CAP定理的分布式数据库支持强事务一致性的呢?最大的影响与存储引擎如何处理并发请求有关,并发请求由支持的隔离级别(即ACID中的“i”)决定。SQL数据库最初使用行级锁进行最严格的并发控制。然而,为了提供更好的吞吐量,引入了多版本并发控制(MVCC),通过创建不同版本的行来进行读写操作,从而放松了对锁的需求。
下面举个例子来说明一下MVCC,在下图中,Session 1对数据进行了修改,并且没有提交。原本这个操作按照ACID的严格控制会阻塞读这个数据的 Session 2。引入MVCC多版本并发控制后,Session 2就不会被正在修改这个数据的Session 1阻塞,它可以读到版本1。
在传统的NoSQL数据库中并不提供MVCC机制。Cassandra等传统的NOSQL数据库并不支持MVCC,这意味着它不会阻止多个并发写入程序写入冲突的值 ,最终冲突判断的规则是: 具有最新客户端时间戳的写入程序获胜。还有一些NOSQL数据库只允许应用程序在使用条件操作时采用乐观并发控制。也就是我们常说的乐观锁,什么是乐观锁呢?就是当我们去修改某个数据的时候并不加锁,当事务提交的时候,才做锁的判断,先提交的会话成功,后提交的会话就会失败。这种并发锁控制对于分布式数据库来说十分简便与高效,不过对于应用来说不够友好。当然有一些应用内可以经过十分简单的修改就从ACID强一致性锁的并发控制修改为支持乐观锁的应用,但是有一些比较复杂的应用需要做大量的修改,才能保证正常运行。比如会话一插入了20万条数据,会话2也做了同样的操作,这二者之间有一条记录是主键冲突的,那么谁先提交谁就成功,而不成功的那个会话就需要找出那条数据冲突了,然后重新写入这20万条数据,这会严重影响应用的性能,也大大增加了应用的复杂度,比较好的处理方法是在各个会话写入数据前首先做严格的检查,确保不同的会话不会写入主键冲突的数据。
目前来说,基于LSM-TREE引擎的大多数数据库采用了类似两阶段提交的乐观锁机制,这也是LSM-TREE支持的最高效率的事务隔离级别控制方式。因此我们在选择使用LSM-TREE作为存储引擎的数据库的时候,一定要确认其锁的实现方式合你的应用系统是否匹配,否则今后应用开发就要吃苦头了。
实现基于数据生命周期的异构存储,从而使用OLTP/OLAP的混合负载,最关键的部分是设计数据的Design Space。如下图
在左侧,数据是以行的方式存储的,适合于OLTP工作负载。由缺省的LSM-Tree存储引擎存储。右边,是一个完全按列存储的,适用于OLAP的存储格式。中间是一种混合设计,其中Level-0面向行,Level 1和2使用CG(列组)的不同组合,并且Level-3开关用于纯列式存储。此设计可能适用于混合或HTAP工作负载,其列组配置取决于数据生命周期中的访问模式。下面我们跳过较为枯燥的学术分析部分,直接看基于数据生命周期的实时LSM-TREE存储引擎的最终效果。测试用例用使用了Q1-Q5五个用例,其中Q1是插入,每秒1万条数据,Q5是update,每秒100条,Q2-Q4是四个查询的场景。Q2是典型的OLTP查询(点查询),Q2A返回30个属性,Q2B返回其中的一半属性(16-30),Q3/Q4为OLAP类的分析。
基于该算法的存储引擎与其他的数据库进行比较的结果如下:
其中LASER就是基于该算法的实时LSM-TREE引擎,可以看出其延时比传统的行数据库PostgreSQL低5倍,比纯列数据库MonetDb高出两个数量级。不过这个比较还不够客观,因为这个测试是用这两种数据库与不带事务控制的Rocksdb进行对比的,不具备可比性。不过从性能上还是看出了基于LSM-TREE的存储引擎在HTAP场景中的潜力。这篇论文虽然没有描述一个完整的RDBMS系统,也没有给出在LSM-TREE上实现MVCC并发控制的方法,基于对开源的Rocksdb的改造,可以很容易实现论文中所说的算法。其实老白写这个系列的目的并不是解析这篇论文,并通过这个算法去构建一个基于LSM-TREE的具有HTAP场景支持的存储引擎,而仅仅是从中窥探到一些HTAP数据库工作负载的实现方式。如果我们从中看懂了对HTAP工作负载的支持,数据库有哪些实现方法,那么我们就可以去分辨身边的号称分布式HTAP数据库的真伪了。虽然很多数据库都号称支持HTAP的工作负载,但是实际上真正支持HTAP负载的数据库系统的开发难度还是很大的。Oracle和IBM都在行存储数据库中增加了列式的附加副本,从而在OLTP数据库中支持一些有限的、临时性的OLAP应用负载。实现这种异构复制的成本还是比较高的,Oracle只是在内存中实现这种列式副本,也主要出于降低成本的考虑。
实际上在CAP原则的控制下,高并发写的OLTP场景单机版数据库具有最低的开销,只要服务器的容量足够,烟囱式的结构肯定具有最小的强事务一致性成本开销。分布式数据库因为分布式事务的存在,不可避免的会增加锁的开销成本。解决该问题的方法是应用上采用一致性的分区方式,让某个数据的大部分的写操作尽可能能按照分区分别处理,从而尽可能减少分区之间的数据交互。
另外一种手段是采用两阶段提交的乐观锁,从而避免在每行数据修改发生时产生锁等待。强一致性的锁的开销是分布式事务中最影响效率的因素,我们曾经做过一个测试,对于并发写入的场景,如果开启乐观锁控制,在一个8节点的分布式数据库集群上,大约每秒写入的数据量为53万,如果关闭乐观锁的控制,写入量提高了4倍。如果使用悲观锁,可能情况更为糟糕,因为当时测试的数据库产品并不支持悲观锁,所以我们手头没有这方面的数据。从这里我们也可以看出并发控制带来的成本。
另外一方面要说的是,任何好处都是需要代价的。在数据库领域,也没有像永动机一样的不需要能量支撑的永恒运动,对某一方面的性能提升往往都是要牺牲另一方面来实现的。对于OLTP和OLAP这两个截然不同的工作负载,到目前为止还没有什么技术手段能够在不付出任何代价的情况下就获得全面的支持。行式存储引擎天然就对OLAP不够友好,而列式存储引擎天然就无法很好的支撑OLTP工作负载。
如果有人告诉你,我的一个基于MYSQL PROXY的分库分表的数据库是一个HTAP数据库,既可以支撑高并发的数据写入,又能够支持复杂的分析场景,那么你可以直接不予采信。LSM-TREE高并发写入的支持也并不是没有代价的。数据写入的时候确实只需要生成WAL并落盘,并将数据写入内存中的SST就可以了,提交的性能几乎等同于WAL写盘的性能。不过当SST写满后,最终还是要落盘的,而盘的性能与内存是有巨大的差距的,因此当内存写满后,写负载仍然没有下降,那么写的性能就不是写入内存的性能,而是接近于SST写入物理盘的性能了。为了实现超高的写入性能,我们必须选择性能较好的存储介质,使用SSD盘甚至NVME的SSD盘可能成为我们的必然选择。LSM-TREE的这种特性也并不是没有代价的,在实现MVCC并发控制以及提供一致性读的时候,这种多层次的树状结构效率是很低的,因此我们必须进行compare-and-merge工作,从而合并数据,提高一致性读取的性能。这种合并并不是没有代价的,需要很高的磁盘IO性能和CPU资源来支撑。这也是基于LSM-TREE的存储引擎需要较高的CPU配置的主要原因。另外如果我们需要采用基于生命周期的异构存储或者生成一个支持OLAP的异构副本同样不是免费的,需要额外的IO与CPU资源来支撑。
现在已经有一些数据库已经在考虑使用GPU来做这些工作,从而节约CPU的资源了。现在计算机的CPU/io等的技术发展都十分快,在十年前我们还在感慨,计算机的发展这么快,是不是很快会出现计算资源过于充足了。事实上看,这种担忧是毫无根据的。很快就会有新的技术会更为充分的利用计算机的资源。因此我们在设计数据库产品的时候,是不是也应该较为超前的做一些设计,也许你的产品成熟的时候,计算资源的问题就已经足够了。
想起老白大学里的毕业设计,当时最大的软盘是1.44M的,普通人都还在使用360K的单密磁盘。我们为了把一个6000个常用汉字的语音库放到一张软盘上,花了几年的时间去压缩优化,希望至少能够存储在一张1.44M的软盘上。当我们的产品比较成熟的时候,10M到20M的硬盘已经比较普及了,我们解决的这个问题除了发表了几篇学术论文外变得一文不值,对硬件发展认知的缺失让我们做了几年的无用功。在信息技术高速发展和多样化发展的今天,能够充分利用充足的计算资源与高性能存储设备替代传统HDD这样的技术发展方向去设计自己的产品,也是一种创新。这也是老白觉得在分布式数据库领域LSM-TREE存储引擎将会越来越流行的主要原因。
现在在MYSQL上已经有了几款基于LSM-TREE的开源存储引擎了,有兴趣的朋友可以去研究研究。谈到后面似乎问题更复杂了,数据库的研发人员要去研究硬件的发展了。不过仔细想想实际上也不矛盾,要让你的数据库跑的更好,充分的考虑其运行的硬件环境与操作系统环境是必须的。一个不懂硬件和操作系统的数据库开发团队,似乎也干不好数据库开发这活吧。
作者:白鳝