4. Apache Iceberg 核心概念

好,咱们进入 Iceberg 最核心的部分。说实话,我当年第一次接触 Iceberg 时,最震撼的就是它的快照隔离机制。你想想看,一个表格式能做到让读写完全不互斥,这在传统数仓里简直不敢想。

4.1 快照隔离:读与写的“平行宇宙”

Iceberg 的每个写操作都会生成一个快照(Snapshot)。快照本质上就是表在某个时间点的完整元数据指针。读操作只认快照,写操作只创建新快照——两者各玩各的。

核心原理:Iceberg 使用乐观并发控制(OCC)。每次提交时,原子性地将表的当前元数据指针从旧快照切换到新快照。读请求始终指向旧快照,直到它主动去拉取最新指针。

我在项目中遇到过一个问题:有个流式任务每 30 秒写一次 Iceberg 表,同时 BI 报表每 5 分钟做一次全表扫描。传统表格式下,写操作会锁表,读操作就得等。但 Iceberg 下,读任务拿到的是 5 分钟前的快照,写任务在后台默默生成新快照——两边完全不受影响。

特性 Iceberg 快照隔离 传统 Hive 表
读写冲突 无冲突,读旧写新 写锁阻塞读
时间旅行 原生支持,按快照 ID 或时间戳查询 不支持
回滚 直接指向旧快照即可 需要重建分区
并发写入 乐观锁 + 重试机制 悲观锁,易死锁

避坑指南:我曾经因为没设置合理的快照过期策略,导致元数据目录下积累了上千个快照文件,每次列出快照都要好几秒。建议根据业务保留周期设置 write.metadata.delete-after-commit.enabled=true 和 write.metadata.previous-versions-max=100。

4.2 Manifest 文件:Iceberg 的“目录索引”

Manifest 文件是 Iceberg 表格式的精髓。说白了,它就是一张数据文件清单,记录了每个数据文件的路径、分区信息、列统计信息(最小值、最大值、空值计数等)。

Iceberg 的元数据层级分三层:

  1. Metadata 文件(.metadata.json):表的全局信息,包含快照列表、Schema、分区规约等。
  2. Manifest List 文件(.snapshot-xxx.avro):每个快照对应的 Manifest 文件列表。
  3. Manifest 文件(.manifest-xxx.avro):具体的数据文件条目。

我个人的习惯是,把 Manifest 文件想象成图书馆的书架索引卡。Metadata 文件是图书馆的总目录,告诉你有哪些书架(快照);Manifest List 告诉你每个书架上有哪些索引卡盒;Manifest 文件就是索引卡本身,每张卡记录了一本书(数据文件)的位置和摘要信息。

注意:Manifest 文件是 Avro 格式,不是人类可读的。如果你想查看内容,可以用 org.apache.iceberg.avro.AvroSchemaUtil 工具类,或者直接用 Spark 读取:spark.read.format("avro").load("path/to/manifest.avro")。

Manifest 文件还有一个关键特性——列统计信息下推。查询引擎在读取 Manifest 时,就能根据列统计信息过滤掉不需要的数据文件。比如你查 WHERE event_date = '2024-01-15',Iceberg 会先看 Manifest 里每个数据文件的 event_date 最小/最大值,直接跳过不包含该日期的文件。

-- 查看表的快照和 Manifest 信息
SELECT snapshot_id, manifest_list, committed_at
FROM my_table.snapshots;

-- 查看某个快照的 Manifest 文件内容(Spark SQL)
SELECT file_path, partition, record_count, lower_bounds, upper_bounds
FROM my_table.manifests
WHERE snapshot_id = 1234567890;

4.3 分区演进:不再“建表定终身”

传统 Hive 表的分区策略一旦确定,改起来就是噩梦。你得重建表、重写数据、还要改所有下游任务。Iceberg 的分区演进(Partition Evolution)彻底解决了这个问题。

分区演进的核心思想是:每个快照可以有自己的分区规约。新写入的数据使用新分区规约,旧数据仍然保留旧分区规约。查询时,Iceberg 自动合并不同分区规约下的数据。

举个例子:你最初按 event_date 天级分区。后来业务量暴增,想改成按 event_hour 小时级分区。在 Iceberg 里,你只需要执行一条 ALTER TABLE 语句,后续写入的数据自动按小时分区,历史数据仍然按天分区。查询引擎完全感知不到这个变化。

-- 初始分区:按天
ALTER TABLE my_table ADD PARTITION FIELD event_date;

-- 演进后:按小时(旧数据仍按天,新数据按小时)
ALTER TABLE my_table ADD PARTITION FIELD event_hour;

-- 甚至可以移除旧分区字段
ALTER TABLE my_table DROP PARTITION FIELD event_date;

我曾经在迁移一个 10TB 的日志表时,就用了分区演进。原来按天分区,每天一个目录,小文件太多。我改成按小时分区后,查询性能提升了 3 倍。关键是——不需要重写历史数据,零停机。

场景 传统 Hive Iceberg 分区演进
修改分区字段 重建表 + 重写数据 ALTER TABLE 即可
历史数据兼容 必须迁移 自动兼容旧分区
查询性能 需手动优化 自动分区裁剪
运维成本 高 低

4.4 隐藏分区:让查询引擎“自己猜”分区

隐藏分区(Hidden Partitioning)是 Iceberg 最容易被忽视但极其强大的特性。传统分区要求用户在查询时手动指定分区字段(比如 WHERE event_date = '2024-01-15'),否则就会全表扫描。

Iceberg 的隐藏分区机制是:分区字段可以基于已有列自动派生。你不需要在表中显式存储分区列,Iceberg 会在写入时根据转换函数自动计算分区值,并在查询时自动应用分区裁剪。

我建议:能用隐藏分区就别用显式分区。它让表对用户更友好——用户只需要关心业务字段,分区逻辑完全由 Iceberg 接管。

Iceberg 内置了多种分区转换函数:

  • year(ts)、month(ts)、day(ts)、hour(ts):时间戳按年/月/日/时分区
  • bucket(N, col):对列做哈希分桶,适合高基数字段
  • truncate(L, col):截断字符串或数值,按前缀分区
  • identity(col):直接使用列值作为分区(等同于传统分区)
-- 创建表时定义隐藏分区
CREATE TABLE my_table (
    event_ts TIMESTAMP,
    user_id STRING,
    event_type STRING,
    payload STRING
)
USING iceberg
PARTITIONED BY (
    month(event_ts),   -- 按月份隐藏分区
    bucket(16, user_id) -- 按用户ID哈希分16个桶
);

-- 查询时,用户只需要写业务条件
SELECT * FROM my_table
WHERE event_ts BETWEEN '2024-01-01' AND '2024-01-31'
  AND user_id = 'u12345';

-- Iceberg 自动推导出分区:month=2024-01 且 bucket=xxx

嗯,这里要注意:隐藏分区不是银弹。如果你的查询条件里没有包含分区转换函数的输入列,分区裁剪照样失效。比如你按 month(event_ts) 分区,但查询只写了 WHERE event_type = 'click',那还是得全表扫描。

下面这张图展示了 Iceberg 的快照隔离和 Manifest 文件如何协同工作:

Iceberg 快照隔离与 Manifest 文件结构 快照 v1 (旧) 快照 v2 (当前) 快照 v3 (写入中) 读操作 (指向 v1) 写操作 (创建 v3) Manifest List (快照 v2 的清单) Manifest 文件 1 分区: month=2024-01 文件数: 5, 记录数: 12000 Manifest 文件 2 分区: month=2024-02 文件数: 8, 记录数: 23000 Manifest 文件 3 分区: month=2024-03 文件数: 12, 记录数: 45000 数据文件 (Parquet/ORC/Avro 存储在 HDFS/S3/OSS) 说明: • 读操作指向快照 v1,即使 v2/v3 存在也不受影响,实现快照隔离。 • Manifest 文件记录数据文件的统计信息,支持分区裁剪和列统计下推。

最后总结一下这四个核心概念的关系:

  • 快照隔离保证了读写不冲突,是 Iceberg 支持并发读写的基础。
  • Manifest 文件是元数据索引,让 Iceberg 能高效定位数据文件。
  • 分区演进让分区策略可以随业务变化灵活调整,不用重写历史数据。
  • 隐藏分区把分区逻辑从用户侧移到引擎侧,降低使用门槛。

这四个概念环环相扣。快照隔离依赖 Manifest 文件来记录每个快照的数据文件清单;分区演进依赖 Manifest 文件来兼容不同分区规约;隐藏分区依赖 Manifest 文件中的列统计信息来实现自动分区裁剪。理解了它们,你就掌握了 Iceberg 表格式的底层逻辑。