Apache Iceberg 是当下最热门的开源数据湖表格式,它把 ACID 事务、Schema 演进和隐藏分区带进了对象存储上的 Parquet 文件。如果说 ClickHouse 解决了实时 OLAP,PostgreSQL 解决了事务型存储,那么 Iceberg 解决的是海量数据湖上的可靠表管理——让数据工程师可以像操作数据库表一样,安全地对 PB 级数据做增删改查与回溯。
一、为什么数据湖需要 Iceberg
传统基于 Hive 的数据湖表格式(Hive Metastore + 目录式分区)存在几个根深蒂固的痛点:写入期间 reader 会读到半成品数据(无原子性)、大规模 INSERT OVERWRITE 失败只能留下脏数据、Schema 变更要重写历史分区、新增分区字段意味着所有旧数据都要回填。当数据规模到 TB/PB 级、且需要流式实时摄入时,这些缺陷会直接拖垮整个分析链路。
Iceberg 的核心思路是把”表”的元数据独立出来,用一份不可变的元数据树(metadata file → manifest list → manifest → data files)来描述表的状态。每次写入都生成一个新的快照(Snapshot),通过原子性的元数据指针切换来实现 ACID,reader 永远读到一致的数据视图。这使得它既能跑在对象存储(S3 / MinIO / OSS)上,又能被 Trino、Spark、Flink、Doris 等多种引擎同时读写。
二、Iceberg 核心概念
2.1 元数据树与快照
一张 Iceberg 表的逻辑结构自顶向下为:Metadata File(记录当前快照、Schema、分区规范、快照历史)→ Manifest List(一次快照包含哪些 manifest)→ Manifest(记录具体 data files 及其统计信息,如行数、分区范围、列的最大最小值)→ Data Files(真实的 Parquet/ORC/Avro 文件)。查询引擎读取时先解析 metadata,再利用 manifest 里的统计信息做文件级裁剪(data skipping),跳过无关文件,这正是它查询快的关键。
2.2 隐藏分区与 Schema 演进
Iceberg 支持隐藏分区(Hidden Partitioning):分区值由字段经 transform(如 day(ts)、bucket(16, id)、truncate(20, name))推导,用户查询时只需按原始列过滤,引擎自动映射到分区,无需在 SQL 里重复写分区条件。配合 Partition Evolution,你可以随时调整分区策略而不必重写历史数据;Schema Evolution 则允许加列、重命名、类型提升(int→long)而不破坏旧数据。
三、快速体验:Docker 一键起 Iceberg + Trino
用 Docker Compose 起一套最小可用的 Iceberg 环境:MinIO 作对象存储、iceberg-rest 作统一元数据服务、Trino 作查询引擎。这也是我们在Docker Compose 一键编排里反复强调的”组合小服务”思路。
version: "3.8"
services:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: password
ports: ["9000:9000", "9001:9001"]
iceberg-rest:
image: apache/iceberg-rest-fixture:latest
environment:
AWS_ACCESS_KEY_ID: admin
AWS_SECRET_ACCESS_KEY: password
CATALOG_WAREHOUSE: s3://warehouse/
CATALOG_IO__IMPL: org.apache.iceberg.aws.s3.S3FileIO
CATALOG_S3_ENDPOINT: http://minio:9000
ports: ["8181:8181"]
depends_on: [minio]
trino:
image: trinodb/trino:latest
ports: ["8080:8080"]
depends_on: [iceberg-rest]
启动后,在 MinIO 中创建名为 warehouse 的 bucket(Trino 的 iceberg 连接器默认 catalog 已指向 iceberg-rest)。
四、用 Trino 建表、写入与查询
进入 Trino 命令行 docker exec -it trino trino,创建库与表并写入示例数据:
CREATE SCHEMA iceberg.demo;
CREATE TABLE iceberg.demo.orders (
id BIGINT,
user_id BIGINT,
amount DOUBLE,
status VARCHAR,
created_at TIMESTAMP
) WITH (
partitioning = ARRAY['day(created_at)', 'bucket(16, user_id)']
);
INSERT INTO iceberg.demo.orders
VALUES (1, 1001, 199.0, 'paid', TIMESTAMP '2026-09-30 10:00:00'),
(2, 1002, 59.5, 'paid', TIMESTAMP '2026-10-01 11:30:00');
SELECT user_id, sum(amount) AS total
FROM iceberg.demo.orders
GROUP BY user_id;
注意分区字段用的是 day(created_at) 与 bucket(16, user_id)——查询时你只需写 WHERE created_at = ...,Iceberg 会自动做分区裁剪,这正是隐藏分区的价值。
五、Schema 演进与隐藏分区实战
业务发展后需要给订单增加”优惠金额”字段,直接加列即可,旧数据新列自动补 NULL:
ALTER TABLE iceberg.demo.orders
ADD COLUMN discount DOUBLE;
UPDATE iceberg.demo.orders
SET discount = 10.0 WHERE id = 1;
-- 隐藏分区下查询不用关心分区字段
SELECT * FROM iceberg.demo.orders
WHERE created_at >= TIMESTAMP '2026-09-30 00:00:00';
若初期按天分区后来发现粒度太粗,可直接演进分区策略,新数据走新分区、旧数据保持原样,查询视图保持一致——这是 Hive 表难以做到的。
六、时间旅行与快照管理
Iceberg 每次写入都产生快照,天然支持时间旅行与回滚,非常适合审计与故障恢复:
-- 查看历史快照
SELECT snapshot_id, committed_at
FROM iceberg.demo."orders$snapshots";
-- 读取某个历史版本的数据
SELECT * FROM iceberg.demo.orders
FOR VERSION AS OF 8123456789012345678;
-- 回滚到指定快照
CALL iceberg.system.rollback_to_snapshot('demo', 'orders', 8123456789012345678);
-- 清理过期快照,控制存储与元数据膨胀
CALL iceberg.system.expire_snapshots('demo', 'orders', TIMESTAMP '2026-09-01 00:00:00');
七、Iceberg 与数据仓库 / 其他表格式怎么选
Iceberg 不是要取代 OLTP 数据库或实时 OLAP 引擎,而是补上”开放数据湖上的可靠表”这一层。选型时可参考下表:
| 方案 | 定位 | 典型场景 | 与 Iceberg 关系 |
|---|---|---|---|
| Apache Iceberg | 开放表格式 | 数据湖上的 ACID 批流表 | 本文主角 |
| Apache Hudi | 开放表格式 | 近实时 upsert / 增量管道 | 同类竞品 |
| Delta Lake | 开放表格式 | Spark 生态湖仓 | 同类竞品 |
| ClickHouse | 列式 OLAP 引擎 | 亚秒级实时分析 | 可读取 Iceberg 做加速 |
| PostgreSQL | OLTP 关系库 | 事务业务存储 | 业务系统,不直接存湖 |
| TiDB | 分布式 HTAP | 海量事务 + 实时分析 | 可与 Iceberg 做冷热分层 |
实践中常见架构是:业务库(如 PostgreSQL、TiDB)通过 CDC 把变更同步进 Iceberg 数据湖,再用 ClickHouse 等引擎做极速 OLAP 查询,形成”事务库 + 数据湖 + OLAP”的分层体系。
八、生产落地检查清单
- 对象存储:优先 MinIO / OSS / S3,开启版本控制便于兜底,但不要依赖版本控制代替快照管理。
- 小文件合并:流式写入易产生大量小文件,定期
CALL iceberg.system.rewrite_data_files与rewrite_manifests合并。 - Compaction:对频繁 upsert 的表做行级 compaction,减少读放大。
- 流式摄入:用 Flink / Spark Structured Streaming 接入 Kafka,落到 Iceberg;可参考我们之前的 Kafka 消息积压排查经验保证管道稳定。
- 权限与审计:结合 Ranger / 云厂商 IAM 控制表级权限,时间旅行天然满足审计需求。
- CI 校验:表结构变更纳入 GitHub Actions 流水线做 schema 评审,避免误删列。
十、流批一体:用 Flink 与 Spark 摄入 Iceberg
10.1 用 Flink 做实时摄入
Iceberg 与 Flink 结合,可实现”流读 Kafka → 写 Iceberg”的准实时管道。Flink 通过 Iceberg connector 支持 streaming 写入与分区演进,配合 checkpoint 保证精确一次(exactly-once)语义。下面是一段 Flink SQL 把 Kafka 订单流写入 Iceberg 的示例(管道稳定性可参考我们之前的 Kafka 消息积压排查 经验):
CREATE TABLE sink_orders (
id BIGINT,
user_id BIGINT,
amount DOUBLE,
created_at TIMESTAMP(3)
) WITH (
'connector' = 'iceberg',
'catalog-name' = 'iceberg_catalog',
'table' = 'demo.orders',
'write.upsert.enabled' = 'true'
);
INSERT INTO sink_orders
SELECT id, user_id, amount, created_at
FROM kafka_source;
10.2 用 Spark 做批量回填与 MERGE
历史数据迁移通常用 Spark 批量写。Spark 对 Iceberg 的支持最成熟,既支持 INSERT OVERWRITE 按分区重写,也支持 MERGE INTO 做增量更新与去重,非常适合把 legacy 表平滑迁移进数据湖:
-- 按分区批量回填
INSERT OVERWRITE iceberg.demo.orders
PARTITIONED BY (day(created_at))
SELECT * FROM legacy_orders;
-- 增量合并:存在则更新,不存在则插入
MERGE INTO iceberg.demo.orders t
USING staging s ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
这两段示例展示了 Iceberg 作为”流批统一存储层”的定位——同一张表既能承接 Flink 的实时写入,也能接受 Spark 的批量回填与 MERGE,查询侧再用 Trino 统一分析,真正打通了 Lambda 架构里的批与流。
九、结语
Apache Iceberg 把”数据湖”从一堆不可靠的文件,变成了可以放心做事务、演进和回溯的”开放表”。对于需要管理 PB 级历史数据、又要兼顾实时分析的技术团队,它是当下最稳妥的表格式选择之一。先用 Docker 把上面的例子跑通,再逐步接入真实的 CDC 与 OLAP 链路,你会发现数据湖的运维复杂度被大幅摊薄了。




