【存储】lotusdb的原理及实现

最近看了lotusdb的源码。lotusdb是一个golang实现的嵌入式的持久化kv存储。

从整体设计上看,lotusdb采用了类似LSM树的架构,并采用了针对SSD的优化,将key和value分开存储。在此基础上,lotusdb将LSM树中存储key的SST使用B+树或者hash table的索引替换。lotusdb作者认为该设计消除了多级SST带来的读放大问题,使lotusdb的读性能更加稳定。这确实没有问题,这样的设计使lotusdb平衡了B+树和LSM树的缺点,同时也平衡了两者的优点,使得lotusdb变得中庸。(中庸不一定不好,实际生产的效果如何还是要看在具体场景下的性能数据)另外,lotusdb还有几个缺陷:

  • 目前并没有提供范围查询的接口;
  • 没有事务的保证;
  • 在容灾恢复方面有所缺陷;
    总的来说,博主认为lotusdb还是一个相对比较稚嫩但挺有意思的项目,如果感兴趣,不妨一读。

文章目录

  • 整体架构
    • 写入流程
    • 后台flush
    • 读取流程
    • 压缩
  • 实现原理
  • 具体实现
    • Index
      • B+树
      • Hash table
    • 容灾恢复
  • 总结

整体架构

lotusdb的整体架构图如下。lotusdb通过memtable的来缓存用户的写入。其维护多个memtable,最新的一个为active memtable,可以写入;其余为immutable memtable,只读。memtable将写入的key和value维护在内存中的跳表中,并通过wal来保证持久性。immutable memtable会定期的在后台刷入持久化存储。

lotusdb采用了针对ssd的优化,将key和value分开存储。key存储在Index中,value存储在value log中。value log比较简单,是多分片的wal;key的部分,Index为接口,lotusdb中没有提供经典LSM树中多层级SST的实现,只提供了B+树(boltdb)和hash table的实现。

写入流程

写入时,将数据写入active memtable即可返回。memtable由wal和内存态的跳表组成。写入时先将数据写入wal,以保证数据的持久性,再将数据插入跳表。active memtable数据满了后,会创建新的memtable,并将当前active memtable转变为immutable memtable,在后台刷入持久化存储。

后台flush

active memtable在写满以后会转变为immutable memtable,并在后台刷入。首先会将对应表中的内容写入value log。value log的实现比较简单,采用多分片的wal,所以value log的写入为批量的追加写。在value log写入完成后,得到key及对应的值在vlaue log中的位置,将key和position的键值对写入Index。对于Index的写入,无论是B+树还是hash表,这里的写入都是批量的随机写。

LSM树的设计就是通过牺牲读性能来提升写性能。lotus的这一揉杂了B+树和LSM树,其读取性能会比LSM好,但会比B+树差;写入性能会比B+树好,但是会比LSM树差(单纯从设计上分析,实际效果得看具体场景下的数据)。这就是文章开头提到的,会使lotusdb变得中庸。当然中庸并不是个贬义词。

读取流程

在读取时,会依次读取memtable。如果获取到数据则直接返回,否则去查询Index,根据postion信息去value log中获取值并返回。

压缩

在LSM树中,多级SST的设计保证了批量追加写来优化写入性能,但同时带来了读放大和冗余数据的问题。除此之外,SST的compact也是一个复杂度比较高的问题。第一是后台的compact会极大的影响性能,博主曾经就遇到过因为es后台compact消耗大量资源导致写入超时的问题。另一个是compact本身的实现复杂度就相对较高,基于LSM的存储有各种不同的compact策略,感兴趣可以自己去查阅,这里不做展开。

在lotusdb中,Index没有提供SST的实现,而是提供了B+树或者hash table的实现,所以key的部分不需要考虑compact。但是value log的是完全追加写,还是需要compact来消除冗余数据。目前lotusdb仅是提供了compact方法供上层调用,但是没有提供具体的compact策略,这对上层来说是不够友好的。

实现原理

原理部分,主要基于WiscKey: Separating Keys from Values
in SSD-conscious Storage,会单独开一篇进行讲解。

具体实现

lotusdb的主要实现如下。和整体架构部分介绍的一样,主要有三大部分组成:

  • memtables。分为active memtable和immutable memtable。
  • index。实现Index接口的索引,目前lotusdb提供了B+树和hash table的实现,用来存储key以及对应value在value log中的位置。
  • vlog。用来存储实际的值。
type DB struct {activeMem *memtable      // Active memtable for writing.immuMems  []*memtable    // Immutable memtables, waiting to be flushed to disk.index     Index          // index is multi-partition indexes to store key and chunk position.vlog      *valueLog      // vlog is the value log.fileLock  *flock.Flock   // fileLock to prevent multiple processes from using the same database directory.flushChan chan *memtable // flushChan is used to notify the flush goroutine to flush memtable to disk.flushLock sync.Mutex     // flushLock is to prevent flush running while compaction doesn't occurmu        sync.RWMutexclosed    booloptions   OptionsbatchPool sync.Pool // batchPool is a pool of batch, to reduce the cost of memory allocation.
}

Index

Index接口定义如下。上面也多次提到了,对于Index,lotus提供了B+树和hash table的实现。

type Index interface {// PutBatch put batch records to indexPutBatch([]*KeyPosition) error// Get chunk position by keyGet([]byte) (*KeyPosition, error)// DeleteBatch delete batch records from indexDeleteBatch([][]byte) error// Sync sync index data to diskSync() error// Close indexClose() error
}

B+树

B+树的实现如下,可以看到其底层采用了多个blotdb来实现。
关于boltdb,前面我们介绍过,这里就不多介绍,可以参见【存储】etcd的存储是如何实现的(3)-blotdb。

// BPTree is the BoltDB index implementation.
type BPTree struct {options indexOptionstrees   []*bbolt.DB
}

Hash table

Hash table的实现同样采用了多个分片的hash表。

type HashTable struct {options indexOptionstables  []*diskhash.Table
}

hash表的实现如下。

该hash表要求value的长度固定,所以适应的场景有限,大多数适合来防止metadata等。每个hash表持有两个文件,分布为primary file和overflow file。其中主要的数据结构为bucket,每个bucket中含有31个slot及一个offset,slot中存储了key及value,offset指向overflow的bucket来解决hash冲突的问题。

  • 读取时,首先根据key来得到primary file中的bucket的index,根据index可以计算出该bucket的offset(每个bucket大小固定)。然后遍历该bucket中的slot,如果没有匹配,则根据offset继续遍历overflow bucket,直到匹配或者遍历结束。
  • 写入时,同样根据key来得到primary file中bucket的offset,遍历该bucket及其overflow bucket中的slot,找到匹配的slot或者空的slot。如果有匹配的slot,则进行update,如果有空的slot,则进行insert。如果既没有匹配的slot,也没有空的slot,则会创建新的overflow bucket并进行写入。
  • 每次写入成功后,会根据load factor来判断是否需要分裂。如果当前负载大于load factor,则会在primary中创建新的bucket并进行rebalance。

容灾恢复

博主认为lotusdb作为一个持久化存储,最有问题的地方在于其容灾恢复方面。

  • 下面是后台刷新的过程,可以看到当flushMemtable方法出错中断,也就是一个immutable写入index和vlog出错了,并没有做任何的补救措施。而是继续刷新过程,这个过程就可能会导致数据的丢失。
func (db *DB) listenMemtableFlush() {sig := make(chan os.Signal, 1)signal.Notify(sig, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)for {select {case table := <-db.flushChan:db.flushMemtable(table)case <-sig:return}}
}func (db *DB) flushMemtable(table *memtable) {db.flushLock.Lock()sklIter := table.skl.NewIterator()var deletedKeys [][]bytevar logRecords []*ValueLogRecord// iterate all records in memtable, divide them into deleted keys and log recordsfor sklIter.SeekToFirst(); sklIter.Valid(); sklIter.Next() {key, valueStruct := y.ParseKey(sklIter.Key()), sklIter.Value()if valueStruct.Meta == LogRecordDeleted {deletedKeys = append(deletedKeys, key)} else {logRecord := ValueLogRecord{key: key, value: valueStruct.Value}logRecords = append(logRecords, &logRecord)}}_ = sklIter.Close()// write to value log, get the positions of keyskeyPos, err := db.vlog.writeBatch(logRecords)if err != nil {log.Println("vlog writeBatch failed:", err)db.flushLock.Unlock()return}// sync the value logif err := db.vlog.sync(); err != nil {log.Println("vlog sync failed:", err)db.flushLock.Unlock()return}// write all keys and positions to indexif err := db.index.PutBatch(keyPos); err != nil {log.Println("index PutBatch failed:", err)db.flushLock.Unlock()return}// delete the deleted keys from indexif err := db.index.DeleteBatch(deletedKeys); err != nil {log.Println("index DeleteBatch failed:", err)db.flushLock.Unlock()return}// sync the indexif err := db.index.Sync(); err != nil {log.Println("index sync failed:", err)db.flushLock.Unlock()return}// delete the walif err := table.deleteWAl(); err != nil {log.Println("delete wal failed:", err)db.flushLock.Unlock()return}// delete old memtable kept in memorydb.mu.Lock()if len(db.immuMems) == 1 {db.immuMems = db.immuMems[:0]} else {db.immuMems = db.immuMems[1:]}db.mu.Unlock()db.flushLock.Unlock()
}

总结

就如文章开始所讲,博主认为lotusdb还是一个相对比较稚嫩但挺有意思的项目,能够反映出作者的一些有意思的想法。其中的问题,随着迭代也会慢慢完善。


如果觉得本文对您有帮助,可以请博主喝杯咖啡~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/123157.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WORD中的表格内容回车行距过大无法调整行距

word插入表格&#xff0c;编辑内容&#xff0c;换行遇到如下问题&#xff1a; 回车后行距过大&#xff0c;无法调整行距。 解决方法&#xff08;并行&#xff09;&#xff1a; 方法1&#xff1a;选中要调整的内容&#xff0c;菜单路径&#xff1a;“编辑-清除-格式” 方法2&am…

No173.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

Java SE 学习笔记(十八)—— 注解、动态代理

目录 1 注解1.1 注解概述1.2 自定义注解1.3 元注解1.4 注解解析1.5 注解应用于 junit 框架 2 动态代理2.1 问题引入2.2 动态代理实现 1 注解 1.1 注解概述 Java 注解&#xff08;Annotation&#xff09;又称Java标注&#xff0c;是JDK 5.0引入的一种注释机制&#xff0c;Java语…

Unity ScrollView最底展示

Unity ScrollView最底展示 问题方案逻辑 问题 比如在做聊天界面的时候我们肯定会使用到ScrollView来进行展示我们的聊天内容&#xff0c;那么这个时候来新消息的时候就需要最底展示&#xff0c;我认为这里有两种方案&#xff1b; 一种是通过算法每一条预制体的高度*一共多少…

汇编运算符和表达式

运算符&#xff1a; 汇编语言由表达式和运算符组成&#xff0c;运算符分为数值运算符和属性运算符。属性运算符面向变量或标号。 数值运算符&#xff1a; 算术运算符&#xff1a; 运算符类型 ✓ ( 正号 ) 、 -( 负号 ) ✓ ( 加 ) 、 -( 减 ) 、 *( 乘 ) 、 /( 除 ) 、 MO…

Linux下的IMX6ULL——开发板的第一个APP和驱动实验(三)

前言&#xff1a; 万事开头难&#xff0c;如果我们在开发板上开发出第一个应用程序&#xff0c;第一个驱动程序&#xff0c;那么后续的开发就会稍微简单点&#xff0c;下面让我们来进行第一个应用程序和第一驱动程序的开发吧。 目录 一、开发板的第1个APP实验 1.通过Git仓库…

批量去除pdf每一页相同未知的同样的内容

例如我想去除每一页右下角的www.alevelcollege.com ①打开acrobat pro ②编辑文件和图像 ③ctrlF输入字符串www.alevelcollege.com替换为空 ④鼠标点击替换 ⑤回车键按下不放&#xff0c;会自动翻页&#xff0c;直到翻页到最后一页。

mac安装jenkins

1、安装jenkins之前确认是否安装了homebrew 2、开始安装jenkins 安装完如下图则安装完成 3、不想用默认ip和端口的自己改一下ip和端口 4、启动jenkins brew services restart jenkins 使用自己修改后的ip:port http://0.0.0.0:8088 根据提示信息&#xff0c;拿到管理员密码&…

深度学习中Transformer的简单理解

Transformer 网络结构 Transformer也是由编码器和解码器组成的。 每一层Encoder编码器都由很多层构成的&#xff0c;编码器内又是self-attention和前馈网络构成的。Self-attention是用来做加权平均&#xff0c;前馈网络用来组合。 但是decoder有点不同&#xff0c;多了一层En…

从0到1之微信小程序快速入门(基础知识)

目录 JSON 配置文件 WXML 模板 WXSS 样式 JS 逻辑交互 微信小程序中&#xff0c;每个页面由4 个基本文件组成&#xff0c;它们分别是&#xff1a;js文件(页面的脚本文件&#xff0c;存放页面的数据、事件处理函数等)、json文件(当前页面的配置文件&#xff0c;配置窗口的外…

html2pdf

页面布局时将需要保存在同一页pdf的dom元素用div包裹&#xff0c;并为该div添加class类名&#xff0c;例如.convertPDF&#xff0c;如果有多页创建多个.convertPDF这个div&#xff0c;再循环保存pdf即可 用到了html2canvas和JsPdf这两个插件&#xff0c;自行站内搜索安装

磁盘管理(初始化,引导块,坏块管理,固态硬盘)

目录 1.磁盘初始化2.引导块3.坏块的管理1.坏块检查2.坏块链表3.扇区备用 4.固态硬盘&#xff08;SSD&#xff09;1.原理2.组成3.读写性能特性4.与机械硬盘相比5.磨损均衡技术 1.磁盘初始化 ①进行低级格式化&#xff08;物理格式化&#xff09;&#xff0c;将磁盘的各个磁道划分…

openEuler 22.03 x86架构下docker运行arm等架构的容器——筑梦之路

为什么要这样做&#xff1f; 随着国产化的普及&#xff0c;国家政策对信创产业的支持&#xff0c;尤其一些金融证券行业、政府单位等&#xff0c;逐渐开始走国产化信创的路线&#xff0c;越来越多接触到国产 CPU &#xff08;arm 平台&#xff0c;比如华为的鲲鹏处理器&#xf…

koa搭建服务器(二)

在上一篇文章已经成功的运行了一个http服务器&#xff0c;接下来就是使用Sequelize ORM&#xff08;官方文档&#xff1a;Sequelize 简介 | Sequelize中文文档 | Sequelize中文网&#xff09;来操作数据库。 1、安装依赖 首先也是需要安装相关的依赖 npm i sequelize npm i …

MySQL面试题

面试题一 1、创建一个数据库 create database db_one; 2、 创建四张表 create table student( s_id int(10) not null comment 学号 primary key, s_name varchar(20) not null comment 姓名, s_birth year comment 生日, s_sex varchar(4) default "女" comment 性…

【PointNet—论文笔记分享】

第一个直接基于原始点云数据进行分割、分类的模型&#xff0c;之前都是基于多视图或者体素的方式。 论文: PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation代码: TensorFlow版 Pytorch版 基本模型架构&#xff1a; 分别对每个点进行特征提取…

微信小程序vue+uniapp旅游景点门票预订系统 名胜风景推荐系统

与此同时越来越多的旅游公司建立了自己的基于微信小程序的名胜风景推荐平台&#xff0c;管理员通过网站可以添加用户、景点分类、景点信息、在线预订、最新推荐&#xff0c;用户可以对景点信息进行在线预订&#xff0c;以及开展电子商务等。互联网的世界里蕴藏无限生机&#xf…

redis6.0源码分析:简单动态字符串sds

文章目录 sds简介与特性(面试)sds结构模型数据结构苛刻的数据优化数据结构优化uintX_t对齐填充 sds优势O(1)时间复杂度获取字符串长度二进制安全杜绝缓冲区溢出自动扩容机制——sdsMakeRoomFor方法 内存重分配次数优化 sds最长是多少部分API源码解读创建sds释放sds sds简介与特…

如何从Android手机上轻松恢复误删除的短信 ?

当您使用 Android 手机时&#xff0c;您可能会误删除一些 Android 短信。如果这些消息对您很重要&#xff0c;您可能想要恢复它们。在这种情况下&#xff0c;您可以尝试使用U1tData安卓数据恢复&#xff08;奇客软件&#xff09; 来完成这项工作。这篇文章将向您展示更多信息。…

MinIO安装

Minio是一个开源的分布式对象存储服务器&#xff0c;它兼容Amazon S3服务接口。它可以用于构建私有云存储&#xff0c;为应用程序提供可扩展的对象存储功能。 安装 docker安装 docker run -d -p 9000:9000 -p 50000:50000 --name minio \ -e "MINIO_ROOT_USERadminpili…