上一章我们讲过了如何将Flink和Iceberg结合,演示了一些常用的操作,并且在文章的最后演示了一个比较全的DEMO。
主要是讲了一些使用上的内容,对于原理没有太过深入,而既然我们的标题是从入门到放弃
,那么必然是要对Iceberg进行深入了解的,不然怎么会放弃呢
所以,今天我们就来对Flink 结合 Iceberg后,写在HDFS上的元数据文件进行解析
不过在开始之前先准备一下工作
先下载avro-tools
点我下载用来分析我们的元数据文件
再将我们上一次表中的所有元数据文件下载下来
hdfs dfs -get /user/hive/warehouse/iceberg_db.db/iceberg_kafka_test/metadata
贴一下官网对元数据文件的解释,同时加上我的翻译&理解
Snapshot
A snapshot is the state of a table at some time.
代表一张表在某个时刻的状态,对应着${TABLE_PATH}/metadata/XXX.metadata.jsonEach snapshot lists all of the data files that make up the table’s contents at the time of the snapshot. Data files are stored across multiple manifest files, and the manifests for a snapshot are listed in a single manifest list file.
每个快照文件列出了在某个时刻所有构成这一次快照的数据文件。数据文件存储在多个manifest files中,而某一次快照的manifests会被展示在一个manifest list中Manifest list
对应着${TABLE_PATH}/metadata/snap-XXX.avro文件
A manifest list is a metadata file that lists the manifests that make up a table snapshot.
一个manifest list 是一个元数据文件,它列出了构成快照的listEach manifest file in the manifest list is stored with information about its contents, like partition value ranges, used to speed up metadata operations.
manifest list中的每个manifest file 都存储着有关其内容的信息,比如分区值范围,用来加速元数据的操作(更容易找到数据文件Manifest file
A manifest file is a metadata file that lists a subset of data files that make up a snapshot.
一个manifest file是一个元数据文件,它列出了组成一个快照的数据文件的子集。Each data file in a manifest is stored with a partition tuple, column-level stats, and summary information used to prune splits during scan planning.
每个manifest中的数据文件都存储有分区元祖、列级统计信息和摘要信息,这些信息用于在Scan时进行优化(过滤无用文件
有些同学可能看不明白,没事,我们进入下一小节
以select * from iceberg_catalog.iceberg_db.iceberg_kafka_test /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s', 'start-snapshot-id'='3821550127947089987') */ ;
这样一条SQL为案例,讲一下三种类型文件,在查询的时候起到什么作用
首先会通过iceberg_catalog
这个Hive Catalog,获取到iceberg_db.iceberg_kafka_test
的信息,类似于我们执行desc formatted iceberg_db.iceberg_kafka_test
得到的信息
主要是为了获取其中的metadata_location
对应的信息
metadata_location
对应的值代表着最新的快照路径,我们将下载到本地的对应路径文件打开
文件中每个字段的解释可以参考[1],在这里展开说太多了
可以看到我们当前的snapshotId是7125985432047681528
,然后在这个文件中,搜索这个id,可以找到这个
{
"snapshot-id": 7125985432047681528,
"parent-snapshot-id": 5099196027648958107,
"timestamp-ms": 1617953528158,
"summary": {
"operation": "append",
"flink.job-id": "f4e22ca9fe284270e6eed311bbdb2acb",
"flink.max-committed-checkpoint-id": "114",
"changed-partition-count": "0",
"total-records": "14208615",
"total-data-files": "4",
"total-delete-files": "0",
"total-position-deletes": "0",
"total-equality-deletes": "0"
},
"manifest-list": "hdfs://hacluster/user/hive/warehouse/iceberg_db.db/iceberg_kafka_test/metadata/snap-7125985432047681528-1-f8876013-1621-4786-bf37-7bebde4ef003.avro"
}
manifest-list
的值,代表着manifest list文件的HDFS路径,我们找到对应路径的本地文件,然后通过我们最开始下载的工具,进行文件内容的展开
java -jar ~/Downloads/avro-tools-1.9.2.jar tojson ~/Downloads/iceberg_kafka_test/metadata/snap-7125985432047681528-1-f8876013-1621-4786-bf37-7bebde4ef003.avro
将控制台输出的内容贴到任意文本中,因为工具原因,其实输出的内容是多行JSON,我们先单独拎出一个JSON来分析
{
"manifest_path": "hdfs://hacluster/user/hive/warehouse/iceberg_db.db/iceberg_kafka_test/metadata/57485d8a-71c4-4365-8d82-d16d8f7d2c83-m0.avro",
"manifest_length": 5642,
"partition_spec_id": 0,
"added_snapshot_id": {
"long": 7393188362216980000
},
"added_data_files_count": {
"int": 1
},
"existing_data_files_count": {
"int": 0
},
"deleted_data_files_count": {
"int": 0
},
"partitions": {
"array": []
},
"added_rows_count": {
"long": 1883380
},
"existing_rows_count": {
"long": 0
},
"deleted_rows_count": {
"long": 0
}
}
manifest_path
对应的值,就是我们manifest file的路径,其他的一些字段可以看这里[2]
我们用同样的方式打开manifest_path
对应的文件,得到这么个JSON
{
"status": 1,
"snapshot_id": {
"long": 7393188362216980000
},
"data_file": {
"file_path": "hdfs://hacluster/user/hive/warehouse/iceberg_db.db/iceberg_kafka_test/data/00000-0-4b4eda19-ef65-431b-b491-7218496b3e5e-00004.parquet",
"file_format": "PARQUET",
"partition": {
},
"record_count": 1883380,
"file_size_in_bytes": 520712,
"block_size_in_bytes": 67108864,
"column_sizes": {
"array": [
{
"key": 1,
"value": 381659
},
{
"key": 2,
"value": 128478
}
]
},
"value_counts": {
"array": [
{
"key": 1,
"value": 1883380
},
{
"key": 2,
"value": 1883380
}
]
},
"null_value_counts": {
"array": [
{
"key": 1,
"value": 0
},
{
"key": 2,
"value": 0
}
]
},
"nan_value_counts": {
"array": []
},
"lower_bounds": {
"array": [
{
"key": 1,
"value": "jë\u0000\u0000"
},
{
"key": 2,
"value": "016e8f531504f847"
}
]
},
"upper_bounds": {
"array": [
{
"key": 1,
"value": "\u0007õ\u0001\u0000"
},
{
"key": 2,
"value": "f75292f8fef16edg"
}
]
},
"key_metadata": null,
"split_offsets": {
"array": [
4
]
}
}
}
终于,我们根据这个JSON里面的file_path
的值,我们找到了iceberg_kafka_test
这张表,一部分数据。如果我们把manifest list文件里面的多个JSON中的manifest_path
对应的文件全部打开,那我们就可以根据这些JSON文件中的file_path
的值,获取到这张表的全部有效数据文件路径
这个JSON和上一个JSON大家可以对照着看一下,有一些有趣的地方,比如某些数值是一样的,那么这些数值又代表着什么呢?还是请大家移步官网查看[3]
总结一下:每次扫描的时候,会先根据Hive Metadata找到表的最新的快照路径,然后根据文件内的当前快照id找到manifest list文件,然后根据文件中每一行的值找到每一个manifest file,然后通过scan planning
去过滤掉不需要的manifest file,根据剩下的manifest file找到真正的数据文件路径
看到这里大家应该对这3种类型的文件的作用,有个大概的了解。可是为什么Iceberg要这么大费周章的去找最后的file_path
呢?统一的写在同一个文件不好吗?而scan planning
又是什么?
Iceberg每次的扫描是通过读取当前快照的manifest files来规划的,已删除的数据和已删除的manifest files将不会被扫描到。
通过file counts 或者 partition summaries来跳过不匹配的manifests。
对于每个manifest,扫描谓词(用于筛选数据行)被转换为分区谓词(用于筛选数据文件和删除文件)。此转换使用分区规范,被用于写入manifest file。
使用包含式projection将扫描谓词转换为分区谓词:如果扫描谓词与某行匹配,则该分区谓词必须与该行的分区匹配。之所以称为包含,是因为分区谓词可能会将与扫描谓词不匹配的行包括在扫描中。
举个栗子:一个带有时间戳列ts的事件表,它按ts_day=day(ts)进行分区,用户根据ts > X
来寻找这张表;此时,包含projection就是ts_day >= day(X),用于选择可能有匹配行的文件。请注意,在大多数情况下,扫描中将包括X之前的时间戳记,因为文件包含与谓词匹配的行和与谓词不匹配的行
扫描谓词还用于使用manifest file中存储列边界和计数的字段,来筛选数据文件和删除文件。对于数据文件和删除文件,可以使用相同的筛选器逻辑,因为这两个文件都存储插入或删除行的指标值。如果指标显示删除文件没有与扫描谓词匹配的行,则可以忽略该文件,就像忽略数据文件一样。
扫描必须读取与查询过滤器匹配的数据文件。
匹配查询过滤器的删除文件必须在读取时应用于数据文件,使用以下规则限制删除文件的范围
当所有以下都为真时,位置删除文件必须应用到数据文件:
数据文件的序列号小于或等于删除文件的序列号
数据文件的分区(spec和分区值)等于删除文件的分区
当所有以下都为真时,必须对数据文件应用等值删除文件
注意:
以上内容是对官网scan-planning进行了翻译和一定的自我理解,如有不对,欢迎指出。位置删除和等值删除可以参考[5]
当然,因为我们的写入引擎是Flink,目前只支持Append的方式写入,所以也就没有删除文件了,所以每次的查询只要找到对应的快照号就行
iceberg-flink
进行源码解读,说白了,就是读过程分析,敬请期待[1]table-metadata
[2]manifest-lists
[3]manifests
[4]scan-planning
[5]delete-format