【技术解读】高性能KV数据库技术

摘要
分布式存储系统的各个子系统,从协议服务到持久化存储,都依赖KVDB存储元数据信息,KVDB的性能直接影响到整个系统的IO性能、故障恢复性能、迁移性能。本文介绍了下一代存储KVDB在性能优化方面的技术,并对未来性能优化方向进行了探讨,旨在为下一代存储构建高性能的元数据KV存储引擎。

RocksDB简介

下一代存储KVDB基于RocksDB构建,并对RocksDB进行了针对性的性能优化。本节对RocksDB架构进行简单介绍,后续章节对性能优化的各项技术进行介绍。

RocksDB是一个LSM树(Log-Structured Merge Tree)架构的KV数据库,LSM树一种Log-Structured数据结构,用户所有写op先顺序追加写入WAL(Write Ahead Log)日志文件,然后写内存MemTable,此时即完成了用户的写op。等到MemTable中的数据达到一定阈值后再通过后台Flush转化为真正的持久化结构SST(Sorted String Table)文件,SST文件又分为多层,每层的SST都是一个键值范围的有序存储结构,各层可能存在相同Key的多份数据,其中较新的数据位于较高层,较旧的数据位于较低层,通过后台的Compaction进行相邻层间新旧数据合并,将上一层的数据合并到下一层。
在这里插入图片描述
由于LSM树架构将用户的随机写转化为了对日志文件的顺序追加写,因此比覆盖写的B树类存储引擎具有更高的写吞吐,但同时也会带来读性能、写放大和空间放大的问题。

写性能优化—减少内存拷贝

RocksDB的写流程分为两个大的步骤,首先将多并发合并写WAL日志,然后并发写内存MemTable。
在这里插入图片描述
在写WAL之前共有三次内存拷贝:

1. 写操作是基于WriteBatch类进行的,首先要将用户一次写的一个或多个op编码为一个WriteBatch的op序列以保证这些op的原子性,这就有一次从用户内存到WriteBatch的内存拷贝。

2. 为了支持并发写,会将多个并发写的WriteBatch合并为一个WriteBatch,这是第二次内存拷贝。

3. 为了统一处理Buffered/Direct IO和支持限速、写完成通知等功能,在文件系统之上封装了WritableFileWriter类,WriteBatch需要进行CRC计算后先写入WritableFileWriter缓冲区,再写WAL,这是第三次内存拷贝。
每一次内存拷贝都是对性能的很大损耗,因此减少内存拷贝成为写性能优化的关键。下一代存储设计了新的WriteBatch类,利用C++函数重载特性重构简化了写流程,将用户key/value内存指针在写流程中一直传递给WAL,消除了三次内存拷贝,并且使用内存池进一步消除内存申请的开销,极大的优化了写性能。

写性能优化—MemTable数据结构优化

写MemTable是写流程两大步骤之一,因此MemTable的写性能直接影响KVDB写操作的性能。此外,当引擎进程发生故障重新拉起/迁移/回迁时,需要回放WAL,就会有WAL的大量数据写MemTable的操作,写MemTable性能就直接关系到故障拉起/迁移/回迁的性能指标。

RocksDB的MemTable采用跳表(Skiplist)数据结构实现,如下图。跳表是一种利用单链表实现查找树的数据结构,由于链表操作的简单性,从而有代码不易出错和多线程并发免锁的优点,但是缺点也很明显,一方面是高层节点基于随机数生成,高层节点到低层的扇出数不稳定,导致时间复杂度不稳定,更为重要的是,链表相比数组来说缓存局部性不好,同层每个key的比较查找都要进行指针跳转,导致Cache Miss严重对性能影响大。因此,需要考虑是否有性能更优的内存索引数据结构替代跳表来提升性能。
在这里插入图片描述
内存索引数据结构有以下几种:

1. HASH表:点查性能好,时间复杂度O(1),但难以支持范围查找、前缀查找;

2. KEY查找树:包括二叉查找树、平衡二叉树、B+树、红黑树、跳表等,点查时间复杂度O(logN),空间复杂度与KEY长度相关;

3. 数字查找树:包括Trie查找树、RADIX树等,点查时间复杂度与KEY长度相关,空间复杂度与KEY长度无关。

其中HASH表由于难以支持范围查找的缺点不能满足需求不予考虑,KEY查找树如B树类数据结构与跳表性能相近,因此我们考虑数字查找树代替跳表。在数字查找树中,ART树(Adaptive Radix Tree)是一种先进的高性能的内存索引数据结构,我们选定ART树作为跳表的候选替代者。

ART树是一个可变长节点的基数为8的RADIX树。传统基数为8的RADIX树扇出为256,节点需要存储256个孩子节点指针,当实际孩子数较少时会造成严重的空间浪费。ART树利用可变长节点改进了这一缺点,分为扇出数为4、16、48、256四种节点如下图,根据实际孩子个数自适应选择合适的节点类型。
在这里插入图片描述
此外,ART树支持路径压缩,即压缩路径上扇出为1的中间节点,进一步的减少了内存占用和提升了性能。

通过对不同长度KEY数据进行对比测试,ART树相比跳表写性能均有10倍以上的提升。

读性能优化—持久化索引全缓存

RocksDB持久化以SST文件形式组织,SST文件格式如下:
在这里插入图片描述
其中的Index block和filter block是索引结构。Index block是每个Data Block的索引,是范围索引;Filter block使用Bloom filter,是范围索引的补充,用来判断一个给定key是否存在。

RocksDB读流程是按照MemTable到SST文件、SST文件从L0层依次向下层的顺序查找。由于MemTable只是写缓存的一小部分数据,大概率查找不到,因此读性能主要取决于查找持久化SST文件的性能,其中L0层SST文件之间key范围是重叠的因此需要对每个SST文件进行查找,而下层各层SST文件是按照key大小排序范围不重叠的,只需对其中一个SST文件进行查找。

每个SST文件的查找流程需要先读取Filter block,判断key是否存在,如果可能存在(存在有一定误判率),则读取Index block二分查找定位到对应的Data block,再读取Data block进行二分查找。假设LSM树有N层,L0层4个SST文件,读取第N层数据(每一层容量是上一层10倍,第N层存储了近90%的数据,大概率在第N层命中)就需要查找N+3个SST文件,每个SST文件至少要读取Filter block,如果误判或者最终命中key还要读取Index block,而Filter block和Index block要比Data block大很多(Data block大小默认是4KB,而Filter block和Index block大小为几百KB到几MB),会造成很大的额外读盘开销,从而影响读性能。

为了提升读性能,需要将所有SST文件的Filter block和Index block全部用内存缓存,从而保证只读盘一次Data block。但是对于千亿级海量对象存储,内存不足以全缓存。下一代存储采用的优化方案为最后一层SST文件不生成Filter,由于最后一层存储了近90%的数据,这样就可以使Filter block空间占用减少90%。按照10bit每key的Bloom filter计算,由于只有10%的数据生成Filter,平均每key的空间占用为0.125字节,Index block每key大致占用0.5字节,平均每key总共占用0.625字节,可以支撑索引全内存缓存。

· 未来性能优化研究方向—持久化索引结构优化

上述优化方案存在一个副作用,就是查找一个不存在的key时(如文件创建操作),由于最后一层不生成filter,最后一层需要一次额外的读盘获取Data block才能判断key不存在。要彻底解决此问题,同时满足持久化索引内存全缓存,就要寻找持久化索引结构的优化替代方案。

我们可以借鉴内存索引查找树数据结构,实际上查找树就是Index+Filter的功能。要将查找树应用于持久化索引结构就需要将树结构进行序列化编码,消除指针以便持久化存储查找和减少空间占用,业界Succinct数据结构研究方向就是解决这一问题。对于查找树来说就是Succinct Tree,LOUDS是其中常用的一种编码方法。
对Trie树使用LOUDS编码,平均每key占用最大可达到2.5字节(每节点扇出数不定)。空间占用是最后一层不生成filter方法的4倍,不足以支撑内存全缓存。通过对Single leaf路径裁剪优化方法,每key的内存占用大致为1.33字节,并能达到接近Bloom filter的假阳误判率。但是与最后一层不生成filter方法相比还不够。

未来研究方向为借鉴当前RocksDB的Data block组织形式,以Data block为单位进行查找树的组织并采用压缩编码方法,以达到接近每key占用0.625字节同时接近Bloom filter假阳误判率的效果。

扩容迁移优化—迭代器优化

分布式存储系统在进行节点扩容时,会在扩容节点上启动新的元数据服务实例,原节点的元数据服务实例会将其上的部分元数据迁移到新的元数据服务实例。元数据以bucket为单位进行组织,也以bucket为单位进行迁移,同一个bucket的元数据具有相同的前缀信息,可以使用RocksDB的迭代器指定前缀读KVDB获取元数据进行迁移。

迁移每个bucket的元数据顺序是:迁移KVDB内的bucket元数据 -> 迁移该bucket增量元数据,其中第二阶段迁移该bucket增量元数据需要将该bucket的写IO暂时挂住,挂住时间长短会影响业务掉零时间,挂住时间取决于增量元数据量,而增量元数据量则取决于第一阶段KVDB内元数据迁移时间。而RocksDB的迭代器实现应用于此场景存在性能问题,会导致业务掉零时间过长。

RocksDB迭代器实现:创建每Memtable/IMemtable、每L0 SST、L1、L2等的组合迭代器,每个子迭代器定位到指定前缀的开始位置(SST的迭代器会将Data block读盘到缓冲区)将开始位置的KV解析出来加入到组合迭代器的最小堆进行排序,最小堆将堆顶最小KV输出到输出缓冲区并去重,最后将输出缓冲区返回给业务,业务再组装至网络消息内存池内存发送给扩容节点。
由于RocksDB迭代器是为通用的范围查询操作设计的,需要进行排序去重,以返回给调用者有序的最新的KV,并不完全适合扩容迁移场景,会引入不必要的内存拷贝4次、最小堆排序的key比较3-4次,以及复杂的KV解析(见下图)。
在这里插入图片描述
为此,下一代存储设计了应用于扩容迁移场景的专用迭代器。考虑到扩容迁移的需求不需要迁移数据整体排序,只需要保证迁移数据的完整性,而去重可以通过由旧到新的数据迁移顺序由扩容节点KVDB后台compaction完成,结合RocksDB数据分布的特点:L1及以后层的SST文件是整level排序的,指定bucket前缀的KV必然是连续大块分布的,因此可以通过大块整体迁移减少内存拷贝、避免KV解析和排序。

具体实现为:L1+层按照从最后一层到L1的顺序进行迭代,每一层迭代根据该层所有文件的Index block信息,找到符合范围的Data block,并按照每次返回的阈值大小向持久化读取一定数量的Data block,并将读取到的网络消息内存池内存直接返回给业务发送给扩容节点避免内存拷贝,扩容节点上再将Data block解析为KV写入KVDB。对于MemTable和L0层的SST文件,因为key范围重叠,指定前缀的KV分布比较分散,不适于大块搬移,因此复用原有的RocksDB迭代器解析排序机制,将指定前缀key/value筛选出来后,拷贝到网络消息内存池内存中返回给业务。

总结

分布式存储系统的各个子系统,从协议服务到持久化存储,都依赖KVDB存储元数据信息,KVDB的性能直接影响到整个系统的IO性能、故障恢复性能、迁移性能。下一代存储基于RocksDB并进行了多项针对性的性能优化,旨在构建一个高性能的元数据KV存储引擎。未来KVDB还会进行持续不断的优化,针对未来优化研究方向,也欢迎一起探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值