本文由高斌(花名:艾伦)撰写于2019年8月12日
前言:
作为企业IT基础架构的核心部分,数据库技术一直是大家讨论的热点,也是很多人关注的领域。如果简单划分的话,数据库内核可以分为计算层和存储层,其中计算层负责接收用户发送过来的SQL语句,调用存储的功能来实现数据的存取,所以存储层的设计会直接影响计算层存取的效率,影响SQL语句的性能。
相对于传统的page based数据库存储方式,OceanBase使用了现在非常流行的LSM Tree作为存储引擎保存数据的基本数据结构,这在分布式的通用关系型数据库当中是很少见的。本文对OceanBase数据库基于LSM Tree结构的存储引擎进行介绍,并和传统的数据库存储引擎进行对比,希望能够为读者带来一些启发。另外需要说明的一点是,本文更多的是从数据存储方式和缓存结构方面来介绍,对于事务相关的内容并不会涉及。
LSM Tree技术简介 :
首先需要说明的是,LSM Tree技术出现的一个最主要的原因就是磁盘的随机写速度要远远低于顺序写的速度,而数据库要面临很多写密集型的场景,所以很多数据库产品就把LSM Tree的思想引入到了数据库领域。
LSM Tree ,顾名思义,就是The Log-Structured Merge-Tree 的缩写。从这个名称里面可以看到几个关键的信息:
第一: log-structred,通过日志的方式来组织的
第二:merge,可以合并的
第三:tree,一种树形结构
实际上它并不是一棵树,也不是一种具体的数据结构,它实际上是一种数据保存和更新的思想。简单的说,就是将数据按照key来进行排序(在数据库中就是表的主键),之后形成一棵一棵小的树形结构,或者不是树形结构,是一张小表也可以,这些数据通常被称为基线数据;之后把每次数据的改变(也就是log)都记录下来,也按照主键进行排序,之后定期的把log中对数据的改变合并(merge)到基线数据当中。下面的图形描述了LSM Tree的基本结构。
图中的C0代表了缓存在内存中的数据,当内存中的数据达到了一定的阈值后,就会把数据内存中的数据排序后保存到磁盘当中,这就形成了磁盘中C1级别的增量数据(这些数据也是按照主键排序的),这个过程通常被称为转储。
当C1级别的数据也达到一定阈值的时候,就会触发另外的一次合并(合并的过程可以认为是一种归并排序的过程),形成C2级别的数据,以此类推,如果这个逐级合并的结构定义了k层的话,那么最后的第k层数据就是最后的基线数据,这个过程通常被称为合并。用一句话来简单描述的话,LSM Tree就是一个基于归并排序的数据存储思想。
从上面的结构中不难看出,LSM Tree对写密集型的应用是非常友好的,因为绝大部分的写操作都是顺序的。但是对很多读操作是要损失一些性能的,因为数据在磁盘上可能存在多个版本,所以通常情况下,使用了LSM Tree的存储引擎都会选择把很多个版本的数据存在内存中,根据查询的需要,构建出满足要求的数据版本。
在数据库领域,很多产品都使用了LSM Tree结构来作为数据库的存储引擎,例如:OceanBase,LevelDB,HBase等。
OceanBase存储引擎基于LSM Tree的实践:
OceanBase数据库采用了基于LSM Tree结构作为数据库的存储引擎,数据被分为基线数据(SSTable)和增量数据(MemTable)两部分,基线数据被保存在磁盘中,当需要读取的时候会被加载到数据库的缓存中,当数据被不断插入(或者修改时)在内存中缓存增量数据,当增量数据达到一定阈值时,就把增量数据刷新到磁盘上,当磁盘上的增量数据达到一定阈值时再把磁盘上的增量数据和基线数据进行合并。
对于LSM Tree结构,如果保存多个层次的MemTable的话,会带来很大的空间存储问题,OceanBase对LSM Tree结构进行了简化,只保留了C0层和C1层,也就是说,内存中的增量数据会被以MemTable的方式保存在磁盘中,这个过程被称之为转储(compaction),当转储了一定的次数之后,就需要把磁盘上的MemTable与基线数据进行合并(merge)。以下是转储与合并的详细解释:
- 转储:由于内存中对数据的修改会持续发生,所以在内存中的MemTable就会越来越多,为了释放内存空间,OceanBase会定义一个MemTable占用内存比例的阈值,当到达这个阈值的时候,就要把一些最旧的MemTable中的信息进行归并排序,并保存到磁盘上,形成C1级别的数据,这个过程称之为转储,OceanBase把这个过程称之为转储(Minor Freeze)。
- 合并:合并操作(Major Freeze)是将动静态数据做归并,也就是产生新的C1层的数据,会比较费时。当转储产生的增量数据积累到一定程度时,通过Major Freeze实现大版本的合并。由于在合并的过程中为了保证数据的一致性,就需要在合并的过程中暂停正在被合并的数据上的事务,这对性能来说是会有影响的,OceanBase对合并操作进行了细化,分为增量合并,轮转合并和全量合并。
下面的表格描述了转储与合并的区别:
OceanBase同时也结合了传统的关系型数据库的特点,也存在数据库块的概念。在OceanBase中,数据文件分配空间的单位称为宏块(marco block),如果大家对Oracle比较熟悉的话,可以简单的认为宏块对应了Oracle中的extent;每个宏块又分成了若干个16k大小的微块,它是每次数据库IO的最小单位(相当于传统数据库的块),数据库中的各种数据就保存在微块当中。由于宏块的大小是2M,而且OceanBase采用了LSM Tree结构来保存数据,数据是按照表的主键排序的。所以,OceanBase的宏块是可以分裂的,而如果数据被删除了,相邻的宏块也可以进行合并。
由于SSTable中的数据是基线数据,绝大部分情况下,这部分数据是静态的,所以OceanBase默认会对这些数据在进行合并时进行分析,并根据各个列的数据分布情况对数据进行编码,目前支持的编码方式有:字典编码、RLE编码、常量编码、差值编码、前缀编码、列间编码等。在对数据进行编码之后,再通过通用的压缩算法对数据进行压缩,就可以实现很好的数据压缩比,同时对于读取性能基本没有影响,而且使合并时的写入性能更好。下面的图片展示了使用字典方式对列rate_id进行编码的基本过程。
OceanBase数据库缓存系统:
由于OceanBase存储引擎和传统的page based数据库存在很多差别,所以缓存系统也和传统数据库存在着较大的不同。对于传统数据库(以Oracle为例),最小的IO单位是数据库块,在内存中是通过hash table的方式把读取的数据缓存起来,并提供了相应的缓存淘汰机制。
而对于OceanBase,由于存储引擎使用了LSM Tree结构,就需要在缓存中设计多种类型的sub-cache来满足不同类型的SQL语句的访问需求。下面的图片基本描述了OceanBase缓存的结构。
首先关注Memory部分的内容:
• Block Cache:微块缓存。这部分内存是用来存放从磁盘上读取到的微块中的信息的,类似于数据库中常提到的buffer pool,主要用于满足比较大的SQL语句。
• Block Index Cache:对应图中的in-memory b+ tree,微块索引缓存(这个类似于一个block cache的索引),由于微块数量很多,需要一个index来把block cache中的信息串起来。
• Row Cache:基线数据和转储数据的行数据缓存(hash table)。对于高频的短查询,使用行cache可以快速的响应请求,同时也是在这里实现mvcc,也就是说,OceanBase的mvcc是在行级别实现的。
• Bloom Filter Cache:宏块布隆过滤器缓存,用于快速判断行在基线数据或转储数据是否存在(这个就是个宏块的hash table)。
在有了各种cache之后,OceanBase就可以满足不同类型的SQL语句请求了。同时,OceanBase也提供了缓存淘汰机制,基本的方式就是当整个缓存中的数据达到了一个阈值之后,会把版本最旧的数据构建成一个SSTable,保存到磁盘中(就是之前提到的转储过程),以便释放缓存。如果用户希望设置某种cache的优先级或者强制刷新某种cache的话,OceanBase也提供了相应的命令来完成对应的操作,让DBA对数据库拥有更好的控制。
最后我们来简单小结一下OceanBase数据库与传统的数据在存储方面的不同。
使用LSM Tree的最佳实践:
最后,我们从磁盘容量,缓存大小,转储,合并这几个方面来介绍OceanBase在存储层面的一些最佳实践。
从磁盘的容量角度来讲,由于OceanBase对磁盘空间的使用是预分配的,所以建议用户首先规划好磁盘的容量,之后在启动OceanBase数据库时指定参数datafile_disk_percentage来决定数据库占用的磁盘空间的百分比。而对于日志所需要占用的空间,是通过参数clog_disk_usage_limit_percentage来确定的,也建议在安装之前规划好各种日志所占用的空间大小。
通常情况下,为数据和日志准备单独的SSD磁盘, 设置datafile_disk_percentage=90 ,clog_disk_usage_limit_percentage=80让OceanBase数据库占用绝大部分的磁盘。如果磁盘很大的话,也可以把这个参数向上适当的调整,例如调整到95\90。
从缓存的角度考虑,我们建议OceanBase服务器独占服务器的内存资源,并不推荐在同一台机器上运行多个OceanBase服务器进程。另外,由于LSM Tree结构的特点,OceanBase也会尽量把更多的数据缓存到内存当中,以便获得更好的读写性能,而且OceanBase的缓存结构设计与传统数据库相比,更加的复杂,所以需要使用更多的内存。可以通过memory_limit_percentage参数来指定OceanBase进程占用的系统内存百分比,通常我们建议把这个参数设置为80,确保OceanBase可以使用大部分的系统内存。
由于OceanBase中存在着转储的概念,那么意味着随着转储的不断发生,基线数据和增量数据的差距就会不断增大,数据读取时为了获得合适版本需要的时间可能更多,那么就需要控制转储与合并的频率,通过minor_freeze_times参数控制。例如:对于写入密集型的应用,建议适当的调大发生转储的内存阈值,通过memstore_limit_percentage参数控制,并且调大可以允许的转储次数确保在业务运行时不发生合并,而是把产生的数据暂时转储到磁盘,当系统空闲时再触发合并。而对于查询密集型的应用,则不需要这么做,而是需要把memory_limit_percentage调大,让OceanBase占用更多的内存。
最后,由于合并需要把转储到磁盘中的数据与基线数据进行合并,这个操作是比较消耗资源的,推荐在系统空闲的时候进行合并操作,如果每天产生的增量很多,推荐为合并任务分配更多的资源;相反,如果每天产生的增量数据不是很多,就不需要为合并任务分配更多的资源。每日合并发生的时间可以通过参数major_freeze_duty_time来指定,合并能够使用的资源多少是通过其他的一系列参数来控制的,这里不再一一介绍。
————————————————————————————————————————————
社区版官网论坛
社区版项目网站提 Issue
欢迎持续关注 OceanBase 技术社区,我们将不断输出技术干货内容,与千万技术人共同成长!!!
搜索🔍钉钉群,或扫描下方二维码,还可进入 OceanBase 技术答疑群,有任何技术问题在里面都能找到答案哦~