2. 数据湖与Iceberg基础
要理解 Iceberg 为什么出现,先要理解数据湖本身经历了什么。早期数据湖的核心思路是“先存后算”:把原始日志、CDC 数据、埋点文件直接写入 HDFS 或对象存储,格式多为 CSV、JSON,查询时再解析。这种方式写入极快,但读取代价高,且没有事务、没有 schema 约束,很快演变成难以治理的“数据沼泽”。
第二阶段是“Hive 表格式 + Parquet/ORC”的普及。列式存储带来了压缩率和扫描效率的提升,分区目录(如 dt=2024-01-01/hour=08)让查询可以裁剪数据。但 Hive 的元数据依赖 Metastore 中的分区列表,分区数量一多,Metastore 压力剧增;更关键的是,它缺乏原子性——一次写入任务失败,可能留下半成品文件,读者会读到脏数据。
第三阶段就是开放表格式(Open Table Format)的兴起,代表是 Iceberg、Delta Lake 和 Hudi。它们把“表”的定义从目录约定升级为一份可版本化的元数据,让数据湖具备数据库级别的事务能力。
Iceberg 的核心特性
Iceberg 最本质的设计,是把表的状态记录在元数据文件树中,而不是依赖目录扫描。它提供以下关键能力:
- ACID 事务:通过原子性地切换元数据指针,实现快照隔离,读者永远看到一致的表状态。
- 隐藏分区:分区不再暴露为目录列,查询时无需写
WHERE dt=...,Iceberg 自动做分区裁剪。 - 分区演进:可以修改分区策略而不重写全表历史数据,旧数据仍按旧分区读取。
- Schema 演进:支持安全地增加、删除、重命名、调整列类型,且不依赖物理文件重写。
- 时间旅行:通过 snapshot ID 或时间戳查询历史版本,便于审计与回滚。
元数据架构:从 Catalog 到数据文件
下面这张图展示了 Iceberg 表的元数据层级与数据流关系。每一层都承担明确的职责,且都支持版本化。
写入时,Iceberg 先生成新的数据文件,再写 manifest,再写 manifest list,最后生成新的 metadata 文件,并由 catalog 原子地替换指针。读取时则反向遍历:从 catalog 拿到 metadata,找到目标 snapshot,读取 manifest list,过滤 manifest,最终定位数据文件。整个过程没有目录扫描,也没有 Metastore 分区列表瓶颈。
表格式对比与适用场景
Iceberg、Delta Lake、Hudi 是当前主流的三种开放表格式,它们的目标相似,但设计取舍不同。
| 维度 | Iceberg | Delta Lake | Hudi |
|---|---|---|---|
| 元数据组织 | 元数据树 + manifest | 事务日志 _delta_log | 时间线 + 文件组 |
| 分区演进 | 原生支持,无需重写 | 支持有限 | 支持,配置较复杂 |
| 隐藏分区 | 支持 | 不支持 | 不支持 |
| 引擎生态 | Spark/Flink/Trino 等广泛 | Spark 生态最强 | Spark/Flink 流式场景强 |
| 典型场景 | 多引擎湖仓、分区频繁调整 | Databricks 体系、批流一体 | 近实时入湖、CDC 同步 |
常见误区与复盘
初学者容易把 Iceberg 当成“更快的 Hive”。实际上,Iceberg 解决的是一致性与可演进性,而不是单纯提速。以下误区值得警惕:
- 以为小文件问题会自动消失:Iceberg 提供 rewrite 动作,但需要定期执行 compaction,否则 manifest 和数据文件都会膨胀。
- 忽略 catalog 的选择:Hive Metastore、REST Catalog、JDBC Catalog 在并发和权限模型上差异很大,生产环境要提前评估。
- 频繁提交小事务:每次提交都会生成新 metadata 和 snapshot,过高的提交频率会拖慢查询规划。
- 把分区演进当成免费操作:旧数据仍按旧分区布局存储,查询跨新旧分区时规划成本会上升,需要合理设计演进节奏。
history.expire.max-snapshot-age-ms 等参数,元数据会无限增长,最终拖垮 catalog 和查询性能。
理解 Iceberg 的元数据架构,是后续学习 Schema 演进与 Partition 演进的前提。只有先看清“表状态如何被版本化记录”,才能在修改 schema 或分区策略时,准确判断哪些操作是元数据级变更、哪些会触发数据重写。