重大更新,Nutsdb重构Bucket管理模块

背景

众所周知,Nutsdb提供了Bucket这个概念,相当于数据库中数据表的概念,方便用户在数据层面做到对数据的业务切分和管理。但是对于这个概念,现有的设计与实现并不是很完美, 对于一些场景的拓展性还是不够的。是怎么一回事呢? 且让我们来捋一捋现在Bucket机制的设计与实现,讲述清楚问题所在,方便读者了解本次我做了什么事情吧~

现状

这时候不得不回到下面这张老图了,相信看过我之前写的文章的小伙伴对这张图肯定不陌生。当下Nutsdb对于数据的存储格式就是这样设计的,header存储这条数据的元信息,bucket,key,value存储的就是这条数据实际的 信息。现在无论是对于数据的增加删除修改操作,还是对与bucket的新增和删除操作,都是通过添加这样一条数据写入到磁盘中,来实现对数据的持久化机制的。也就是说,bucket的值(比如你有个的bucket叫aaa)实际上是和数据(比如你要存储的kv数据是key_1, value_1)存储在一起的。另外对数据的增加删除和修改,以及对bucket的增加删除和修改,都是通过写入一条以下格式的数据来实现的。

这么做比较简单直白,浅显易懂,同时也会带来一些问题。什么问题呢?且看下面为你娓娓道来。

问题是什么?

1. 重复存储bucket的值

从上面entry数据的存储协议设计来看,我们可以明确的一点是,每存储一条数据,会连带着bucket也存储一次。比如这个bucket的名字叫做bucket_1, 另外他有100万条数据,这时候其实bucket_1也存储了100万次。这样会造成存储空间上的浪费。

2. 读取重复的bucket值

另外对于Bitcask存储模型来说,重启的时候需要读取所有的数据来重新构建索引以恢复DB原有的运行状态。再拿上面的例子来分析的话,就是这一百万条同一个bucket下的数据,其中的bucket的值需要读取一百万次。这里也是不少的性能损耗。一条两条数据可能没什么。如果几万,几十万,几百万数据,累积起来就是不小的损失了。

另外一个关于重启的性能损失,假设现在有这样的一个场景。

  1. 创建bucket, bucket的名字是bucket_1
  2. 往这个bucket中写入100万条数据
  3. 删除这个bucket

在这个场景下,重启的时候,读取每一条数据的时候,是不会管你这条数据对应的bucket的状态是怎么样的,因为确实也管不了。当前的实现上,读取到一条数据的时候是不会知道这条数据是否被删除的。直到读取到对应删除bucket的那条数据之后,才会知道这个bucket被删除了。这样造成的结果是,我们在读取到删除bucket的时候之前,会把之前的一百万条数据都放进内存中构建好索引,然后在读取删除bucket数据之后会删除这部分内存索引,然后这部分的内存空间在下一次GC的时候才会被回收掉。

3. bucket级别的更新变得难以支持

几个月前在我们的交流群中一个用户提到了这个点,现在bucket只支持新增和删除操作,用户建议新增一个更新操作,我觉得用户的这个诉求也是合理的,只是对于现在这个实现方案来说,很难在这个基础上实现这个feature。因为在某一个时刻更新了一个一个bucket的名字,从bucket_1改成bucket_2, 那么在这之前写入的,和bucket_1关联的数据该怎么办呢?总不能重新再写一次这些数据吧?

How,如何改善现状?

好了,以上就是我能想到的现有的实现方式带来的问题,那么我们要怎么优化这个问题呢?给这个问题带来一个有效的解决方案呢?实际上这个解决方法我去年就已经想到了,当时建了个issue,然后由于种种原因一直没有坐上去。还是太懒了呀。。

那么具体的方案是什么呢?既然这些问题都是bucket信息和具体的数据存在一起导致的,那么把bucket单独拿出来另外存在一个地方,这不就可以解决了吗?

嗯?怎么解决啊?各位看官且看我下面娓娓道来。

技术方案

首先我们讲讲分离bucket存储为什么可以解决上述的这些问题。

首先对于第一个问题,如果不存bucket的值了,那么就需要存储一个与之相关联的值,因为我们需要知道这条数据对应的bucket是什么。这个值可以叫做bucket_id, 是给一个bucket的一个身份标识。那么理性分析一下,如果bucket_id的值是uint32类型,这时候占4个byte的存储空间,只要bucket的名字长度大于4个byte,这里就会起到省存储空间的作用。

另外,对于第二个问题,如果我把所有的bucket信息存储到一个单独的文件中,比如bucket.meta. 在启动的时候我可以先读取这个文件,恢复在db关闭的时候,这个db的bucket现场。这时候我就知道什么bucket是确实存在的,什么bucket已经被删除了(内存中会有一个模块管理当前db中所有的bucket信息,在这个模块中找不到这个bucket了,就算是已经被删除了) 然后我们再读取具体存储数据的文件。在构建索引之前先做一次判断,发现数据中的bucket_id在bucket管理模块中已经不存在了,就不构建这个bucket相关的索引了,因为现在构建了,后面也是需要删掉的。这时候第二个问题也被解决了。

关于第三个问题,就更好解决了,我们给每一个bucket都分配了一个独一无二的id,这时候更新bucket的话可以记录在bucket的管理文件bucket_meta中, id还是不变的,这样做简单高效,无需做太多的操作就解决了这个问题。

好,我们在上面已经讲完了解决问题的大概思路,下面我们来稍微展开一下是如何实现的。

实现

上面讲的是比较笼统的解决方法和思路,我这里会做一个详细的展开,以求读者可以更清晰的理解这个feature都做了什么改动。

整体架构调整

首先是整体架构的调整。在原来的版本中整体架构是这样子的。在内存中会有一个内存索引模块,为磁盘中的每一条数据提供索引信息。

现在的整体架构应该变成这个样子。首先我们可以看到,内存中不仅仅有索引模块了,还有bucket的管理模块,这一块的作用是存放现在db中最新的bucket状态,也就是db系统运行到当前这个时刻,所有bucket的状态,当然被删除的bucket是不会出现在这里的。

其次磁盘中也不仅仅有数据模块了,同时存在的是存储bucket信息的bucket.meta文件,这个文件实现用户对bucket操作的持久性的。也就是说用户对bucket的所有操作都会存储在这个文件之中,db恢复的时候会读取这个文件恢复bucket现场。在我的设计里,这个文件也是append only的,也就是说所有对bucket的新增,删除,修改,都会以一条数据的形式写入到这个文件中。

bucket存储格式

在上述的bucket.meta文件中,每一条数据存储格式都是上图这个样子的。让我们来解释一下每个字段的意思吧~

  1. Meta: 元信息,是整条数据的描述信息。

    1. CRC 数据校验值。磁盘内部如果发生01互换,可以通过读取这条数据的时候计算比对这个值来判断数据是否异常。
    2. Op代表当下这条数据是用户具体什么操作带来的结果,如更新,删除,新增。
    3. Size,表示数据有效部分的长度。就是后面id,ds,name的总长度。
  2. id,是每条数据的唯一标识。

  3. ds,代表bucket的数据结构是什么,当前我们支持多个数据结构,每个数据结构可以有名字相同的bucket,以这个字段作为区分。

  4. name是原来存储在数据中bucket的值。

如何运作解决问题?

我们想象在db运行过程中产生了下面这样的bucket操作

  1. add bucket 1
  2. add bucket 2
  3. delete bucket 1

在重启db的时候读取bucket.meta文件是从前往后读,因为bucket.meta文件写入顺序就是从前往后写,所以越往后的操作就是越新的,bucket的最终状态就是在最后读到这个bucket信息的状态。所以我们可以想象,重启恢复的时候,我们要做的操作就是模拟用户的所有操作,从而恢复了bucket的现场。至于更新bucket操作也是一样的,因为bucket id是不变的,取最后更新的值就好了,完美解决所有问题。

相关推荐
毅航18 分钟前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题33 分钟前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油42 分钟前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug2 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕3 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A13 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈
蜗牛快跑1233 小时前
github 源码阅读神器 deepwiki,自动生成源码架构图和知识库
前端·后端
嘻嘻嘻嘻嘻嘻ys3 小时前
《Vue 3.4响应式超级工厂:Script Setup工程化实战与性能跃迁》
前端·后端
橘猫云计算机设计3 小时前
net+MySQL中小民营企业安全生产管理系统(源码+lw+部署文档+讲解),源码可白嫖!
数据库·后端·爬虫·python·mysql·django·毕业设计
执念3653 小时前
MySQL基础
后端