Apache Iceberg 背后的设计

news2024/11/24 18:28:44

原文地址: 阿帕奇冰山:幕后的建筑外观 |德雷米奥 (dremio.com)

绝对的精品文章!!!

机器翻译和自我调整组成了这篇文章,供大家学习。

介绍

数据湖的构建希望是实现数据民主化,以允许越来越多的人员、工具和应用程序使用越来越多的数据。实现这一目标所需的一项关键功能是对用户隐藏底层数据结构和物理数据存储的复杂性。实现这一目标的事实标准是Facebook在2009年发布的Hive表格式,它解决了其中一些问题,但在数据,用户和应用程序规模方面存在不足。那么答案是什么呢?

Apache Iceberg。

在本文中,我们将介绍:

  • 表格格式(table format)的定义,因为表格格式的概念传统上被嵌入在“Hive”的保护伞下.

  • 并且隐含 长期的事实标准,Hive表格式的详细信息,包括它的优缺点。我们将看到这些问题如何产生对定义全新表格格式的需求。

  • Apache Iceberg 表格格式是如何作为这种需求的结果创建的。我们还将深入研究 Iceberg 表的体系结构结构,包括从规范角度和逐步了解在执行创建、读取、更新和删除 (CRUD) 操作时 Iceberg 表中发生的情况

  • 最后,我们将展示此体系结构如何实现此设计带来的好处

什么是表格格式(table format)?

定义表格式的一个好方法是组织数据集的文件以将它们呈现为单个“表”。

从用户的角度来看,另一个更简单的定义是回答“此表中有哪些数据?

该问题的单个答案允许多个人员、组和工具同时与表中的数据进行交互,无论他们是写入表还是从表中读取。

表格式的主要目标是为人员和工具提供表的抽象,并允许他们有效地与该表的基础数据进行交互。

表格式并不是什么新鲜事——自从System R、Multics和Oracle首次实现Edgar Codd的关系模型以来,它们就已经存在了,尽管当时还不是“表格式”这个术语。这些系统使用户能够将一组数据作为表格引用。数据库引擎拥有并管理以文件的形式将数据集的字节放在磁盘上,并解决了出现的复杂性,例如事务需求。

与底层数据的所有交互,如写入和读取数据,都由数据库的存储引擎处理。没有其他引擎可以直接与文件交互而不会损坏系统。这在相当长的一段时间内工作正常。但是在当今的大数据世界中,传统的RDBMS无法解决问题,管理对底层数据的所有访问的单个封闭引擎是不可行的。

有了这么简单的概念,我们为什么需要一个新的概念?

随着时间的推移,大数据社区了解到,当试图满足数据、用户和应用程序规模的业务需求时,在将数据集呈现为用户和工具的表格时,需要考虑很多因素。

遇到的一些挑战是旧的——RDBMS已经遇到并解决了这些挑战,但由于RDBMS无法满足大数据世界的关键要求时必须使用不同的技术,这些挑战再次出现。但是,推动挑战的业务需求并没有改变。

由于大数据世界中技术和规模的差异,遇到的一些挑战是新的挑战。

为了解释为什么我们真的需要一种新的表格格式,让我们来看看传统上事实上的标准表格格式是如何形成的,它面临的挑战,以及尝试了哪些解决方案来应对这些挑战。

我们是怎么到这里来的?简史

2009年,Facebook意识到,虽然Hadoop满足了他们的许多需求,如规模和成本效益,但在向许多不是技术专家的用户改善数据民主化方面,它也有一些缺点需要解决:

  1. 任何想要使用这些数据的用户都必须弄清楚如何将他们的问题放入MapReduce编程模型中,然后编写Java代码来实现它。

  1. 没有元数据定义有关数据集的信息,例如其架构。

为了将数据交到更多用户手中并解决这些缺点,他们构建了Hive。

为了解决问题 #1,他们意识到他们需要以人们熟悉的更通用的编程模型和语言 - SQL 提供访问。他们会构建Hive来获取用户的SQL查询并将其转换为MapReduce作业,以便他们能够获得答案。

解决 #1 以及解决问题 #2 所产生的要求是需要定义数据集的架构是什么以及如何将该数据集引用为用户 SQL 查询中的表。

为了满足第二个要求,定义了 Hive 表格式(仅通过白皮书中的 3 个要点和 Java 实现),并且从那时起一直是事实上的标准。

让我们仔细看看 Hive 表格式,这是数据湖之上的关系层,旨在大规模地向非技术专家提供分析。

HIVE单元表格式

在配置单元表格式中,表被定义为一个或多个目录的全部内容——即,实际上是一个或更多个目录的ls。对于非分区表,这是一个单独的目录。对于在现实世界中更常见的分区表,表由多个目录组成-每个分区一个目录。

组成表的数据在目录级别进行跟踪,该跟踪在Hive元存储中完成。分区值通过目录路径定义,格式为/path/to/table/Partition_column=Partition_value。

下面是由列k1和k2划分的Hive表的示例架构图。

优点

鉴于其在过去 10 年左右作为事实上的标准的地位,Hive 表格式显然提供了一组有用的功能和好处:

  • 它最终基本上适用于所有处理引擎,因为它是大数据领域唯一的表格格式——自从更广泛地采用大数据以来,它一直是事实上的标准。

  • 多年来,它不断发展并提供机制,使 Hive 表能够提供比对每个查询(如分区和存储桶)执行全表扫描更有效的访问模式。

  • 它与文件格式无关,允许公司和社区开发更适合分析的文件格式(例如,Parquet,ORC),并且在将数据在Hive表中可用之前不需要转换(例如,Avro,CSV / TSV)。

  • Hive 元存储存储以 Hive 表格式布局的表,为需要在读取端和写入端与表交互的整个工具生态系统提供了“此表中有哪些数据?”的单一中心答案。

  • 它提供了通过 Hive 元存储中的原子交换在整个分区级别原子更改表中数据的功能,因此实现了一致的全局视图。

缺点

但是,当在更大的数据、用户和应用程序规模上使用 Hive 表格式时,许多问题开始变得越来越严重:

  1. 对数据的更改效率低下

  • 由于分区存储在事务存储(由关系数据库支持的 Hive 元存储)中,因此可以以事务方式添加和删除分区。但是,由于文件的跟踪是在不提供事务功能的文件系统中完成的,因此无法以事务方式在文件级别添加和删除数据。

  • 常规解决方法是在分区级别解决此问题,方法是将整个分区复制到后台的新位置,在复制分区时进行更新/删除,然后将分区在元存储中的位置更新为新位置。

  • 此方法效率低下,尤其是当分区较大、更改分区中的数据量相对较少和/或频繁进行更改时

  1. 无法在一次操作中安全地更改多个分区中的数据

  • 由于更改数据的唯一事务一致性操作是交换单个分区,因此不能以一致的方式同时更改多个分区中的数据。即使是像将文件添加到两个分区这样简单的事情也无法以事务一致的方式完成。因此,用户看到的数据不一致,最终在做出正确决策和信任数据时遇到问题。

  1. 实际上,多个作业修改同一数据集并不是安全的操作

  • 在表格格式中,没有一种很好的方法来处理一次更新数据的多个流程/人员。有一种方法,但它非常严格,会导致只有 Hive 遵守的问题。这导致要么严格控制谁可以写入以及何时写入,组织必须自行定义和协调,要么多个进程同时更改数据,导致数据丢失,因为最后一次写入获胜。

  1. 大型表所需的所有目录列表都需要很长时间

  • 因为您没有所有分区目录中的文件列表,所以您需要在运行时获取此列表。获取所需所有目录列表的响应通常需要很长时间。

  • Netflix 的 Iceberg 创建者 Ryan Blue 谈到了一个示例用例,由于这些目录列表,仅计划查询就需要 9 分钟以上。

  1. 用户必须知道表格的物理布局

  • 如果表按事件发生的时间进行分区,这通常是通过多级分区完成的 — 首先是事件的年份,然后是事件的月份,然后是事件的天,有时是较低的粒度。但是,当用户看到事件时,在特定时间点之后获取事件的直观方法如下所示。在这种情况下,查询引擎将执行全表扫描,这比执行可用分区修剪以限制数据所花费的时间要长得多。

WHERE event_ts >= ‘2021-05-10 12:00:00’
  • 发生此全表扫描是因为没有从用户所知的事件时间戳 (2021-05-10 12:00:00) 到物理分区方案(year=2021 month=05 day=10)的映射。

  • 相反,所有用户都需要了解分区方案并将他们的查询编写为(如果您要查看 2020 年 5 月之后的事件,此分区修剪查询会变得更加复杂)。

WHERE event_ts >= ‘2021-05-10 12:00:00’ AND event_year >= ‘2021’ AND event_month >= ‘05’ AND (event_day >= ‘10’ OR event_month >= '06')
  1. 配置单元表统计信息通常过时

  • 由于表统计信息是在异步定期读取作业中收集的,因此统计信息通常已过期。此外,由于收集这些统计信息需要昂贵的读取作业,需要大量扫描和计算,因此这些作业很少运行(如果有的话)。

  • 由于这两个方面,Hive 中的表统计信息通常已经过时(如果它们存在的话),导致优化器选择的计划不佳,这使得一些引擎甚至完全忽略了 Hive 中的任何统计信息。

  1. 文件系统布局在云对象存储上性能较差

  • 任何时候你想要读取一些数据,云对象存储(例如,S3,GCS)架构规定这些读取应该有尽可能多的不同前缀,所以它们由云对象存储中的不同节点处理。但是,由于在 Hive 表格式中,分区中的所有数据都具有相同的前缀,并且您通常会读取分区中的所有数据(或至少读取分区中的所有 Parquet/ORC 页脚),因此这些数据都命中同一个云对象存储节点,从而降低了读取操作的性能。

这些问题被大规模放大了——是时候采用新格式了

虽然上述问题存在于较小的环境中,但它们在数据、用户和应用程序规模方面会变得更糟。

与大数据历史上的许多其他成功项目一样,通常是科技公司首先遇到规模问题并构建工具来解决这些问题。然后,当其他组织遇到这些相同的规模问题时,他们会采用这些工具。大多数数据驱动型组织现在已经遇到或开始处理这些问题。

几年前,Netflix遇到了这些问题,并采用了标准的解决方法,但成功与否参差不齐。在长期处理这些问题后,他们意识到可能有比继续实施这些解决方法更好的方法。因此,他们退后一步,思考正在发生的问题,以及解决这些问题的最佳方法。

他们意识到Hive表格格式上的更多创可贴不是解决方案,而是需要一种新的表格格式。

那么,Netflix是如何解决这些问题的呢?

Netflix发现,Hive表格格式的大多数问题都源于它的一个方面,这个方面起初可能看起来相当小,但最终会产生重大后果 - 表中的数据在文件夹级别进行跟踪

Netflix发现,解决Hive表格格式引发的主要问题的关键是在文件级别跟踪表格中的数据

他们不是指向一个目录或一组目录的表,而是将表定义为文件的规范列表。

事实上,他们意识到文件级跟踪不仅可以解决他们使用 Hive 表格格式遇到的问题,还可以为实现更广泛的分析目标奠定基础:

  • 提供始终正确且始终一致的表视图

  • 在做出数据驱动的决策时,这些决策需要基于可靠的数据。如果在更新表时执行一个报表操作,产出的报表只能看到部分(而非全部)更改,则可能会产生不正确的结果,导致错误或次优决策,并破坏组织内对数据的更广泛信任。

  • 实现更快的查询规划和执行

  • 如上面的 #4 所述,Netflix 的一个用例查询仅计划查询就花了 9 分钟,并且只使用了 1 周的数据。至少,他们需要改进规划时间以及整体查询执行时间,以提供更好的用户体验,并增加用户可以提交的操作数据,以便做出更多数据驱动的决策。

  • 为用户提供良好的响应时间,而无需他们知道数据的物理布局

  • 如上面的 #5 中所述,如果用户没有完全按照数据的分区方式查询数据,则查询可能需要更长的时间。

  • 为了解决这个问题,你可以帮助每个用户,让他们做一些非直观的事情,但像这样的情况几乎总是在软件中更好地解决用户体验和数据民主化。

  • 实现更好、更安全的表演进

  • 表随时间而变化,随着业务需求的演变、额外的规模和额外的数据源而变化。由于变更是不可避免的,因此表格格式大大简化了变更管理,因此应用层或数据工程不需要处理它。

  • 如果变更管理存在风险,则变更不应该像需要的那样频繁发生,从而业务敏捷性和灵活性会降低。

  • 在数据、用户和应用程序规模上实现上述所有目标

让我们仔细看看这种名为 Iceberg 的新表格格式,以及它如何解决 Hive 表格格式的问题,以及实现这些更广泛的业务和分析目标。

Iceberg表格式

在深入研究格式本身之前,因为“Hive”是一个有点模糊和超载的术语,由于其历史,对不同的人意味着不同的东西,让我们清楚地定义什么是Iceberg,它不是什么:

Iceberg是什么

Iceberg不是什么

- 表格式规范

- 用于引擎与遵循该规范的表交互的一组API和库

- 存储引擎

- 执行引擎

- 服务

Iceberg 表的架构

现在,让我们来看看使 Iceberg 能够解决 Hive 表格式的问题,并通过查看 Iceberg 表背后如何实现上述目标的体系结构和规范。

下面是冰山表结构的体系结构图:

Iceberg组件

现在,让我们浏览一下上图中的每个组件。

在我们浏览它们时,我们还将逐步完成 SELECT 查询通过组件读取 Iceberg 表中数据的过程。您将在下面标有此图标的框中看到这些表示:

iceberg表的架构中有 3 层:

  1. iceberg目录(The Iceberg catalog)

  1. 元数据层(The metadata layer),其中包含元数据文件、清单列表和清单文件

  1. 数据层(The data layer)

Iceberg目录

任何从表中读取(更不用说 10、100 或 1,000)的人都需要知道首先去哪里——他们可以去某个地方找出给定表的数据读/写的位置。对于任何想要阅读该表的人来说,第一步是找到当前元数据指针的位置(请注意,术语“当前元数据指针”不是一个官方术语,而是一个描述性术语,因为此时没有官方术语,社区也没有对此进行反击)。

查找当前元数据指针的当前位置的中心位置是 Iceberg 目录(Iceberg catalog)

Iceberg 目录的主要要求是它必须支持用于更新当前元数据指针的原子操作(例如,HDFS、Hive Metastore、Nessie)。这就是允许Iceberg表上的事务是原子的并提供正确性保证的原因。

在目录(catalog)中,每个表都有一个指向该表的当前元数据文件的引用或指针。例如,在上图中,有 2 个元数据文件。表中的当前元数据指针在目录中的值是右侧元数据文件的位置。

这些数据看起来像什么取决于正在使用的Iceberg目录。举几个例子:

  • 使用 HDFS 作为目录,在表的元数据文件夹中有一个文件version-hint.text,其内容是当前元数据文件的版本号。

  • 使用 Hive 元存储作为目录时,元存储中的表条目具有一个表属性,用于存储当前元数据文件的位置。

  • 使用Nessie作为目录,Nessie存储表的当前元数据文件的位置。

因此,当 SELECT 查询读取 Iceberg 表时,查询引擎首先转到 Iceberg 目录(catalog),然后检索要读取的表的当前元数据文件位置的条目,然后打开该文件。

元数据文件Metadata file

顾名思义,元数据文件存储有关表的元数据。它包含有关表的schema、分区信息、快照以及当前快照的信息。

虽然以上是一个用于说明的简短示例,下面是元数据文件完整内容的示例:

v3.metadata.json

{
    "format-version" : 1,
    "table-uuid" : "4b96b6e8-9838-48df-a111-ec1ff6422816",
    "location" : "/home/hadoop/warehouse/db2/part_table2",
    "last-updated-ms" : 1611694436618,
    "last-column-id" : 3,
    "schema" : {
        "type" : "struct",
        "fields" : [ {
            "id" : 1,
            "name" : "id",
            "required" : true,
            "type" : "int"
        }, {
            "id" : 2,
            "name" : "ts",
            "required" : false,
            "type" : "timestamptz"
        }, {
            "id" : 3,
            "name" : "message",
            "required" : false,
            "type" : "string"
        } ]
    },
    "partition-spec" : [ {
        "name" : "ts_hour",
        "transform" : "hour",
        "source-id" : 2,
        "field-id" : 1000
    } ],
    "default-spec-id" : 0,
    "partition-specs" : [ {
        "spec-id" : 0,
        "fields" : [ {
            "name" : "ts_hour",
            "transform" : "hour",
            "source-id" : 2,
            "field-id" : 1000
        } ]
    } ],
    "default-sort-order-id" : 0,
    "sort-orders" : [ {
        "order-id" : 0,
        "fields" : [ ]
    } ],
    "properties" : {
        "owner" : "hadoop"
    },
    "current-snapshot-id" : 1257424822184505371,
    "snapshots" : [ {
        "snapshot-id" : 8271497753230544300,
        "timestamp-ms" : 1611694406483,
        "summary" : {
            "operation" : "append",
            "spark.app.id" : "application_1611687743277_0002",
            "added-data-files" : "1",
            "added-records" : "1",
            "added-files-size" : "960",
            "changed-partition-count" : "1",
            "total-records" : "1",
            "total-data-files" : "1",
            "total-delete-files" : "0",
            "total-position-deletes" : "0",
            "total-equality-deletes" : "0"
        },
        "manifest-list" : "/home/hadoop/warehouse/db2/part_table2/metadata/snap-8271497753230544300-1-d8a778f9-ad19-4e9c-88ff-28f49ec939fa.avro"
    }, 
    {
        "snapshot-id" : 1257424822184505371,
        "parent-snapshot-id" : 8271497753230544300,
        "timestamp-ms" : 1611694436618,
        "summary" : {
            "operation" : "append",
            "spark.app.id" : "application_1611687743277_0002",
            "added-data-files" : "1",
            "added-records" : "1",
            "added-files-size" : "973",
            "changed-partition-count" : "1",
            "total-records" : "2",
            "total-data-files" : "2",
            "total-delete-files" : "0",
            "total-position-deletes" : "0",
            "total-equality-deletes" : "0"
        },
        "manifest-list" : "/home/hadoop/warehouse/db2/part_table2/metadata/snap-1257424822184505371-1-eab8490b-8d16-4eb1-ba9e-0dede788ff08.avro"
    } ],
    "snapshot-log" : [ {
        "timestamp-ms" : 1611694406483,
        "snapshot-id" : 8271497753230544300
    }, 
    {
        "timestamp-ms" : 1611694436618,
        "snapshot-id" : 1257424822184505371
    } ],
    "metadata-log" : [ {
        "timestamp-ms" : 1611694097253,
        "metadata-file" : "/home/hadoop/warehouse/db2/part_table2/metadata/v1.metadata.json"
    }, 
    {
        "timestamp-ms" : 1611694406483,
        "metadata-file" : "/home/hadoop/warehouse/db2/part_table2/metadata/v2.metadata.json"
    } ]
}

当 SELECT 查询正在读取 Iceberg 表,并在从目录中的表条目获取其位置后打开其当前元数据文件时,查询引擎将读取current-snapshot-id的值。然后,它使用此值在snapshots数组中查找该快照的条目,然后检索该快照条目manifest-list的值,并打开该位置指向的清单列表。

清单列表manifest list

另一个恰当命名的文件,清单列表是清单文件的列表。清单列表包含有关构成该快照的每个清单文件的信息,例如清单文件的位置、作为其一部分添加的快照,以及有关它所属的分区的信息,以及它跟踪的数据文件的分区列的下限和上限。下面是清单列表文件的完整内容的示例:(转换为 JSON)snap-1257424822184505371-1-eab8490b-8d16-4eb1-ba9e-0dede788ff08.avro

{
    "manifest_path": "/home/hadoop/warehouse/db2/part_table2/metadata/eab8490b-8d16-4eb1-ba9e-0dede788ff08-m0.avro",
    "manifest_length": 4884,
    "partition_spec_id": 0,
    "added_snapshot_id": {
        "long": 1257424822184505300
    },
    "added_data_files_count": {
        "int": 1
    },
    "existing_data_files_count": {
        "int": 0
    },
    "deleted_data_files_count": {
        "int": 0
    },
    "partitions": {
        "array": [ {
            "contains_null": false,
            "lower_bound": {
                "bytes": "¹Ô\\u0006\\u0000"
            },
            "upper_bound": {
                "bytes": "¹Ô\\u0006\\u0000"
            }
        } ]
    },
    "added_rows_count": {
        "long": 1
    },
    "existing_rows_count": {
        "long": 0
    },
    "deleted_rows_count": {
        "long": 0
    }
}
{
    "manifest_path": "/home/hadoop/warehouse/db2/part_table2/metadata/d8a778f9-ad19-4e9c-88ff-28f49ec939fa-m0.avro",
    "manifest_length": 4884,
    "partition_spec_id": 0,
    "added_snapshot_id": {
        "long": 8271497753230544000
    },
    "added_data_files_count": {
        "int": 1
    },
    "existing_data_files_count": {
        "int": 0
    },
    "deleted_data_files_count": {
        "int": 0
    },
    "partitions": {
        "array": [ {
            "contains_null": false,
            "lower_bound": {
                "bytes": "¸Ô\\u0006\\u0000"
            },
            "upper_bound": {
                "bytes": "¸Ô\\u0006\\u0000"
            }
        } ]
    },
    "added_rows_count": {
        "long": 1
    },
    "existing_rows_count": {
        "long": 0
    },
    "deleted_rows_count": {
        "long": 0
    }
}

当 SELECT 查询正在读取 Iceberg 表,并在从元数据文件中获取快照的位置后为快照打开清单列表时,查询引擎将读取清单路径条目的值,并打开清单文件。它还可以在此阶段进行一些优化,例如使用行计数或使用分区信息过滤数据。

清单文件

清单文件跟踪数据文件以及有关每个文件的其他详细信息和统计信息。如前所述,允许 Iceberg 解决 Hive 表格式问题的主要区别是在文件级别跟踪数据 — 清单文件是执行此操作的地面靴子。

每个清单文件都会跟踪数据文件的子集,以实现大规模并行性和重用效率。它们包含许多有用的信息,用于在从这些数据文件读取数据时提高效率和性能,例如有关分区成员身份、记录计数以及列的下限和上限的详细信息。这些统计信息是在写入操作期间为每个清单的数据文件子集写入的,因此比 Hive 中的统计信息更有可能存在、准确且是最新的。

至于不把婴儿和洗澡水一起扔出去,Iceberg是文件格式不可知的,所以清单文件还指定了数据文件的文件格式,如Parquet、ORC或Avro。

下面是清单文件的完整内容的示例:(转换为 JSON)eab8490b-8d16-4eb1-ba9e-0dede788ff08-m0.avro

{
    "status": 1,
    "snapshot_id": {
        "long": 1257424822184505300
    },
    "data_file": {
        "file_path": "/home/hadoop/warehouse/db2/part_table2/data/ts_hour=2021-01-26-01/00000-6-7c6cf3c0-8090-4f15-a4cc-3a3a562eed7b-00001.parquet",
        "file_format": "PARQUET",
        "partition": {
            "ts_hour": {
                "int": 447673
            }
        },
        "record_count": 1,
        "file_size_in_bytes": 973,
        "block_size_in_bytes": 67108864,
        "column_sizes": {
            "array": [ {
                "key": 1,
                "value": 47
            },
            {
                "key": 2,
                "value": 57
            },
            {
                "key": 3,
                "value": 60
            } ]
        },
        "value_counts": {
            "array": [ {
                "key": 1,
                "value": 1
            },
            {
                "key": 2,
                "value": 1
            },
            {
                "key": 3,
                "value": 1
            } ]
        },
        "null_value_counts": {
            "array": [ {
                "key": 1,
                "value": 0
            },
            {
                "key": 2,
                "value": 0
            },
            {
                "key": 3,
                "value": 0
            } ]
        },
        "lower_bounds": {
            "array": [ {
                "key": 1,
                "value": "\\u0002\\u0000\\u0000\\u0000"
            },
            {
                "key": 2,
                "value": "\\u0000„ ,ù\\u0005\\u0000"
            },
            {
                "key": 3,
                "value": "test message 2"
            } ]
        },
        "upper_bounds": {
            "array": [ {
                "key": 1,
                "value": "\\u0002\\u0000\\u0000\\u0000"
            },
            {
                "key": 2,
                "value": "\\u0000„ ,ù\\u0005\\u0000"
            },
            {
                "key": 3,
                "value": "test message 2"
            } ]
        },
        "key_metadata": null,
        "split_offsets": {
            "array": [
                4
            ]
        }
    }
}

当 SELECT 查询正在读取 Iceberg 表并在从清单列表中获取其位置后打开清单文件时,查询引擎将读取每个data-file对象的条目file-path值,并打开数据文件。它还可以在此阶段进行一些优化,例如使用行计数或使用分区或列统计信息筛选数据。

了解了 Iceberg 表的不同组件以及访问 Iceberg 表中数据的任何引擎或工具所采用的路径后,现在让我们更深入地了解在 Iceberg 表上执行 CRUD 操作时会发生什么。

CRUD的底层执行

创建表

首先,让我们在我们的环境中创建一个表。

CREATE TABLE table1 (
    order_id BIGINT,
    customer_id BIGINT,
    order_amount DECIMAL(10, 2),
    order_ts TIMESTAMP
)
USING iceberg
PARTITIONED BY ( HOUR(order_ts) );

执行此语句后,环境将如下所示:

上面,我们创建了一个名为table1 在数据库 db1的表。该表有 4 列,并按时间戳列order_ts的小时粒度进行分区(稍后会详细介绍)。

执行上述查询时,将在元数据层中创建带有快照s0的元数据文件(快照s0不指向任何清单列表,因为表中尚不存在任何数据)。然后,更新db1.table1当前元数据指针的目录条目,以指向此新元数据文件的路径。

插入

现在,让我们向表中添加一些数据(尽管是文本值)。

INSERT INTO table1 VALUES (
    123,
    456,
    36.17,
    '2021-01-26 08:10:23'
);

当我们执行此 INSERT 语句时,会发生以下过程:

  1. 首先创建Parquet文件形式的数据 –table1/data/order_ts_hour=2021-01-26-08/00000-5-cae2d.parquet

  1. 然后,创建一个指向此数据文件的清单文件(包括其他详细信息和统计信息)–table1/metadata/d8f9-ad19-4e.avro

  1. 然后,创建一个指向此清单文件的清单列表(包括其他详细信息和统计信息) –table1/metadata/snap-2938-1-4103.avro

  1. 然后,基于先前的元数据文件且添加新快照s1的当前元数据文件 从而创建新的元数据文件,并跟踪以前的快照s0,指向此清单列表(包括其他详细信息和统计信息) –table1/metadata/v2.metadata.json

  1. 然后,db1.table1的当前元数据指针的值在目录中自动更新,现在指向这个新的元数据文件。

在所有这些步骤中,任何读取表的人都将继续读取第一个元数据文件,直到原子步骤#5完成,这意味着任何使用数据的人都不会看到表状态和内容的不一致视图。

合并到/更新插入

现在,让我们逐步完成合并到/更新插入操作。

假设我们已将一些数据放入我们在后台创建的临时表中。在这个简单的示例中,每次订单发生更改时都会记录信息,我们希望保留此表,显示每个订单的最新详细信息,因此如果订单 ID 已在表中,我们将更新订单金额。如果我们还没有该订单的记录,我们希望为此新订单插入一条记录。

在此示例中,阶段表包括表 (order_id=123) 中已有的订单的更新和表中尚未包含的新订单,该更新发生在 2021 年 1 月 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 语句时,将发生以下过程:

  1. 按照前面详述的读取路径,确定table1和table1_stage中具有相同order_id的所有记录。

  1. 包含来自表1的order_id=123的记录的文件被读取到查询引擎的内存(00000-5-cae2d.parquet)中,该内存副本中order_id=13的记录随后更新其order_amount字段,以反映表1_stage中匹配记录的新order_amount。然后将原始文件的修改副本写入新的Parquet文件–table1/data/order_ts_hour=2021-01-26-08/000001-aef71.Parquet

  • 即使文件中有其他记录与order_id更新条件不匹配,仍会复制整个文件,并在复制时更新一个匹配的记录,然后将新文件写出来,这是一种称为“写时复制”的策略。Iceberg即将推出一种新的数据更改策略,称为“读取时合并”(merge-on-read),它在封面下的行为会有所不同,但仍为您提供相同的更新和删除功能。

  1. table1_stage中与table1中的任何记录都不匹配的记录将以新的Parquet文件的形式写入,因为它与匹配记录table1/data/order_ts_hour=2021-01-27-10/00000-3-0fa3a.Parquet属于不同的分区

  1. 然后,将创建指向这两个数据文件的新清单文件(包括其他详细信息和统计信息)–table1/metadata/0d9a-98fa-77.avro

  • 在本例中,快照s1中的唯一数据文件中的唯一记录已更改,因此没有重复使用清单文件或数据文件。通常情况下并非如此,清单文件和数据文件在快照之间重用。

  1. 然后,创建一个指向此清单文件的新清单列表(包括其他详细信息和统计信息) –table1/metadata/snap-9fa1-3-16c3.avro

  1. 然后,基于具有新快照s2的先前当前元数据文件创建新的元数据文件,并跟踪以前的快照s0和s1 ,指向此清单列表(包括其他详细信息和统计信息) –table1/metadata/v3.metadata.json

  1. 然后,db1.table1的当前元数据指针的值在目录中自动更新,现在指向这个新的元数据文件。

虽然此过程有多个步骤,但一切都发生得很快。一个例子是Adobe做了一些基准测试,发现他们每分钟可以实现15次提交

在上图中,我们还显示,在执行此 MERGE INTO 之前,运行了一个后台垃圾回收作业来清理未使用的元数据文件 — 请注意,我们创建表时快照s0的第一个元数据文件不再存在。由于每个新的元数据文件还包含以前元数据文件所需的重要信息,因此可以安全地清理这些信息。未使用的清单列表、清单文件和数据文件也可以通过垃圾回收进行清理。

选择select

让我们再次回顾一下 SELECT 路径,但这次是在我们一直在处理的冰山表上。

SELECT * FROM db1.table1

执行此 SELECT 语句时,将发生以下过程:

  1. 查询引擎转到iceberg目录

  1. 然后,它检索db1.table1的当前元数据文件位置条目

  1. 然后,它打开此元数据文件并检索当前快照s2的清单列表位置的条目

  1. 然后,它会打开此清单列表,检索唯一清单文件的位置

  1. 然后,它会打开此清单文件,检索两个数据文件的位置

  1. 然后,它读取这些数据文件,并且由于它是s2,因此将数据返回到客户端

隐藏分区Hidden Partitioning

回想一下,在这篇文章的前面,我们讨论了Hive表格式的问题之一是用户需要知道表的物理布局,以避免非常慢的查询。

假设用户想要查看一天的所有记录,例如 2021 年 1 月 26 日,因此他们发出以下查询:

SELECT *FROM table1WHERE order_ts = DATE '2021-01-26'

回想一下,当我们创建表时,我们在订单首次发生时的时间戳的小时级别对其进行了分区。在 Hive 中,此查询通常会导致全表扫描。

让我们来看看 Iceberg 如何解决这个问题,并为用户提供以直观的方式与表交互的能力,同时仍然实现良好的性能,避免整个表扫描。

执行此 SELECT 语句时,将发生以下过程:

  1. 查询引擎转到Iceberg目录。

  1. 然后,它检索 db1.table1的当前元数据文件位置条目。

  1. 然后,它将打开此元数据文件,检索当前快照db1.table1的清单列表位置的条目。它还会在文件中查找分区规范,并看到表在字段order_ts的小时级别进行分区。

  1. 然后,它会打开此清单列表,检索唯一清单文件的位置。

  1. 然后,它会打开此清单文件,查看每个数据文件的条目,以将数据文件所属的分区值与用户查询请求的分区值进行比较。此文件中的值对应于自 Unix 纪元以来的小时数,然后引擎使用该小时来确定只有一个数据文件中的事件发生在 2021 年 1 月 26 日(换句话说,在 2021 年 1 月 26 日 00:00:00 和 2021 年 1 月 26 日 23:59:59 之间)。

  1. 具体来说,唯一匹配的事件是我们插入的第一个事件,因为它发生在 2021 年 1 月 26 日 08:10:23。另一个数据文件的订单时间戳是 2021 年 1 月 27 日 10:21:46,不是 2021 年 1 月 26 日,因此它与筛选器不匹配。

  1. 然后,它只读取一个匹配的数据文件,并且由于它是一个 SELECT *,它将数据返回到客户端。

历史快照Time Travel

查询历史某个时间点的表的快照数据。

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 语句时,将发生以下过程:

  1. 查询引擎转到Iceberg目录

  1. 然后,它检索 db1.table1的当前元数据文件位置条目

  1. 然后,它打开此元数据文件并查看snapshots数组中的条目(其中包含创建快照的毫秒 Unix 纪元时间,因此成为最新的快照),确定在请求的时间点(2021 年 1 月 28 日午夜)哪个快照处于活动状态,并检索该快照的清单列表位置的条目, 即s1

  1. 然后,它会打开此清单列表,检索唯一清单文件的位置

  1. 然后,它会打开此清单文件,检索两个数据文件的位置

  1. 然后,它读取这些数据文件,并且由于它是SELECT * ,因此将数据返回到客户端

请注意,在上图中的文件结构中,尽管旧的清单列表、清单文件和数据文件未在表的当前状态下使用,但它们仍然存在于数据湖中并可供使用。

当然,虽然保留旧的元数据和数据文件在这些用例中提供了价值,但在某个时候,您将拥有不再访问的元数据和数据文件,或者允许人们访问它们的价值超过了保留它们的成本。因此,有一个异步后台进程可以清理称为垃圾回收的旧文件。垃圾回收策略可以根据业务需求进行配置,并且是在要用于旧文件的存储量与回溯的时间和粒度之间进行权衡。

压缩Compaction

iceberg 只是一套表结构,所以压缩过程的调度和触发需要依赖外部其他工具或引擎的完成

作为Iceberg设计的一部分,另一个关键功能是压缩,它有助于平衡写侧和读侧的权衡。

在 Iceberg 中,压缩是一个异步后台进程,它将一组小文件压缩成较少的大文件。由于它是异步的,并且在后台,因此不会对您的用户产生负面影响。事实上,它基本上是一种特定类型的普通 Iceberg 写入作业,与输入和输出具有相同的记录,但在写入作业提交其事务后,文件大小和属性对于分析有了很大的改进。

每当您处理数据时,对于您希望实现的目标,都需要权衡取舍,并且通常,写入端和读取端的激励措施会朝着相反的方向发展。

  • 在写入端,您通常希望低延迟 - 使数据尽快可用,这意味着您希望在获得记录后立即写入,甚至可能不将其转换为列格式。但是,如果要对每条记录都执行此操作,则最终每个文件将获得一条记录(小文件问题的最极端形式)。

  • 在读取端,您通常需要高吞吐量 — 在单个文件中以列格式包含许多记录,因此与数据相关的可变成本(读取数据)超过固定成本(记录保存、打开每个文件的开销等)[降低文件打开等操作占数据读取的比例]。您通常还需要最新的数据,但您需要为读取操作支付成本。

压缩有助于平衡写入端和读取端的权衡 — 您可以在获得数据后立即将其写入,极端情况下是每个文件以行格式记录 1 条记录,读者可以立即查看和使用,而后台压缩过程会定期获取所有这些小文件并将它们合并成更少的文件, 较大的列式格式文件。

通过压缩,您的读取可以持续拥有他们想要的高吞吐量形式的 99% 的数据,但仍能看到最近 1% 的低延迟低吞吐量形式的数据。

对于此用例,同样重要的是要注意,压缩作业的输入文件格式和输出文件格式可以是不同的文件类型。一个很好的例子是从流写入 Avro,这些写入被压缩到更大的 Parquet 文件中进行分析。

另一个重要的注意事项是,由于 Iceberg 不是引擎或工具,调度/触发和实际的压缩工作由与 Iceberg 集成的其他工具和引擎完成

格式的设计优势

Iceberg写实更新,数据在写入时及及时更新元数据信息。保证任何时候读取数据都是最新的,最完整的可用数据。

现在,让我们将到目前为止所经历的应用到体系结构和设计提供的更高层次的价值中。

  • 事务的快照隔离

  • 在冰山表上进行读取和写入不会相互干扰。

  • Iceberg 通过乐观并发控制提供并发写入功能。

  • 所有写入都是原子的。

  • 更快的计划和执行

  • 这两个好处都源于这样一个事实,即您正在写入有关您在写入路径上写入的内容的详细信息,而不是在读取路径上获取该信息。

  • 由于文件列表是在对表进行更改时写入的,因此无需在运行时执行昂贵的文件系统列表操作,这意味着在运行时要做的工作和等待工作要少得多。

  • 由于有关文件中数据的统计信息是在写入端写入的,因此统计信息不会丢失、错误或过时,这意味着基于成本的优化器可以在决定哪个查询计划提供最快的响应时间时做出更好的决策。

  • 由于有关文件中数据的统计信息是在文件级别跟踪的,因此统计信息不是粗粒度的,这意味着引擎可以执行更多的数据修剪,处理更少的数据,因此响应时间更快。

  • 在本文前面链接的 Ryan Blue 演示文稿中,他分享了 Netflix 的一个示例用例的结果:

  • 对于 Hive 表上的查询,仅规划查询就花费了 9.6 分钟

  • 对于 Iceberg 表上的相同查询,计划和执行查询只需 42 秒

  • 抽象物理,公开逻辑视图

  • 在本文前面,我们看到使用 Hive 表,用户通常需要了解表的潜在不直观物理布局,以实现相当不错的性能。

  • Iceberg 提供了持续向用户公开逻辑视图的能力,将逻辑交互点与数据的物理布局分离。我们看到了这在隐藏分区和压缩等功能中非常有用。

  • Iceberg 提供了通过schema演化、分区演化和排序顺序演化功能随时间透明地演化表的功能。有关这些的更多详细信息可以在 Iceberg 文档站点上找到。

  • 数据工程在后台试验不同的、可能更好的表布局要容易得多。提交后,更改将生效,用户无需更改其应用程序代码或查询。如果实验结果使情况变得更糟,则可以回滚事务,并将用户返回到以前的体验。使实验更安全可以执行更多的实验,因此可以让你找到更好的做事方法。

  • 所有引擎都会立即看到变化

  • 因为构成表内容的文件是在写入端定义的,并且一旦文件列表发生更改,所有新读取器都会指向此新列表(通过从目录开始的读取流),因此一旦编写器对表进行更改,使用此表的所有新查询都会立即看到新数据。

  • 事件侦听器

  • Iceberg 有一个框架,允许在 Iceberg 表上发生事件时通知其他服务。目前,此功能处于早期阶段,只能发出扫描表时的事件。但是,此框架提供了未来功能的功能,例如保持缓存、物化视图和索引与原始数据保持同步。

  • 高效进行较小的更新

  • 由于数据是在文件级别跟踪的,因此可以更有效地对数据集进行较小的更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

十五天学会Autodesk Inventor,看完这一系列就够了(八),图框自定义

所周知,Autocad是一款用于二维绘图、详细绘制、设计文档和基本三维设计,现已经成为国际上广为流行的绘图工具。Autodesk Inventor软件也是美国AutoDesk公司推出的三维可视化实体模拟软件。因为很多人都熟悉Autocad,所以再学习Inventor&#x…

【数据库数据恢复】华为云mysql数据库数据被delete的数据恢复案例

数据库数据恢复环境: 华为云ECS,linux操作系统; mysql数据库,实例内数据表默认存储引擎为innodb。 数据库故障: 在执行数据库版本更新测试时,用户误将本应在测试库测试的sql脚本执行在生产库中&#xff0c…

拉伯证券|芯片半导体迎来“行业底部出清”,大资金进场迹象明显

近期关于小芯片的利好不断,英特尔近期就发布了根据小芯片技能的处理器,而近期长电科技也在小芯片范畴获得突破。据长电科技在互动平台表明,公司现已完成4nm工艺制程手机芯片的封装,最大封装体面积约为1500平方毫米的体系级封装。长…

人工智能所需高等数学知识大全(收藏版)

来源:投稿 作者:愤怒的可乐 编辑:学姐 不懂数学是学不好人工智能的,本系列文章就汇总了人工智能所需的数学知识。本文是高等数学篇。 另有线代篇和概率论篇 函数与极限 函数 yf(x) ,x是函数f的自变量,y是因变量 函…

数据结构(4)线段树、延迟标记、扫描线

活动 - AcWing 参考-《算法竞赛进阶指南》 一、延迟标记(懒标记) 在线段树的区间查询命令中,每当遇到被查询区间[l,r]完全覆盖节点时,可以直接把节点上的答案作为备选答案返回。我们已经证明,这样操作的复杂度为O(4…

01-React(脚手架+MVC/MVVM+JSX)

使用 create-react-app 构建React工程化项目 安装 create-react-app $ npm i create-react-app -g 「mac需要加sudo」 基于脚手架创建项目「项目名称需要符合npm包规范」 $ create-react-app xxx 目录结构: |- node_modules 包含安装的模块 |- public 页面模板…

79.循环神经网络的从零开始实现

从头开始基于循环神经网络实现字符级语言模型。 这样的模型将在H.G.Wells的时光机器数据集上训练。 和之前一样, 我们先读取数据集。 %matplotlib inline import math import torch from torch import nn from torch.nn import functional as F from d2l import to…

Rockchip开发系列 - 9.watchdog看门狗

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 dts中的watchdog节点watchdog驱动文件TRM watchdog:WDT框图功能描述计数器中断系统复位复位脉冲长度操作流程图寄存器描述寄存器设置…

Nessus Host Discovery

系列文章 Nessus介绍与安装 Nessus Host Discovery 1.启动nessus cd nessus sh qd_nessus.sh2.进入nessus网站 https://192.168.3.47:8834/3.点击【New Scan】 4.选择【Host Discovery】 5.输入name【主机发现】,Description【主机发现】,Targets【…

Android 蓝牙开发——服务启动流程(二)

首先我们要知道,主要系统服务都是在 SystemServer 启动的,蓝牙也是如此: 1、SystemServer 源码路径:/frameworks/base/services/java/com/android/server/SystemServer.java private void startOtherServices(NonNull TimingsT…

labelme(2)json文件转类别灰度图

首先感谢大佬:https://blog.csdn.net/tzwsg/article/details/114653071一、上代码,json2gray.py:#!/usr/bin/python # -*- coding: UTF-8 -*- # !H:\Anaconda3\envs\new_labelme\python.exe import argparse import json import os import os…

go语言中变量和常量的注意点

1、类型转换:大类型可以转换成小类型但是精度丢失;小类型不能转换成大类型。例如: package mainimport "fmt"//golang中使用" var "关键字来定义变量 //定义变量的语法:1、var var_name1[,var_name2,...] va…

day16|654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

654.最大二叉树 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。 返回 nums 构…

深度学习入门基础CNN系列——填充(padding)与步幅(stride)

填充(padding) 在上图中,输入图片尺寸为333\times333,输出图片尺寸为222\times222,经过一次卷积之后,图片尺寸为222\times222,经过一次卷积之后,图片尺寸变小。卷积输出特征图的尺寸…

el-table表头添加勾选框

el-table表头添加勾选框嘚吧嘚实现嘚吧嘚 table的行勾选是比较常规的操作,但是有的时候就有各种奇葩的需求蹦出来。😭 比如最近有一个需求,不仅需要勾选行,还需要勾选列,其实我心中有了一万头可爱的小羊驼&#xff0c…

NISP三级证书含金量如何

国家信息安全水平测试(NationalInformationSecurityTestProgram,通称NISP),是通过中国信息安全测评中心执行塑造我国网络空间安全优秀人才的一个项目。 为培养大量出色的实践型网络安全人才,中国信息安全测评中心上线…

SpringCloud Alibaba微服务 -- Seata的原理和使用

文章目录一、认识Seata1.1 Seata 是什么?1.2 了解AT、TCC、SAGA事务模式?AT 模式前提整体机制如何实现写隔离如何实现读隔离TCC 模式Saga 模式Saga 模式适用场景Saga 模式优势Saga 模式缺点二、Seata安装2.1 下载2.2 创建所需数据表2.2.1 创建 分支表、全局表、锁表2.2.2 创建…

Qt OpenGL(10)光照模型基础

文章目录物体的光照模型立方体坐标构建立方体的6个面代码框架widget.cpp顶点着色器片元着色器Ambient 环境光Diffuse 漫反色法向量计算漫反射分量Specular Highlight镜面高光计算镜面反射分量补充:半程向量的使用物体的光照模型 出于性能的原因,一般使用…

思科Cisco交换机的基本命令

一、设备的工作模式1、用户模式Switch>可以查看交换机的基本简单信息,且不能做任何修改配置!2、特权模式Switch> enable Switch#可以查看所有配置,且不能修改配置!3、全局配置模式switch# configure terminal switch(config…

Redis基础——SpringDataRedis快速入门

文章目录1. SpringDataRedis介绍2. SpringDataRedis快速入门2.1 SpringDataRedis的使用步骤1. SpringDataRedis介绍 SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis 官方网址 提供了对不同Redi…