作者:申江伟 MO研发工程师
目录
Part 1.
Layout需要满足的条件及解决的问题
Part 2.
结构解析
Part 3.
通过Extent读数据
Part 4.
版本兼容
本文字数:4200字+ 阅读时间:8分钟+
MatrixOne从0.5设计开始就已经确定采用列存结构来存储数据集,原因如下:
很容易对AP优化;
通过引入Column Family的概念可以对负载灵活适配。假如所有列都是一个Column Family,也就是所有的列数据保存在一起,这就跟数据库的HEAP文件非常类似,可以表现出行存类似的行为,典型的OLTP数据库如PostgreSQL就是基于HEAP来做的存储引擎。假如每个列都是独立的Column Family,也就是每一列都独立存放,那么就是典型的列存。通过定义Column Family,用户可以方便地在行存和列存之间切换,这只需要在DDL表定义中指定即可。
Part 1
Layout需要满足的条件及解决的问题
设计一种Layout,首先要提出一个疑问。
我们到底需要满足什么功能和需求?
对于MatrixOne来说,需要存储的数据类型有很多种,比如:数据库表数据、元数据、数据库业务trace log、查询结果的cache... ...
功能1:支持存储MatrixOne所有的业务类型数据。
MatrixOne需对接S3或共享对象存储,每一个对象需要存储指定行数或Size的存储集,所以一个对象会存储多个数据单元,对于每一个数据单元我们称之为Block。这些Block需要高效读取和管理,比如一个查询需要读取哪些Block,首先需要拿到Block的元数据并分析,然后再根据这些Block的元数据得到真正数据的存放地址并读取。
功能2:便捷高效的元数据。
当MatrixOne运行一段时间,客户需求不断增加,这时不得不修改当前的Layout。
功能3:支持数据的版本兼容和控制。
支持数据分析工具和添加Scrub任务。
功能4:支持从数据对象文件中重建MatrixOne的表结构。
根据上诉几个问题,我们设计了现在的Layout,它由Header、数据区、索引区、元数据区和Footer组成。这些区域的作用如下:
Header :记录了当前Layout的版本、元数据区的位置等信息。
数据区 :存储了数据对象的数据信息。
索引区 :存储了数据对象的索引信息。
元数据区 :存储了数据对象的元数据信息,这些元数据包括数据对象的类型、数据对象的大小、行数、列数、压缩算法等信息。
Footer :可以看作是一个Header的镜像。
Part 2
结构解析
Extent
在MatrixOne中,Extent负责记录一个IOEntry的位置信息。
+------------+----------+-------------+-------------+
| Offset(4B) | Size(4B) | OSize (4B) | Algo (1B) |
+------------+----------+-------------+-------------+
Offset = IOEntry的起始地址
Size = IOEntry存储在对象中的大小
oSize = IOEntry压缩前的原始大小
Algo = 压缩算法类型
ObjectMeta
为什么我们先介绍ObjectMeta而不是Header,因为MatrixOne的查询业务是从ObjectMeta开始的。 MatrixOne向S3写入数据成功后,会返回一个记录ObjectMeta位置的Extent,并保存到Catalog中。 当MatrixOne执行查询操作需要读取数据时,可以通过这个Extent拿到ObjectMeta,从而拿到Block的真实数据。
ObjectMeta由多个BlockMeta、一个MetaHeader、一个BlockIndex组成。 MetaHeader和BlockMeta结构一致,用来记录整个Object的全局信息,比如dbID,TableID,ObjectName和Object全局数据的的ZoneMap、Ndv、nullCut等等。
BlockIndex记录后面每一个BlockMeta的地址。ObjectMeta在MatrixOne系统中使用非常频繁, 虽然每一个BlockMeta的地址可以遍历算出来,但是为了节省开销和提高性能,还是记录了BlockIndex。
BlockMeta记录了每一个Block的元数据信息。
BlockMeta&MetaHeader
BlockMeta由一个Header和多个ColumnMeta组成。
Header记录了BlockID、Block的Rows、Column Count等信息。
ColumnMeta记录了每一列的数据地址、Null Count(当前列Null值的数量)、Ndv(当前列有多少不同的值)等信息。
>>> Block Meta Header
+----------------------------------------------------------------------------------------------------+
| Header |
+----------+------------+-------------+-------------+------------+--------------+--------------------+
| DBID(8B) | TableID(8B)|AccountID(4B)|BlockID(20B) | Rows(4B) | ColumnCnt(2B)| BloomFilter(13B) |
+----------+------------+-------------+-------------+------------+--------------+--------------------+
| Reserved(37B) |
+----------------------------------------------------------------------------------------------------+
| ColumnMeta |
+----------------------------------------------------------------------------------------------------+
| ColumnMeta |
+----------------------------------------------------------------------------------------------------+
| ColumnMeta |
+----------------------------------------------------------------------------------------------------+
| .......... |
+----------------------------------------------------------------------------------------------------+
DBID = Database id
TableID = Table id
AccountID = Account id
BlockID = Block id。
Rows = MetaHeader中为对象的行数,BlockMeta中为当前Block的行数
ColumnCnt = 在对象或Block中有多少列
BloomFilter = 存储BloomFilter区域的地址,只在MetaHeader中有效
>>> Column Meta
+--------------------------------------------------------------------------------+
| ColumnMeta |
+--------+---------+-------+-----------+---------------+-----------+-------------+
|Idx(2B) |Type(1B) |Ndv(4B)|NullCnt(4B)|DataExtent(13B)|Chksum(4B) |ZoneMap(64B) |
+--------+---------+-------+-----------+---------------+-----------+-------------+
| Reserved(32B) |
+--------------------------------------------------------------------------------+
Idx = Column的序号
Ndv = Column中有多少不同的值
NullCnt = Column中有多少Null值
DataExtent = Column数据的位置
Chksum = Column数据的checksum
ZoneMap = Column的ZoneMap,大小固定为64B
>>> Header
Header和Footer记录信息一致,只不过位置不同。
+---------+------------+---------------+----------+
|Magic(8B)| Version(2B)|MetaExtent(13B)|Chksum(4B)|
+---------+------------+---------------+----------+
Magic = Engine identity (0x0xFFFFFFFF)
Version = 对象文件的版本号
MetaExtent = ObjectMeta的位置信息
Chksum = ObjectMeta的checksum
>>> Footer
+----------+----------------+-----------+----------+
|Chksum(4B)| MetaExtent(13B)|Version(2B)| Magic(8B)|
+----------+----------------+-----------+----------+
Part 3
通过Extent读数据
在介绍ObjectMeta时,我们知道,MatrixOne向S3写入数据成功后, 会返回一个记录ObjectMeta位置的Extent。这里时候我们会通过Extent来执行读数据的操作。
首先,通过Extent中的地址,向S3请求读一个IO Entry,并且放入MatrixOne的cache中。 这个IO Entry就是ObjectMeta的全部内容。通过位移计算可以拿到BlockIndex,根据BlockIndex记录的Extent可以得到被读的 BlockMeta,到此为止我们元数据操作已经结束,剩下就是向S3请求读一个个Column Data了。
+-----------+
| Extent |
+-----------+
|
|
+-------------------------+
| IO Entry |
+-------------------------+
| ObjectMeta |
+-------------------------+
|
|
+---------------------------------------------------------------------
| BlockIndex |
+--------+------------+------------+------------+-----------+--------+
| Count | <Extent-1> | <Extent-2> | <Extent-3> | Extent-4> | ...... |
+--------+------------+------------+------------+-----------+--------+
| |
| |
+------------------------------------------------+ |
| Block1(BlockMeta) | |
+--------------+--------------+--------------+---+ |
|<ColumnMeta-1>|<ColumnMeta-2>|<ColumnMeta-3>|...| |
+--------------+--------------+--------------+---+ |
| | | |
| | | +------------------+
+----------+ +----------+ +----------+ | Block4(BlockMeta)|
| IO Entry | | IO Entry | | IO Entry | +------------------+
+----------+ +----------+ +----------+ | <ColumnMeta>... |
|ColumnData| |ColumnData| |ColumnData| +------------------+
+----------+ +----------+ +----------+ |
|
+------------------+
| IO Entry... |
+------------------+
Part 4
版本兼容
>>> IOEntry
IOEntry表示一个IO单元,具体对应在Layout中就是:ObjectMeta、每一个Column的数据、BloomFilter区还有Header和Footer。 除了Header和Footer,我们需要在每一个IOEntry头添加两个flag:Type&Version。
每一个结构或模块需要实现Encode/Decode函数,然后注册到MatrixOne中,MatrixOne读出IOEntry后会根据这两个flag来选择对应的代码。
const (
IOET_ObjectMeta_V1 = 1
IOET_ColumnData_V1 = 1
IOET_BloomFilter_V1 = 1
...
IOET_ObjectMeta_CurrVer = IOET_ObjectMeta_V1
IOET_ColumnData_CurrVer = IOET_ColumnData_V1
IOET_BloomFilter_CurrVer = IOET_BloomFilter_V1
)
const (
IOET_Empty = 0
IOET_ObjMeta = 1
IOET_ColData = 2
IOET_BF = 3
...
)
以ObjectMeta为例。我们需注册V1的Encode/Decode函数代码,设置IOET_ObjectMeta_CurrVer为V1,并写入数据。
const (
IOET_ObjectMeta_V1 = 1
IOET_ObjectMeta_V2 = 2
...
IOET_ObjectMeta_CurrVer = IOET_ObjectMeta_V2
...
)
func EncodeObjectMetaV2(meta *ObjectMeta) []byte {
...
}
func DecodeObjectMetaV2(buf []byte) *ObjectMeta {
...
}
RegisterIOEnrtyCodec(IOET_ObjMeta,IOET_ObjectMeta_V2,EncodeObjectMetaV2,DecodeObjectMetaV2)
ObjectMeta.Write(IOET_ObjMeta, IOET_ObjectMeta_CurrVer)