leveldb的两处设计缺陷和几处亮点

2022-03-10 00:00:00 事务 优化 内存 读写 序号

前阵子改造leveldb底层代码,发现两处设计缺陷

个是序号部分

每个key后面附加了8个字节

后一个字节是操作属性,有8位只用了1位,浪费7位。

这一位是表达修改还是删除,逻辑上value是空就可以表达删除,

看遍所有算法代码,也没发现一定要用删除标志的必要性

出于谨慎,这个位也没敢去掉,但另外7位是可以安全使用的。

可能严谨点,值不存在和值是空是两种情况,但看有些代码并没有区分这两种情况。

这个特性在使用的时候没太大价值,还不如看成一种情况


另处7个字节表达修改序列号,正常情况下通常多4,5字节足够了。

这个在我改动的时候,采用了可变长,能省多少是多少。

改的时候要注意,表达可变长的位前后都要设置,后面不设置不知道序号长,前面不设置排序会出问题。


第二个是合并文件部分

文件里的keyvalue是按key排序的,逻辑上有大量key头部是重复的,系统应该对这些存储上做优化。

看了一下底层代码,并没有。

以前做过性能优化,看到这种大批量的冗余还是很痛苦的。

也理解,这个改动不能明显提高读写性能,但容易带来bug风险

bug是自己的,硬盘投资是公司的。

想了一想,忍了,用的时候,这块也没敢动手。


不能总挑毛病,说几点好处吧。

总的说来leveldb的代码极其厚重,考虑到了生产环境上大量细节问题。

和boltdb来比,boltdb是一个精巧的玩具,精巧到你挑不出毛病,有很强的学术性,但离生产环境远了些。

写优先的做法越想越有道理,因为读有太多优化方式,写很少。

明白这一点,boltdb上的读优化马上就失色了,写上的一些缺陷就更明显了。

在事务上的缺陷更影响落地。

leveldb上的事务是独占式的,会卡死整个系统,是用于写大批数据的,并不适合用在业务上。估计通常也不适合直接面向业务层,更偏后一些。

为了方便,用的时候,我就加了一个cas(CompareAndSwap)方式的事务,没冲突递交,冲突就整个操作再走一次。大体过程如下:

事务需要新建一个内存表,同时冻结一下序号,不要让原来的主内存表写盘。后续操作都基于这个新建的内存表,递交的时候,因为key上有序号,写数据时也要去原来的内存表中查找key,查找的时候同步读出序号,和事务的begin时比一下,如果大于begin的就是改过的,当然不在内存表的一定是没有修改过的数据。整个过程性能上几乎是0成本。这都是leveldb序号设计的优点,可以玩出很多花样。

加事务的工作量和复杂度都在兼容迭代器上。


另外,业务方便,用极轻的耦合方式加了Redis相关的api.除了架构上的技巧,因为Redis的操作都是几个kv合成的,使用leveldb批量写的接口外部不需要考虑锁,舒服很多。


leveldb里用的跳表也是让人一亮,刚开始的时候没想出比B+树好在哪里。 后来才发现,大的好处是不动树结构,虽然慢一些,但不是数量级的。不动树结构衍生出来的好处太多了。一个好处是大大降低了读写冲突,改动点调整一下就可以。针对leveldb更是可以做到完全的读写分离。大部分节点的算法和其他节点的内容没有耦合。

这个设计思想值得借鉴

还有一个看bigtable论文,就是leveldb的连续写思想很早就有了,但有一个优化10年后才出来,这个方案才有机会用在生产环境中。

好的解决方案也需要时间的沉淀。

相关文章