作者:申江伟 MatrixOne 研发工程师
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 结构解析
1 Extent
在 MatrixOne 中,Extent 负责记录一个 IOEntry 的位置信息。
+------------+----------+-------------+-------------+
| Offset(4B) | Size(4B) | OSize (4B) | Algo (1B) |
+------------+----------+-------------+-------------+
Offset = IOEntry的起始地址
Size = IOEntry存储在对象中的大小
oSize = IOEntry压缩前的原始大小
Algo = 压缩算法类型
复制代码
2 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 的元数据信息。
3 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)
复制代码
MatrixOrigin 官网:新一代超融合异构开源数据库-矩阵起源(深圳)信息科技有限公司 MatrixOne
评论