1. 模块概述

MetaFile模块向CachedMTreeStore提供可靠的MTree文件存储功能,提供MNode在文件中的读写接口。

本模块有以下两个特征:

  1. 以存储组子树为粒度,组织整个元数据树的持久化存储
  2. 单个文件内部为树形文件格式
  3. 用户可配置文件文件记录大小
  4. 设置文件整理线程,定期优化文件内的数据组织,将其整理为查询友好的格式
  5. 重启恢复时,文件可以恢复到一个没有脏数据的完整可用的状态


2. 文件组织设计

元数据树的持久化文件以存储组为单位进行组织,每个存储组代表的子树维护一个MTreeFile。

  1. 文件名以存储组名(full path)命名
  2. MTreeFile内部以该存储组节点为根节点


对于存储组层往上的前缀树,也使用一个MTreeFile进行管理。

  1. 该MTreeFile内部以全局MTree的root节点为根节点
  2. 该MTreeFile存储StorageGroupMNode父节点以上的MNode
  3. StorageGroupMNode父节点在该文件中指向StorageGroupMNode的地址指针值为-1,标志子节点需至具体的存储组文件进行查询


3. 树形文件格式设计

3.1. 文件整体设计

文件整体采用定长记录的组织方式,即一个定长的Header+定长Node序列。Header中存储文件的描述性信息,Node中存储MNode的序列化信息。

Header中目前的字段信息如下表所示。

字段名类型大小描述
header lengthbyte1 byte文件头的长度,1byte 可保证 256 个字节的存储,已足够当前 header 的信息存储,当前的header使用64 byte
node lengthshort2 byte

一个节点记录的长度,2 byte长度可保证65536个字节的存储;

目前代码中多使用512B或1024B,实验表明类似sg1、sg2、d1、d2这种的简单命名,1KB的Node大小可支撑100个子节点

root positionlong8 byte根节点的地址
reserved

保留字段

3.2. 文件记录Node设计

MNode对象的序列化信息在MTree文件中,以定长Node记录链表的形式进行存储。Node记录中的信息包含两个部分,Node Header与 data body, 其中Node Header包含bitmap、pre position、extension position三个字段。Node记录有如下几种类型:

  1. Root Node,根节点。

  2. Internal Node,中间节点记录形式,主要存储name、parent、children等结构信息。

  3. StorageGroup Node,在中间节点的基础上额外存储一个dataTTL字段。

  4. Entity Node,实体节点,在中间节点基础上额外存储aliasChildren、template、LastCacheMap等信息。

  5. StorageGroupEntity Node,实体节点与存储组节点的结合形式,为二者信息取并集。

  6. Measurement Node,在中间节点的基础上额外存储alias、tagOffset、schema以及lastCache信息。

  7. Extension Node,每个Node记录内依次存储字段信息,如果一个Node记录空间不够,则开额外的Extension Node进行使用,即MNode对象的序列化信息被组织成链表进行存储。

Internal Node的格式如下:

字段

描述

长度

内容

Header

bitmap

1 byte

1-3位用于标识类别

Pre position

8 byte

若Node不是Extension Node,则表示父节点地址,Extension Node的此位表示链表前置Node的地址

Next position

8 byte

链表中下一个Extension Node的地址

Body



Name

var

MNode节点名,String的存储一律采用length+content的形式,其中length采用varInt类型,content采用byte[]

templateName

var

模板名

isUsingTemplate1 byte是否使用模板

children




size

var int

child数量

name

position

var


子节点map,存储采用键值对序列的形式,其中name采用string格式,表示子节点名,position采用long,表示子节点地址,最后一个end tag 即1 byte的 00000000 表示map结束

......

name

position


StorageGroup Node的格式如下:

字段

描述

长度

内容

Header

bitmap

1 byte

1-3位用于标识类别

Pre position

8 byte

若Node不是Extension Node,则表示父节点地址,Extension Node的此位表示链表前置Node的地址

Next position

8 byte

链表中下一个Extension Node的地址


Body







Name

var

MNode节点名,String的存储一律采用length+content的形式,其中length采用varInt类型,content采用byte[]

dataTTL

8 byte

存储组中的数据生存时间

templateName

var

模板名

isUsingTemplate1 byte是否使用模板

children




sizevar intchild数量

name

position

var



子节点map,存储采用键值对序列的形式,其中name采用string格式,表示子节点名,position采用long,表示子节点地址,最后一个end tag 即1 byte的 00000000 表示map结束



......

name

position


EntityMNode

字段

描述

长度

内容

Header

bitmap

1 byte

1-3位用于标识类别

Pre position

8 byte

若Node不是Extension Node,则表示父节点地址,Extension Node的此位表示链表前置Node的地址

Next position

8 byte

链表中下一个Extension Node的地址

Body










Name

var

MNode节点名,String的存储一律采用length+content的形式,其中length采用varInt类型,content采用byte[]

templateName

var

模板名

isUsingTemplate1 byte是否使用模板

children

size

var int

child数量

name

position

var



子节点map,存储采用键值对序列的形式,其中name采用string格式,表示子节点名,position采用long,表示子节点地址,最后一个end tag 即1 byte的 00000000 表示map结束

......

name

position

aliasChildren

sizevar int

child数量

name

position

var



子节点别名map,存储方式与children相同

......

name

position


StorageGroupEntityMNode

字段

描述

长度

内容

Header

bitmap

1 byte

1-3位用于标识类别

Pre position

8 byte

若Node不是Extension Node,则表示父节点地址,Extension Node的此位表示链表前置Node的地址

Next position

8 byte

链表中下一个Extension Node的地址

Body











Name

var

MNode节点名,String的存储一律采用length+content的形式,其中length采用varInt类型,content采用byte[]

dataTTL

8 byte

存储组中的数据生存时间

templateName

var

模板名

isUsingTemplate1 byte是否使用模板

children

size

var int

child数量

name

position

var



子节点map,存储采用键值对序列的形式,其中name采用string格式,表示子节点名,position采用long,表示子节点地址,最后一个end tag 即1 byte的 00000000 表示map结束

......

name

position

aliasChildren

size

var int

child数量

name

position

var



子节点别名map,存储方式与children相同

......

name

position


Measurement Node的格式如下:

字段

描述

长度

内容

Header

bitmap

1 byte

1-3位用于标识类别

Pre position

8 byte

若Node不是Extension Node,则表示父节点地址,Extension Node的此位表示链表前置Node的地址

Next position

8 byte

链表中下一个Extension Node的地址

Body










Name

var

MNode节点名,String的存储一律采用length+content的形式,其中length采用varInt类型,content采用byte[]

alias

var

Measurement MNode节点别名

tagOffset

8 byte

标签/属性信息在TagFile中的位置

schema

type

1 byte

数据类型

encoding

1 byte

编码方式

compressor

1 byte

压缩方式

props

key

value

var




压缩参数

key

value

......

End tag




4. 文件读写功能设计

目前的文件读写功能的实现主要分三个层次

  1. MetaFile 层:对外接口为 MetaFileAccess,提供 MNode 文件读写功能;主要负责交互逻辑的处理,存储组文件的管理;与外部交互所使用的数据类为PersistenceInfo。目前的一个具体实现为 MetaFile 类。

  2. MTreeFile 层:在单个文件内,实现基于地址的 MNode 文件读写功能,包括MNode序列化和反序列化、基于地址的节点读写与节点删除等功能。文件二进制数据的读取和写入通过调用 SlottedFile 层进行实现。

  3. SlottedFile 层:对外接口为 SlottedFileAccess,提供基于地址和长度的二进制数据读取功能。实现文件头加定长记录序列的这种格式的文件数据读写,以块为数据传输单位进行文件读写。目前的一个实现为 SlottedFile 类。



4.1. MNode读取

目前的MNode读取以寻址读取为主。在获取到MNode的存储地址后,直接对文件对应位置的序列化数据进行读取,主要分为以下几个步骤:

  1. 获取二进制数据:首先读取第一个Node的bitmap中的节点类型,将文件中的Node链表中的每个Node的data body读取为完整的ByteBuffer。
  2. 反序列化:基于节点类型创建对应的对象,依次读取ByteBuffer中的字段信息。

4.2. MNode写入

MNode对象的创建与修改,都以写入的方式在文件中实现。MNode的写入分以下两个步骤:

  1. 序列化:按照MNode在文件中的字段存储顺序,将所有属性信息序列化为一个ByteBuffer。
  2. 二进制数据分割与写入:将1中得到的ByteBuffer,根据文件Node的data body长度进行分割,分割过程中给每一个Extension Node分配空闲空间,数据分割与地址分配完成后,进行数据写入


4.3. Node跨页读写

针对链表型的二进制数据,设计合理的数据结构,数据从磁盘读进内存时直接读入该数据结构并自动实现切割。

该数据结构需支持跨块/跨Node字段的二进制数据组合读取和分割写入: 每个Node的body结尾设置end tag,表示该页已结束,需要使用下一页。

该数据结构可避免序列化数据的切割和二次copy。


4.4. MNode删除与文件整理

上层执行节点删除后,其父节点中指向该节点的指针被清除,意味着该子树的存储空间不会再被访问,成为文件内碎片。

设计一个文件整理线程,定期地将MTreeFile整理为查询友好的格式。

文件整理线程的任务:

  1. 清空文件内部碎片,提高磁盘利用率
  2. 对MNode顺序进行重新排列,支持后续的功能开发,例如:
    1. 一个MNode的子节点按name字典序排列,支持按name过滤的范围查询,如查找device编号在device1-device20之间的device节点
    2. 后期引入统计信息,识别热点序列/MNode,将经常查询的一条路径上的MNode尽量存储在同一个磁盘块或相近的磁盘块

文件整理线程的触发时机:

  1. 如果仅为了处理文件碎片,可以根据delete的操作次数或delete的子树节点数量/size进行统计和触发
  2. 后期为了优化查询,可以设计定时整理


4.5. 两阶段提交

MTree文件为随机读写文件,为了保证文件的可用性和完整性,避免宕机时脏数据破坏文件,参照Mysql设计两阶段写功能。

创建tmp文件,作为磁盘buffer,记录目标地址和写入数据。


4.5.1. 磁盘buffer的格式设计

逻辑上一个磁盘buffer应具备如下三个状态:

  1. 未被使用:此时的重启恢复无需处理磁盘buffer,也证明文件处于完整可用的状态
  2. 正在准备数据:此时重启恢复需要清空磁盘buffer,新数据仍未写入文件,抛弃该次写入操作
  3. 完成数据准备,正在执行数据迁移:此时重启恢复需要重做数据迁移,直接覆盖目标地址空间


因此磁盘buffer的格式可有如下的初步设计:

  1. 标记位,是否已正在被使用
  2. 标记位,是否已完成buffer数据的准备
  3. 目标地址
  4. 待刷盘数据,以链表为粒度一次性准备好


4.5.2. 写入流程

  1. 将目标地址刷入磁盘buffer,并将其设置为正在使用且未完成数据准备状态
  2. 将所有已修改的MNode的数据写入磁盘buffer
  3. 将磁盘buffer更新为正在使用且数据准备完成状态
  4. 将磁盘buffer的数据写入MNode在MTreeFile的指定位置(覆盖原有数据)
  5. 将磁盘buffer更新为未使用状态


4.5.3. 重启恢复

单个MTreeFile重启时检查磁盘buffer状态

  1. 数据未准备好,则清空磁盘buffer,结束恢复
  2. 数据已准备好,重新将数据更新至文件中指定地址,清空buffer,结束恢复
  • No labels