Apache Iceberg Delete File 解析

原文链接:www.inlighting.org/archives/un...

Iceberg 默认使用 Copy On Write 技术,也就是当你删除一行数据时,它会读取原有的文件,删除目标行,然后再重新写一遍,这样开销显然很大。后面 Iceberg 引入了 Merge On Read 技术,通过标记的方式,实现高效的数据删除。

Iceberg 的 Delete File 负责进行标记删除,目前 Delete File 有三种实现:

  • Position Delete:将会在 V3 被废弃,由 Deletion Vector 替代。
  • Equality Delete:主要用于 Flink 这种流式写入系统。
  • Deletion Vector:Position Delete 进化版,V3 引入。

Position Delete

Position Delete 算一种特别的 Data File(Parquet、ORC 等等),一般和你 Data File 的格式保持一致,里面存储 Data File 里面被删除的行号。

数据构造

这里用 Spark-SQL 创建 Position Delete 表

bash 复制代码
./bin/spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.9.0\
    --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
    --conf spark.sql.catalog.spark_catalog=org.apache.iceberg.spark.SparkSessionCatalog \
    --conf spark.sql.catalog.spark_catalog.type=hive \
    --conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \
    --conf spark.sql.catalog.local.type=hadoop \
    --conf spark.sql.catalog.local.warehouse=$PWD/warehouse \
    --conf spark.sql.defaultCatalog=local
sql 复制代码
use local;
create database test;
use test;

CREATE TABLE position_delete (id bigint, data string) USING iceberg
TBLPROPERTIES ('write.delete.mode'='merge-on-read', 'write.update.mode'='merge-on-read');

INSERT INTO position_delete VALUES (1, '1'), (2, '2'), (3, '3'), (4, '4');

update position_delete set data = 'v3' where id = 3;

文件结构

Position Delete 一共有三列:

  • file_path:对应删除 Data File 的路径
  • pos:被删除的行号,从 0 开始计数。
  • row:保存被删除行的完整数据。这一列是可选的,Spark 默认写是不带这一列的。可以用于数据审计、一致性检查啥的。
bash 复制代码
parquet cat data/00000-5-060ba8dc-6166-4918-8b5c-9809dbea7012-00001-deletes.parquet
{"file_path": "/warehouse/test/position_delete/data/00002-2-6a844605-c2e7-450a-a9a3-42679bdf2d18-0-00001.parquet", "pos": 0}

从 Position Delete 的列结构看出,Position Delete 是一对多的,一个 Position Delete 能同时对应多个 Data File。

Equality Delete

Equality Delete 算一种特别的 DataFile(Parquet、ORC 等等),里面存储 Data File 里面被删除的

数据构造

Equality Delete 目前只能用 Flink 造出来,Flink 环境有点复杂,故这里的步骤详细点。

这里使用 Flink 1.20.1。启动前需要设置 HADOOP_CLASSPATH,不然写 Iceberg 的一些 Hadoop 依赖会找不到。

这里我用的是 Hadoop 3.4.1。

bash 复制代码
wget https://archive.apache.org/dist/hadoop/core/hadoop-3.4.1/hadoop-3.4.1.tar.gz
tar zxvf hadoop-3.4.1.tar.gz
HADOOP_HOME=`pwd`/hadoop-3.4.1
export HADOOP_CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath`

下载 iceberg 依赖到 flink/lib 目录下。

bash 复制代码
cd flink/lib/
wget https://repo.maven.apache.org/maven2/org/apache/iceberg/iceberg-flink-runtime-1.20/1.9.0/iceberg-flink-runtime-1.20-1.9.0.jar

启动 Flink

bash 复制代码
./bin/start-cluster.sh

启动 Flink-SQL

bash 复制代码
./bin/sql-client.sh
sql 复制代码
CREATE CATALOG iceberg WITH (
  'type'='iceberg',
  'catalog-type'='hadoop',
  'warehouse'='file:///Users/smith/Software/flink-1.20.1/data/iceberg'
); 

use catalog iceberg;

create database test;

use test;

CREATE TABLE equality_delete (
    `id` INT COMMENT 'unique id',
    `name` string,
    `data` STRING NOT NULL,
    PRIMARY KEY(`id`, `name`) NOT ENFORCED
) with ('format-version'='2', 'write.upsert.enabled'='true');

insert into equality_delete values(1, 'smith', 'a'), (2, 'danny', 'b'), (3, 'alice', 'c'), (4, 'bob', 'd');
insert into equality_delete values(4, 'bob', 'e'), (5, 'dennis', 'f'), (6, 'jasson', 'g'), (7, 'parker', 'h');

这里设置了 idname 为主键列。Flink 在每次 insert 前,都会先写一遍 Equality Delete,把之前的数据给删了,然后再插入新数据。

比如执行 insert into equality_delete values(1, 'smith', 'a'); ,Flink 会先生成一个 Equality Delete,标记删除之前 id=1 AND name='smith' 的所有行。再生成一个 Data File 插入 (1, 'smith', 'a')

文件结构

Equality Delete 的列数等同于你的主键列。

bash 复制代码
❯ parquet cat data/00000-0-7995d4c6-810e-48ca-a72c-9f6b41ec6936-00002.parquet
{"id": 4, "name": "bob"}
{"id": 5, "name": "dennis"}
{"id": 6, "name": "jasson"}
{"id": 7, "name": "parker"}

Equality Delete 同样是一对多关系,一个 Equality Delete 可以对应删除多个 Data File。

Deletion Vector

Deletion Vector 是在 V3 中推出,用于取代 Position Delete。其存储格式是一个 bitmap,第 n 个 bit 被设置意味着其对应的 Data File 第 n 行被删除。

Deletion Vector 和 Data File 是一对一关系。Deletion Vector 存储着该 Data File 所有被删除的行。这就意味着写入引擎每一次删除数据时,都要把过去的删除记录读出来,进行合并,生成一个新的 Deletion Vector。

Deletion Vector 通过 referenced_data_file 字段关联。

DeleteFileIndex 构建逻辑

Iceberg 在 planFiles() 的时候,通过 DeleteFileIndex 处理 Data File 和 Delete File 的映射关系。

DeleteFileIndex 内部维护了如下几个索引结构:

java 复制代码
class PartitionKey {
	int partitionSpecId;
	Object partitionValue;
}

class DeleteFileIndex {
	List<EqualityDeleteFile> globalEqualityDeletes;
	Map<PartitionKey, EqualityDeleteFile> equalityDeleteFilesByPartition;
	Map<PartitionKey, PositionDeleteFile> positionDeleteFilesByPartition;
	Map<String, PositionDeleteFile> positionDeleteFilesByPath;
	Map<String, DeletionVector> deletionVectorsByPath;
}

PartitionKey

PartitionKey 由 PartitionSpecId + PartitionValue 组成。

注意即使一个表是非分区表,其 DeleteFile 也是有 PartitionKey 的,可以简单的认为是:

java 复制代码
class PartitionKey {
	int partitionSpecId = 0; // 所有表都有一个初始为 0 的 PartitionSpecId
	Object partitionValue = null;
}

索引创建逻辑

java 复制代码
for (DeleteFile file : files) {
  switch (file.content()) {
    case POSITION_DELETES:
      if (isDeletionVector(file)) {
	      // Deletion Vector 是一定有 referenced_data_file
        addDeleteVectorsByPath(file); 
      } else {
	      if (hasReferencedDataFileLocation(file)) {
		      addPositionDeletesByPath(file);
	      } else {
		      addPositionDeletesByPartition(file);
	      }
      }
      break;
    case EQUALITY_DELETES:
	    if (isPartitioned(file)) {
		    addEqualityDeleteFilesByPartition(file);
	    } else {
		    // 如果该 Equality Delete 是没有分区的,那么意味着它是 Global 的,即对应所有分区
		    addGlobalEqualityDeleteFiles(file);
		  }
      break;
    default:
      throw new UnsupportedOperationException("Unsupported content: " + file.content());
  }
}

GlobalEqualityDeletes

如果一个 Equality Deletes 是没有分区的,那么其就会被加入 GlobalEqualityDeletes,即匹配所有分区。

Partition Spec 通过如下代码判断其是没有分区的:

java 复制代码
  public boolean isPartitioned() {
    return fields.length > 0 && fields().stream().anyMatch(f -> !f.transform().isVoid());
  }

Partition Spec 里面的 transform Void 是不算分区的。Void 的意思就是以前他是一个分区列,后面 schema evolution 被删除了,v1 表为了兼容性问题,拿 Void 做个占位。iceberg.apache.org/spec/#parti...

EqualityDeletesByPartition / PositionDeletesByPartition

没什么好说的,存储 PartitionKey → 对应 EqDeleteFiles / PosDeleteFiles 的映射。

PositionDeleteFilesByPath / DeletionVectorsByPath

如果 Delete File 的 referenced_data_file 有值,那说明其有直接对应的 Data File 文件,针对这种情况,直接加入 PositionDeleteFilesByPath / DeletionVectorsByPath 索引。

像 Deletion Vector 是一对一关系,其 referenced_data_file 是一定有值的。

Data File 和 Delete File 匹配规则

官方有具体的描述规则:iceberg.apache.org/spec/#scan-...

现在假设把一个 Data File 输入构建好的 DeleteFileIndex,DeleteFileIndex 执行如下匹配逻辑:

  1. 查找 globalEqualityDeletes,满足:
    1. Data File 的 sequence_number < Equality Delete 的 sequence_number。
    2. 检查主键列的统计信息和 Data File 的列统计信息是否匹配。
  2. 查找 equalityDeleteFilesByPartition ,满足:
    1. Data File 和 Equality Delete 含有相同的 Partition Key。
    2. Data File 的 sequence_number < Equality Delete 的 sequence_number。
    3. 检查主键列的统计信息和 Data File 的列统计信息是否 overlap。
  3. 查找 deletionVectorsByPath,满足:
    1. referenced_data_file 对应即可。
  4. 如果该 Data File 存在对应的 Deletion Vector,那么就不用找 Position Delete 了,直接返回即可。返回 found_global_eq_delete + found_partition_eq_delete + found_deletion_vector
  5. 如果没有 Deletion Vector,查询 positionDeleteFilesByPartition ,满足:
    1. Data File 的 Partition Key 等于 Position Delete 的 Partition Key。
    2. Data File 的 sequence_number ≤ Position Delete 的 sequence_number。
  6. 查询 positionDeleteFilesByPath,满足:
    1. referenced_data_file 对应即可。
  7. 返回 found_global_eq_delete + found_partition_eq_delete + found_partition_pos_delete + found_path_pos_delete

为什么 Equality Delete 的 sequence_number 不是 ≤ 关系?

Sequence number 是每一次 snapshot commit 的时候,自增提交的。在一批次 commit 中,可能会同时夹杂了 Delete File 和 Data File。如果你允许了 ≤,那么请问在这一批次中,你的 Equality Delete 知道要对应哪些 Data File 吗。

且在 Flink upsert 的场景中,他 commit 里面的 Equality Delete 本来就是用于删除过去 sequence_number 中出现的主键,所以这么设计很合理。

至于 Position Delete 和 Deletion Vector ≤ 没关系的原因是,Position Delete 的文件里面,本来就有 file_path 告诉你对应哪一个 Data File。Deletion Vector 则由 referenced_data_file 来告诉你对应的 Data File。

对应关系小结

  • 一个 Data File 可以同时关联 Equality Delete + (Position Delete / Deletion Vector)。
  • 如果一个 Data File 已经关联了 Deletion Vector,那么其一定不存在 Position Delete。

传统 Position/Equality Delete 存在的性能问题

Position Delete 和 Equality Delete 都是一对多模型。在现在 AP 引擎里面,一个 Data File 会被切分成多个 split,然后每个 split 被分发到不同的节点上面。

比如一个 Data File 关联了 2 个 Delete File,此时该 Data File 被分割成两个 split,分发到两个节点上去。此时需要加载 4(2*2) 个 Delete File。相同于相同的 Delete File 被重复读取了一次。

此外在处理关联关系的时候,也很麻烦,毕竟上面的 DeleteFileIndex 的构建逻辑这么复杂。

Deletion Vector 则通过强制一对一的关系,有效缓解了这种情况。同时查询引擎在处理关联时也更加高效。

至于 Equality Delete,可以通过改写查询计划避免重复读取 Equality Delete。把一个 scan 查询,改写成 left anti join。左表是 Equality Delete,右表是 Data File。Join on 的 key 是主键。

个人觉得 Equality Delete 比较鸡肋,本质上就是 Iceberg 无法实现主键模型,然后为了迎合 Flink,硬凑出的产物。重点是 Iceberg 自己本身还不能完全保证主键不重复,全靠写引擎自己自觉。

反正 Equality Delete Flink 用的是爽,就是恶心读引擎了。

本质上还是 Iceberg 就不应该涉足主键模型,主键模型交给 Hudi、Paimon 这种专业的就行了。

结尾

以上不一定都对,如果错误,欢迎评论。

相关推荐
marteker1 小时前
电通宣布与Criteo战略整合,纳入零售受众数据
大数据·人工智能·零售
摩尔元数1 小时前
质检滞后?物料浪费?MES系统破解传统制造七大死结
大数据·人工智能·制造
延凡科技3 小时前
低空数联无人机AI智慧巡查平台
大数据·运维·人工智能·无人机·智慧城市·制造
yczn1233 小时前
3D可视化数字孪生智能服务平台-物联网智控节能控、管、维一体化技术架构
大数据·网络·人工智能
静听山水3 小时前
PostgreSQL/Hologres 系统表 pg_class 详解
大数据
远方16093 小时前
57-Oracle SQL Profile(23ai)实操
大数据·数据库·sql·oracle·database
189228048616 小时前
NW849NX721美光固态闪存NX745NX751
大数据·服务器·科技
致Great12 小时前
MCP出现的意义是什么?让 AI 智能体更模块化
大数据·人工智能·rag
远方160912 小时前
53-Oracle sqlhc多版本实操含(23 ai)
大数据·数据库·sql·oracle·database