表格式的进化:Iceberg 表格式核心概念与元数据三层架构

聊到 Iceberg,很多人第一反应是「又一种表格式」。其实不然。它重新定义了「什么是表」。我刚开始接触时也觉得,不就是个元数据管理嘛,能有多复杂?直到有一次,我在生产环境里亲眼看到 Hive 表因为一个小文件问题导致整个查询 hang 住,才意识到——表格式的进化,本质上是对数据管理权的重新分配。

今天咱们就掰开揉碎,看看 Iceberg 的元数据三层架构到底长什么样。

从 Hive 到 Iceberg:表格式的痛点

先说说 Hive 表格式的问题。你想想看,Hive 的元数据存在 Metastore 里,数据文件散落在 HDFS 上。查询的时候,Metastore 告诉你「这个分区下有 1000 个文件」,然后引擎就得挨个去读。如果文件数暴涨到 10 万呢?

我遇到过最夸张的情况:一个表的分区下有 50 万个小文件,查询光列举文件就花了 3 分钟。这就是典型的「元数据与数据脱节」——Metastore 只知道文件路径,不知道文件内容、列统计、数据分布。

Iceberg 的解决思路很直接:把元数据也做成文件,并且分层管理。这样,引擎不需要跟文件系统频繁交互,读一次元数据文件就能拿到全部信息。

Iceberg 元数据三层架构

Iceberg 的元数据分为三层,从顶到下依次是:

  • Catalog —— 指向当前最新的元数据文件
  • Metadata File —— 记录表的全局信息,包括 Schema、分区、Snapshot 列表
  • Manifest List + Manifest + Data Files —— 具体的数据文件索引

说白了,Catalog 就是「入口」,Metadata File 是「目录」,Manifest 是「索引卡片」。咱们一层层往下看。

第一层:Catalog 与 Metadata File

Catalog 的作用很简单:告诉你「当前表的最新状态在哪」。它可以是 Hive Metastore、Hadoop 文件、JDBC 数据库,甚至是一个内存 Map。我个人习惯在生产环境用 Hive Metastore 做 Catalog,因为它跟现有生态兼容最好。

Metadata File 是 JSON 格式,里面记录了:

  • 表的 Schema(列名、类型)
  • 分区定义
  • Snapshot 列表(每个 Snapshot 的 ID、时间戳、Manifest List 路径)
  • 当前 Snapshot ID

每次提交(commit)都会生成一个新的 Metadata File。Iceberg 用原子性替换来保证一致性——要么全部成功,要么全部回滚。

核心要点:Metadata File 是表的「快照指针」。它不存数据,只存「去哪找数据」的信息。

第二层:Snapshot 与 Manifest List

Snapshot 是 Iceberg 最巧妙的设计之一。它代表表在某个时间点的完整状态。每次写入、删除、合并操作,都会生成一个新的 Snapshot。

Manifest List 是 Snapshot 的「目录」。它列出了该 Snapshot 包含的所有 Manifest 文件,以及每个 Manifest 的元数据:

  • Manifest 文件路径
  • 分区范围(比如 dt=2024-01-01 到 dt=2024-01-31)
  • 文件数量、行数、大小
  • 列统计信息(最小值、最大值、空值数)

为什么要多这一层?直接让 Snapshot 指向 Manifest 不行吗?

嗯,这里要注意。如果没有 Manifest List,查询引擎就得读所有 Manifest 才能知道哪些分区有数据。有了 Manifest List,引擎可以先看分区范围,跳过不相关的 Manifest。这在分区数很多时,性能提升非常明显。

我曾经优化过一个查询,原来要读 200 个 Manifest,加上 Manifest List 后只读了 12 个——查询时间从 40 秒降到了 3 秒。

第三层:Manifest 与 Data Files

Manifest 是 Iceberg 元数据的最小单元。每个 Manifest 文件对应一组数据文件,记录了:

  • 数据文件路径
  • 文件格式(Parquet、ORC、Avro)
  • 分区值
  • 列统计信息(每列的最小值、最大值、空值数)
  • 文件大小、行数
  • 文件内容类型(DATA 或 POSITION_DELETES)

Manifest 本身也是不可变的。每次写入新数据,会生成新的 Manifest,旧的 Manifest 保留在历史 Snapshot 中。

小技巧:Manifest 文件通常很小(几十 KB 到几 MB),因为只存元数据。但如果你有上万个数据文件,Manifest 也会膨胀。这时候可以用 Iceberg 的 rewriteManifests 操作来合并。

三层架构如何协同工作?

咱们用一个查询例子串起来:

  1. 引擎通过 Catalog 找到当前 Metadata File 路径
  2. 读取 Metadata File,拿到当前 Snapshot ID 和对应的 Manifest List 路径
  3. 读取 Manifest List,根据查询条件(比如 dt > '2024-06-01')过滤出相关的 Manifest
  4. 读取这些 Manifest,拿到数据文件路径和列统计信息
  5. 根据列统计信息进一步过滤(比如 age > 30),只读取匹配的数据文件

整个过程,引擎只需要读几个小文件,就能精确知道要读哪些数据文件。这就是 Iceberg 能处理百万级文件的原因。

元数据三层架构示意图

下面这张图展示了三层架构的完整链路:

Iceberg 元数据三层架构 Catalog Metadata File (v2.json) Snapshot (s1) Manifest List Manifest (分区A) Manifest (分区B) Manifest (分区C) data1.parquet data2.parquet data3.parquet 查询流程 Catalog → Metadata → Snapshot → Manifest List → Manifest → Data

Snapshot 的隔离与时间旅行

每个 Snapshot 都是独立的。这意味着:

  • 读隔离:查询 A 读 Snapshot s1,查询 B 写 Snapshot s2,互不影响
  • 时间旅行:可以指定读取某个历史 Snapshot,比如 SELECT * FROM table FOR SYSTEM_VERSION AS OF 123456
  • 增量读取:从 Snapshot s1 到 s2,只读取新增的文件

我有个朋友做实时数仓,每天凌晨用时间旅行对比两个 Snapshot 的数据差异,做增量 ETL。代码写起来特别清爽,不用自己维护偏移量。

Manifest 的两种类型

Manifest 根据内容分为两类:

类型 内容 用途
DATA 普通数据文件 存储实际行数据
POSITION_DELETES 删除标记文件 记录被删除的行位置(用于 Merge-on-Read)

POSITION_DELETES 是 Iceberg 实现行级删除的关键。传统 Hive 要删除数据只能重写整个分区,Iceberg 只需要写一个小文件记录「哪些行被删了」。查询时,引擎读数据文件 + 删除文件,过滤掉已删除的行。

注意:POSITION_DELETES 文件会随着删除操作增多而膨胀。建议定期执行 rewriteDataFiles 合并数据文件并清理删除标记,否则查询性能会下降。

元数据三层架构的优势

总结一下,这种设计带来了几个实实在在的好处:

  • 轻量级元数据操作:读元数据只涉及几个小文件,不依赖 Metastore 的性能
  • 精确的谓词下推:Manifest List 和 Manifest 都存了列统计信息,引擎可以提前过滤
  • 快照隔离:读写不冲突,支持并发写入
  • 文件级管理:可以单独优化某个 Manifest 或数据文件,不影响全局

说白了,Iceberg 把「元数据」从 Metastore 的数据库表里解放出来,变成了文件系统上的普通文件。这样,元数据的规模不再受限于数据库,而是受限于文件系统的容量——这差距可不是一星半点。

嗯,三层架构的核心逻辑就这些。下一层咱们会深入 Metadata File 的 JSON 结构,看看里面到底存了什么,以及如何手动解析它来排查问题。