照片由Torsten Dederichs拍摄,来自Unsplash
在我的最近一篇文章《我的 S3 表实验》中,我展示了如何从您的笔记本电脑使用官方的 Spark docker 容器连接到 Amazon S3 表。这次,我们将把示例电影数据集加载到新的 Iceberg 表中,并对其进行查询!这次,我们将探索 Iceberg 数据文件在 S3 上的实际存储位置。我来做这些辛苦的活儿,让你省心省力。
首先,我们需要运行加载了Hadoop API的Docker容器,并使用AWS SDKv1。接着,保留Iceberg Runtime和S3 Tables的jar文件,并记得将配置指向你的S3 Tables桶,这是我们上一篇中设置的Spark配置项sql.catalog.s3tablesbucket.warehouse
的任务。
docker run -it \
-v ./custom-jars:/custom-jars \
-v ./app:/app \
--env-file ./.env \
spark-plotext \
/opt/spark/bin/pyspark \
--jars "/custom-jars/bundle-2.29.38.jar,/custom-jars/s3-tables-catalog-for-iceberg-0.1.3.jar,/custom-jars/iceberg-spark-runtime-3.5_2.12-1.6.1.jar,/custom-jars/commons-configuration2-2.11.0.jar,/custom-jars/caffeine-3.1.8.jar,/custom-jars/aws-java-sdk-bundle-1.12.661.jar,/custom-jars/hadoop-aws-3.3.4.jar" \
--conf spark.sql.catalog.s3tablesbucket=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.s3tablesbucket.catalog-impl=software.amazon.s3tables.iceberg.S3TablesCatalog \
--conf spark.sql.catalog.s3tablesbucket.warehouse=arn:aws:s3tables:us-west-2:xxx:bucket/test \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
或者使用 git diff。
首先,我想先澄清一件事。当你查询你的Iceberg S3 Table桶中的表时,引用表的格式应该是如下:
SELECT * FROM {CATALOG_NAME}.{DB_NAME}.{TABLE_NAME}.历史表 LIMIT 5
你使用Spark配置键定义你的_CATALOGNAME\.
在Spark SQL中,可以使用spark.sql.catalog.<目录名称>
来指定目录名称。
所以在我所有的例子(以及 AWS 的例子)中都使用了 s3tablesbucket
,但这随意,你可以选择你想要的:
〈spark.sql.catalog.s3tablesbucket=org.apache.iceberg.spark.SparkCatalog〉
设置Spark的目录为S3表桶的Iceberg目录。
这也在 S3 Tables 的 GitHub repo 上有记录。
你的 _DBNAME\ 被定义为命名空间,现在我们来创建一个。
>>> spark.sql("CREATE NAMESPACE IF NOT EXISTS s3tablesbucket.movies")
创建命名空间 s3tablesbucket.movies(如果不存在)
DataFrame[]
我们可以通过 AWS CLI 或在 Spark 中查看所有命名空间(namespace):
$ aws s3tables list-namespaces --table-bucket-arn arn:aws:s3tables:us-west-2:xxx:bucket/test
# 列出指定存储桶中的命名空间信息。
{
"命名空间": [
{
"命名空间": [
"example_namespace"
],
"创建时间": "2024-12-22T23:22:16.898166+00:00",
"创建者": "xxx",
"所有者账户ID": "337909785149"
},
{
"命名空间": [
"movies"
],
"创建时间": "2024-12-29T22:21:51.951906+00:00",
"创建者": "xxx",
"所有者账户ID": "xxx"
}
]
}
# JSON输出的结束
>>> spark.sql("show schemas in s3tablesbucket").show()
+-----------------+
| 模式|
+-----------------+
|example_namespace|
| movies|
+-----------------+
显示S3表bucket中的模式
你的 _TABLENAME 后面在帖子中定义。
加载我们的数据到一张 Iceberg 表里(注:Iceberg 表是指冰山表格)首先,我们加载我们的电影数据框(DataFrame)。
df = spark.read.json(f"s3a://337909785149-ss-test-data/movies.ndjson")
读取S3上337909785149-ss-test-data文件夹中的movies.ndjson文件,并将其作为JSON格式加载到DataFrame中。
接下来我们将创建分区的表和非分区的表:
>>> df.writeTo("s3tablesbucket.movies.movies_nonpartitioned") \
.tableProperty("format-version", "2") \
.createOrReplace()
# 执行成功
>>> df.sortWithinPartitions("year").writeTo("s3tablesbucket.movies.movies_partitioned") \
.tableProperty("format-version", "2") \
.partitionedBy("year") \
.createOrReplace()
# 执行成功
自动创建表很方便,我们可以看到其使用的DDL;如果不满意,我们也可以轻松手动重新创建。
>>> print(spark.sql("show create table s3tablesbucket.movies.movies_partitioned").collect()[0].createtab_stmt)
CREATE TABLE s3tablesbucket.movies.movies_partitioned (
cast STRING数组,
extract STRING,
genres STRING数组,
href STRING,
thumbnail STRING,
thumbnail_height BIGINT,
thumbnail_width BIGINT,
title STRING,
year BIGINT)
USING 冰山
PARTITIONED BY (year)
LOCATION 's3://0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3'
TBLPROPERTIES (
'current-snapshot-id' = '4713978848538992706',
'format' = 'iceberg/parquet',
'format-version' = '2',
'write.parquet.compression-codec' = '写入.parquet压缩编码zstd')
现在这样还行。
请注意,后台使用了一个托管的S3桶来支持Iceberg仓库。看起来每个表似乎都有一个按照uuid(如0ce83714–971c-4910
)命名的S3桶:
$ aws s3tables 列出表单 --表桶ARN arn:aws:s3tables:us-west-2:xxx:bucket/test
{
"表": [
…
{
"命名空间": [
"movies"
],
"名称": "movies_partitioned",
"类型": "customer (用户类型)",
"tableARN": "arn:aws:s3tables:us-west-2:xxx:bucket/test/table/0ce83714-971c-4910-99eb-1ba4411fc0f1",
"创建时间": "2024-12-29T22:31:48.389684+00:00",
"最后修改时间": "2024-12-29T22:32:03.907349+00:00"
}
…
]
}
没办法列出桶的内容😉
$ aws s3 ls s3://0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3
调用 ListObjectsV2 操作时出现错误 (MethodNotAllowed):该指定的方法, 不被此资源支持。
你还可以试试其他酷的事情。
看看您的快照历史记录吧
>>> spark.sql("""select * from s3tablesbucket.movies.movies_nonpartitioned.history""").show(5, False)
# 运行以下SQL查询来显示表中的前五行数据:
+-----------------------+-------------------+---------+-------------------+
|made_current_at |snapshot_id |parent_id|is_current_ancestor|
+-----------------------+-------------------+---------+-------------------+
|2024-12-29 22:26:30.806|1095916791927335283|NULL |true |
+-----------------------+-------------------+---------+-------------------+
这是查询返回的结果,显示了表中的前五行数据。False 表示不显示行号。
查看 Parquet 文件的详细信息(无论是否分片)(视文件是否分片略有不同):
spark.sql("""select * from s3tablesbucket.movies.movies_nonpartitioned.files""").show(1, vertical=True)
-记录 0----------------------------------
内容 | 0
file_path | s3://76c6cb38-292...
file_format | PARQUET
spec_id | 0
record_count | 8592
file_size_in_bytes | 781297
column_sizes | {2 -> 451065, 4 -...
value_counts | {2 -> 8592, 4 -> ...
null_value_counts | {2 -> 1427, 4 -> ...
nan_value_counts | {}
lower_bounds | {2 -> [0A 0A 42 7...](例如:0A 0A 42 7...)表示十六进制值
upper_bounds | {2 -> [6C 6F 62 6...](例如:6C 6F 62 6...)表示十六进制值
key_metadata | 空
split_offsets | [4]
equality_ids | 空
sort_order_id | 0
readable_metrics | {{48347, 19311, 3...
spark.sql("""select * from s3tablesbucket.movies.movies_partitioned.files""").show(1, vertical=True)
-记录 0----------------------------------
内容 | 0
file_path | s3://0ce83714-971...
file_format | PARQUET
spec_id | 0
partition | {1958}
record_count | 271
file_size_in_bytes | 53476
column_sizes | {2 -> 31977, 4 ->...
value_counts | {2 -> 271, 4 -> 2...
null_value_counts | {2 -> 0, 4 -> 0, ...
nan_value_counts | {}
lower_bounds | {2 -> [41 20 43 6...](例如:41 20 43 6...)表示十六进制值
upper_bounds | {2 -> [59 6F 75 6...](例如:59 6F 75 6...)表示十六进制值
key_metadata | 空
split_offsets | [4]
equality_ids | 空
sort_order_id | 0
readable_metrics | {{5280, 704, 3, N...
只显示前1行:
我们可以确认s3文件路径上的分区情况。
print(spark.sql("select * from s3tablesbucket.movies.movies_nonpartitioned.files").collect()[0].file_path) # 打印非分区表文件路径
s3://76c6cb38-2923-411d-ne19um1xsxbq67mssjs7wq39anjc4usw2b--table-s3/data/00000-6-80f3a540-8979-4a2a-bc8b-da06e34dd001-0-00001.parquet
print(spark.sql("select * from s3tablesbucket.movies.movies_partitioned.files").collect()[0].file_path) # 打印分区表文件路径
s3://0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3/data/year=1958/00000-18-9d5362aa-734c-4021-9a3e-39eefc7aa1c9-0-00007.parquet
如果我们想,我们可以下载 Parquet 文件。
$ aws s3api get-object --bucket 0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3 --key data/year=1958/00000-18-9d5362aa-734c-4021-9a3e-39eefc7aa1c9-0-00007.parquet myfile.parquet
{
"AcceptRanges": "bytes",
"LastModified": "2024-12-29T22:31:51+00:00",
"ContentLength": 53476,
"ETag": "\"1ef14cb929e176c62535d2496cbc18ce\"",
"VersionId": "pif6x4Z0oHVxfXUajbEQRQ164pl.zqPU",
"ContentType": "application/octet-stream",
"ServerSideEncryption": "AES256",
"Metadata": {}
}
$ file myfile.parquet
myfile.parquet: Apache Parquet文件格式
但其他我尝试过的 s3api 命令都无法运行——它们都报了同样的错误:该资源不允许指定的方法。
我们可以通过加载一部新的影片来展示 Iceberg 的快照能力。
spark.sql("""
INSERT INTO s3tablesbucket.movies.movies_partitioned
SELECT
array('Brad Pitt', 'Edward Norton') as cast,
'一个沮丧的男人发起了一个地下搏击俱乐部。' as extract,
array('剧情', '惊悚') as genres,
'https://example.com/fight-club' as href,
'https://example.com/thumbnail.jpg' as thumbnail,
300 as thumbnail_height,
200 as thumbnail_width,
'《搏击俱乐部》' as title,
1999 as year
""").show()
我们现在可以看到历史元数据已经更新了:
以下是查询结果:
spark.sql("""SELECT * FROM s3tablesbucket.movies.movies_partitioned.history""").show()
|---------|-------------------|-------------------|-------------------| | 当前时间 | 快照ID号 | 父级ID | 是否当前祖先 | |---------|-------------------|-------------------|-------------------| | 2024-12-29 22:32:... | 4713978848538992706 | 空 | 是 | | 2024-12-30 03:21:... | 5122250828426963270 | 4713978848538992706 | 是 | |---------|-------------------|-------------------|-------------------|
我们可以看到这两张快照之间的不同(仅支持追加操作,在不支持Spark SQL语法的情况下):
>>> spark.read.format("iceberg") \
.option("start-snapshot-id", 4713978848538992706) \
.option("end-snapshot-id", 5122250828426963270) \
.load(f"s3tablesbucket.movies.movies_partitioned").show()
+--------------------+--------------------+-----------------+--------------------+--------------------+----------------+---------------+----------+----+
| cast| 剧情简介| 电影类型| href| thumbnail|thumbnail_height|thumbnail_width| title|year|
+--------------------+--------------------+-----------------+--------------------+--------------------+----------------+---------------+----------+----+
|[Brad Pitt, Edwar...|一个沮丧的男人...| [剧情, 惊悚]|https://example.c...|https://example.c...| 300| 200|搏击俱乐部|1999|
+--------------------+--------------------+-----------------+--------------------+--------------------+----------------+---------------+----------+----+
我们可以从这些快照中看到更多细节:
>>> spark.sql("""SELECT * FROM s3tablesbucket.movies.movies_partitioned.快照""").show(vertical=True,truncate=False)
-RECORD 0------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
committed_at | 2024年12月29日 22:32:01.9
snapshot_id | 4713978848538992706
parent_id | 空
operation | 追加
manifest_list | s3://0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3/元数据/快照-4713978848538992706-1-0e38cde2-6f84-4b06-876d-9ba3cd5092dd.avro
摘要 | {spark应用ID -> local-1735510866862, 增加的数据文件 -> 124, 增加的记录 -> 36273, 增加的文件大小 -> 7100580, 更改的分区数量 -> 124, 总记录数 -> 36273, 总文件大小 -> 7100580, 总数据文件数 -> 124, 总删除文件数 -> 0, 总位置删除数 -> 0, 总相等删除数 -> 0, 引擎版本 -> 3.5.3, 应用ID -> local-1735510866862, 引擎名称 -> spark, Iceberg版本 -> Apache Iceberg 1.6.1 (commit 8e9d59d299be42b0bca9461457cd1e95dbaad086)}
-RECORD 1------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
committed_at | 2024年12月30日 03:21:46.948
snapshot_id | 5122250828426963270
parent_id | 4713978848538992706
operation | 追加
manifest_list | s3://0ce83714-971c-4910-femhbmc7bxj4z5eishz5e7iypuawyusw2b--table-s3/元数据/快照-5122250828426963270-1-ecb38699-c4a7-4f86-a8ec-5bc2f6895664.avro
摘要 | {spark应用ID -> local-1735523685096, 增加的数据文件 -> 1, 增加的记录 -> 1, 增加的文件大小 -> 3434, 更改的分区数量 -> 1, 总记录数 -> 36274, 总文件大小 -> 7104014, 总数据文件数 -> 125, 总删除文件数 -> 0, 总位置删除数 -> 0, 总相等删除数 -> 0, 引擎版本 -> 3.5.3, 应用ID -> local-1735523685096, 引擎名称 -> spark, Iceberg版本 -> Apache Iceberg 1.6.1 (commit 8e9d59d299be42b0bca9461457cd1e95dbaad086)}
我们也可以根据需要查看旧的快照(或最新的),如下所示或当我们想看的时候。
>>> spark.sql("""SELECT count(*) FROM s3tablesbucket.movies.movies_partitioned VERSION AS OF 4713978848538992706""").show()
+--------+
|count(1)|
+--------+
| 36273|
+--------+
>>> spark.sql("""SELECT count(*) FROM s3tablesbucket.movies.movies_partitioned VERSION AS OF 5122250828426963270""").show()
+--------+
|count(1)|
+--------+
| 36274|
+--------+
其他
其他 Iceberg DDL 语句(如 Spark DDL 中的)也适用:
>>> spark.sql("describe table s3tablesbucket.movies.movies_partitioned").show()
+--------------------+-------------+-------+
| col_name| data_type|comment|
+--------------------+-------------+-------+
| cast|array<string>| NULL|
| extract| string| NULL|
| genres|array<string>| NULL|
| href| string| NULL|
| thumbnail| string| NULL|
| thumbnail_height| bigint| NULL|
| thumbnail_width| bigint| NULL|
| title| string| NULL|
| year| bigint| NULL|
|# 分区信息...| | |
| # col_name| data_type|comment|
| year| bigint| NULL|
+--------------------+-------------+-------+
我们的表格很小,分区可能不划算,但试着玩一下还是挺有趣的。
from pyspark.sql.functions import *
table = spark.table("s3tablesbucket.movies.movies_partitioned")
bytes = spark.sql("SELECT * FROM s3tablesbucket.movies.movies_partitioned.files").agg(sum("file_size_in_bytes")).collect()[0][0]
megabytes = bytes / (1024 * 1024)
print(f"表的大小为: {megabytes:.2f} MB")
表的大小为: 6.77 MB
我们可以在定时查询中加入一个简单的where条件。
import time
def timed_query(query):
start_time = time.time()
result = spark.sql(query).show()
end_time = time.time()
print(f"查询执行时间: {end_time - start_time:.2f} 秒")
return result
>>> timed_query("select count(*) from s3tablesbucket.movies.movies_nonpartitioned where year = 1958")
+--------+
|count(1)|
+--------+
| 271|
+--------+
查询执行时间: 6.84 秒
>>> timed_query("select count(*) from s3tablesbucket.movies.movies_partitioned where year = 1958")
+--------+
|count(1)|
+--------+
| 271|
+--------+
查询执行时间: 2.21 秒
我们来看看查询的速度和原始 json 文件的比较:
>>> df.createOrReplaceTempView("movies_temp")
>>> timed_query("select count(*) from movies_temp where year = 1958")
+--------+
|count(1)|
+--------+
| 271|
+--------+
查询耗时:2.02 秒
因为数据集很小——原始的 JSON 数据表现得不错。
概述这就是我这次的所有内容。我们从一个 json 文件加载了电影样本数据,分别放入了分区和非分区的 Iceberg 表中,检查了一些元数据,发现 AWS 在后台使用了许多桶来存储我们的 Iceberg 表。我们在添加了一行之后尝试了时间旅行,使用了添加新行后的快照,并进行了几次测试查询来检查查询速度。
记得我上一篇文章中的代码和含有 docker
命令的 Makefile
,文章在此处:https://medium.com/@mattgillard/my-s3-tables-experiment-a789493c5512,而 Makefile
位于此处:https://github.com/mattgillard/spark-local-aws-demo。
共同学习,写下你的评论
评论加载中...
作者其他优质文章