继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

FFIVE
关注TA
已关注
手记 460
粉丝 70
获赞 459

照片由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

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP