本文介绍了Databend基础。Fuse引擎,一个强大的列式存储引擎。该引擎是由Databend社区设计的,其原则如下。强大的性能,简单的架构,以及高可靠性。
在我们开始之前,请看看Databend完成的一项具有挑战性的任务。使用部署在AWS S3上的Fuse引擎,一个交易在大约一个半小时内写入了22.89TB的原始数据。
MySQL
mysql> INSERT INTO ontime_new SELECT * FROM ontime_new;
Query OK, 0 rows affected (1 hour 34 min 36.82 sec)
Read 31619274180 rows, 22.89 TB in 5675.207 sec., 5.57 million rows/sec., 4.03 GB/sec.
同时,以下条件也得到了满足。
-
分布式事务:多个计算节点可以同时读写同一个数据(这是一个将存储和计算分离的架构必须解决的第一个问题)。
-
快照隔离:不同版本的数据不会相互影响,所以可以对表进行零拷贝克隆。
-
追溯能力:允许你切换到任何版本的数据,所以你可以用时间旅行功能进行恢复。
-
数据合并:合并后可以生成新版本的数据。
-
简单而强大:数据关系用文件来描述,你可以根据这些文件来恢复整个数据系统。
从上面来看,你会发现Fuse Engine是 "受Git启发的"。在介绍Fuse Engine的设计之前,我们先来看看Git的底层是如何工作的。
Git是如何工作的
Git实现了分布式环境下的数据版本控制(包括分支、提交、签出和合并)。基于Git的语义,我们可以创建一个分布式存储引擎。市场上也有一些类似于Git的产品,比如Nessie--数据湖的事务性目录和lakeFS。为了更好地探索 Git 的底层工作机制,我们使用 Git 的语义,从数据库的角度来完成一系列的 "数据 "操作。
准备一个名为cloud.txt 的文件,内容为:
纯文本
2022/05/06, Databend, Cloud
将文件cloud.txt 提交给 Git。
外壳
git commit -m "Add cloud.txt"
Git 生成了一个快照(提交ID:7d972c7ba9213c2a2b15422d4f31a8cbc9815f71 )。
壳牌
git log
commit 7d972c7ba9213c2a2b15422d4f31a8cbc9815f71 (HEAD)
Author: BohuTANG <overred.shuttler@gmail.com>
Date: Fri May 6 16:44:21 2022 +0800
Add cloud.txt
准备另一个名为warehouse.txt
的纯文本
2022/05/07, Databend, Warehouse
将文件warehouse.txt 提交给 Git
壳
git commit -m "Add warehouse.txt"
Git 生成另一个快照(提交编号:15af34e4d16082034e1faeaddd0332b3836f1424 )。
壳牌
commit 15af34e4d16082034e1faeaddd0332b3836f1424 (HEAD)
Author: BohuTANG <overred.shuttler@gmail.com>
Date: Fri May 6 17:41:43 2022 +0800
Add warehouse.txt
commit 7d972c7ba9213c2a2b15422d4f31a8cbc9815f71
Author: BohuTANG <overred.shuttler@gmail.com>
Date: Fri May 6 16:44:21 2022 +0800
Add cloud.txt
现在,Git 为我们的数据保留了两个版本。
纯文本
ID 15af34e4d16082034e1faeaddd0332b3836f1424,Version2
ID 7d972c7ba9213c2a2b15422d4f31a8cbc9815f71,Version1
我们可以通过提交ID在不同的版本之间切换,这实现了时间旅行和表零拷贝的功能。Git是如何在底层实现这些功能的呢?这不是火箭科学。Git引入了这些类型的对象文件来描述这种关系。
-
提交:描述了树的对象信息
-
树型:描述blob对象的信息
-
Blob:描述文件信息
HEAD文件
首先,我们需要知道HEAD的指针。
Shell
cat .git/HEAD 15af34e4d16082034e1faeaddd0332b3836f1424
提交文件
提交文件记录了与提交有关的元数据,如当前的树和父级,以及提交者等。
文件路径。
纯文本
.git/objects/15/af34e4d16082034e1faeaddd0332b3836f1424
文件内容。
外壳
git cat-file -p 15af34e4d16082034e1faeaddd0332b3836f1424
tree 576c63e580846fa6df2337c1f074c8d840e0b70a
parent 7d972c7ba9213c2a2b15422d4f31a8cbc9815f71
author BohuTANG <overred.shuttler@gmail.com> 1651830103 +0800
committer BohuTANG <overred.shuttler@gmail.com> 1651830103 +0800
Add warehouse.txt
树形文件
树形文件记录了当前版本的所有文件
文件路径。
纯文本
.git/objects/57/6c63e580846fa6df2337c1f074c8d840e0b70a
文件内容。
外壳
git cat-file -p 576c63e580846fa6df2337c1f074c8d840e0b70a
100644 blob 688de5069f9e873c7e7bd15aa67c6c33e0594dde cloud.txt
100644 blob bdea812b9602ed3c6662a2231b3f1e7b52dc1ccb warehouse.txt
Blob文件
Blob文件是原始数据文件。你可以使用git cat-file 来查看文件内容(如果你使用Git来管理代码,blob就是我们的代码文件)。
外壳
git cat-file -p 688de5069f9e873c7e7bd15aa67c6c33e0594dde
2022/05/06, Databend, Cloud
git cat-file -p bdea812b9602ed3c6662a2231b3f1e7b52dc1ccb
2022/05/07, Databend, Warehouse
融合引擎
Databend的Fuse引擎从设计角度看与Git非常相似。它引入了三个描述文件:
-
快照。描述段对象的信息。
-
段落。描述块对象的信息。
-
块。描述parquet文件的信息。
让我们在Fuse Engine中重复刚才对Git所做的操作。
SQL
CREATE TABLE git(file VARCHAR, content VARCHAR);
将cloud.txt 到Fuse Engine
SQL
INSERT INTO git VALUES('cloud.txt', '2022/05/06, Databend, Cloud');
Fuse Engine生成了一个快照(快照ID:6450690b09c449939a83268c49c12bb2 )。
SQL
CALL system$fuse_snapshot('default', 'git');
*************************** 1. row ***************************
snapshot_id: 6450690b09c449939a83268c49c12bb2
snapshot_location: 53/133/_ss/6450690b09c449939a83268c49c12bb2_v1.json
format_version: 1
previous_snapshot_id: NULL
segment_count: 1
block_count: 1
row_count: 1
bytes_uncompressed: 68
bytes_compressed: 351
将warehouse.txt 写给Fuse Engine。
SQL
INSERT INTO git VALUES('warehouse.txt', '2022/05/07, Databend, Warehouse');
Fuse引擎生成另一个快照(快照ID:efe2687fd1fc48f8b414b5df2cec1e19 ),该快照与前一个快照(快照ID:6450690b09c449939a83268c49c12bb2 )有关联。
SQL
CALL system$fuse_snapshot('default', 'git');
*************************** 1. row ***************************
snapshot_id: efe2687fd1fc48f8b414b5df2cec1e19
snapshot_location: 53/133/_ss/efe2687fd1fc48f8b414b5df2cec1e19_v1.json
format_version: 1
previous_snapshot_id: 6450690b09c449939a83268c49c12bb2
segment_count: 2
block_count: 2
row_count: 2
*************************** 2. row ***************************
snapshot_id: 6450690b09c449939a83268c49c12bb2
snapshot_location: 53/133/_ss/6450690b09c449939a83268c49c12bb2_v1.json
format_version: 1 previous_snapshot_id: NULL
segment_count: 1
block_count: 1
row_count: 1
Fuse Engine现在保留了我们数据的两个版本。
纯文本
ID efe2687fd1fc48f8b414b5df2cec1e19,Version2
ID 6450690b09c449939a83268c49c12bb2,Version1
这与Git非常相似。对吗?
HEAD
Git需要一个HEAD作为条目。Fuse Engine也是如此。查看Fuse Engine的HEAD。
SQL
SHOW CREATE TABLE git\G;
*************************** 1. row ***************************
Table: git
Create Table: CREATE TABLE `git` (
`file` VARCHAR,
`content` VARCHAR
) ENGINE=FUSE SNAPSHOT_LOCATION='53/133/_ss/efe2687fd1fc48f8b414b5df2cec1e19_v1.json'
SNAPSHOT_LOCATION 是HEAD,它默认指向最新的快照 ,那么我们如何切换到ID为 的快照数据呢?首先,检查当前表的快照信息。efe2687fd1fc48f8b414b5df2cec1e19 6450690b09c449939a83268c49c12bb2
SQL
CALL system$fuse_snapshot('default', 'git')\G;
*************************** 1. row ***************************
snapshot_id: efe2687fd1fc48f8b414b5df2cec1e19
snapshot_location: 53/133/_ss/efe2687fd1fc48f8b414b5df2cec1e19_v1.json
format_version: 1 previous_snapshot_id: 6450690b09c449939a83268c49c12bb2
segment_count: 2
block_count: 2
row_count: 2
*************************** 2. row ***************************
snapshot_id: 6450690b09c449939a83268c49c12bb2
snapshot_location: 53/133/_ss/6450690b09c449939a83268c49c12bb2_v1.json
format_version: 1 previous_snapshot_id: NULL
segment_count: 1
block_count: 1
row_count: 1
然后创建一个新的表(git_v1),将SNAPSHOT_LOCATION ,指向你需要的快照文件。
SQL
CREATE TABLE git_v1(`file` VARCHAR, `content` VARCHAR) SNAPSHOT_LOCATION='53/133/_ss/6450690b09c449939a83268c49c12bb2_v1.json';
SELECT * FROM git_v1;
+-----------+-----------------------------+
| file | content |
+-----------+-----------------------------+
| cloud.txt | 2022/05/06, Databend, Cloud |
+-----------+-----------------------------+
快照文件
存储分段信息。
文件路径。
纯文本
53/133/_ss/efe2687fd1fc48f8b414b5df2cec1e19_v1.json
文件内容。
JSON
{
"format_version":1,
"snapshot_id":"efe2687f-d1fc-48f8-b414-b5df2cec1e19",
"prev_snapshot_id":[
"6450690b-09c4-4993-9a83-268c49c12bb2",
1
],
"segments":[
[
"53/133/_sg/df56e911eb26446b9f8fac5acc65a580_v1.json"
],
[
"53/133/_sg/d0bff902b98846469480b23c2a8f93d7_v1.json"
]
]
... ...
}
段落文件
存储区块信息。
文件路径。
纯文本
53/133/_sg/df56e911eb26446b9f8fac5acc65a580_v1.json
文件内容。
JSON
{
"format_version":1,
"blocks":[
{
"row_count":1,
"block_size":76,
"file_size":360,
"location":[
"53/133/_b/ba4a60013e27479e856f739aefeadfaf_v0.parquet",
0
],
"compression":"Lz4Raw"
}
]
... ...
}
块文件
Fuse Engine的底层数据使用Parquet格式,每个文件由多个块组成。
摘要
在Databend的Fuse引擎的早期设计阶段(2021年10月),需求非常明确,但解决方案的选择并不顺利。当时,Databend社区调查了市场上大量的表格式解决方案(如Iceberg)。我们面临的挑战是如何在使用现有的解决方案和建立一个新的解决方案之间做出选择。最后,我们决定开发一个简单而合适的存储引擎,使用Parquet标准作为存储格式。Fuse Engine将Parquet Footer单独存储,以减少不必要的Seek操作,并引入了更灵活的索引机制,例如,聚合和Join等操作可以有自己的索引来加速。