目的 通過本篇文章帶大家理解一下 Databend 的存儲結構。Databend 內置的 Table 引擎為 Fuse table engine,也是接下來要花重點篇幅要講的。 另外,Databend 還支持外置的 Hive table 及 Icebreg Table ( 即將到來)。Fuse ta ...
目的
通過本篇文章帶大家理解一下 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 Engine 基礎概念
在 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 。
存儲配置Databend
存儲配置
[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'
這裡可以看到:
- wubx 的 db_id 是:17818
- tb1 的 table_id 是:17825
- 對應的第一個 snapshot 文件是: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)
驗證1 總結:
- 任何一次寫入都會生成對應的 snapshot (用於 time travel)
- 生成的 block 會被 Segment 引用,一個寫入產生的 block 數量在小於 1000 個的情況下都會屬於一個 segment 中,如果超過 1000 個 block 會生成多個 segement (這個操作太大了,就不證明瞭)
- 如果上面情況,一次 insert 也會生成:一個 snapshot , 一個 segment ,一個 block,一個 bloom fliter block
基於上面的原理:
對於 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 被回收及報錯的問題。
存儲優化Tips
Q1:大量小的 block 文件,是不是可以進行合併?
可以合併的。
目前需要用戶進行手工觸發。
optimize table tbname compact;
這個命令的作用:
- 把原有的 block 塊 max_threads 進行併發合併,生成一份最佳的 Block size 文件列表
- 每個 thread 任務對應一個 segment 文件,超過 1000 個 block 會生成多個 segment
- 最終生成一個 snapshot 文件
經過 Compact 的最佳的 Block 塊,後續在運行 compact 動作會直接跳過。
Q2: 什麼時間決定需要運行 tb 的 compact?
目前 Databend 對於 Block 判定要執行 compact 的條件:
- 單個 block 塊里行數少於 80 萬行且 block 小於 100M 會進行合併
- 單個 block 塊超過 100 萬行,block 會被拆分。
可以用一個簡單的條件來判斷
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
Databend 是一款開源、彈性、低成本,基於對象存儲也可以做實時分析的新式數倉。期待您的關註,一起探索雲原生數倉解決方案,打造新一代開源 Data Cloud。
- Databend 文檔:https://databend.rs/
- Twitter:https://twitter.com/Datafuse_Labs
- Slack:https://datafusecloud.slack.com/
- Wechat:Databend
- GitHub :https://github.com/datafuselabs/databend