2022-02-07 Iceberg源码阅读(一)

数据湖是近年来比较火热的领域,Apache Iceberg被誉为数据湖技术“三剑客”(Delta Lake、Hudi、Iceberg)之一,而iceberg高度抽象和优雅的设计成为了它最吸引人的优势,这一点也是我阅读iceberg源码的主要动力。

写这一篇iceberg源码阅读主要目的,是想系统整理下过去Iceberg源码阅读过程中留下的记录,一方面是回顾,另一方面也是方便在之后继续阅读源码的过程中能更高效地查阅,正所谓“书读百遍,其义自现”,因此内容会比较基础,不会面面俱到,也会掺杂一些我个人的理解。当然,对于想要接触Iceberg源码的同学,尤其是对于大数据体系还不太熟悉的同学,这篇内容也能有所帮助。

本篇主要围绕Iceberg文件组织和具体实现展开。

准备工作:

Iceberg的基础介绍推荐Iceberg的官方文档:https://iceberg.apache.org/

Iceberg的源码见github(0.12版本):https://github.com/apache/iceberg

为节省篇幅,文中不会大篇幅引用代码,因此推荐将代码拉取到本地配合阅读。

基础类介绍

一开始阅读源码的主要难点在于放眼望去都是陌生的类,不清楚这些类是什么作用,以及已经应该从哪里入手,或者看完官方的文档后,对文档中功能的实现和代码不能映射起来,这部分主要就是来解决这个问题。

1.Iceberg 文件组织概述

阅读源码之前,首先应该对Iceberg的功能设计做一个系统的了解,这部分官方文档做了比较清晰的介绍,我只对核心部分做一个简要描述,看完后对这部分仍然不太理解的同学建议阅读下官方文档。

在我看来,Iceberg最核心的功能就是对数据文件组织管理,正如Iceberg对自己的介绍:Apache Iceberg is an open table format for huge analytic datasets. 所谓的table format(表格式),其实就是将底层的数据文件以特定的方式组织好,再以表的方式暴露出来。

插一句题外话,从文件组织的功能上来说,这一点和Hive是类似的,只不过Hive对文件的组织比较粗糙,是以目录的形式进行组织,即只是声名了某个分区的文件在哪个目录下,而Iceberg精确到了文件级别,并且有单独的metadata文件来索引数据文件,metadata文件中保存了数据文件的信息,比如最大值、最小值等,这样,只需要读少量的metadata文件,就能过滤掉大量不需要访问的数据文件,这是Iceberg的一个重要特点。

Iceberg的文件组织如下图所示

image-20220207163958879

我们重点关注下面的metadata layer层和data layer层,metadata layer层中的文件就是metadata文件,而data layer层中的文件就是数据文件。

metadata layer层中的文件分为三类:metadata file、manifest list、manifest file。名字比较相似,需要仔细区分,下面会反复出现这些词汇。

  • metadata file本质上就是Iceberg表的所有信息,可以说从一个metadata file就可以构建出一个完整的Iceberg Table,包括表结构、分区字段、所有快照等等信息,后面介绍的代码的部分会详细说明,每次对Iceberg Table做一次操作,都会产生一个新的metadata file文件。

  • manifest list文件向上对应的是一个快照(图中的s0、s1),向下对应的是若干个manifest file文件。

  • manifest file文件是对一系列数据文件的索引,保存了每个数据文件的路径等信息,也就是在这里,Iceberg对文件的组织精确到了文件级别。

补充:

之所以要拆分manifest list和manifest file两层,主要的目的是为了复用历史的manifest file,如图中所示,一个manifest file可以被多个manifest list索引;相反,如果删掉manifest list这一层,只保留manifest file,则可以想象,每个manifest file都要索引到当前快照到全部文件,不同manifest file之间会存在大量重复,这点仔细推演一下,想必可以理解。

在这样的设计下,Iceberg构建了一套快照体系,这是Iceberg支持ACID特性、Time travel等功能的核心。简单来说,对Iceberg数据文件的任何增加或删除,都会产生一个新的快照(伴随产生一个新的manifest list文件),一系列快照组成了一个连续的快照链,可以指定任何快照进行读取(只要快照没有过期)。

为了更具体的展示,我们实际创建一张iceberg表,并append两张数据文件,可以看到文件组织如下所示:

iceberg_table_test1/
├── metadata
│ ├── 4b461a01-cdca-49fe-85bc-cb0702fb76d1-m0.avro
│ ├── snap-2427807540524010093-1-4b461a01-cdca-49fe-85bc-cb0702fb76d1.avro
│ ├── v1.metadata.json
│ ├── v2.metadata.json
│ └── version-hint.text
├── data_file1
└── data_file2

iceberg有一个单独的metadata目录来保存metadata文件

  • V1.metadata.json、V2.metadata.json文件即metadata file
  • snap-2427807540524010093-1-4b461a01-cdca-49fe-85bc-cb0702fb76d1.avro,这个文件就是manifest list
  • manifest file,如4b461a01-cdca-49fe-85bc-cb0702fb76d1-m0.avro

数据文件data_file1、data_file2默认在根目录,实际上可以写在任意目录下。

2.文件组织相关的基础类

1.Table(org.apache.iceberg.Table)

推荐阅读代码的入口,是定义了对Iceberg表的所有操作的接口,实现类先关注org.apache.iceberg.BaseTable。

2.TableMetadata(org.apache.iceberg.TableMetadata)

从逻辑上看:TableMetadata就是表的元数据,这个元数据不仅是最新的数据,也包含了变更历史,比如schema、partition、sortOrders、snapshot的变更历史都记录在TableMetadata中。

从物理上看:映射到磁盘的metadata.json文件(metadata file),每个TableMetadata对象对应一个metadata.json文件;

需要注意的是对表的每次操作都会产生一个新的metadata.json文件,最新的一个metadata.json文件已经包含了完整的信息,包括历史信息在内。因此,理论上只保留最后一个metadata.json文件即可。

对TableMetadata的读写等操作由TableOperations接口封装。

3.ManifestFile(org.apache.iceberg.ManifestFile)

从逻辑上看:ManifestFile是对磁盘上manifest file的指针(索引),记录了manifest file的位置和一些统计信息;

从物理上看:ManifestFile会持久化到磁盘的manifest list文件,每个ManifestFile对象对应manifestList文件中的一行记录;

snapshot对应一个唯一的manifestList文件(待确认描述是否精确);

4.ManifestEntry(org.apache.iceberg.ManifestEntry)

从逻辑上看:ManifestEntry是对磁盘上一个数据文件的指针(索引),记录了文件的位置和一些统计信息,此外还标注了文件的status,snapshot_id,sequence_number

从物理上看:ManifestEntry会持久化到磁盘的manifest file,每个ManifestEntry对象对应manifest file中的一行记录;

补充:

ManifestEntry中的status:entry中标注了文件的三种状态(Status):EXISTING ADDED DELETED,分别表示这个文件是本次提交之前就存在的、本次提交新增的、本次提交删除的,需要注意的是,这里的DELETED和DeleteFile是两个概念,需要区分清楚;

ManifestEntry中的snapshot_id:ADDED和EXISTING,指的是文件第一次ADD时的snapshot_id,之后不会变化;DELETED,文件删除时的snapshot_id;

ManifestEntry中的sequence_number:ADDED和EXISTING,指的是文件第一次ADD时的sequence_number,之后不会变化;DELETED,null;

一个比较细节的疑问:在提交时历史的manifest不一定会被重写,因此单纯通过ManifestEntry中记录的ADDED/EXISTING判断是否是本次提交添加的是不准确的,需要上游manifestlist中的snapshot_id协助判断,才能真正确定文件是不是这次提交添加的。

5.ContentFile(org.apache.iceberg.ContentFile)

从物理上看:ContentFile是上述ManifestEntry中的一个属性,也是ManifestEntry中的核心信息;

首先要明确一点,ContentFile代表的只是数据文件的元数据信息,而不是文件的实际数据。

文件的元数据信息主要包括:文件类型、文件路径、文件格式、分区信息、以及一些统计信息等。

最顶层接口ContentFile:F是指子类的具体类型

ContentFile分为两种:接口DataFile和接口DeleteFile,这两个接口的共同的实现逻辑在抽象类BaseFile中。

BaseFile主要是实现了IndexedRecord接口,这是一个avro接口,实现了这个接口意味着这个类可以持久化到avro文件中的一行记录,以及反过来从avro文件中构造出原来的对象(使用了反射的方式实现,这一部分和avro的SpecificData.SchemaConstructable的接口声明有关,如果类实现了这个街口,则承诺一定提供一个构造器允许传入avro的schema,用来构造这个类)。

除了DataFile和DeleteFile,还有一个IndexedDataFile是ContentFile的直接实现,IndexedDataFile是ContentFile的装饰器,增加了IndexedRecord的实现,因此,它的作用也是和avro文件进行交互。

此外,ContentFile还有一个方法splitOffset()方法,是这个文件推荐的划分位置点,说明这个文件可以在这些位置点上被划分为多个部分,进行并发读取。

补充:

IndexedRecord接口是avro文件的接口,表示avro文件内的一行记录,且这行记录的各个列的值是可以按照0、1、2、3...随机访问的,因此称为indexed,类似于ArrayList

public interface IndexedRecord extends GenericContainer {
  /**
   * Set the value of a field given its position in the schema.
   * 

* This method is not meant to be called by user code, but only by * {@link org.apache.avro.io.DatumReader} implementations. */ void put(int i, Object v); /** * Return the value of a field given its position in the schema. *

* This method is not meant to be called by user code, but only by * {@link org.apache.avro.io.DatumWriter} implementations. */ Object get(int i); }

IndexedRecord的上级接口是GenericContainer,GenericContainer要求提供Schema。

public interface GenericContainer {
  /** The schema of this instance. */
  Schema getSchema();
}

类似于IndexedRecord,iceberg也定义了自己的StructLike,相比IndexedRecord,StructLike可以返回列的数量,而且可以在取出值时动态指定值的类型。

public interface StructLike {
  int size();

   T get(int pos, Class javaClass);

   void set(int pos, T value);
}

将StructLike装饰成IndexedRecord的工具类为IndexedStructLike,使用这个工具,需要提供avroSchema,然后就可以将iceberg的StructLike写成avro文件的一行记录。

3.其他基础类

还有一些很重要的基础类,比如和表结构相关的Schema Type,和快照相关的Snapshot等,不放在本篇介绍,本篇聚焦在文件组织上。

文件组织的实现

文件组织实际上就是Iceberg对metadata layer层的操作,这一步操作最终会落实到对metadata filemanifest listmanifest file文件这三类文件的操作。

这些对文件的组织全部在Table接口中暴露出来,包括对文件的增加、删除、替换等等,所有这些操作的继承关系如下。

image-20211218175224157

1.顶级接口:PendingUpdate

最上层的接口是PendingUpdate,这个接口逻辑很简单,主要定义了apply和commit两个操作步骤,后来还为监听机制添加了updateEvent接口(可以参考https://github.com/apache/iceberg/pull/939,暂时不在这里讨论)。

2.第二级接口:重点SnapshotUpdate

第二级接口总共包括:ReplaceSortOrder、UpdateLocation、Rollback、UpdateProperties、UpdateSchema、ExpireSnapshots、UpdatePartitionSpec、ManageSnapshots、SnapshotUpdate

当前重点关注的:SnapshotUpdate

SnapshotUpdate是所有新生成snapshot的操作的顶级接口,额外定义了三个操作:

  • set:设置snapshot summary
  • deleteWith:重新定义删除文件的方式
  • stageOnly:参照https://github.com/apache/iceberg/pull/342

3.第三级接口:

SnapshotUpdate的下级接口,首先是功能描述接口,如下:

  • ReplacePartitions
  • RewriteFiles
  • RowDelta
  • AppendFiles
  • RewriteManifests
  • OverwriteFiles
  • DeleteFiles

然后是上述功能接口的具体实现:SnapshotProducer

SnapshotProducer是SnapshotUpdate的抽象实现,上述功能接口的实现都是基于SnapshotProducer,而这些功能接口的绝大多数实现都是基于SnapshotProducer的抽象子类MergingSnapshotProducer,只有两个具体实现是特例:FastAppend BaseRewriteManifests。

因此,对于实现我们重点来讨论这一对父子:SnapshotProducer和MergingSnapshotProducer。

首先:对于SnapshotProducer

SnapshotProducer提供的通用实现:

  • apply
  • Commit

SnapshotProducer扩展的方法主要是一些对manifest的通用的操作实现,可供子类调用,比较核心的只有一个apply方法

  • protected abstract List apply(TableMetadata metadataToUpdate);

总的来说,SnapshotProducer只需要子类实现一个核心逻辑:在旧的TableMetadata的基础上实现产生新的manifestFiles,其他问题SnapshotProducer会解决。

接着:对于MergingSnapshotProducer,从类名上就可以看出,这个类要进行merge操作,这个merge指的是manifest file的合并;与此相对照的是FastAppend,FastAppend每次都是生成新的manifest file,不进行manifest file的合并,因此FastAppend提交的代价更低,但是可能引起manifest file的碎片化。除了FastAppend操作之外,其他继承自MergingSnapshotProducer的操作都是要进行manifest合并的,比如和FastAppend相对应的MergeAppend。

MergingSnapshotProducer提供的通用实现:

  • apply:实现了上述SnapshotProducer要求的接口,因此MergingSnapshotProducer的子类不需要再实现apply,只有特殊情况下需要额外修改一下apply方法,专指BaseReplacePartitions。

具体看下MergingSnapshotProducer对apply的实现:

围绕4个成员变量:

  • filterManager:对data文件的manifest file的过滤,类ManifestFilterManager
  • mergeManager:对data文件的manifest file的合并,类ManifestMergeManager
  • deleteFilterManager:对delete文件的manifest file的过滤,类ManifestFilterManager
  • deleteMergeManager:对delete文件的manifest file的合并,类ManifestMergeManager

先看下ManifestMergeManager ManifestFilterManager的作用,这两个类没有继承关系。

ManifestFilterManager是进行ManifestFile过滤的工具,简单来说,是将(磁盘上)旧的manifest中的需要删除的entry剔除,生成新的manifest file(或者不存在需要删除的entry时,保留旧的manifest file)。

ManifestMergeManager是进行ManifestFile合并操作的工具,简单来说,是将(磁盘上)旧的manifest合并成若干个大小不超过8m的新的manifest(合并不是必然触发的,受TableProperties控制)。

合并后写入到新的manifest时,会剔除对旧的Status=DELETED文件的索引,如果manifest上记录的是本次添加的文件,则记录为ADD,如果是之前添加的文件,则记录为EXISTED,如果是本次删除的文件,则记录为DELETED,如果是之前产生的deleteFile,则忽略。

不管是ManifestMergeManager还是ManifestFilterManager都会进行manifest文件的读写,因此都可能是很耗时的操作。

MergingSnapshotProducer没有将上述4个成员变量直接暴露给子类操作,而是暴露了一些语义更加简单的方法,比如添加数据文件、删除数据文件、按照Expression过滤等,这样各种文件组织操作的具体子类,只需要调用这些接口就能实现自己的功能,各类操作的详细实现不再展开讨论。

你可能感兴趣的:(2022-02-07 Iceberg源码阅读(一))