跳转至

Apache Iceberg

Explanation · 原理资深

一句话定位

最完整、最"协议化"的开源湖表格式。定义了一套与引擎无关的表规范,让 Spark / Flink / Trino / DuckDB / StarRocks / 商业引擎都按同一本 spec 读写同一张表。它的核心不是"一个系统",而是"一本 spec"

TL;DR

  • 三层元数据:metadata.jsonManifest ListManifest → Data / Delete File
  • 写入不会修改历史:全 snapshot 化、Time Travel 免费
  • 提交靠 Catalog 的 CASS3 Conditional PUT(catalog 实现层选择,spec 不标准化提交协议)
  • 列 ID 机制让 schema 演化不破历史
  • Hidden Partitioning:表不暴露分区列,SQL 自动利用分区裁剪
  • Puffin 扩展索引:当前官方 blob 仅 theta sketch + deletion-vector(v3);向量索引 / bloom 是社区 proposal
  • REST Catalog:协议化的 catalog 接入,解耦引擎与元数据实现

1. 它解决什么 · 没有 Iceberg 的世界

在 Iceberg 出现前(Hive 时代),"一张表"约等于"一个目录 + HMS 里一条记录"。业界日常面对的是:

问题 典型代价
LIST 开销随分区数线性增长 10 万分区扫描要 30-90s
Schema 演化靠用户自觉(改列名破数据) 某公司 2019:改列名后历史数据全错位,3 天恢复
没有 Snapshot = 没有 Time Travel 回滚只能从冷备份捞,RTO 小时级
原子提交靠不住(HDFS rename / S3 fake rename) 并发写脏读成常态
HMS 是单点瓶颈 百万分区时 HMS 拖垮整个 ETL 栈
多引擎读写不一致 Spark 写 / Hive 读 = 半生不熟的一堆 bug

Netflix 主导的 Iceberg 就是对这些痛点的协议化重写。核心突破:

把"表是什么"从 Hive 的"目录 + HMS 两个不可靠的真相源"变成"一个 metadata.json 指针 + 元数据文件树的单一协议"

2. 架构深挖

flowchart TD
  catalog["Catalog<br/>(HMS / REST / Glue / Nessie)<br/>CAS 指针切换"] -->|"指向"| meta["metadata.json<br/>当前 Snapshot"]
  meta --> history["snapshot history<br/>(N-1, N-2, ...)"]
  meta --> list["Manifest List<br/>avro"]
  list --> m1["Manifest 1 (avro)<br/>+ 列统计"]
  list --> m2["Manifest 2"]
  m1 --> d1["Data File A<br/>Parquet/ORC/Lance"]
  m1 --> del["Delete File<br/>Position / Equality"]
  m2 --> d2["Data File B"]
  m2 --> d3["Data File C"]
  meta -.-> puffin["Puffin<br/>(扩展索引:统计直方图、Bloom、向量索引)"]

每一层的职责

文件 职责
Catalog 外部(HMS / REST / Glue / Nessie / Polaris) 维护"表 → 当前 metadata.json"的指针,唯一需要 CAS 的地方
Metadata v<N>.metadata.json 表的规格书:schema、partition spec、sort order、所有历史 snapshot
Manifest List snap-<id>-<hash>.avro 索引 manifest,一级目录
Manifest <hash>.avro 记录一批数据文件的路径、分区值、列级统计(min/max/null_count/nan_count/counts)
Data File Parquet / ORC / Lance 真实列式数据
Delete File Parquet MoR 模式的行级删除
Puffin 可选扩展文件 放 sketch / bloom / 未来的向量索引

提交流程(spec v2)

sequenceDiagram
  participant W as 写入方
  participant C as Catalog
  participant S as Object Store
  W->>S: 1. 写 Data File (新 Parquet)
  W->>S: 2. 写 Manifest (avro)
  W->>S: 3. 写 Manifest List (avro)
  W->>S: 4. 写 metadata.json (新 Snapshot)
  W->>C: 5. CAS: set table pointer to v<N+1>
  alt CAS 成功
    C-->>W: OK
  else CAS 失败 (并发冲突)
    C-->>W: Reject; W 重试 (或 abort)
  end

关键:写入所有新文件都是幂等追加,提交只在最后一步 CAS。失败可以直接重试、旧文件作为垃圾由 expire_snapshots 清理。

Snapshot Ancestry

每个 Snapshot 记录 parent_id。形成一棵祖先树

  • 线性主干:常规追加
  • 回滚:指针切回旧 Snapshot(树的旧分支)
  • 分支 / 标签(v2+):打 tag 或 branch,支持 Git-like 工作流

3. 关键机制

机制 1 · Schema Evolution(列 ID)

操作 做法 代价
加列 metadata 加 ID 0 数据重写
删列 metadata 标记 deleted 0
改名 metadata 改 name, ID 不变 0
扩展类型(int → long) metadata 改 type 0
缩窄类型 ❌ 不允许

反例:直接 ALTER TABLE ... RENAME COLUMN 在 Hive 时代意味着"老 Parquet 文件还是旧名字,查询对不上"。Iceberg 用列 ID 解决。

机制 2 · Partition Evolution

  • 旧数据按旧分区写;新数据按新分区写
  • 查询会分别按各自分区裁剪
  • 真正强的地方:不需要重写历史
-- 旧表按 days(ts) 分区,新策略改成 hours(ts)
ALTER TABLE sales REPLACE PARTITION FIELD days(ts) WITH hours(ts);

机制 3 · Row-Level Delete(v2 引入)

两种 Delete File:

类型 内容 场景
Position Delete (file_path, row_position) GDPR 删除、精确删几行
Equality Delete WHERE key = V 条件 CDC / upsert

读时合并(MoR):数据文件行 − Delete File 指向的行 = 当前有效行。

机制 4 · Hidden Partitioning

传统 Hive:PARTITIONED BY (dt),查询要写 WHERE dt = '2024-01-01' 才能利用。

Iceberg:PARTITIONED BY (days(ts))——表不暴露 dt 列,查询写 WHERE ts >= '2024-01-01' 引擎自动走分区。

好处: - SQL 对业务友好(没有人造列) - 演化分区策略不破 SQL 兼容

机制 5 · Puffin 索引

Puffin 是 Iceberg 独立的辅助索引文件格式(二进制)。官方 blob 类型目前只有两种(按 Puffin spec): - apache-datasketches-theta-v1 —— NDV 精确去重(Trino / Spark 已消费) - deletion-vector-v1 —— v3 引入的行级删除载体,取代 v2 的 position-delete file

Bloom Filter / HNSW / 位图索引等是社区 proposal,尚未被 Puffin spec 接纳。"湖上跑向量"的完整落地仍需等 blob type 标准化或走 Lance 那条路径。详见 Puffin

机制 6 · v3 spec · 2025-06 ratified · 引擎侧 rolling out

v3 是 Iceberg 近几年最大一波能力补强。spec 本身于 2025-06 正式 ratified(Apache Iceberg project vote 通过);引擎侧按"草案先行 · spec 定稿后稳定"的节奏 rolling out:

  • Iceberg 1.8.0(2025-02)预览 Binary Deletion Vectors + Variant(基于 v3 草案 · spec 未最终定稿)
  • 2025-06 · v3 spec 正式 ratified(关键分水岭)
  • 1.9.0(2025-04)扩展 Row Lineage + Default Values + Multi-arg Transforms(过渡期)
  • 1.10.0(2025-09)v3 定稿后首个稳定版本 · DV "ready for prime time"
  • Spark 4.0+ 是目前 V3 支持最完整的引擎;Trino / Flink / DuckDB 仍在追赶

timeline 理解

1.8.0 的 "引入" 是基于 v3 草案 · 定稿前可能有 spec 变动。生产环境优先用 1.10.0+(v3 定稿后的版本)· 避免踩 spec 变更坑。

新能力 说明
Binary Deletion Vectors 以 Puffin blob 形式取代 v2 的 position-delete file,读更快
Row Lineage Manifest 引入 first-row-id · last-updated-sequence-number,行级血缘可追溯
Variant 类型 半结构化 JSON 的高性能存储(借鉴 Spark Variant)
Geometry / Geography GeoParquet 对齐,地理空间类型原生支持
Nanosecond Timestamp(tz) 纳秒精度时间戳
Default Values 加列时指定默认值,旧行不读 null
Multi-arg Partition Transforms 组合转换(如 bucket(N, truncate(col))
Encryption Keys 表级密钥元数据,为端到端加密铺路

选型指引:新项目可直接上 v3(Spark 4.0+ 栈尤其推荐 · Iceberg 1.10.0+ 是 spec 定稿后的首个稳定版本);混合引擎环境先确认消费侧 reader 都已升级——老 reader 读 v3 表会失败。v2 / v3 长期共存(2025-2027)是现实 · 不是短期过渡。v3 五大能力深度(Deletion Vectors · Row Lineage · Multi-Table Transaction · Geo · Variant)· 兼容矩阵 · 采用决策Iceberg v3 · spec 演进与采用

机制 7 · View Spec · 跨引擎共享视图(2023 spec v1)

Iceberg View Spec 1.0(2023 正式发布、2024 广泛实现)把 View 从"每个引擎自家概念"变成跨引擎共享的对象:

{
  "view-uuid": "...",
  "format-version": 1,
  "current-version-id": 2,
  "versions": [
    {
      "version-id": 2,
      "representations": [
        { "type": "sql", "sql": "SELECT ...", "dialect": "spark" },
        { "type": "sql", "sql": "SELECT ...", "dialect": "trino" }
      ],
      "schema-id": 0,
      "default-namespace": ["db"]
    }
  ],
  "schemas": [...]
}

关键特性

  • 多方言表示:同一 View 可存多套 SQL(Spark / Trino / Flink 方言),读者选自己会的
  • 版本化:View 也有 snapshot history,可回滚/审计 View 定义变更
  • Schema 独立演化:View 的 schema 和底层表独立版本化
  • REST Catalog 协议原生支持GET /v1/views/...

2026 状态:Spark 3.5+ / Trino / Snowflake / Dremio 都已读写;它把"业务语义层"变成了和表同级别的一等元数据对象——这是 BI 语义层(dbt、Cube、Malloy)上湖的关键协议支撑。

机制 8 · Materialized View · 主要是 connector 层能力(非 spec 标准化)

重要区分

  • Iceberg spec 本身 · 截至 2026-Q2 没有 MV spec(社区有讨论但未形成标准)
  • Trino Iceberg connector 有较成熟的 MV 管理能力(397+ 逐步完善、479 加 GRACE PERIOD)——但这是引擎层功能
  • 跨引擎一致性:Trino 建的 MV 被 Spark / Flink 读取时"就是一张 Iceberg 表",但 freshness 判定与刷新语义不自动共享

核心思路(按 Trino 实现):

  • MV = 一个 view 定义(Iceberg View Spec) + 一张 storage table(普通 Iceberg 表)+ freshness 元数据
  • REFRESH MATERIALIZED VIEW 触发时对比源表 snapshot id,增量或全量重算
  • 查询 MV 时 connector 判 stale,超过 GRACE PERIOD 可 fallback 到源表执行

相比 Paimon / Delta: - Paimon aggregation merge engine 做"类 MV",天然基于流式 CDC(更实时) - Delta Materialized View 是 Databricks Runtime 能力(未进开源 protocol) - Iceberg MV 的跨引擎标准化仍是未完成事项

对 AI 场景启发:MV 是离线 ETL → 在线特征仓这条路径的标准载体。Embedding 表 / 特征表做成 MV,订阅源表 snapshot,就是湖上 Feature Store 的雏形。

机制 9 · REST Catalog Scan Planning(spec 1.5+)

传统 Iceberg 读:引擎自己读 metadata.json → manifest list → manifest → 生成 split 计划。客户端要下载大量 Avro

Scan Planning endpoint(REST Catalog spec 1.5+ 引入):

POST /v1/{prefix}/namespaces/{ns}/tables/{table}/plan
  body: { snapshot-id, filter, case-sensitive }
  response: { plan-tasks: [...] }  // 服务端已完成剪枝和 split

意义

  • Client-server 架构:metadata 可以不给客户端看(只给 split 计划)
  • 服务端统一剪枝:服务端可以用 Bloom / Index 等客户端没有的信息做更精准剪枝
  • 协议化多租户:SaaS 形态湖仓(Snowflake Iceberg / Databricks Managed)可以只暴露 REST 接口
  • 混合计划:服务端给小表直接返回 inline data,大表返回 split

这是 Iceberg 从"纯存储格式"演进到"存算分离协议"的关键一步。

机制 10 · Managed Iceberg Tables · Catalog-as-a-Service

2024-2025 多家云厂把 Iceberg 包成托管产品

产品 方式 2026 状态
Snowflake Managed Iceberg Tables Snowflake 既是 Catalog 又是主写 GA
AWS S3 Tables (2024-12) S3 原生 Iceberg 托管(内置维护) GA
Google BigQuery Iceberg BigQuery 作为读者 + 写者 GA
Databricks Managed Iceberg (Uniform + Unity) Delta 主写 + Iceberg 读 GA
Apache Polaris (原 Snowflake Polaris) 中立开源 REST Catalog 2024-07 开源,孵化中

关键观察Iceberg 从"表格式"进化为"云厂商的统一 Catalog 协议"。选 Iceberg 越来越像选 "HTTP"——它不是最好,而是所有人都支持。Catalog 端细节见 catalog/Apache Polaris · catalog/Unity Catalog · catalog/Iceberg REST Catalog

机制 11 · Encryption · 表级加密(v3)

v3 spec 为端到端加密铺路:

  • Envelope encryption:表级 DEK(data encryption key)+ 外部 KMS 管理 KEK(key encryption key)
  • Parquet Modular Encryption 对齐:列级加密 + per-file DEK
  • Manifest 里记录 per-file 的加密 metadata
  • RBAC 分层:部分列只给部分 role(和 Column Masking 衔接)

2026 状态:spec 定义好,主流引擎正在实现;金融/合规场景值得关注。

关键配置

参数 默认 建议 说明
write.target-file-size-bytes 512MB 256-512MB 单数据文件目标大小
write.delete.mode copy-on-write 流场景 merge-on-read MoR 写快读合并
write.parquet.compression-codec gzip zstd 压缩率好 + 快
format-version 2 2 v1 别用了
history.expire.min-snapshots-to-keep 1 100+ 保留历史数
history.expire.max-snapshot-age-ms 5 天 7-30 天 过期时间

运维命令

-- 合并小文件(日常必跑)
CALL system.rewrite_data_files('db.sales');

-- 清理过期快照(metadata 膨胀时跑)
CALL system.expire_snapshots('db.sales', TIMESTAMP '2024-01-01');

-- 合并 manifest(manifest 过多时跑)
CALL system.rewrite_manifests('db.sales');

-- 清理孤儿文件(写失败产生的垃圾)
CALL system.remove_orphan_files('db.sales');

Catalog 选型

Catalog CAS 可靠性 生态 推荐场景
REST Catalog 强(协议标准) 2024+ 成为事实标准 新建项目首选
Nessie 强 + Git-like 分支 开源,支持多引擎 要做数据分支实验时
HMS 兼容老系统 已有 Hive 栈迁移
AWS Glue AWS 绑定 纯 AWS 栈
Hadoop / File-based 弱(S3 Conditional PUT 可以救) 测试 / POC
Unity / Polaris 商业 / 新晋 特定生态

详见 Catalog 全景对比

4. 性能数字

场景 规模 基线
Planning(百万文件) 10M data files Iceberg ~数百 ms vs Hive LIST 数分钟
提交延迟 单次 CAS 50-500ms
写吞吐(Spark 批) 集群 GB/s 级
扫描吞吐(Parquet) 单 executor 100+ MB/s
metadata.json 大小 10k snapshots 几 MB(需 expire)
单表规模 PB 级 Netflix 某表 > 10 PB

Netflix 数据点(引自 Netflix Tech Blog · 2022 及 Iceberg Summit 2022 演讲,2024+ 实际规模未公开更新): - 全司 Iceberg 表 > 10 万张 - 最大表 PB 级、数十亿 data files - Planning 从 Hive 的分钟级降到 Iceberg 的秒级

5. 代码示例

建表 + 写入(PySpark + Iceberg)

spark.sql("""
CREATE TABLE iceberg.db.events (
  event_id BIGINT,
  user_id  BIGINT,
  event    STRING,
  ts       TIMESTAMP
) USING iceberg
PARTITIONED BY (days(ts))
TBLPROPERTIES (
  'write.target-file-size-bytes' = '268435456',
  'format-version' = '2'
)
""")

df.writeTo("iceberg.db.events").append()

REST Catalog 连接(Python pyiceberg)

from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog(
    "rest_catalog",
    uri="https://catalog.mycorp.internal",
    warehouse="s3://data-lake/warehouse",
    credential="client-id:client-secret",
)

table = catalog.load_table("db.events")
df = table.scan(row_filter="ts >= '2024-01-01'").to_pandas()

时间旅行 + 回滚

-- 按 snapshot_id
SELECT * FROM events VERSION AS OF 1234567890;

-- 按时间
SELECT * FROM events TIMESTAMP AS OF '2024-12-01 00:00:00';

-- 回滚(危险!)
CALL system.rollback_to_snapshot('db.events', 1234567890);

Branch / Tag(v2+ 数据 Git-like)

-- 打标签
ALTER TABLE events CREATE TAG `release-2024-12-01` AS OF VERSION 1234;

-- 开发分支
ALTER TABLE events CREATE BRANCH `feature-xyz` AS OF VERSION 1234;

-- 分支上改完合并
CALL system.fast_forward('db.events', 'main', 'feature-xyz');

Schema / Partition 演化命令

-- Schema 演化
ALTER TABLE db.events ADD COLUMN device STRING;
ALTER TABLE db.events RENAME COLUMN payload TO raw_payload;
ALTER TABLE db.events ALTER COLUMN amount TYPE DECIMAL(20,2);
ALTER TABLE db.events DROP COLUMN device;

-- Partition 演化
ALTER TABLE db.events ADD PARTITION FIELD region;
ALTER TABLE db.events REPLACE PARTITION FIELD days(ts) WITH hours(ts);
ALTER TABLE db.events DROP PARTITION FIELD region;

-- 增量读取(两 snapshot 之间)
SELECT * FROM db.events.incremental_changes(
  start_snapshot => 100,
  end_snapshot   => 200
);

6. 维护与运维命令

维护 procedures · 定期跑

-- 合并小文件(必做 · 按表大小定频率)
CALL system.rewrite_data_files(
  table => 'db.events',
  options => map('target-file-size-bytes', '536870912')
);

-- 合并 position deletes(MoR 表)
CALL system.rewrite_position_deletes('db.events');

-- 过期 snapshot · 释放空间
CALL system.expire_snapshots(
  'db.events',
  TIMESTAMP '2026-03-01'
);

-- 清理孤儿文件(!! 谨慎 · 先 --dry-run)
CALL system.remove_orphan_files(
  table => 'db.events',
  older_than => TIMESTAMP '2026-03-01'
);

元数据表 · 观测 / 调试

SELECT * FROM db.events.snapshots;     -- 所有 snapshot
SELECT * FROM db.events.manifests;     -- manifest 列表
SELECT * FROM db.events.files;         -- 每个 data file
SELECT * FROM db.events.history;       -- 变更历史
SELECT * FROM db.events.partitions;    -- 当前分区
SELECT * FROM db.events.refs;          -- 分支 / 标签引用
SELECT * FROM db.events.delete_files;  -- delete files (MoR)

性能诊断速查

症状 先看 常见原因
查询慢 files · manifests 数量 小文件没合
写入冲突 snapshots 顺序 并发 commit
空间膨胀 snapshots count 过期未清理
Time travel 失败 snapshots 保留 expire_snapshots 过狠
列出分区慢 分区粒度 粒度过细或无
MoR 查询慢 delete_files 数量 delete 积压未合

TBLPROPERTIES 参考

# 写入
write.format.default                = parquet
write.target-file-size-bytes        = 536870912    (512 MB)
write.parquet.compression-codec     = zstd | snappy
write.parquet.compression-level     = 3
write.parquet.row-group-size-bytes  = 134217728    (128 MB)
write.parquet.page-size-bytes       = 1048576      (1 MB)
write.distribution-mode             = hash | range | none

# 历史 / 维护
history.expire.max-snapshot-age-ms  = 2592000000   (30 d)
history.expire.min-snapshots-to-keep = 20
commit.manifest-merge.enabled       = true
commit.retry.num-retries            = 4
commit.retry.min-wait-ms            = 100

# Delete mode
write.delete.mode                   = copy-on-write | merge-on-read
write.update.mode                   = copy-on-write | merge-on-read
write.merge.mode                    = copy-on-write | merge-on-read

# v3 特性(需引擎支持 · property 详见 iceberg-v3.md)
format-version                      = 2 | 3

v3 新配置(Binary DV / Row Lineage / Variant / Geo 等)具体 property 名和配置方式 · 见 Iceberg v3 · spec 演进与采用

7. 陷阱与反模式

  • 写入不配 compaction → 小文件炸 → 查询崩 → 必须定时 rewrite_data_files
  • expire_snapshots 从不跑 → metadata.json 膨胀到几 MB → 每次查询加载慢
  • MoR 模式但不 compact delete files → Delete File 堆积 → 查询合并慢
  • Catalog 混用(部分 Spark 用 HMS、部分 Trino 用 Glue)→ 脏提交 / 找不到表
  • 高基数分区PARTITIONED BY (user_id))→ 百万分区 → 即使 Iceberg 也炸
  • 版本 v1 依然在用 → 没 MoR、没 row-level delete → 升 v2
  • 直接手改 Parquet 文件 → metadata 不一致 → 查询结果错
  • 多 writer 无协调(两个 Spark 都写 branch=main)→ 频繁 CAS 冲突 → 应该用分区隔离Paimon changelog

8. 横向对比 · 延伸阅读

权威阅读

一手规范

原始文献与演讲

工程博客 / 长期更新

相关