nutsdb自动化Merge提案整理

2023年 7月 16日 75.2k 0

背景

实际上这个想法去年就提出来了,一直没有去实现,为什么呢?因为随着社区的发展壮大,不能总让少部分人去做一些feature,要给大家充足的发挥空间,增进社区成员对项目的了解,更有参与感和成就感。下一个版本我们打算实现这个feature。本次是我领队负责整体的进展,招募了几个小伙伴一起参与开发工作。在最近几次周会中我总是在反复的和小伙伴解释这个想法,因为有不断的有新人加入,每次都需要解释整个事情的来龙去脉,所以想着总结成一篇文章,后面有文档可依,就不需要再多做解释啦。另外有了文档之后,后续可以通过这个文档展开开发工作,也让大家知道这个项目最近在做的事情。这些是这篇文章的意义所在。本篇文章会按照,问题是什么,为什么,怎么解决来展开叙述。好,不必多说,让我们那进入正文。

image-20230308083125870

问题现状

Bitcask和LSM-Tree一样,都是通过追加写的形式记录数据,这意味着这条数据的插入,修改,删除操作本质上都是通过添加新数据的形式。举个例子,如下面这样的操作流程:

SET key_1 value_1
SET key_1 value_2
DELETE key_1

上面记录的是关于key_1这条数据的三个操作,初始化key_1的值为value_1.第二个操作是更新key_1的值将其改为value_2, 第三个操作是删除这个key_1。经过这三个操作之后,在我们的感官里key_1应该就是消失不见了。但是事实并不是这样的,这个时候在磁盘中数据的状态是这样的。

image-20230306213341671

我们可以看到在磁盘中实际上是有三条数据存在的,一条是值为value_1的数据, 一条是值为value_2的数据,最后一条数代表这条数据已经被删除的数据。实际上此时只有最后一条代表删除的数据是有用的,内存的索引也只指向了这条数据(关于这部分详情可以看Bitcask paper),前面两条数据在系统看来都是无效的。但是还占据着一部分磁盘空间。而Merge方法的工作就是回收这些无效的磁盘空间。那么Merge现在是怎么实现的呢?

要解释清楚这个问题首先让我们来看看整个nutsdb的数据存储分布是怎么样的。整个DB是由一个个文件组成的,初始化db的时候会初始化一个文件大小的阈值,当当前这个文件写到阈值大小了,会开辟出下一个文件来存储将要到来的数据。

image-20230306213847715

当前的Merge方法实现如下:

  • 遍历每一个文件中的每一条数据(最新的用于写入的文件除外)
  • 根据内存索引判断这条数据是不是最新的(PS:内存索引中存储的索引信息指向的永远是这条数据最新的版本的索引信息)
  • 如果这条数据是最新的,就重新写入这条数据。并且该条数据的内存索引将其执行最新数据的位置。
  • 当整个文件的最新数据都完成了重新写入,那么整个文件的数据都是老数据了,此时就可以删除整个文件。
  • 将db所有文件(最新的写入文件除外)之后,整个db的数据回收流程就结束了。
  • 好,到这里整个问题的现状就交代完了。下面让我们来看看当前这样做的问题是什么?

    问题是什么?

    首先我们通过提供一个Merge的方法给外部的用户自行去回收这些无用数据。问题一:实际上用户可能并不知道什么时候该回收数据。或者换个说法,他并不知道此时db中的数据的情况。

    或许他会遇到一些极端情况,比如磁盘中有一百条数据,99条是有效的,1条是无效的,这时候调用Merge方法代价,从磁盘中读取100条出来,重新写入99条,成功回收1条。这个是问题二:这种情形下执行Merge代价太高,而效果太一般了。

    另外由于当前db使用一整把读写锁,重新将最新数据写入db会申请写锁,重新写入的数据多了,整个db hang住的时间就越长,会影响用户的写入流程。如果db数据很多,遍历所有db的文件的耗时可能会到达一个不能接受的地步。

    解决方案

    那么怎么解决上述问题呢?其实上述的问题通过一个方法就可以解决,那就是建立一个对磁盘数据监控的机制,触发了某种指标的阈值,就自动执行Merge操作,就可以解决了。首先用户不知道磁盘中数据的情况,就不知道该不该执行Merge操作去回收掉无效的磁盘空间。我们帮助他去监控磁盘情况,也帮助他触发回收,最后把这个操作弄成配置,用户可以选择开启或者关闭这个功能即可。

    那么新的问题来了,该建立怎么样的监控体系呢?来让我们看下下面的问题拆解。

    如何建立监控体系

    首先让我们来回顾整个Merge操作的流程,我们会发现,对于一个文件来说,Merge操作会读出所有的数据,也就是全量读取这个文件是不可避免的,判断当前这条数据是最新的之后会重新写入这条数据。也就是所有这个文件中的最新数据都会重新写入一遍。嗯?等会?如果写入少了是不是就更省写入资源了?操作也就更快了,db hang住的时间也就更短了? 优化点这不就来了吗?

    问题的解决之道在上面的分析中已经找到了,首先对于整个文件的读取,我们是没办法避免的,能优化的地方就是让Merge操作尽量少的写入。那么整个流程效率就会高很多。所以要建立的指标有以下两个:

  • 统计一个文件中的数据总量以及无效数据的量(有效的也可以)
  • 统计一个文件中有效的磁盘空间和无效的磁盘空间。
  • 影响写入的因素有两种,一是写入数据的次数,二是写入数据的大小。通过建立上述两个指标,在设置一个阈值,这两个指标其中之一到达阈值之后我们可以触发一次针对一个文件的Merge。Merge精细到文件粒度,也就是说每次触发Merge只会Merge单个文件,而不会Merge整个db的文件,这样可以减少Merge的操作时间,从而降低对用的影响。

    FAQ

    FAQ环节记录在和社区小伙伴们开周会期间被问到的一些问题。

    1. 有了针对单个文件的Merge是否还需要针对db所有文件的Merge?

    答:我认为是不需要的。为什么呢?如果我们设置了整个db的回收阈值是40%,也就是说db的无效key或空间达到了40%的时候进行整个dbd的Merge。这个40%可以理解为是db中所有文件的无效平均值,那么总有一些文件高于这个值,总有一些文件低于这些值,高于这个值的当然可以进行回收,但是低于这个值的文件回收效率就会达不到预期。举个例子,如果db中一个文件的无效数据占比是100%,而另外一个是0%,这时候是我们建了一个针对整个db的自动回收策略,很明显第一个文件应该早早被Merge,而另外一个不应该进行回收Merge。简而言之,针对整个db的回收策略无法避免数据的分布问题。

    2. 是否会存在老数据覆盖新数据的问题?

    答:不存在。在Merge的时候会根据索引判断当前数据是不是最新的,如果不是最新数据不会进行重新写入。如果是最新的,进行重新写入后会更新内存索引,不存在这个问题。

    总结

    本期主要给大家分享这个问题的前因后果以及解决的方案,接下来的我作为整个feature的owner会推动这个事情落地,目前只是想法与开始落地阶段,可能实现会有一些差异,整个feature开发完之后会再有一片总结文章。关于这个issue有任何问题可以私信联系我,或者在下方github地址下评论。我看到会回复你的。感谢。

    延伸阅读

  • issue地址: github.com/nutsdb/nuts…
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论