moss是一款纯go实现的面向内存的K-V存储引擎。
moss采用一种排序线段栈的结构来实现K-V存储,支持append-only的持久化写入方式。
API
NewCollection创建一个只驻留内存的实例OpenStoreCollection创建一个有持久化的实例,如果文件存在,会加载文件内的内容
存储结构
moss将一个实例称之为一个collection,每个collection的存储分为4个区域(top/mid/base/clean):
+---------------------+
| collection |
| |
| * top |
| * mid |
| * base |
| * clean |
| |
+---------------------+
top每当有新数据写入时,会写入top区域。mid当top写入很多新数据,触发merge时,merge后的数据会移动到mid区域。base当配置持久化选项时,持久化后的数据会被移动到base区域。clean当collection配置了CachePersisted选项后,当进行持久化后的segment会被移动到clean区。
排序线段栈
moss使用一个栈(stack)结构来存储每次写入的新数据,栈中的每一个元素为一个线段(segment)。每次新写入的 数据在栈顶(top)。
moss的全部写操作都要运行在一个batch中,因此每次要有写操作时,需要先调用NewBatch获取一个batch。 batch为moss的操作提供原子性、独立性保证。 每个batch中包含一个segment和任意个子batch(当前子collection的batch)。batch继承自segment。
segment为拥有一个byte数组和一个uint64数组的数据结构,每个操作都会成为一个记录,写入segment的byte数 组中,然后在uint64数组中写入偏移量和长度进行标记定位(操作类型、key长度和value长度聚合在一个uint64中)。 由于segment的数据排序算法的局限性限制,在每个batch/segment中,每个key只能记录一个操作。
在操作完相关操作后,需要调用moss的ExecuteBatch方法来应用batch中的相关操作。在调用ExecuteBatch 后会对segment中记录的相关操作记录进行排序,成为不可变的数据段。然后调用buildStackDirtyTop, 构建/压入segment栈(segmentStack) 放入top区域,新写入的segment将放入栈顶:
+---------------------+
| * top |
| segment-0 |
| * mid |
| * base |
| * clean |
+---------------------+
当经过很多batch操作后,会变为:
+---------------------+
| collection |
| |
| * top |
| segment-4 |
| segment-3 |
| segment-2 |
| segment-1 |
| segment-0 |
| * mid |
| * base |
| * clean |
| |
+---------------------+
当collection启动(Start) 时,会启动一个merger协程,它会被用户主动触发、 被新的写入触发 或者idle超时触发。 进行merge时,首先把top和mid区域的segment stack合并成一个segment stack,放入mid区域:
+---------------------+
| collection |
| |
| * top |
| * mid |
| segment-4 |
| segment-3 |
| segment-2 |
| segment-1 |
| segment-0 |
| * base |
| * clean |
| |
+---------------------+
然后再调用mergerMain, 将mid和base区域合并起来。然后生成一个堆合并迭代器iterator, 然后将多个segment stack合并成一个新segment stack:
+---------------------+
| collection |
| |
| * top |
| * mid |
| segment-0...4 |
| * base |
| * clean |
| |
+---------------------+
持久化
在collection启动(Start) 时,还会启动一个Persister 进行持久化的逻辑。当用户配置了持久化回调 时,持久化的逻辑会被触发。当进行完上面描述的merge操作后,会调用mergerNotifyPersister 将mid区域的segment stack移动到base区域:
+---------------------+
| collection |
| |
| * top |
| segment-5 |
| * mid |
| * base |
| segment-0...4 |
| * clean |
| |
+---------------------+
然后,Persister会调用LowerLevelUpdate将base区域的segment stack传递给用户/默认的持久化回调进行持久化 合并操作,返回一个合并后的snapshot,然后将该snapshot存储在lowerLevelSnapshot。
当用户配置了CachePersisted 时,在持久化完成时,会将base区域的segment stack移动到clean区域:
+---------------------+
| collection |
| |
| * top |
| * mid |
| * base |
| * clean |
| segment-0...4 |
+---------------------+
| lower-level-storage |
| |
| mutations from... |
| segment-0...4 |
+---------------------+
否则会被清除:
+---------------------+
| collection |
| |
| * top |
| * mid |
| * base |
| * clean |
| |
+---------------------+
| lower-level-storage |
| |
| mutations from... |
| segment-0...4 |
+---------------------+
默认持久化
如果使用默认的持久方式时,使用的默认持久化回调为:
func(higher Snapshot) (Snapshot, error) {
var ss Snapshot
ss, err = s.Persist(higher, persistOptions)
if err != nil {
return nil, err
}
if storeSnapshotInit != nil {
storeSnapshotInit.Close()
storeSnapshotInit = nil
}
return ss, err
}
其核心函数为persist 它主要做几件事:
- 将
base区域的数据与之前的snapshot进行合并、压缩; - 以特定格式将
snapshot写入新的持久化文件。
默认持久化采用的写文件的函数为persistBasicSegment。
持久化文件格式
moss持久化文件的命名格式为data-xxxxxx-.moss,中间部分为文件序号,序号最大的文件为最新一份持久化文件。
文件开头4K字节为Header 部分,存储一些元数据。
文件结尾为4K字节(可能超过4K字节,但是4K字节对其存储)的Footer 部分,存储每个SegmentLoc在文件中的偏移量。SegmentLoc 记录了每个segment持久化后的信息。
moss也支持自定义持久化的方式,只要将实现CollectionOptions中LowerLevelInit/LowerLevelUpdate这些对象/回调 即可。其原生持久化也是依赖这些接口来实现的。自定义实现持久化时,就不需要使用OpenStoreCollection来创建collection了, 直接实现对应接口,使用NewCollection来创建collection即可。
总结
moss作为一个嵌入式cache引擎,嵌入到用户软件中,作为内存型的K-V存储,支持自定义持久化回调,可以方便的将数据 持久化到默认文件或者自定义的database中。但是其多层的线段栈结构也有很大的局限性:
- 每次操作的
batch都会形成一个segment,因此,每次batch的操作必须操作很多数据才能将segment合并的代价摊薄, 同时每个batch操作时,每个Key都只能操作一次,因此还可能需要使用map结构来去重 因此,适用的使用场景还是相当的有限。 - 持久化的方式使用异步的方式,就丧失了
crash consistency。而且其实现,使得用户难捕捉到持久化失败时详细的错 误,很难精确处理每条数据持久化的错误,因此其就丧失了称为一款存储引擎的资格,充其量只能称为一个不太注重持久化 的cache,如memcahced。
参考
文档信息
- 本文作者:Neal Hu
- 本文链接:https://lrita.github.io/2017/08/08/moss/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)