4、Apache Iceberg基础:Iceberg表格式、快照与版本管理、时间旅行查询

聊到流批一体架构,绕不开的一个核心组件就是 Apache Iceberg。我最早接触它是在做实时数仓改造的时候,当时被 Hive 的锁机制和文件膨胀搞得焦头烂额。后来换成 Iceberg,嗯,确实省心不少。

这一节我们重点拆解三个东西:Iceberg 的表格式到底长什么样、快照和版本管理怎么运作、以及那个让人眼前一亮的时间旅行查询。说白了,理解了这些,你才算真正入了 Iceberg 的门。

4.1 Iceberg 表格式:不止是一张表

传统 Hive 表,元数据全在 Metastore 里,数据文件散落在 HDFS 上。Iceberg 不一样,它在表和文件之间加了一层——元数据层。

Iceberg 的表结构分三层:

  • Catalog 层:记录表名和当前元数据指针。通常存在 Hive Metastore 或自定义 Catalog 里。
  • Metadata 层:包含 metadata.json、snapshot 文件、manifest list。这一层记录了表的所有版本信息。
  • Data 层:真正的 Parquet / ORC / Avro 数据文件。

我习惯把 Iceberg 的表想象成一个「带目录的档案柜」。Catalog 是柜子上的标签,Metadata 是抽屉里的索引卡,Data 才是真正的文件。每次写数据,不是直接往柜子里塞文件,而是先更新索引卡。

核心差异:Iceberg 的元数据是自描述的,不依赖外部 Metastore 的版本管理。这意味着你可以把整个表复制到另一个集群,只要元数据文件在,表就能读。

来看一个实际的表目录结构:

/warehouse/db/table/
├── metadata/
│   ├── 00001-abc.metadata.json
│   ├── 00002-def.metadata.json
│   ├── snap-1234567890.avro
│   └── snap-1234567891.avro
└── data/
    ├── partition_col=2024/
    │   ├── 00000-0.parquet
    │   └── 00000-1.parquet
    └── partition_col=2025/
        └── 00001-0.parquet

每个 metadata.json 文件对应一个表版本。Iceberg 通过递增的版本号来管理这些文件,不会覆盖旧的。

4.2 快照与版本管理:Iceberg 的「后悔药」

Iceberg 的每次写入、删除、更新操作,都会生成一个新的快照(Snapshot)。快照本质上是一个指向当前所有有效数据文件的指针集合。

为什么说它是「后悔药」?因为 Iceberg 不会立即删除旧文件。它只是把旧快照标记为「历史」,新快照指向新文件。旧文件还在,只是不再被当前快照引用。

我曾经在生产环境遇到过一个问题:某天凌晨的批处理任务写错了分区,把昨天的数据覆盖了。如果是 Hive,基本只能从备份恢复。但 Iceberg 呢?直接回滚到上一个快照就行,几秒钟的事。

小技巧:Iceberg 默认保留最近 10 个快照。你可以通过 write.metadata.delete-after-commit.enabled 和 write.metadata.previous-versions-max 来控制保留数量。我个人建议生产环境至少保留 30 个快照,方便回溯。

快照的生命周期大致如下:

  1. 创建快照:每次 INSERT、DELETE、MERGE 或 UPDATE 操作完成后,Iceberg 自动生成一个新快照。
  2. 快照引用:当前快照是 current,历史快照可以通过 snapshot-id 或时间戳访问。
  3. 快照过期:Iceberg 的 expireSnapshots 操作会清理超过保留期限的快照及其引用的孤立文件。

用 SQL 查看快照信息:

-- 查看表的所有快照
SELECT * FROM db.table.snapshots;

-- 查看当前快照的详细内容
SELECT snapshot_id, committed_at, operation, summary
FROM db.table.snapshots
WHERE snapshot_id = (SELECT current_snapshot_id FROM db.table.metadata);

输出示例:

snapshot_id committed_at operation summary
1234567890 2024-01-15 10:00:00 append {"added-files": "5", "added-records": "10000"}
1234567891 2024-01-15 11:30:00 overwrite {"deleted-files": "2", "added-files": "3"}
1234567892 2024-01-15 12:00:00 delete {"deleted-files": "1", "deleted-records": "2000"}

注意:快照过期操作是异步的,不会自动触发。你需要定期执行 ALTER TABLE db.table EXECUTE expire_snapshots(retention_threshold = 7) 来清理。否则,旧文件会一直占用存储空间。

4.3 时间旅行查询:回到过去看数据

时间旅行(Time Travel)是 Iceberg 最吸引人的特性之一。你可以查询表在过去某个时间点或某个快照下的数据状态。

为什么需要这个?我举几个真实场景:

  • 数据审计:财务对账时,需要看昨天下午 3 点的数据,而不是当前数据。
  • 错误回滚:发现今天凌晨的批处理写入了脏数据,直接查回昨天的快照。
  • 增量对比:比较两个快照之间的数据差异,找出哪些记录被修改了。

Iceberg 支持三种时间旅行方式:

方式 语法 适用场景
按快照 ID SELECT * FROM db.table FOR SYSTEM_VERSION AS OF 1234567890 你知道具体的快照 ID
按时间戳 SELECT * FROM db.table FOR SYSTEM_TIME AS OF '2024-01-15 10:00:00' 你知道大概的时间点
按分支/标签 SELECT * FROM db.table FOR SYSTEM_VERSION AS OF 'daily_audit' 你预先给重要快照打了标签

举个例子,假设今天凌晨的批处理写错了数据,我想看昨天下午 5 点的数据:

-- 方式一:按时间戳查询
SELECT order_id, amount, status
FROM orders FOR SYSTEM_TIME AS OF '2024-01-14 17:00:00'
WHERE status = 'completed';

-- 方式二:先找到那个时间点对应的快照 ID
SELECT snapshot_id, committed_at
FROM orders.snapshots
WHERE committed_at <= '2024-01-14 17:00:00'
ORDER BY committed_at DESC
LIMIT 1;

-- 然后用快照 ID 查询
SELECT order_id, amount, status
FROM orders FOR SYSTEM_VERSION AS OF 1234567890
WHERE status = 'completed';

你想想看,这在传统 Hive 里得怎么做?要么全量备份,要么拉分区历史。Iceberg 一行 SQL 就搞定了。

个人经验:我建议对关键业务表定期打标签。比如每天凌晨跑完批处理后,打一个 daily_YYYYMMDD 的标签。这样回滚时不用查快照 ID,直接 FOR SYSTEM_VERSION AS OF 'daily_20240114',清晰又方便。

打标签的 SQL:

-- 给当前快照打标签
ALTER TABLE orders CREATE TAG `daily_20240114`;

-- 查看所有标签
SELECT * FROM orders.refs;

-- 基于标签查询
SELECT * FROM orders FOR SYSTEM_VERSION AS OF `daily_20240114`;

4.4 核心流程:一张图看懂

下面这张图展示了 Iceberg 从写入到查询的完整流程,以及快照和时间旅行的关系。

Iceberg 快照与时间旅行核心流程 Iceberg 快照与时间旅行核心流程 写入操作 INSERT / DELETE / UPDATE 生成新快照 snapshot-1234567892 更新元数据指针 metadata.json 指向新快照 快照存储区(历史版本保留) 快照 1234567890 2024-01-15 10:00 快照 1234567891 2024-01-15 11:30 快照 1234567892 2024-01-15 12:00 (当前) 时间旅行查询 按快照 ID 查询 按时间戳查询 按标签/分支查询 底层数据文件(Parquet / ORC / Avro)

从这张图可以看得很清楚:每次写入操作生成一个新快照,旧快照不会被删除。时间旅行查询可以指向任意一个历史快照,然后 Iceberg 根据快照的 manifest 文件找到对应的数据文件。整个过程对用户透明,你只需要告诉 Iceberg「我要看哪个时间点的数据」。

4.5 避坑指南

最后分享几个我在项目中踩过的坑:

  • 快照过期别设太短:我曾经把保留天数设为 1 天,结果某次凌晨任务失败,想回滚到前一天的快照,发现已经被清理了。建议至少保留 7 天。
  • 时间旅行查询性能:查询历史快照时,如果那个快照对应的数据文件已经被 compaction 合并过,性能会好很多。否则,Iceberg 需要扫描大量小文件。
  • 标签和分支的区别:标签是只读的,分支是可写的。日常审计用标签就够了,别手滑创建了分支。
  • 元数据文件膨胀:频繁的小批量写入会产生大量 metadata 文件。我建议控制写入频率,或者定期执行 rewriteManifests 合并元数据。