为了账号安全,请及时绑定邮箱和手机立即绑定

亚马逊S3表格 — 数据加载与查询及幕后揭秘

照片由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语法的情况下):

(详情见链接)[https://docs.aws.amazon.com/prescriptive-guidance/latest/apache-iceberg-on-aws/iceberg-spark.html#spark-incremental-queries]

    >>> 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

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消