moss 源码解析

2017/08/08 database moss

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区域。
  • midtop写入很多新数据,触发merge时,merge后的数据会移动到mid区域。
  • base 当配置持久化选项时,持久化后的数据会被移动到base区域。
  • cleancollection配置了CachePersisted选项后,当进行持久化后的segment会被移动到clean区。

排序线段栈

moss使用一个栈(stack)结构来存储每次写入的新数据,栈中的每一个元素为一个线段(segment)。每次新写入的 数据在栈顶(top)。

moss的全部写操作都要运行在一个batch中,因此每次要有写操作时,需要先调用NewBatch获取一个batchbatchmoss的操作提供原子性、独立性保证。 每个batch中包含一个segment和任意个子batch(当前子collectionbatch)。batch继承自segment

segment为拥有一个byte数组和一个uint64数组的数据结构,每个操作都会成为一个记录,写入segment的byte数 组中,然后在uint64数组中写入偏移量和长度进行标记定位(操作类型、key长度和value长度聚合在一个uint64中)。 由于segment的数据排序算法的局限性限制,在每个batch/segment中,每个key只能记录一个操作

在操作完相关操作后,需要调用mossExecuteBatch方法来应用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时,首先把topmid区域的segment stack合并成一个segment stack,放入mid区域:

    +---------------------+
    | collection          |
    |                     |
    | * top               |
    | * mid               |
    |     segment-4       |
    |     segment-3       |
    |     segment-2       |
    |     segment-1       |
    |     segment-0       |
    | * base              |
    | * clean             |
    |                     |
    +---------------------+

然后再调用mergerMain, 将midbase区域合并起来。然后生成一个堆合并迭代器iterator, 然后将多个segment stack合并成一个新segment stack

    +---------------------+
    | collection          |
    |                     |
    | * top               |
    | * mid               |
    |     segment-0...4   |
    | * base              |
    | * clean             |
    |                     |
    +---------------------+

持久化

collection启动(Start) 时,还会启动一个Persister 进行持久化的逻辑。当用户配置了持久化回调 时,持久化的逻辑会被触发。当进行完上面描述的merge操作后,会调用mergerNotifyPersistermid区域的segment stack移动到base区域:

    +---------------------+
    | collection          |
    |                     |
    | * top               |
    |     segment-5       |
    | * mid               |
    | * base              |
    |     segment-0...4   |
    | * clean             |
    |                     |
    +---------------------+

然后,Persister会调用LowerLevelUpdatebase区域的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 它主要做几件事:

  1. base区域的数据与之前的snapshot进行合并、压缩;
  2. 以特定格式将snapshot写入新的持久化文件。

默认持久化采用的写文件的函数为persistBasicSegment

持久化文件格式

moss持久化文件的命名格式为data-xxxxxx-.moss,中间部分为文件序号,序号最大的文件为最新一份持久化文件。

文件开头4K字节为Header 部分,存储一些元数据。

文件结尾为4K字节(可能超过4K字节,但是4K字节对其存储)的Footer 部分,存储每个SegmentLoc在文件中的偏移量。SegmentLoc 记录了每个segment持久化后的信息。

moss也支持自定义持久化的方式,只要将实现CollectionOptionsLowerLevelInit/LowerLevelUpdate这些对象/回调 即可。其原生持久化也是依赖这些接口来实现的。自定义实现持久化时,就不需要使用OpenStoreCollection来创建collection了, 直接实现对应接口,使用NewCollection来创建collection即可。

总结

moss作为一个嵌入式cache引擎,嵌入到用户软件中,作为内存型的K-V存储,支持自定义持久化回调,可以方便的将数据 持久化到默认文件或者自定义的database中。但是其多层的线段栈结构也有很大的局限性:

  • 每次操作的batch都会形成一个segment,因此,每次batch的操作必须操作很多数据才能将segment合并的代价摊薄, 同时每个batch操作时,每个Key都只能操作一次,因此还可能需要使用map结构来去重 因此,适用的使用场景还是相当的有限。
  • 持久化的方式使用异步的方式,就丧失了crash consistency。而且其实现,使得用户难捕捉到持久化失败时详细的错 误,很难精确处理每条数据持久化的错误,因此其就丧失了称为一款存储引擎的资格,充其量只能称为一个不太注重持久化 的cache,如memcahced

参考

Search

    Table of Contents