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
。