Apache Iceberg简介
Apache Iceberg是一种用于庞大分析数据集的开放表格式。它的设计目标是解决传统数据湖存储格式(如Hive)在管理大规模数据时遇到的关键问题,提供可靠的数据存储和管理功能。
基本定义与特性
- Apache Iceberg被定义为一种表格式,用于确定如何管理、组织和跟踪构成表的所有文件。
- 它为计算引擎(如Spark、Trino、PrestoDB、Flink、Hive和Impala)添加了高性能的表,这些表的工作方式类似于SQL表。
- Iceberg使用快照模式保证读写的隔离性,确保读者永远不会看到部分或未提交的更改。
关键特性与优势
- ACID事务支持:Iceberg支持原子性、一致性、隔离性和持久性(ACID)事务,确保数据的可靠性和一致性。
- 时间旅行与快照隔离:允许用户查看和查询数据的历史版本,同时不同的查询可以在相互隔离的快照上运行,避免读写冲突。
- 高效的元数据管理:使用基于文件的元数据存储,避免了集中式元数据存储可能带来的瓶颈问题,从而提升了大规模数据表的存储效率。
- 模式演化:支持无停机的模式更改,如添加、删除或重命名列,使得数据处理更加灵活。
应用场景与生态系统
- Iceberg适用于需要管理数百TB或PB级别数据集的大规模数据湖管理场景。
- 其在复杂事务处理(如金融交易数据管理)和数据审计与回溯分析(如合规性检查)方面也表现出色。
- 作为Apache软件基金会下的一个开源项目,Iceberg的生态系统正在不断发展壮大,吸引了越来越多的开发者和数据工程师使用。
综上所述,Apache Iceberg作为一种现代数据湖存储格式,通过其强大的ACID事务支持、高效的元数据管理、时间旅行与快照隔离等特性,为大规模数据管理、复杂事务处理和历史数据分析提供了高效、可靠的解决方案。
Iceberg与Hudi的对比
设计目标与定位
- Iceberg:专为对象存储设计,致力于提供高效的表格式数据湖解决方案,强调数据一致性、事务支持和查询性能。
- Hudi:设计初衷是满足快速upsert、数据插入索引以及原子性数据操作的需求,特别关注于数据更新和删除场景。
关键特性
- Iceberg特性:
- ACID事务能力,确保数据一致性和并发控制。
- 快照隔离,支持时间旅行和数据版本管理。
- 高效的元数据管理,减少存储瓶颈。
- 模式演化,灵活应对数据模式变更。
- 隐藏分区和分区进化,优化查询性能。
- Hudi特性:
- 快速upsert操作,支持数据的高效更新。
- 原子性数据操作与回滚功能,保障数据操作的可靠性。
- 写入器之间的快照隔离,确保数据一致性。
- 管理文件大小和使用统计数据布局,优化存储和查询效率。
- 异步压缩和柱状数据存储,提升数据处理性能。
性能与优化
- Iceberg:
- 在数据组织方式上充分考虑对象存储特性,避免耗时的操作。
- 支持流式读取增量数据和结构化流计算模型,适应不同计算场景。
- 提供丰富的配置参数,但学习成本相对较低。
- Hudi:
- 通过避免创建小文件和生成大小合适的文件来优化查询性能。
- 提供插入索引以加速数据定位,适用于大量数据更新的场景。
- 支持多种数据写入方式(如COPY_ON_WRITE和MERGE_ON_READ),灵活应对不同需求。
生态系统与兼容性
- Iceberg:
- 支持多种计算引擎(如Spark、Flink、Trino等),具有良好的生态系统兼容性。
- 可以存储在任意的云存储系统和HDFS中,适应不同的存储环境。
- Hudi:
- 与Hadoop生态系统紧密集成,支持Hive、Spark等主流数据处理工具。
- 提供丰富的表服务和功能,满足复杂的数据处理需求。
使用与学习成本
- Iceberg:
- 提供的配置参数相对较少,学习成本较低。
- 文档和社区支持不断完善,便于用户快速上手。
- Hudi:
- 提供的配置参数较多,需要一定的调优经验和学习成本。
- 社区活跃度高,但不同版本间可能存在功能差异和兼容性问题。
综上所述,Iceberg和Hudi在设计目标、关键特性、性能优化、生态系统与使用学习成本等方面各有侧重。Iceberg更注重数据一致性和查询性能,适用于对数据准确性要求较高的场景;而Hudi则更擅长处理大量数据更新和删除操作,适用于需要频繁修改数据的场景。
Apache Iceberg与Paimon的对比
Apache Iceberg 和 Apache Paimon(原 Flink Table Store)都是用于管理和优化大规模数据集的开源表格式,旨在提升数据湖的查询和处理性能。尽管两者都关注数据管理,但它们在设计目标、功能特性和应用场景上有所不同。
设计理念
- Apache Iceberg:
- 提供一种灵活且高效的表格式,专注于改善大规模数据集的存储和查询性能。
- 强调ACID事务、时间旅行和高效的快照管理。
- 设计上更加关注批处理性能和数据的一致性。
- Apache Paimon:
- 主要关注于实时数据流处理,提供低延迟的增量数据处理能力。
- 旨在为Apache Flink等流处理引擎提供高效的存储支持。
- 强调实时性和对流批一体化处理的支持。
数据处理模式
- Iceberg:
- 侧重于批处理和历史数据分析,支持高效的快照和时间旅行。
- 提供隐藏分区和多种查询优化技术,以提升批处理查询性能。
- Paimon:
- 强调流批一体化处理,支持实时数据更新和低延迟查询。
- 提供对流数据的高效存储和管理能力,适合于需要实时处理的大数据应用。
事务和一致性
- Iceberg:
- 提供完整的ACID事务支持,确保数据一致性。
- 支持快照管理,使得时间旅行和数据回溯变得简单。
- Paimon:
- 也提供ACID语义,但更侧重于流数据的处理和一致性。
- 设计上更关注实时数据的一致性和低延迟处理。
查询性能和优化
- Iceberg:
- 注重批处理查询性能,通过优化分区和元数据管理减少查询延迟。
- 与多种计算引擎(如Spark、Flink、Trino)深度集成,提供高效的查询能力。
- Paimon:
- 设计上优化了对流数据的处理和查询性能,支持低延迟查询。
- 提供对实时数据流的高效管理,适合需要快速响应的应用场景。
生态系统和集成
- Iceberg:
- 与多种计算引擎(如Apache Spark、Apache Flink、Presto、Trino)有良好的集成。
- 支持各种云存储和对象存储服务。
- Paimon:
- 深度集成于Apache Flink生态系统,优化了流数据的处理。
- 适合与流处理引擎的紧密结合,支持实时数据分析。
使用场景
- Iceberg:
- 适合需要复杂查询和数据版本控制的大规模数据湖。
- 更适合历史数据分析和需要高效批处理的场景。
- Paimon:
- 适合需要实时数据处理和流批一体化的应用场景。
- 常用于需要低延迟数据更新和查询的实时分析。
Iceberg 和 Paimon 都提供了对大规模数据集的管理和优化,但它们在具体的应用场景和设计目标上有所不同。Iceberg 更适合于需要高效批处理和数据一致性的场景,而 Paimon 则在实时流数据处理和低延迟查询上具有优势。选择哪个工具取决于具体的需求和应用场景。
Apache Iceberg的架构
Apache Iceberg 的架构可以分为三个主要层次:Iceberg Catalog、元数据层和数据层。
Iceberg Catalog(目录)
Iceberg Catalog 是 Iceberg 的顶层组件,负责管理所有 Iceberg 表的元数据和元数据操作。
Catalog 管理表的架构和元数据,提供了创建、查询和修改表的接口,是 用户和系统与Iceberg表交互的入口点。
Iceberg Catalog (catalog 目录)提供了一个中心位置,用户可以通过它找到每个表当前元数据文件的位置,是读取和写入 Iceberg 表的关键组件。
- 当前元数据指针:Iceberg Catalog 中保存每个表的当前元数据文件的指针(current metadata pointer),确保用户能够获取到最新的元数据。
- 原子操作支持:Catalog 必须支持原子操作,以确保在更新当前元数据指针时能够提供事务的原子性和正确性。常见的支持方式包括 HDFS、Hive Metastore 和 Nessie。
- 元数据存储方式:不同的 Catalog 方案存储当前元数据指针的方式不同:
- HDFS:在表的元数据文件夹中有一个名为 version-hint.txt 的文件,内容为当前元数据文件的版本号。
- Hive Metastore:表在元存储中的条目包含一个属性,存储当前元数据文件的位置。
- Nessie:Nessie 存储每个表的当前元数据文件的位置。
- 查询流程:当执行 SELECT 查询时,查询引擎首先访问 Iceberg Catalog,获取目标表的当前元数据文件位置,然后打开该文件进行数据读取。
元数据层 (metadata layer)
Iceberg 的元数据层负责管理和存储有关表的关键信息,确保高效的数据读取与操作。元数据主要包括三个部分:元数据文件、清单列表和清单文件。
元数据文件(Metadata File)
元数据文件保存关于表的基本信息:
- 表的 schema:定义表中字段的类型和名称。
- 分区信息:说明数据如何在表中分区以优化查询性能。
- 快照(Snapshots):记录表在不同时间点的状态。每个快照里面会列出表在某个时刻的所有 data files 列表。data files是存储在不同的manifest files里面,manifest files是存储在一个Manifest list文件里面,而一个Manifest list文件代表一个快照。
- 当前快照的引用:标识哪个快照是表的最新状态。
当执行 SELECT 查询时,查询引擎首先通过目录获取当前元数据文件的位置,然后读取当前快照的 ID,并在快照数组中查找该 ID,最终打开与之对应的清单列表。
清单列表(Manifest List)
清单列表是一个指向多个清单文件的列表。每个清单文件记录一个快照的详细信息:
- 清单文件的位置:清单文件的存储位置。
- 快照 ID:该清单文件所属的快照 ID。
- 分区信息:记录哪些分区包含在该清单中。
- 列的范围:跟踪数据文件的下限和上限。
查询引擎打开清单列表后,读取清单路径并加载清单文件。此时,可以进行一些优化,例如基于行数或分区信息过滤数据。
清单文件(Manifest File)
清单文件是 Iceberg 管理数据文件的核心,主要职责包括:
- 跟踪数据文件及其详细信息和统计数据。
- 每个清单文件追踪一部分数据文件,以实现并行读取和提高效率。
- 文件路径:数据文件的存储位置。
- 数据文件格式:指明使用的文件格式,如 Parquet、ORC 或 Avro。
- 记录计数:文件中记录的数量。
- 列的上下限:用于数据过滤和优化的统计信息。
当查询引擎打开清单文件后,读取文件路径和相关统计信息,以便访问实际的数据文件,并利用统计信息进行优化。
数据层 (Data Layer)
数据层是实际存储数据的地方,Data Files数据文件是Apache Iceberg表真实存储数据的文件。
数据文件 (Data Files):
- Iceberg 支持多种数据文件格式,如 Parquet、ORC 和 Avro。这些文件按照列式存储,便于高效的读写和压缩。如果文件格式选择的是parquet,那么文件是以“.parquet”结尾。
- 数据文件按照 Iceberg 的分区策略进行组织,以优化数据访问。
- Iceberg每次更新会产生多个数据文件(data files)。
Apache Iceberg的CRUD
CREATE TABLE
首先,让我们在我们的环境中创建一个表。
CREATE TABLE table1 ( order_id BIGINT, customer_id BIGINT, order_amount DECIMAL(10, 2), order_ts TIMESTAMP ) USING iceberg PARTITIONED BY ( HOUR(order_ts) );
执行此语句后,环境将如下所示:
在上面,我们创建了一个名为 in database 的表。该表有 4 列,并按 timestamp 列的小时粒度进行分区(稍后将详细介绍)。
执行上述查询时,将在元数据层中创建带有快照的元数据文件(快照不指向任何清单列表,因为表中尚不存在数据)。然后,将更新 的当前元数据指针的目录条目,以指向此新元数据文件的路径。
INSERT
现在,让我们向表中添加一些数据(尽管是文本值)。
INSERT INTO table1 VALUES ( 123, 456, 36.17, '2021-01-26 08:10:23' );
当我们执行此 INSERT 语句时,将发生以下过程:
- 首先创建 Parquet 文件形式的数据
- 然后,将创建指向此数据文件的清单文件(包括其他详细信息和统计信息)
- 然后,将创建指向此清单文件的清单列表(包括其他详细信息和统计信息)
- 然后,根据以前当前的元数据文件创建一个新的元数据文件,其中包含一个新快照,并跟踪上一个快照,指向此清单列表(包括其他详细信息和统计信息)
- 然后,当前元数据指针的值将在目录中以原子方式更新,以现在指向这个新的元数据文件。table1
在所有这些步骤中,读取表的任何人都将继续读取第一个元数据文件,直到原子步骤 #5 完成,这意味着使用该数据的人永远不会看到表的状态和内容的不一致视图。
MERGE INTO / UPSERT
现在,让我们逐步完成 MERGE INTO / UPSERT 操作。
假设我们已经将一些数据放入我们在后台创建的临时表中。在这个简单的示例中,每次订单发生更改时都会记录信息,我们希望此表显示每个订单的最新详细信息,因此,如果表中已有订单 ID,我们会更新订单金额。如果我们还没有该订单的记录,我们想为这个新订单插入一条记录。
在此示例中,stage 表包括表中已有顺序的更新和尚未出现在表中的新顺序,该顺序发生在 2021-01-27 10:21:46。
MERGE INTO table1 USING ( SELECT * FROM table1_stage ) s ON table1.order_id = s.order_id WHEN MATCHED THEN UPDATE table1.order_amount = s.order_amount WHEN NOT MATCHED THEN INSERT *
当我们执行这个 MERGE INTO 语句时,会发生以下过程:
- 遵循前面详述的读取路径来确定 中和具有相同的所有记录。
- 包含带有 from 的记录的文件将读入查询引擎的内存,然后此内存副本中的记录将更新其字段,以反映新匹配记录。然后,原始文件的修改后副本将写入新的 Parquet 文件。
- 即使文件中还有其他记录与更新条件不匹配,仍会复制整个文件,并在复制时更新匹配的记录,并写出新文件。这种策略称为写入时复制。Iceberg 中即将推出一种新的数据更改策略,称为 merge-on-read,它的实际行为会有所不同,但仍然为您提供相同的更新和删除功能。
- 与任何记录都不匹配的记录以新的 Parquet 文件的形式写入,因为它与匹配的记录属于不同的分区
- 然后,创建一个指向这两个数据文件的新清单文件(包括其他详细信息和统计信息)
- 在这种情况下,快照中唯一数据文件中的唯一记录已更改,因此没有重复使用清单文件或数据文件。通常情况并非如此,清单文件和数据文件将在快照之间重复使用。
- 然后,将创建一个指向此清单文件的新清单列表(包括其他详细信息和统计数据)
- 然后,根据以前当前的元数据文件创建一个新的元数据文件,其中包含新快照,并跟踪以前的快照和 ,指向此清单列表(包括其他详细信息和统计信息)
- 然后,当前元数据指针的值将在目录中以原子方式更新,以现在指向这个新的元数据文件。
虽然这个过程有多个步骤,但一切都发生得很快。一个例子是 Adobe 进行了一些基准测试,发现他们可以达到每分钟 15 次提交。
在上图中,我们还显示了在执行此 MERGE INTO 之前,运行了后台垃圾回收作业以清理未使用的元数据文件 — 请注意,我们在创建表时用于快照的第一个元数据文件已不存在。由于每个新的元数据文件还包含以前元数据文件所需的重要信息,因此可以安全地清理这些信息。未使用的清单列表、清单文件和数据文件也可以通过垃圾回收进行清理。s0
SELECT
让我们再次回顾一下 SELECT 路径,但这次是在 Iceberg 表上,我们一直在努力。
SELECT * FROM db1.table1
执行此 SELECT 语句时,将发生以下过程:
- 查询引擎转到 Iceberg 目录
- 然后,它会检索table1
- 然后,它会打开此元数据文件并检索当前快照的清单列表位置的条目。
- 然后,它会打开此清单列表,检索唯一清单文件的位置
- 然后,它会打开此清单文件,检索两个数据文件的位置
- 然后,它会读取这些数据文件,并且由于它是 ,因此会将数据返回给客户端
隐藏分区
假设用户想要查看一天(例如 2021 年 1 月 26 日)的所有记录,因此他们发出以下查询:
SELECT * FROM table1 WHERE order_ts = DATE '2021-01-26'
回想一下,当我们创建表时,我们在订单首次出现的时间戳的小时级别对其进行分区。在 Hive 中,此查询通常会导致全表扫描。
让我们来看看 Iceberg 如何解决这个问题,并为用户提供以直观的方式与表交互的能力,同时仍能获得良好的性能,避免整个表扫描。
执行此 SELECT 语句时,将发生以下过程:
- 查询引擎将转到 Iceberg 目录。
- 然后,它会检索 的当前元数据文件位置条目。table1
- 然后,它会打开此元数据文件,检索当前快照的清单列表位置的条目。它还在文件中查找分区规范,并看到表在字段的小时级别进行了分区。
- 然后,它会打开此清单列表,检索唯一清单文件的位置。
- 然后,它会打开此清单文件,查看每个数据文件的条目,以将数据文件所属的分区值与用户查询请求的分区值进行比较。此文件中的值对应于自 Unix 纪元以来的小时数,然后引擎使用该小时数来确定仅其中一个数据文件中的事件发生在 2021 年 1 月 26 日(或者换句话说,在 2021 年 1 月 26 日 00:00:00 和 2021 年 1 月 26 日 23:59:59 之间)。
- 具体来说,唯一匹配的事件是我们插入的第一个事件,因为它发生在 2021 年 1 月 26 日 08:10:23。另一个数据文件的订单时间戳是 2021 年 1 月 27 日 10:21:46,即不是 2021 年 1 月 26 日,因此它与筛选条件不匹配。
- 然后,它只读取一个匹配的数据文件,并且由于它是一个 ,因此它将数据返回给客户端。
时间旅行
Iceberg 表格格式支持的另一个关键功能是称为 “时间旅行” 的功能。
为了跟踪表随时间推移的状态以实现合规性、报告或可重现性目的,数据工程传统上需要编写和管理在特定时间点创建和管理表副本的作业。
相反,Iceberg 提供了开箱即用的功能,可以查看表在过去不同时间点的外观。
例如,假设今天用户需要查看截至 2021 年 1 月 28 日的表内容,由于这是一篇静态文本文章,因此假设是在 1 月 27 日的订单插入表之前,以及 1 月 26 日的订单通过我们上面执行的 UPSERT 操作更新其订单金额之前。他们的查询将如下所示:
SELECT * FROM table1 AS OF '2021-01-28 00:00:00' -- (timestamp is from before UPSERT operation)
执行此 SELECT 语句时,将发生以下过程:
- 查询引擎转到 Iceberg 目录
- 然后,它会检索table1
- 然后,它会打开此元数据文件并查看数组中的条目(其中包含创建快照的毫秒 Unix 纪元时间,因此成为最新的快照),确定在请求的时间点(2021 年 1 月 28 日午夜)哪个快照处于活动状态,并检索该快照的清单列表位置的条目。 哪个是snapshotss1
- 然后,它会打开此清单列表,检索唯一清单文件的位置
- 然后,它会打开此清单文件,检索两个数据文件的位置
- 然后,它会读取这些数据文件,并且由于它是 ,因此会将数据返回给客户端
请注意,在上图的文件结构中,尽管旧的清单列表、清单文件和数据文件未在表的当前状态中使用,但它们仍然存在于数据湖中,可供使用。
当然,虽然保留旧的元数据和数据文件在这些用例中提供了价值,但在某个时候,您将拥有不再访问的元数据和数据文件,或者允许人们访问它们的价值超过了保留它们的成本。因此,有一个异步后台进程可以清理旧文件,称为垃圾回收。垃圾回收策略可以根据业务要求进行配置,这是您希望用于旧文件的存储量与要提供的时间回溯和粒度之间的权衡。
数据压缩
作为 Iceberg 设计的一部分,另一个关键功能是压缩,这有助于平衡写入端和读取端的权衡。
在 Iceberg 中,压缩是一个异步后台进程,它将一组小文件压缩为较少的大文件。由于它是异步的并且在后台进行,因此不会对您的用户产生负面影响。事实上,它基本上是一种特定类型的普通 Iceberg 写入作业,其记录与输入和输出相同,但在写入作业提交其事务后,文件大小和属性对于分析有了很大的改进。
无论何时使用数据,都需要对要实现的目标进行权衡,通常,写入端和读取端的激励措施会朝着相反的方向发展。
- 在写入方面,您通常希望低延迟,使数据尽快可用,这意味着您希望在获得记录后立即写入,甚至可能不需要将其转换为列格式。但是,如果你对每条记录都这样做,你最终会得到每个文件一条记录(小文件问题的最极端形式)。
- 在读取方面,您通常需要高吞吐量,在单个文件中以列式格式包含许多记录,因此与数据相关的可变成本(读取数据)超过固定成本(记录保存、打开每个文件等的开销)。您通常还需要最新数据,但您需要在执行读取操作时支付该费用。
压缩有助于平衡写入端和读取端的权衡,您可以在接近数据的情况下立即写入数据,在极端情况下,每个文件有 1 条行格式的记录,读者可以立即查看和使用,而后台压缩过程会定期获取所有这些小文件并将它们合并为更少的文件。 较大的列式格式文件。
使用压缩后,您的读取器会持续以他们想要的高吞吐量形式拥有 99% 的数据,但仍然以低延迟、低吞吐量的形式看到最近 1% 的数据。
此外,请务必注意,对于此使用案例,压缩作业的输入文件格式和输出文件格式可以是不同的文件类型。这方面的一个很好的示例是从流式写入Avro,这些写入被压缩到更大的 Parquet 文件中以供分析。
另一个重要的注意事项是,由于 Iceberg 不是引擎或工具,因此调度/触发和实际的压缩工作由与 Iceberg 集成的其他工具和引擎完成。
格式的设计优势
现在,让我们将到目前为止所经历的应用于架构和设计提供的更高级别价值。
- 事务的快照隔离
- 对 Iceberg 表的读取和写入不会相互干扰。
- Iceberg 通过Optimistic Concurrency Control 提供并发写入功能。
- 所有写入都是原子的。
- 更快的规划和执行
- 这两个好处都源于这样一个事实,即你在 write-path 上编写有关你所写内容的详细信息,而不是在 read-path 上获取该信息。
- 因为文件列表是在对表进行更改时写入的,所以不需要在运行时执行昂贵的文件系统列表操作,这意味着在运行时要做的工作和等待要少得多。
- 由于有关文件中数据的统计信息是在写入端写入的,因此统计信息不会缺失、错误或过时,这意味着基于成本的优化器可以在决定哪个查询计划提供最快的响应时间时做出更好的决策。
- 由于有关文件中数据的统计数据是在文件级别跟踪的,因此统计数据不那么粗粒度,这意味着引擎可以执行更多的数据修剪,处理更少的数据,因此具有更快的响应时间。
- 抽象物理,暴露逻辑视图
- 使用 Hive 表时,用户通常需要了解表可能不直观的物理布局,才能实现不错的性能。
- Iceberg 提供了持续向用户公开逻辑视图的能力,从而将逻辑交互点与数据的物理布局分离。我们看到了这在隐藏分区和压缩等功能中非常有用。
- Iceberg 提供了通过架构演变、分区演变和排序顺序演变功能,随着时间的推移透明地演变您的表的能力。
- 数据工程在幕后尝试不同的、可能更好的表格布局要容易得多。提交后,更改将生效,用户无需更改其应用程序代码或查询。如果实验结果使情况变得更糟,则可以回滚事务,并将用户返回到以前的体验。使实验更安全可以执行更多的实验,因此可以找到更好的做事方法。
- 所有引擎都会立即看到变化
- 因为构成表内容的文件是在写入端定义的,并且一旦文件列表发生更改,所有新读取器都会指向这个新列表(通过从目录开始的读取流),一旦写入器对表进行更改,使用此表的所有新查询都会立即看到新数据。
- 事件侦听器
- Iceberg 有一个框架,允许在 Iceberg 表上发生事件时通知其他服务。该功能目前处于早期阶段,仅支持在扫描表时发出事件。但是,此框架提供了未来功能的能力,例如使缓存、具体化视图和索引与原始数据保持同步。
- 高效地进行较小的更新
- 由于数据是在文件级别跟踪的,因此可以更高效地对数据集进行较小的更新。
参考链接: