【分布式系统工程实践】随机访问KV存储引擎

Key-Value系统只需要支持简单的随机读(Get),写(Put)和删除(Del)操作。由于磁盘是顺序存储介质,因此可以往数据文件追加Key-Value记录并在内存中存放记录所在的磁盘位置,即索引信息。由于对同一个Key的更新(Put)操作以最后一个为准,内存中的索引只需要记录最新的Key-Value对位置即可,而对于删除操作,也是往数据文件追加一个删除记录。由于内存的大小有限,需要尽可能减小索引记录的大小,比如只支持64位的整数Key(其它类型的Key可以通过md5运算得到64位的整数值)。

内存中的索引信息包括64位的Key(8字节),Key-Value记录所在的磁盘位置(8字节),Key-Value记录在磁盘中的长度(4字节),另外,假设采用Hash的方式组织索引信息并采用采用链地址法解决冲突,每个索引项在Hash表中占用额外的8字节,最后,考虑到64位机的内存对齐,可以大致认为每个索引项占用32字节的内存空间,8GB内存存放的索引项条数为8GB / 32 = 2.5亿。如果每个Key-Value记录的平均大小为1KB,系统设计的磁盘内存比为1KB / 32 = 32 : 1,16GB的内存对应512GB的磁盘数据,满足大多数线上服务的需求。存储引擎需要实现如下几种操作:

读取操作(Get):先通过内存中的Hash索引找到Key-Value对的磁盘存储位置,接着执行一次随机读取操作。

写操作(Put):无论是插入还是更新已有的Key-Value记录,只需要将新数据追加到数据文件末尾,并更新内存中的Hash索引信息。

删除操作(Del):将删除操作追加到数据文件末尾,并删除内存中的Hash索引项。

如果不对索引信息进行持久化,系统宕机重启时需要重新读取所有的数据以重建索引,宕机恢复时间很长。将索引信息以同样的方式追加到索引文件中,写操作及删除操作的流程修改为:a, 追加数据到数据文件;b, 追加索引信息到索引文件;c, 更新内存中的Hash索引结构。当机器宕机时,通过读取索引文件来重建内存中的索引表,由于a和b两个操作不是原子的,可能出现数据文件更新但是索引文件没有更新的情况,因此,宕机恢复时除了读取索引文件的索引信息外,可能还需要读取数据文件中的最后几条记录。

更新和删除操作会产生很多的无用数据,这些垃圾数据的回收是通过定时合并操作实现的,一般可选择每天服务的低峰期,比如凌晨两点启动每日合并任务。

定时合并(Merge):采用0/1目录的方式,假设当前的服务目录编号为0,合并过程如下:

1, 关闭目录0的数据文件和索引文件,后续的更新操作(包括合并过程中的更新操作)都写入目录1中新开的文件;

2, 顺序读取目录0的索引文件,对每一个索引项,对比是否与内存中的内容一致,如果一致,说明是最新的有效索引,将对应的数据追加到目录1中的数据文件,同时生成相应的索引信息追加到目录1的索引文件中并修改内存中的索引项;

3, 合并过程结束时可以回收目录0中的数据文件和索引文件;

由于合并过程中可能有更新操作,且都需要追加目录1中的索引文件,因此,需要将索引文件编号分成两段,比如合并过程中写入的索引文件从1开始编号,最大不超过1000;更新操作写入的索引文件从1001开始编号。

上述介绍的存储引擎的特点就是简单,满足特定类型的应用,且随机读取基本做到了最优。当然,工程实践中还有很多工作需要细化,比如将数据分组从而利用多块磁盘,定时合并过程中记录进度从而宕机后可以从上次合并的位置开始继续执行,追加数据和索引文件是否fsync,如何通过异步group commit写磁盘保证不丢数据的前提下不损失并发能力,等等。总之,表面看似简单的事情做到极致往往很不简单。

说明:第一次接触随机存储引擎是在百度,一位目前在创业的大牛的作品,目前douban的Beansdb也使用了类似思想。另外,可以参考一篇文章:Bitcask: A Log-Structured Hash Table for Fast Key/Value Data

上一篇:Java入门 - 语言基础 - 01.Java简介


下一篇:linux设备驱动归纳总结(三):3面向对象思想和lseek、container_of、write、read 【转】