通过本篇文章带大家理解一下 Databend 的存储结构。Databend 内置的 Table 引擎为 Fuse table engine,也是接下来要花重点篇幅要讲的。
另外,Databend 还支持外置的 Hive table 及 Icebreg Table ( 即将到来)。Fuse table 是 Databend 直接把数据存储到 S3 类对象存储上,从而让用户达到一个按需付费,无须关注存储的高可用及扩容,副本这些问题。
Hive Table 是利用 Databend 替换 Hive 的查询能力,从而减少 Hive 计算节点,起到降本增效的效果(该功能已经使用)。
Iceberg Table 正在规划中 https://github.com/datafuselabs/databend/issues/8216
在 Fuse Table 中有一些基础概念先做一个解释方便更想 Databend Fuse Table 的存储结构。
1. 什么是 db_id?
这是 Databend 中的一个 internal 的标识 (u64),不是暴露给用户使用,Databend 对于 create database 会在对应的 bucket/[root] 下面创建一个整数命名的目录。
2. 什么是 table_id?
这是 Databend 中的一个 internal 的标识 (u64),不是暴露给用户使用,Databend 对于 create table 会在 /bucket/[root]/<db_id>/ 创建一个整数命名的目录。
3. Databend 的存 block 文件是什么?
Databend 最终存储 block 是以 Parquet 为格式存储,在存储上以表为单位,文件名为:[UUID].parquet, 存储路径为:
/bucket/[root]/<db_id>/<table_id>/_b/<32 位 16 进制字符串 >_v0.parquet
如:d5ee665801a64a079a8fd2711a71c780_v0.parquet
4. Databend 中 segment 文件是什么?
Databend 中用于组织 Block 的文件。一个 segment 可以多含的 Block 块,文件是 json 格式: /bucket/[root]/<db_id>/<table_id>/_sg/<32 位 16 进制字符串 >_v1.json 。
如:3b5e1325f68e47b0bd1517ffeb888a36_v1.json
5. Snapshot 是什么?
snapshot 相当于每一个数据的一个版本号(uuid, 32 位 16 进制字符串)。每个写入动作都会有一个唯一的版本号, json 格式,内部包含对应的 segment 文件, /bucket/[root]/<db_id>/<table_id>/_ss/<32 位 16 进制字符串 >_v1.json。
如:e7ccbdcff8d54ebe9aee85d9fbb3dbcb_v1.json
6. Databend 支持什么索引?
Databend 目前支持三类索引:min/max index, sparse index, bloom filter index 。其中 min/max, sparse index 在 Block 的 parquet 及对应的:ss, segment 中都有存储,bloom fliter 是单独存储为 parquet 文件。
Databend 存储结构 Databend 整体上的存储结构大概如下:
/bucket/[root]/snapshot 下面有 N 多的 segment , 一个 segment 里包含至少一个 block, 最多 1000 个 block 。
存储配置
[storage]
# fs | s3 | azblob | obs
type = "s3"
# To use S3-compatible object storage, uncomment this block and set your values.
[storage.s3]
bucket = "testbucket"
root = "20221012"
endpoint_url = "url"
access_key_id = "=user"
secret_access_key = "mypassword"
上面这段配置的作用:以 s3 方式把文件存到 testbucket 下面的 20221012 目录, 最终会形成如下的结构:
其中配置中 root 可以省略。
例如:/testbucket/20221012/17818/17825 对应的是 /bucket/root/db_id/table_id 这样一个结构。
table_id 里面每个目录的意义
| 目录 | 意义 |
|---|---|
| _b | 用于存储数据的真正block, 以parquet 格式存储 |
| _i_b_v2 | 数据本身的 bloom fliter 索引,以 parquet 格式存储 |
| _sg | 全称:segment 用于管理 block 组成,json 文件格式, 一个 sg 文件最少包含一个 block ,最多包含 1000 个 block |
| _ss | 全称:snapshot, 用于关联一个版本对应的 segment |
| last_snapshot_location_hint | 指向最后一个 snapshot 存储的位置 |
验证1 ss/sg/_b/_i_b_v2 关系
为了分析他们的关系,这里通过一个 create database/ create table / insert 例子来看看他们是怎么生成的。
create database wubx;
use wubx;
create table tb1(id int, c1 varchar);
insert into tb1 values(1, 'databend');
show create table tb1;
最后通过 show create table 可以看到:
CREATE TABLE `tb1` (
`id` INT,
`c1` VARCHAR
) ENGINE=FUSE SNAPSHOT_LOCATION='17818/17825/_ss/e7ccbdcff8d54ebe9aee85d9fbb3dbcb_v1.json'
这里可以看到:
1.查询对应的 snapshot
MySQL [wubx]> select snapshot_id, snapshot_location from fuse_snapshot('wubx','tb1')\G;
*************************** 1. row ***************************
snapshot_id: e7ccbdcff8d54ebe9aee85d9fbb3dbcb
snapshot_location: 17818/17825/_ss/e7ccbdcff8d54ebe9aee85d9fbb3dbcb_v1.json
1 row in set (0.005 sec)
2.接下来我们看一下,这个 snapshot 中包含那些 segment:
MySQL [wubx]> select * from fuse_segment('wubx','tb1', 'e7ccbdcff8d54ebe9aee85d9fbb3dbcb')\G;
*************************** 1. row ***************************
file_location: 17818/17825/_sg/3b5e1325f68e47b0bd1517ffeb888a36_v1.json
format_version: 1
block_count: 1
row_count: 1
bytes_uncompressed: 28
bytes_compressed: 296
1 row in set (0.006 sec)
从这个查询中可以看到 snapshot: e7ccbdcff8d54ebe9aee85d9fbb3dbcb 只包含一个 segment: 17818/17825/_sg/3b5e1325f68e47b0bd1517ffeb888a36_v1.json, 而这个 segment 只有一个 1 block,这个 Block 只有 1 行数据。对应的 JSON 文件:
{
"format_version": 1,
"blocks": [
{
...
"location": [
"17818/17825/_b/d5ee665801a64a079a8fd2711a71c780_v0.parquet",
0
],
"bloom_filter_index_location": [
"17818/17825/_i_b_v2/d5ee665801a64a079a8fd2711a71c780_v2.parquet",
2
],
"bloom_filter_index_size": 470,
"compression": "Lz4Raw"
}
],
"summary": {
...
}
}
原始文件较长,有兴趣的可以详细阅读一个原文件。
3.对应的 block 查询
MySQL [wubx]> select * from fuse_block('wubx','tb1')\G;
*************************** 1. row ***************************
snapshot_id: e7ccbdcff8d54ebe9aee85d9fbb3dbcb
timestamp: 2022-10-14 06:53:55.147359
block_location: 17818/17825/_b/d5ee665801a64a079a8fd2711a71c780_v0.parquet
block_size: 28
bloom_filter_location: 17818/17825/_i_b_v2/d5ee665801a64a079a8fd2711a71c780_v2.parquet
bloom_filter_size: 470
1 row in set (0.006 sec)
基于上面的原理:
对于 Databend 写入推荐使用批量写入,不推荐单条的 insert 做生成中的数据生成。在 Databend 海量数据写入推荐使用 copy into, streaming_load , clickhouse http handler 这三种方法, 其中前两种吞吐能力最好。
验证1 ss/sg/_b/_i_b_v2 关系
多次重复制执行:Insert into tb1 select * from tb1; 共执行 10 次,加上原来 1 次,总共会形成 11 个 snapshot:
接下来看 tb1 的 snapshot 指向:17818/17825/_ss/5a0ba62a222441d3acd2d93549e46d82_v1.json
show create table tb1;
CREATE TABLE `tb1` (
`id` INT,
`c1` VARCHAR
) ENGINE=FUSE SNAPSHOT_LOCATION='17818/17825/_ss/5a0ba62a222441d3acd2d93549e46d82_v1.json'
Q1:snapshot 主要用来做什么?
Databend 基于 snapshot 获取相应版本的数据,Databend 也是基于 snapshot 实现事务的 RR 隔离级别。
例如:Select count() from tb1;相当于:select count() from tb1 at(snapshot=>'5a0ba62a222441d3acd2d93549e46d82');
这个 at 语句是 time travel 的一个特性,对于 time travel 可以参考:https://databend.rs/doc/reference/sql/query-syntax/dml-at#obtaining-snapshot-id-and-timestamp
Q2:snapshot 是否可以被清理?
可以的。
清理 snapshot 命令:optimize table tb1; 或是 optimize table tb1 purge;
MySQL [wubx]> optimize table tb1;
Query OK, 0 rows affected (0.013 sec)
MySQL [wubx]> select snapshot_id, snapshot_location from fuse_snapshot('wubx','tb1');
+----------------------------------+----------------------------------------------------------+
| snapshot_id | snapshot_location |
+----------------------------------+----------------------------------------------------------+
| 5a0ba62a222441d3acd2d93549e46d82 | 17818/17825/_ss/5a0ba62a222441d3acd2d93549e46d82_v1.json |
+----------------------------------+----------------------------------------------------------+
1 row in set (0.005 sec)
但清理后,time travel 功能需要针对后面的数据才能生效,前面的 time travel 数据已经丢掉。
Q3:是否可以创建一个不带 time travel 的表?
可以的。
Databend 支持:CREATE TRANSIENT TABLE .. 创建的表
参考:https://databend.rs/doc/reference/sql/ddl/table/ddl-create-table#create-transient-table-
该方式创建的表存在一个缺点:在高并发写入读取中,容易造成正在读取的 snapshot 被回收及报错的问题。
Q1:大量小的 block 文件,是不是可以进行合并?
可以合并的。
目前需要用户进行手工触发。
optimize table tbname compact;
这个命令的作用:
经过 Compact 的最佳的 Block 块,后续在运行 compact 动作会直接跳过。
Q2: 什么时间决定需要运行 tb 的 compact?
目前 Databend 对于 Block 判定要执行 compact 的条件:
可以用一个简单的条件来判断
a. Block 数量大于 max_threads* 4 倍
select count(*) from fuse_block('db','tb');
b.表里 block 数据少于 100M 且行数低于 80 万的数量超过 100 个
select if(count(*)>100,'你需要运行compact','你的block大小非常合理') from fuse_block('db','tb') where file_size <100*1024*1024 and row_count<800000;
Q3: 当出现大量的 segment 文件,是不是需要对 segment 文件合并?
是的。
对于 segment 合并也可以引入一条简单的规则
select count(*),avg(block_count),if(avg(block_count)<500,'need compact segment','segment file is ok') from fuse_segment('db','tb','snapshot_id');
如果 segment 总数超过 1000 ,而且每个 segment 平均 block 数小于 500 需要运行:
optimize table tb compact segment;
对于频繁写入的场景建议定期运行一下 compact segment ,这样来压缩一下 ss 及对 segemnt 文件的大小, 方便 meta 信息进行缓存。
Q4:进行合并操作后文件占用空间比较大,如何释放?
Databend 是一个多版本及支持 Time travel 特性的云数仓,随着历史增长,会出现挺多的版本数据,对于存在的历史版本数据可以使用
optimize table table_name purge;
现在 purge 动作会把当前的 snapshot 之外的版本全部清理掉,造成 time travel 失效的问题。后续 purge 会支持传入 snapshot 或是时间指定清理到什么位置。
Q5:如何进行 compact 和同时清理过旧的数据?
optimize table table_name all;
这个命令相当于:optimize table table_name compact; optimize table table_name purge;
Q6:如何真正删除一张表?
Databend 中 Drop table 为了支持 undrop table 不会所表直正删除,如果你需要立即 Drop 一张表建议使用:
drop table table_name all;
目前需要删除一个 Database 也面临这样的问题,需要先做表的删除,再删 Database 。
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
我正在编写一个简单的静态Rack应用程序。查看下面的config.ru代码:useRack::Static,:urls=>["/elements","/img","/pages","/users","/css","/js"],:root=>"archive"map'/'dorunProc.new{|env|[200,{'Content-Type'=>'text/html','Cache-Control'=>'public,max-age=6400'},File.open('archive/splash.html',File::RDONLY)]}endmap'/pages/search.
我去了这个website查看Rails5.0.0和Rails5.1.1之间的区别为什么5.1.1不再包含:config/initializers/session_store.rb?谢谢 最佳答案 这是删除它的提交:Setupdefaultsessionstoreinternally,nolongerthroughanapplicationinitializer总而言之,新应用没有该初始化器,session存储默认设置为cookie存储。即与在该初始值设定项的生成版本中指定的值相同。 关于
我正在关注Hartl的railstutorial.org并已到达11.4.4:Imageuploadinproduction.我做了什么:注册亚马逊网络服务在AmazonIdentityandAccessManagement中,我创建了一个用户。用户创建成功。在AmazonS3中,我创建了一个新存储桶。设置新存储桶的权限:权限:本教程指示“授予上一步创建的用户读写权限”。但是,在存储桶的“权限”下,未提及新用户名。我只能在每个人、经过身份验证的用户、日志传送、我和亚马逊似乎根据我的名字+数字创建的用户名之间进行选择。我已经通过选择经过身份验证的用户并选中了上传/删除和查看权限的框(而不
我正在使用mechanize登录网站,然后检索页面。我遇到了一些问题,我怀疑这是由于cookie中的某些值造成的。当Mechanize登录网站时,我假设它存储了cookie。如何通过Mechanize打印出存储在cookie中的所有数据? 最佳答案 代理有一个cookie方法。agent=Mechanize.newpage=agent.get("http://www.google.com/")agent.cookiesagent.cookies.to_scookie返回一个Mechanize::Cookiesobject
我以为它们存储在cookie中-但不,检查cookie没有任何结果。session也不存储它们。那么,我在哪里可以找到它们?我需要这个来直接设置它们(而不是通过flashhash)。 最佳答案 它们存储在inyoursessionstore.自rails2.0以来的默认设置是cookie存储,但请检查config/initializers/session_store.rb以检查您是否使用默认设置以外的东西。 关于ruby-on-rails-闪存消息存储在哪里?,我们在StackOverf
我是一名决定学习Ruby和RubyonRails的ASP.NETMVC开发人员。我已经有所了解并在RoR上创建了一个网站。在ASP.NETMVC上开发,我一直使用三层架构:数据层、业务层和UI(或表示)层。尝试在RubyonRails应用程序中使用这种方法,我发现没有关于它的信息(或者也许我只是找不到它?)。也许有人可以建议我如何在RubyonRails上创建或使用三层架构?附言我使用ruby1.9.3和RubyonRails3.2.3。 最佳答案 我建议在制作RoR应用程序时遵循RubyonRails(RoR)风格。Rails
对于我正在编写的Rails3应用程序,我正在考虑从本地文件系统上的XML、YAML或JSON文件中读取一些配置数据。重点是:我应该把这些文件放在哪里?Rails应用程序中是否有用于存储此类内容的默认位置?附带说明一下,我的应用程序部署在Heroku上。 最佳答案 我经常做的是:如果文件是通用配置文件:我在目录/config中创建一个YAML文件,每个环境有一个上层key如果我为每个环境(大项目)创建一个文件:我为每个环境创建一个YAML并将它们存储在/config/environments/然后我在加载YAML的地方创建了一个初始化
有没有办法将RubyVM::InstructionSequence存储到文件中并稍后读取?我尝试了Marshal.dump但没有成功。我收到以下错误:`dump':no_dump_dataisdefinedforclassRubyVM::InstructionSequence(TypeError) 最佳答案 是的,有办法。首先,您需要使InstructionSequence的load方法可访问,默认情况下该方法是禁用的:require'fiddle'classRubyVM::InstructionSequence#RetrieveR
下面是我用来从应用程序中解析CSV的代码,但我想解析位于AmazonS3存储桶中的文件。当推送到Heroku时它也需要工作。namespace:csvimportdodesc"ImportCSVDatatoInventory."task:wiwt=>:environmentdorequire'csv'csv_file_path=Rails.root.join('public','wiwt.csv.txt')CSV.foreach(csv_file_path)do|row|p=Wiwt.create!({:user_id=>row[0],:date_worn=>row[1],:inven