本章中,我们将讨论架构和规范,使得Apache Iceberg能够解决Hive表格格式固有的问题,通过深入了解Iceberg表格的内部情况。我们将介绍Iceberg表格的不同结构,以及每个结构提供和启用的功能,这样您就可以了解发生了什么以及如何最好地设计基于Apache Iceberg的数据湖。
正如在第一章中提到的,Apache Iceberg表格有三个不同的层:目录层、元数据层和数据层。图2-1显示了构成每个层的不同组件。
在接下来的几节中,我们将详细介绍每个组件。由于从熟悉的概念开始更容易理解您的新概念,我们将从底层开始,从数据层开始逐步讲解。
数据层
Apache Iceberg表的数据层存储着表的实际数据,主要由数据文件本身组成,尽管删除文件也包括在内。数据层提供用户查询所需的数据。虽然在一些例外情况下,元数据层中的结构可以提供结果(例如,获取列X的最大值),但通常数据层参与提供用户查询的结果。数据层中的文件构成了Apache Iceberg表的树形结构的叶子节点。
在实际使用中,数据层由分布式文件系统(例如Hadoop分布式文件系统[HDFS])或类似分布式文件系统的东西支持,如对象存储(例如Amazon简单存储服务[Amazon S3]、Azure数据湖存储[ADLS]、Google云存储[GCS])。这使得数据湖架构能够建立在这些极具可扩展性和低成本的存储系统之上,并从中获益。
数据文件
数据文件存储着数据本身。Apache Iceberg不依赖于特定的文件格式,目前支持Apache Parquet、Apache ORC和Apache Avro。这一点非常重要,原因如下:
- 许多组织会以多种文件格式存储数据,因为不同的团队能够或曾经能够自行选择他们想要使用的文件格式。对于成长并根据规模和需求变化文件格式的公司也是如此。
- 它提供了根据特定工作负载的最佳适用性来选择不同格式的灵活性。例如,Parquet可能用于大规模在线分析处理(OLAP)分析,而Avro可能用于低延迟流式分析表。
- 它未来保证了组织对文件格式的选择。如果开发出更适合一组工作负载的新文件格式,那么该文件格式可以在Apache Iceberg表中使用。
虽然Apache Iceberg不依赖于特定的文件格式,但在现实世界中,最常用的文件格式是Apache Parquet。Parquet最常用的原因是它的列式结构为OLAP工作负载带来了巨大的性能提升,而且它已经成为了行业的事实标准,这意味着基本上每个引擎和工具都支持Parquet。其列式结构为性能特性奠定了基础,例如单个文件可以分成多种方式增加并行性,每个分割点都有统计信息,以及增加的压缩,从而提供更小的存储容量和更高的读取吞吐量。
在图2-2中,您可以看到给定的Parquet文件有一组行(图中的"Row group 0"),然后这些行的所有值会按照给定列一起存储(图中的"Column a")。给定列的所有值进一步分解为这一列的行值子集,称为页面(图中的"Page 0")。每个级别都可以被引擎和工具独立读取,因此每个级别都可以被给定的引擎或工具并行读取。此外,Parquet存储统计信息(例如,给定行组的给定列的最小值和最大值),使引擎和工具能够决定是否需要读取所有数据,或者是否可以删除不符合查询的行组。
删除文件
删除文件跟踪数据集中已删除的记录。由于将数据湖存储视为不可变的是一种最佳实践,因此无法直接更新文件中的行。相反,您需要编写一个新文件。这个新文件可以是旧文件的副本,其中的更改反映在一个新的副本中(称为写时复制[COW]),或者它可以是一个只包含写入更改的新文件,然后读取数据的引擎将这些更改合并(称为读时合并[MOR])。删除文件启用了执行对Iceberg表的更新和删除的MOR策略。也就是说,删除文件仅适用于MOR表(我们将在第四章中详细介绍原因)。请注意,删除文件仅在Iceberg v2格式中受支持,在撰写本文时,几乎所有支持Iceberg的工具都广泛采用了该格式,但仍然需要注意这一点。图2-3是一个简化的图表,显示了在对MOR表运行删除操作之前和之后的数据文件。
有两种方式可以确定需要从逻辑数据集中删除的给定行,当引擎读取数据集时:要么通过其在数据集中的确切位置标识该行,要么通过行的一个或多个字段的值来标识该行。因此,有两种类型的删除文件。前者称为位置删除文件,后者称为相等性删除文件。
这两种方法各有优缺点,因此在不同的情况下,一种方法可能优于另一种方法。当我们讨论COW与MOR时,我们将在第四章更深入地讨论这些考虑因素和情况,但以下是一个高层次的描述。
位置删除文件
位置删除文件表示哪些行已被逻辑删除,因此当引擎使用该表时,它会从其对表的表示中将它们删除,通过确定行位于表中的确切位置。它通过指定包含该行的特定文件的文件路径以及该文件中的行号来实现此目的。
图2-4显示了删除订单号为1234的行。假设文件中的数据按order_id升序排序,则此行位于文件#2中,并且是第234行(请注意,行引用是从零开始的,因此文件#2中的第0行是order_id = 1000,因此文件#2中的第234行是order_id = 1234)。
相等删除文件
相等删除文件用于标识逻辑上已删除的行,因此在使用表时,读取数据的引擎需要根据行的一个或多个字段的值,从其表的表示中删除这些行。最好的情况是,表中每一行都有一个唯一标识符(即主键),以便单个字段的值可以唯一地标识一行(例如,"删除 order_id 为 1234 的行")。但是,也可以通过此方法删除多行(例如,"删除所有 interaction_customer_id = 5678 的行")。
图 2-5 显示了使用相等删除文件删除 order_id 为 1234 的行。引擎会编写一个删除文件,其中指定"删除 order_id = 1234 的所有行",然后读取该文件的任何引擎都会执行这个操作。请注意,与位置删除文件相比,这些行的位置在表中没有参考信息。
另请注意,可能会出现这样一种情况,即相等删除文件通过列值删除记录,然后在后续提交中,添加一个与删除文件的列值匹配的记录到数据集中。在这种情况下,当查询引擎读取逻辑表时,不希望删除新添加的记录。Apache Iceberg 中的解决方案是序列号。例如,针对图 2-5 中的情况,清单文件会注明图 2-5 左侧的数据文件都具有序列号 1。然后,跟踪右侧删除文件的清单文件将具有序列号 2。然后,在插入 order_id 为 1234 的新行的后续操作中创建的数据文件将具有序列号 3。因此,当引擎读取表时,它知道将删除文件应用于所有具有小于 2 的序列号(删除文件的序列号)的数据文件,但不应用于具有 2 或更高序列号的数据文件。通过此方法,随着时间的推移,维护表的正确状态。
元数据层
元数据层是 Iceberg 表架构的一个组成部分,包含了 Iceberg 表的所有元数据文件。它是一个树状结构,用于跟踪数据文件及其元数据以及导致它们创建的操作。这个树状结构由三种文件类型组成,所有这些文件都与数据文件共同存放:清单文件、清单列表和元数据文件。元数据层对于高效管理大型数据集并实现诸如时间旅行和模式演化等核心功能至关重要。
Manifest文件
清单文件跟踪数据层中的文件(即数据文件和删除文件),以及每个文件的其他详细信息和统计信息,例如数据文件列的最小值和最大值。如第1章所述,Iceberg 能够解决 Hive 表格式的问题的主要区别在于在文件级别跟踪表中的数据。清单文件就是在元数据树的叶子级别进行这种跟踪的文件。
每个清单文件跟踪一部分数据文件。它们包含有关分区成员资格、记录计数以及用于在读取这些数据文件时提高效率和性能的列的下限和上限等信息。虽然其中一些统计信息也存储在数据文件本身中,但单个清单文件会为多个数据文件存储这些统计信息,这意味着从单个清单文件中的统计信息进行修剪大大减少了打开许多数据文件的需要,这可能会影响性能(即使你只是打开许多数据文件的页脚,这仍可能需要很长时间)。这个过程将在第3章中深入讨论。这些统计信息是由引擎/工具在跟踪的每个数据文件子集上的写操作期间编写的。
由于这些统计信息是由每个引擎为其写入的数据文件子集以较小批量编写的,因此与 Hive 表格式相比,编写这些统计信息要轻量得多。在 Hive 表格式中,统计信息是通过长时间且昂贵的读取作业收集和存储的,该作业要求引擎读取整个分区或整个表,计算所有数据的统计信息,然后为该分区/表写入统计信息。这是因为数据的编写者已经处理了要编写的所有数据,因此对于这个编写者来说,添加一个步骤来在处理数据时收集这些统计信息更加轻量级。在实践中,这意味着在使用 Hive 表格式时,统计信息收集作业很少重新运行(如果有的话),这会导致查询性能较差,因为引擎没有必要的信息来决定如何执行给定的查询。因此,Iceberg 表更有可能具有最新和准确的统计信息,使引擎在处理它们时能够做出更好的决策,从而提高作业性能。
您可以在本书的 GitHub 存储库中找到清单文件(Chapter_2/manifest-file.json)的完整内容示例。请注意,在 Iceberg 中,清单文件采用 Avro 格式,尽管为了方便查看,我们已将 Avro 内容转换为 JSON 格式。
Manifest列表
清单列表是 Iceberg 表在特定时间点的快照。对于那个时间点的表,它包含所有清单文件的列表,包括位置、所属分区以及它所跟踪的数据文件的分区列的上限和下限。
清单列表包含一个结构数组,每个结构跟踪一个单独的清单文件。该结构的模式详见表 2-1,该表已经改编自公共 Iceberg 文档。还请注意,这是针对 Iceberg v2 表的。
Always present? | Field name | Data type | Description |
---|---|---|---|
Yes | manifest_path | string | Location of the manifest file |
Yes | manifest_length | long | Length of the manifest file in bytes |
Yes | partition_spec_id | int | ID of a partition spec used to write the manifest; refers to an entry listed in partition-specs in the table's metadata file |
Yes | content | int with meaning: 0: data, 1: deletes | Types of files tracked by the manifest, either datafiles or delete files |
Yes | sequence_number | long | Sequence number when the manifest was added to the table |
Yes | min_sequence_number | long | Minimum data sequence number of all live datafiles or delete files in the manifest |
Yes | added_snapshot_id | long | ID of the snapshot where the manifest file was added |
Yes | added_files_count | int | Number of entries in the manifest file that have ADDED (1) as the value for the status field |
Yes | existing_files_count | int | Number of entries in the manifest file that have EXISTING (0) as the value for the status field |
Yes | deleted_files_count | int | Number of entries in the manifest file that have DELETED (2) as the value for the status field |
Yes | added_rows_count | long | Sum of the number of rows in all files in the manifest that have ADDED as the value for the status field |
Yes | existing_rows_count | long | Sum of the number of rows in all files in the manifest that have EXISTING as the value for the status field |
Yes | deleted_rows_count | long | Sum of the number of rows in all files in the manifest that have DELETED as the value for the status field |
No | partitions | array<field_summary> (see Table 2-2) | List of field summaries for each partition field in the spec; each field in the list corresponds to a field in the manifest file's partition spec |
No | key_metadata | binary | Implementation-specific key metadata for encryption |
正如在表2-1中倒数第二行所引用的那样,field_summary是一个具有以下模式的结构。
始终存在? | 字段名 | 数据类型 | 描述 |
---|---|---|---|
是 | contains_null | 布尔型 | 是否有至少一个分区的字段值为空 |
否 | contains_nan | 布尔型 | 是否有至少一个分区的字段值为NaN |
否 | lower_bound | 字节 | 分区字段中非空且非NaN值的下限,如果所有值都为空或NaN,则为null;该值被序列化为字节 |
否 | upper_bound | 字节 | 分区字段中非空且非NaN值的上限,如果所有值都为空或NaN,则为null;该值被序列化为字节 |
您可以在该书的 GitHub 存储库中找到一个完整的清单列表示例(Chapter_2/manifest-list.json)。请注意,Iceberg 中的清单列表是以 Avro 格式保存的,但出于查看方便,我们已将 Avro 内容转换为 JSON 格式。
Metadata Files
元数据文件跟踪着清单列表。另一个名字恰当的文件,元数据文件存储了关于某个时刻 Iceberg 表的元数据。这包括有关表的架构、分区信息、快照以及哪个快照是当前的信息。
每当对 Iceberg 表进行更改时,都会创建一个新的元数据文件,并通过目录原子地将其注册为元数据文件的最新版本,我们将在下一节介绍这一点。这确保了表提交的线性历史,并在诸如并发写入的情况下提供帮助,即多个引擎同时写入数据。此外,这种方式还可以确保引擎在读取操作期间始终看到表的最新版本。
元数据文件的模式如表 2-3 所示,该表已从公共 Iceberg 文档中进行了调整。
始终存在? | 字段名 | 数据类型 | 描述 |
---|---|---|---|
是 | format-version | 整数 | 格式的整数版本号。当前,这可以是 1 或 2,根据规范确定。如果表的版本高于支持的版本,则实现必须引发异常。在撰写本文时,默认值为 2。 |
是 | table-uuid | 字符串 | 标识表的 UUID,在创建表时生成。如果表的 UUID 与刷新元数据后预期的 UUID 不匹配,则实现必须引发异常。 |
是 | location | 字符串 | 表的基本位置。写入器使用此位置确定存储数据文件、清单文件和表元数据文件的位置。 |
是 | last-sequence-number | 64 位有符号整数 | 表的最高分配序列号。这是一个单调递增的长整型,跟踪表中快照的顺序。 |
是 | last-updated-ms | 64 位有符号整数 | 表最后更新时的 Unix 时间戳(毫秒)。每个表元数据文件应在写入前更新此字段。 |
是 | last-column-id | 整数 | 表的最高分配列 ID。用于确保在演变模式时始终为列分配未使用的 ID。 |
是 | schemas | 数组 | 以 schema-id 对象形式存储的模式列表。 |
是 | current-schema-id | 整数 | 表的当前模式的 ID。 |
是 | partition-specs | 数组 | 作为完整分区规范对象存储的分区规范列表。 |
是 | default-spec-id | 整数 | 写入器默认应使用的"当前"规范的 ID。 |
是 | last-partition-id | 整数 | 表的所有分区规范中分配的最高分区字段 ID。用于确保在演变规范时始终为分区字段分配未使用的 ID。 |
否 | properties | 映射 | 表属性的字符串到字符串的映射。用于控制影响读写的设置,不应用于任意元数据。例如,commit.retry.num-retries 用于控制提交重试次数。 |
否 | current-snapshot-id | 64 位有符号整数 | 当前表快照的 ID。这必须与 refs 中主分支的当前 ID 相同。 |
否 | snapshots | 数组 | 有效快照的列表。有效快照是文件系统中存在所有数据文件的快照。不得从文件系统中删除数据文件,直到列出它的最后一个快照被垃圾回收。 |
否 | snapshot-log | 数组 | 编码表的当前快照的更改的时间戳和快照 ID 对列表。每次更改 current-snapshot-id 时,都应添加一个新条目,其中包含 last-updated-ms 和新的 current-snapshot-id。 |
否 | metadata-log | 数组 | 编码表的上一个元数据文件的更改的时间戳和元数据文件位置对列表。每次创建新的元数据文件时,应向列表添加一个上一个元数据文件位置的新条目。在提交后,表可以配置为删除最旧的元数据日志条目,并保留最新条目的固定大小日志。 |
是 | sort-orders | 数组 | 作为完整排序顺序对象存储的排序顺序列表。 |
是 | default-sort-order-id | 整数 | 表的默认排序顺序 ID。请注意,写入器可能会使用此 ID,但在读取时不使用,因为读取使用清单文件中存储的规范。 |
否 | refs | 映射 | 快照引用的映射。映射键是表中唯一的快照引用名称,在 refs 中始终存在一个指向当前快照 ID 的主分支引用。 |
否 | statistics | 数组 | 表统计信息的列表(可选)。 |
您可以在该书的 GitHub 存储库中找到完整元数据文件的示例内容(Chapter_2/metadata-file.json)。
Puffin Files
虽然数据文件和删除文件中有一些结构可以增强与 Iceberg 表中数据交互的性能,但有时您需要更高级的结构来提升特定类型的查询性能。
例如,假设您想知道过去 30 天有多少独特的人向您下订单。正如我们马上会看到的,数据文件中的统计数据(而不是元数据文件)可以涵盖这种用例。当然,您可以使用这些统计数据来提高一定程度的性能(例如,仅修剪出过去 30 天的数据),但仍然需要读取那些 30 天内的每个订单,并在引擎中进行聚合,这可能会花费太长时间,具体取决于数据的大小、为引擎分配的资源以及字段的基数等因素。
这时就需要引入 Puffin 文件格式。Puffin 文件存储关于表中数据的统计信息和索引,这些统计信息和索引可以提升更广泛范围查询的性能,比如上述示例。
该文件包含一组称为块(blobs)的任意字节序列,以及用于分析这些块所需的相关元数据。图 2-6 显示了 Puffin 文件的结构。
尽管此结构可以支持任何类型的统计信息和索引结构(例如布隆过滤器),但目前唯一支持的类型是 Apache DataSketches 库中的 Theta Sketch。该结构使得能够计算给定一组行的某一列的近似不同值的数量,从而使计算速度大大加快并且使用的资源大大减少,通常是数量级的减少。当操作需要知道某列的不同值的数量(例如,每个区域的用户数量),但查找准确数量的成本或时间过高时,这就变得非常有价值。当用例允许进行近似计算时,尤其是当该操作需要重复运行时(例如用于仪表板),这也非常有价值。
目录
任何需要从表中读取数据的人(更不用说处理数十、数百或数千个表的人了)都需要知道首先去哪里;他们需要一个地方去查找给定表的数据读写位置。任何想要与表交互的人的第一步是找到当前元数据指针的位置。
这个你去查找当前元数据指针位置的中心地方就是 Iceberg 目录。Iceberg 目录的主要要求是必须支持用于更新当前元数据指针的原子操作。这种对原子操作的支持是必需的,这样所有读者和写者都能在特定时间点看到表的相同状态。
在目录中,对于每个表都有一个指向该表当前元数据文件的引用或指针。例如,在图 2-1 中有两个元数据文件。目录中表的当前元数据指针的值就是元数据文件的位置。
由于 Iceberg 目录的唯一要求是需要存储当前元数据指针并提供原子性保证,因此有许多不同的后端可以作为 Iceberg 目录。然而,不同的目录以不同的方式存储当前元数据指针。以下是一些示例:
- 作为目录的 Amazon S3 中有一个名为 version-hint.text 的文件,位于表的元数据文件夹中,其内容是当前元数据文件的版本号。请注意,每当使用分布式文件系统(或类似分布式文件系统的东西)来存储当前元数据指针时,实际上使用的目录称为 Hadoop 目录。
- 作为目录的 Hive Metastore 中,Hive Metastore 中的表条目有一个名为 location 的表属性,用于存储当前元数据文件的位置。
- 作为目录的 Nessie 中,Nessie 中的表条目有一个名为 metadataLocation 的表属性,用于存储该表的当前元数据文件的位置。
在前面的示例中,我们使用了 AWS Glue Catalog 作为目录。通过利用关于表的 Iceberg 元数据,我们可以看到目录所表示的当前元数据文件是什么。运行以下查询可以给出表的当前状态详情,尤其是当前元数据文件的位置:
sql
SELECT *
FROM my_catalog.iceberg_book.orders.metadata_log_entries
ORDER BY timestamp DESC
LIMIT 1
时间戳 | 元数据文件 | 最新快照 ID | 最新模式 ID | 最新序列号 |
---|---|---|---|---|
2023-03-21 22:55:31.868 | s3://jason-dremio-product-us-west-2/iceberg-book/iceberg_book.db/orders/metadata/00002-509f0747-4dc4-4965-b354-ce5fb747c2f5.metadata.json | 8619686881304977663 | 0 | 2 |
因此,如果我们想要从使用 Glue Catalog 的该表中读取数据,我们就知道需要获取位于路径 s3://jason-dremio-product-us-west-2/iceberg-book/iceberg_book.db/orders/metadata/00002-509f0747-4dc4-4965-b354-ce5fb747c2f5.metadata.json 的元数据文件。
总结
在本章中,我们讨论了 Apache Iceberg 表的架构和格式,使它们能够解决 Hive 表格式的问题,并实现诸如数据湖上的 ACID 事务等功能。我们涵盖的三个层级------数据层、元数据层和目录------以及它们的文件类型和结构被引擎和工具利用,以高效地读写数据,并实现更高级的功能,如时间旅行和模式演变。
在第三章中,我们将讨论在这些引擎和工具中运行的查询的生命周期,以了解这些文件类型和结构是如何被利用的。