第
11 章 讲清了 Iceberg
的提交协议:一次提交本质上是把表的「当前元数据指针」从
vN.metadata.json 原子地换成
vN+1.metadata.json。但那一章刻意回避了一个问题——这个原子
swap 到底由谁来做、靠什么保证原子。答案就是
catalog。
catalog 不是可有可无的目录服务。在没有数据库进程的湖仓里,它是唯一的串行化点:所有写入者通过它竞争同一个指针,所有读取者通过它找到「现在应该读哪个 metadata」。catalog 选错,轻则多引擎读不到对方写的数据,重则两个并发提交互相覆盖、丢数据而不报错。
本章先把 catalog 的两件职责拆到最小,再逐个对比主流形态的锁与原子性语义,最后单独讲 2024 年开源的 Apache Polaris 与 Unity Catalog——它们把「catalog」从一个元数据存储,扩展成了带权限治理、凭证派发的服务层。
版本锚定:Iceberg 表规范 V2、Iceberg REST Catalog OpenAPI(
apache/iceberg的open-api/rest-catalog-open-api.yaml)。Polaris 引用其 Apache 孵化期(2024-08 进入孵化)与 GitHub 主线;Unity Catalog 引用其 OSS 0.x(2024-06 开源,Apache 2.0)。涉及具体默认值与端点以对应版本规范为准。
一、catalog 到底负责什么
把一切营销词去掉,开放表格式语境下的 catalog 只有两件不可让渡的职责。
1.1 职责一:表标识符 → 当前元数据指针
读一张 Iceberg 表的第一步,不是去 list 数据目录,而是问
catalog:「db.events 这张表,现在的 metadata
文件在哪?」catalog 返回一个指针,通常是对象存储上某个
…/metadata/00042-<uuid>.metadata.json
的路径。
这一步是整个 lakehouse「不 list 目录」承诺的起点。第 8 章 描述的四层元数据树——metadata.json → manifest list → manifest → data files——全都挂在这个指针下面。指针一旦拿到,后续 planning 全靠读元数据文件,与目录里到底躺了多少文件无关。
flowchart LR
R[读/写客户端] -->|1 loadTable db.events| C[catalog]
C -->|2 返回 metadata-location| R
R -->|3 读 metadata.json| M[vN.metadata.json]
M --> ML[manifest list]
ML --> MF[manifest files]
MF --> D[data files on S3]
1.2 职责二:原子提交点
写入更难。提交一个新 snapshot,等价于让 catalog 把指针从
metadata-location = vN 改成
vN+1,且必须满足:
- 原子:要么所有读者立刻看到
vN+1,要么还是vN,不存在「读到一半」的中间态。 - 基于前提条件:提交时声明「我是在
vN基础上算出来的」,如果此刻指针已经不是vN(被别人抢先提交了),提交必须失败,让客户端重试。
这就是乐观并发控制(optimistic concurrency control)的 compare-and-swap:
\[ \text{CAS}(\text{pointer},\; \text{expected}=v_N,\; \text{new}=v_{N+1}) \]
成功的前提是 \(\text{pointer} = v_N\) 仍然成立。catalog 形态之间的根本差异,就在于这个 CAS 用什么底层机制实现、以及它在并发下到底安不安全。 后面所有对比都围绕这一点。
1.3 为什么不能让客户端直接改对象存储
一个自然的疑问:既然 metadata 都在 S3 上,写入者直接把指针写进某个约定文件不就行了?问题在于 S3 这类对象存储的语义(见 第 6 章):
- 没有跨对象的事务;
- 传统上没有「仅当不存在时写入」的原子保证(虽然 S3 已支持
If-None-Match,但并非所有兼容实现都支持,且语义有限); - 没有原子 rename。
于是「谁来仲裁并发」这件事必须外包给一个能提供线性化语义的组件——可能是关系数据库的行锁、可能是一个 HTTP 服务背后的事务,也可能就是对象存储自己的条件写。catalog 就是这个仲裁者的抽象。
二、Iceberg 的 catalog 抽象
Iceberg 把 catalog
定义成一个接口(org.apache.iceberg.catalog.Catalog
及
TableOperations),所有实现都要回答上面两个问题。关键方法语义:
| 操作 | 语义 |
|---|---|
loadTable(id) |
返回当前 metadata-location 指向的表对象 |
createTable(id, schema, …) |
注册新表标识符 → 初始 metadata 文件 |
commit(base, metadata) |
原子地把指针从 base(旧 metadata)换到新
metadata,失败抛 CommitFailedException |
renameTable / dropTable |
改 / 删指针,数据文件视 purge
决定是否删 |
commit
的实现是分水岭。TableOperations.commit(base, metadata)
拿到「我看到的旧 metadata」base
和「我想提交的新 metadata」,由具体 catalog
决定如何原子地完成替换。下面逐个看主流实现怎么做这件事。
三、形态对比:锁与原子性语义
3.1 Hive Metastore(HMS)
Hive Metastore 是最老的玩家:一个 Thrift 服务,背后是关系数据库(Derby/MySQL/PostgreSQL),最初为 Hive 目录式分区表设计。
Iceberg 通过 HiveCatalog 复用 HMS:表在 HMS
里登记为一个 entry,真正的指针存在表属性
metadata_location 里。提交时:
- 读出 HMS 中当前的
metadata_location,确认等于base; - 写出新的
metadata.json到对象存储; - 通过 HMS 的
alter_table,把metadata_location改成新值,同时把旧值写入previous_metadata_location做校验。
原子性来自 HMS 对该表加的锁(早期实现用
HMS 的 lock 接口;不同版本用 alter_table
配合后端数据库事务做条件更新)。要点:
- 原子性最终落在后端关系数据库的事务/锁上,HMS 只是中间层。
- HMS 锁是表级粗粒度,且历史上 lock 实现有过 bug
与超时配置坑(
hive.txn.timeout等)。 - HMS 是有状态服务,常成为单点;多个引擎共享一套 HMS 时,schema/分区元数据要靠 HMS 与 Iceberg 各存一份,存在不一致风险。
HMS 的价值是生态惯性:Hive、Spark、Trino、Flink 都原生认 HMS,遗留数仓迁移时它是默认落点。但它把「Hive 表模型」和「Iceberg 表模型」混在一个服务里,治理与演进都受 Hive 历史包袱拖累。
3.2 JDBC Catalog
JdbcCatalog 直接把指针存进一张关系表,省掉
HMS 这层 Thrift 服务。Iceberg 在配置的数据库里建一张表(默认
iceberg_tables,列含 catalog
名、namespace、表名、metadata_location、previous_metadata_location)。
提交的原子性来自一条带 WHERE 条件的 UPDATE:
UPDATE iceberg_tables
SET metadata_location = :new,
previous_metadata_location = :base
WHERE catalog_name = :cat
AND table_namespace = :ns
AND table_name = :tbl
AND metadata_location = :base; -- CAS 前提条件WHERE … metadata_location = :base 就是
CAS:如果别人抢先改过指针,base
不再匹配,UPDATE 影响行数为
0,提交失败、客户端重试。原子性由数据库单语句的事务隔离保证,干净直接。
适用场景:不想跑 HMS、又已有一套可靠关系数据库(RDS/PG)时,JDBC catalog 是最轻的「正确」实现。代价是每个客户端都要能直连数据库并持有凭证——这恰恰是 REST catalog 想消灭的耦合。
3.3 Hadoop Catalog(file-based,并发风险)
HadoopCatalog
不依赖任何外部服务,指针完全靠文件系统约定:metadata
文件按
v1.metadata.json、v2.metadata.json
递增命名,外加一个 version-hint.text
记录当前版本号。提交就是写出 vN+1.metadata.json
然后更新 hint。
在 HDFS 上,它依赖文件系统的原子 rename:提交者把新版本 rename 到目标名,rename 失败说明被人抢先,于是重试。HDFS 的 rename 是原子的,这条路勉强成立。
在 S3 这类对象存储上,它不安全。
原因正是 第 6
章 讲的:S3 没有原子 rename(rename = copy +
delete,非原子),传统上也不保证「create-if-absent」。两个写入者可能同时认为自己拿到了
vN+1,先后覆盖,造成丢提交。Iceberg
官方文档明确警告:HadoopCatalog
不应在对象存储上用于并发写入的生产表。
所以 Hadoop catalog 的定位是:本地文件系统 / HDFS 上的单写入者场景、教学与测试。一旦上 S3 且有并发,就该换 REST/JDBC/Glue。
3.4 AWS Glue Catalog
GlueCatalog 用 AWS Glue Data Catalog
当元数据后端,指针同样存在表属性里。原子性依赖 Glue
的乐观锁:Glue 的表对象带一个 version
标识,UpdateTable
时带上期望版本,版本不符则失败。
注意历史细节:早期为了在 Glue
上获得更强的并发保证,Iceberg 文档曾建议配合
DynamoDB
锁表(LockManager)做额外串行化;随着
Glue 自身乐观锁完善,新版本可不依赖外部锁。具体以所用
Iceberg 版本的 AWS
模块文档为准——这是个典型的「版本边界」结论,不能照抄老博客。
Glue 的吸引力在于和 AWS 生态(Athena、EMR、Lake Formation 权限)打通;代价是绑定 AWS,且跨云互通要另想办法。
3.5 Nessie:类 git 的分支与标签
Project Nessie 把 catalog 做成了版本控制系统。核心抽象不是「表的当前指针」,而是「一棵提交树」:
- branch / tag:像 git
一样,
main是一条分支,可以拉etl_dev分支做实验,验证完再 merge 回main。 - commit:每次写入是一次 commit,引用父 commit,形成 DAG。
- 多表事务:一次 commit 可以原子地改动多张表——这是单表 CAS 模型给不了的能力,对「一批表必须一起切换版本」的 ETL 很关键。
原子性由 Nessie 服务端保证(后端可用 RocksDB、关系库、Mongo 等),提交是基于「期望 branch HEAD」的 CAS:HEAD 变了就冲突重试,语义和 git 的 non-fast-forward 拒绝一致。
flowchart TD
M0["main@c0"] --> M1["main@c1"]
M1 --> M2["main@c2"]
M1 -->|branch| D1["etl_dev@c1"]
D1 --> D2["etl_dev@d2 改 t1,t2"]
D2 -.->|验证后 merge| M2
合并(merge)时,Nessie 会检测冲突:如果两条分支都改了同一张表,且基点不同,merge 会像 git 一样报冲突,需要决策(要哪一方、或重新基于最新 main 做)。冲突粒度通常在「表内容(content)」级别——两个分支各自把某张表推到了不同 snapshot,merge 不能简单二选一时即冲突。
垃圾回收(GC)在 Nessie 里也变成版本控制问题:一个 data file 只要还被任意一条 branch/tag 可达的 commit 引用,就不能删。Nessie 的 GC 要遍历所有 ref 的可达集合再做差集——这比单表 expire 复杂,但语义清晰:可达即存活。
Nessie 适合需要「数据版本化、跨表原子、分支隔离做 ETL/回归」的团队,代价是引入一个新的有状态服务与心智模型。它也可以作为 Iceberg REST catalog 的后端实现之一对外暴露——此时引擎看到的是标准 REST 端点,Nessie 在背后提供分支与多表事务能力。
3.6 Apache Gravitino
Gravitino(Apache 孵化中)定位是统一元数据湖(metadata lake):不只是 Iceberg 一种表,而是把关系库、消息、文件集、不同表格式的 catalog 聚合到一个「catalog 的 catalog」之下,并对外提供包括 Iceberg REST 在内的接口。它解决的是「一个组织里有 N 套 catalog,如何统一发现与治理」的上层问题,而不是替换某个具体 catalog 的 CAS 实现。在本系列语境里,把它理解为「多 catalog 之上的联邦层」即可。
3.7 小结对比
| Catalog | 指针存储 | 原子性来源 | 对象存储并发安全 | 多表事务 | 主要绑定 |
|---|---|---|---|---|---|
| Hive Metastore | HMS + 后端 RDB | 表锁 + DB 事务 | 安全(依赖 HMS) | 否 | Hadoop 生态 |
| JDBC | 关系表一行 | 单语句条件 UPDATE | 安全(依赖 DB) | 否 | 一套 RDB |
| Hadoop(file) | vN.metadata.json + hint |
文件系统原子 rename | 不安全(S3) | 否 | HDFS / 本地 |
| AWS Glue | Glue 表属性 | Glue 乐观锁(+可选 DDB 锁) | 安全 | 否 | AWS |
| Nessie | 提交 DAG | 服务端 CAS(类 git) | 安全 | 是 | Nessie 服务 |
| REST Catalog | 由后端决定 | 服务端事务 | 安全 | 取决于实现 | HTTP 协议 |
| Polaris / Unity | 见第五、六节 | 服务端 | 安全 | 取决于实现 | 各自服务 |
「多引擎共享兼容性」的判据,不在于 catalog 能不能存指针,而在于:所有引擎是否都把同一处当作唯一权威指针,且对 CAS 语义理解一致。HMS 时代多引擎要各自实现 HMS 客户端、各自处理锁,互通靠运气;REST catalog 把这件事变成一份 HTTP 契约,下一节展开。
四、Iceberg REST Catalog 规范
REST Catalog 是 Iceberg 社区给「catalog
互通」开的标准答案:定义一份 OpenAPI
规范(apache/iceberg 仓库的
open-api/rest-catalog-open-api.yaml),客户端只认
HTTP 端点,完全不关心服务端背后是 HMS、JDBC、Nessie
还是某云厂商实现。
4.1 解耦:客户端只说 HTTP
传统 catalog 的耦合在于:客户端要打包 HMS Thrift 客户端、或持有数据库凭证、或带 AWS SDK。引擎升级、catalog 换后端,客户端都得跟着改。REST catalog 把这层全部收到服务端:
flowchart LR
subgraph clients[多引擎客户端]
SP[Spark]
TR[Trino]
FL[Flink]
PY[PyIceberg]
end
clients -->|HTTP / OpenAPI| RS[REST Catalog 服务]
RS --> BK[(后端: HMS/JDBC/Nessie/自研)]
RS --> OS[(对象存储)]
客户端只需要一个 URI、一个 token、可能一个
warehouse 名。引擎侧的 catalog 实现统一成一个薄
HTTP 客户端。
4.2 关键端点
规范的核心端点(路径中 {prefix}
用于多租户/多 warehouse 路由):
| 端点 | 作用 |
|---|---|
GET /v1/config |
客户端启动握手,服务端返回默认与覆盖配置 |
GET /v1/{prefix}/namespaces |
列 namespace |
POST /v1/{prefix}/namespaces |
建 namespace |
GET /v1/{prefix}/namespaces/{ns}/tables |
列表 |
POST /v1/{prefix}/namespaces/{ns}/tables |
建表 |
GET …/tables/{table} |
loadTable,返回 metadata 与(可选)凭证 |
POST …/tables/{table} |
commit:提交 updates,带 requirements |
4.3 提交语义:requirements + updates
REST catalog 的提交不是「把新指针 PUT 上去」,而是发一个
UpdateTableRequest,里面分两部分:
requirements:一组前提断言,服务端逐条校验,全部成立才接受。updates:一组元数据变更操作(加 snapshot、改 schema、设属性等)。
这正是 CAS 的 HTTP 表达。requirements
里典型的断言类型:
| requirement | 含义 |
|---|---|
assert-create |
断言这是新建(表此前不存在) |
assert-table-uuid |
表 UUID 必须等于期望值(防张冠李戴) |
assert-ref-snapshot-id |
某个 ref(如 main)当前指向的 snapshot-id
必须等于期望值 |
assert-last-assigned-field-id |
上次分配的 field-id 不变(防 schema 并发演进冲突) |
assert-current-schema-id |
当前 schema-id 等于期望 |
assert-default-spec-id |
默认分区 spec-id 等于期望 |
assert-last-assigned-partition-id |
上次分配的 partition field-id 不变 |
服务端在一个事务里校验全部 requirements
再应用
updates。assert-ref-snapshot-id
就是「我基于 snapshot X 算的,提交时 main
还得是 X」——和 第
11 章
的乐观并发完全对应,只是从客户端本地逻辑挪到了服务端契约里。请求体骨架:
{
"requirements": [
{ "type": "assert-ref-snapshot-id", "ref": "main", "snapshot-id": 4387548087484650653 }
],
"updates": [
{ "action": "add-snapshot", "snapshot": { "snapshot-id": 4044267194436670622, "...": "..." } },
{ "action": "set-snapshot-ref", "ref-name": "main", "type": "branch", "snapshot-id": 4044267194436670622 }
]
}若 main 已经被别人推到了别的
snapshot,assert-ref-snapshot-id
失败,服务端返回 409,客户端重新基于最新 snapshot
计算再试。
4.4 凭证派发(credential vending)
REST catalog
还能解决一个治理痛点:数据文件在对象存储上,谁有权读?
传统做法是每个引擎客户端各自配 S3 凭证,权限散落各处。REST
规范允许 loadTable
响应里携带作用域受限的临时凭证(vended
credentials)——服务端按调用者权限,只发放「能读这张表对应前缀」的短期
token。权限判断集中在 catalog
服务,客户端不再长期持有宽泛的存储密钥。Polaris 和 Unity
Catalog 都把凭证派发当作核心卖点。
4.5
启动握手:GET /v1/config
客户端连上 REST catalog
第一件事是握手。GET /v1/config
返回两组配置:
defaults:服务端建议的默认值,客户端没设的就用它;overrides:服务端强制覆盖,客户端就算设了也以它为准。
合并规则是:overrides > 客户端显式配置
> defaults。这让服务端能集中下发「这个
warehouse 的数据根路径」「该用哪种
FileIO」「是否启用凭证派发」等,客户端无需硬编码。典型响应骨架:
{
"defaults": { "write.parquet.compression-codec": "zstd" },
"overrides": { "warehouse": "s3://lake/prod", "io-impl": "org.apache.iceberg.aws.s3.S3FileIO" }
}握手是 REST catalog「服务端集中治理、客户端薄」的起点:连配置都不让客户端自己拍板。
4.6 认证
REST 规范本身不绑死某一种认证,但约定了 OAuth2 风格的
token 流:客户端用 token 或走 OAuth2
客户端凭证拿到 Bearer token,之后每个请求带
Authorization: Bearer …。服务端据此识别调用者身份,再决定:能不能访问这个
namespace/表、loadTable
该派发什么作用域的存储凭证。这把「身份认证」与「存储授权」都收到了
catalog 服务端,是与 HMS/JDBC
时代「客户端各自持密钥」最大的治理差异。
4.7 多表事务
单表提交是
POST …/tables/{table}。规范还定义了事务端点(POST /v1/{prefix}/transactions/commit),允许在一个请求里携带多张表的
requirements +
updates,由服务端原子地一起提交——要么全部成功,要么全部失败。这把
Nessie
的「多表原子」能力纳入了标准接口(具体是否支持取决于服务端实现)。对「一批维表必须和事实表同时切版本」的场景,这是单表
CAS 给不了的。
4.8 一次提交冲突的完整往返
把前面拼起来,看一次「我和别人同时提交」的真实过程。两个
writer A、B 都基于 main = snapshot 100
计算各自的新 snapshot。A 先提交:
POST /v1/prod/namespaces/db/tables/events
Authorization: Bearer <A 的 token>
{ "requirements": [{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":100}],
"updates": [{"action":"add-snapshot", "snapshot":{"snapshot-id":101, ...}},
{"action":"set-snapshot-ref","ref-name":"main","type":"branch","snapshot-id":101}] }
服务端校验 main 确实是
100,接受,main 推进到 101,A 拿到 200。随后 B
提交同样的前提(它也以为 main
是 100):
POST /v1/prod/namespaces/db/tables/events
{ "requirements": [{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":100}],
"updates": [ ... 把 main 推到 B 的 102 ... ] }
此刻 main 已经是
101,assert-ref-snapshot-id
失败,服务端返回冲突:
HTTP/1.1 409 Conflict
{ "error": { "message": "Requirement failed: branch main has changed", "type": "CommitFailedException", "code": 409 } }
B 必须重新基于 101 算出新 snapshot 再试。这就是乐观并发的全貌:不加悲观锁,谁先到谁赢,输的一方重试。冲突能否自动重试取决于变更是否仍可在新基线上成立(纯 append 几乎总能重试成功;改了同一批行的操作可能需要重新计算)。这套语义和 第 11 章 的提交协议是同一件事,只是搬到了 HTTP 契约上。
五、Apache Polaris
5.1 来历与定位
Polaris 由 Snowflake 发起并开源(GitHub 仓库 2024 年 5 月建立),2024 年 8 月 9 日进入 Apache 孵化器,后于 2026 年初毕业为 Apache 顶级项目,Apache 2.0 许可。它的一句话定位:一个厂商中立、实现 Iceberg REST Catalog API 的开源 catalog,目标是让 Spark、Flink、Trino、Doris、StarRocks、Dremio 等引擎通过同一份 REST 契约共享 Iceberg 表。
换句话说,Polaris 不发明新的 catalog 协议,它就是 Iceberg REST Catalog 规范的一个完整服务端实现,外加一套管理与权限 API。
5.2 两套 API
Polaris 对外暴露两类 API(均有 OpenAPI 规范):
| API | 作用 |
|---|---|
| Iceberg REST Catalog API | 引擎读写表走这套,即第四节的标准端点 |
| Polaris Management API | 管理 catalog、principal、role、grant 等治理对象 |
引擎只认前者,所以任何支持 Iceberg REST catalog 的引擎开箱即用;治理操作走后者,由平台管理员使用。
5.3 内部 catalog 与外部 catalog
Polaris 区分两种 catalog:
- Internal catalog:表的权威指针由 Polaris 自己管理,读写都经 Polaris 提交,享受其权限与凭证派发。
- External catalog:指向一个 Polaris 之外已存在的 catalog(如另一套系统),Polaris 做联邦式接入。
这让 Polaris 既能做新表的权威 owner,也能纳管存量。
5.4 权限模型(RBAC)
Polaris 的治理是基于角色的访问控制(RBAC),层级大致是:
flowchart TD
P[principal 主体] -->|被授予| PR[principal role]
PR -->|被授予| CR[catalog role]
CR -->|持有| GR[privileges 授权]
GR --> OBJ[作用对象: catalog / namespace / table]
- principal:一个可认证的身份(服务账号 / 用户)。
- principal role:把主体归组。
- catalog role:在某个 catalog 内定义的角色,持有一组 privilege。
- privilege:如
TABLE_READ_DATA、TABLE_WRITE_DATA、NAMESPACE_CREATE等,作用在 catalog / namespace / table 粒度。
授权链是:principal → principal role → catalog role →
privilege on object。配合凭证派发,Polaris
可以做到「principal X 只能读
db.events,且只拿到读该表数据前缀的临时存储凭证」。
5.5 与 REST 规范的关系
一句话:Polaris = Iceberg REST Catalog 规范的实现 + 治理层。它对 Iceberg 生态的兼容性来自「严格实现规范端点与提交语义」,互通性是规范给的,不是 Polaris 私有的。这也是它能被 ASF 接纳、被多引擎直接对接的根本原因。
六、Unity Catalog(OSS)
6.1 来历与定位
Unity Catalog 由 Databricks 在 2024 年 6 月(Data + AI Summit)开源,Apache 2.0 许可,托管在 Linux Foundation 旗下的 LF AI & Data。与 Polaris「专注 Iceberg catalog」不同,Unity Catalog 的定位更宽:面向数据与 AI 的统一治理目录,治理对象不止表,还包括卷(volumes,非结构化文件)、函数、AI 模型。
6.2 多协议兼容
Unity Catalog OSS 的关键设计是同时兼容多套接口标准:
- 自有的 Unity REST API(OpenAPI 定义);
- Iceberg REST Catalog API——这意味着 Iceberg 引擎生态可以直接把 Unity Catalog 当 Iceberg catalog 用;
- Hive Metastore 接口兼容;
- 通过 Delta Lake UniForm(见 第 14 章),同一张底层表能被 Delta、Iceberg、Hudi 客户端分别读取。
凭证派发同样是核心能力:客户端访问底层云存储要先经 Unity Catalog 鉴权,由它发放受限凭证。
6.3 三层命名空间与治理对象
Unity Catalog 采用 catalog.schema.table
三层命名空间,治理对象比纯表 catalog 丰富:
| 对象 | 说明 |
|---|---|
| catalog | 顶层命名空间 |
| schema | 中层(即 namespace) |
| table | 表(Delta / Iceberg via UniForm / Parquet / CSV / JSON …) |
| volume | 非结构化数据(文件目录) |
| function | UDF |
| model | AI / ML 模型 |
这反映了 Databricks 的视角:治理边界不止「表」,而是「数据 + AI 资产」。
6.4 与 REST 规范的关系
Unity Catalog 实现了 Iceberg REST Catalog API 作为其多协议的一面,但它本身有更大的 API surface(卷、函数、模型、Delta 专属能力)。所以:
- 站在 Iceberg 引擎角度,Unity Catalog 是一个合规的 Iceberg REST catalog,可互通;
- 站在治理角度,它管的东西超出 Iceberg 规范覆盖范围。
需要强调版本边界:Unity Catalog OSS 的 API 在 0.x 阶段明确标注「仍在演进、不应假设稳定」。本节描述的是其设计意图与公开能力,具体端点行为以所用 OSS 版本为准。
6.5 Polaris vs Unity Catalog
| 维度 | Apache Polaris | Unity Catalog (OSS) |
|---|---|---|
| 发起方 | Snowflake | Databricks |
| 治理归属 | Apache 软件基金会 | LF AI & Data(Linux Foundation) |
| 开源时间 | 2024(08 入 ASF 孵化) | 2024-06 |
| 许可 | Apache 2.0 | Apache 2.0 |
| 核心定位 | Iceberg REST catalog + 治理 | 数据 + AI 统一治理目录 |
| 治理对象 | catalog/namespace/table | + volume/function/model |
| Iceberg REST | 原生实现 | 实现之一(多协议) |
| HMS 兼容 | 否(聚焦 REST) | 是 |
| 多格式 | Iceberg | Delta / Iceberg(UniForm) / Hudi / Parquet… |
| 权限模型 | principal/role/privilege RBAC | 三层命名空间 + 权限 |
二者都实现 Iceberg REST Catalog API,所以从 Iceberg 引擎看,它们是可替换的 catalog 后端。差异在治理广度(Unity 更宽)与生态中立性叙事(Polaris 进 ASF)。选型时真正要问的是:你的资产是否只有 Iceberg 表(偏 Polaris),还是要统一管 Delta + 文件 + 模型(偏 Unity)。
七、本章实验用到的 catalog
为了让 第 16 章 与 第 17 章 的实验可复现,本系列实验统一用 PyIceberg 的 SQL catalog(SQLite 后端)——它正是 3.2 节 JDBC catalog 思路的轻量版:指针存在 SQLite 一张表里,提交靠单语句条件更新。环境与版本:
OS: Arch Linux on WSL2, kernel 6.6.87.2-microsoft-standard-WSL2
CPU: 12th Gen Intel Core i9-12900K (24 logical)
Python: 3.14.5
PyIceberg: 0.11.1
PyArrow: 24.0.0
对象存储: 本地文件系统(file://)替代 S3,便于离线复现
最小可运行配置(后续两章复用):
from pyiceberg.catalog.sql import SqlCatalog
catalog = SqlCatalog(
"demo",
uri="sqlite:////tmp/ice_wh/catalog.db", # JDBC 思路:指针存这里
warehouse="file:///tmp/ice_wh", # 数据与 metadata 落这里
)
catalog.create_namespace("db")换成生产里的 REST catalog(如 Polaris / Unity / 任意合规实现),客户端只需改 catalog 配置,表读写代码不变——这正是 REST 规范「解耦」承诺的直接体现:
from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog(
"prod",
uri="https://polaris.example.com/api/catalog",
token="…",
warehouse="my_catalog",
)说明:本章是 catalog 的形态与语义对比,结论锚定各 catalog 的接口定义与 Iceberg REST 规范;不构造性能数字。涉及实测的提交/读写在第 16、17 章用上面的 SQL catalog 真跑。
八、把表注册进 catalog
选定 catalog 后,存量表与跨 catalog 迁移是绕不开的实操。Iceberg 提供几条路径,差别在「动不动数据文件」「指针归谁」:
| 操作 | 做什么 | 是否重写数据 | 典型场景 |
|---|---|---|---|
register_table |
把一个已有 Iceberg 表的 metadata.json
登记到目标 catalog |
否 | 跨 catalog 搬迁同一张 Iceberg 表 |
add_files |
把已存在的 Parquet/ORC 文件「收编」进 Iceberg 表的 manifest | 否(只建元数据) | 把目录式数据快速纳管 |
snapshot |
为 Hive 表创建一个 Iceberg 的「影子」表做验证 | 否(共享数据文件) | 迁移前试跑、不影响原表 |
migrate |
原地把 Hive 表转成 Iceberg 表 | 否(in-place,复用文件) | 正式迁移 Hive 表 |
| 重写导入 | 读源表,按新表写入 | 是 | 需要顺带改文件布局/分区 |
要点与风险:
- 唯一权威指针:
register_table跨 catalog 搬迁时,务必保证只有一个 catalog 认这张表为权威,否则两边并发写就没有共同串行化点(见下一节陷阱)。 add_files的统计:收编的文件若没有 Iceberg 期望的列统计,文件级裁剪效果有限,可能需要后续 compaction/重算统计(第 17 章)。migrate不可轻易回头:原地迁移后原 Hive 元数据语义改变,迁移前应先用snapshot影子表验证查询正确性。
flowchart TD
H[已有 Hive 表] -->|snapshot 影子表| V[验证查询正确]
V -->|通过| MIG[migrate 原地转 Iceberg]
P[已有 Parquet 目录] -->|add_files| ICE[纳管为 Iceberg 表]
E[别处的 Iceberg 表] -->|register_table| TGT[目标 catalog]
九、选型与陷阱
把前面的语义落到决策上:
| 场景 | 倾向 | 理由 |
|---|---|---|
| 遗留 Hadoop/Hive 数仓迁移 | HMS(过渡)→ REST | 生态惯性,但应规划迁出 |
| 已有可靠 RDB、单团队 | JDBC | 最轻的正确实现 |
| 本地/HDFS 单写入者、测试 | Hadoop(file) | 简单,勿上 S3 并发 |
| 深度 AWS | Glue | 与 Athena/EMR/Lake Formation 打通 |
| 需数据版本化/分支/跨表原子 | Nessie | 类 git 能力独一份 |
| 多引擎互通、要标准契约 | REST(Polaris/Unity/其他) | 客户端解耦、凭证派发 |
| 只有 Iceberg、要中立 | Polaris | 专注 + ASF |
| 统一管 Delta+文件+模型 | Unity Catalog | 治理面最宽 |
常见陷阱:
- HadoopCatalog 上 S3 并发写:最经典的丢数据坑。看似能跑,并发提交时静默覆盖。任何生产并发表都不要用它配对象存储。
- 多个 catalog 指向同一批数据文件:两套 catalog 各存一份指针,互不知道对方的提交,等于两个写入者没有共同串行化点——并发安全荡然无存。一张表的权威指针必须唯一。
- 把 catalog 当纯目录、忽略它是并发仲裁者:选 catalog 不只看「能不能列表」,要看它的 CAS 在你的存储与并发模型下到底安不安全。
- REST catalog 凭证派发未启用,仍给客户端宽泛存储密钥:上了 REST 却没用上集中授权,治理收益打折。
- 版本边界混淆:Glue 是否需要 DynamoDB 锁、Unity OSS 某端点是否稳定,都随版本变;照抄两年前博客容易踩坑。
十、小结
- catalog 在 lakehouse 里只有两件不可让渡的职责:表名 → 当前元数据指针、原子提交点(基于前提条件的 CAS)。形态之间的根本差异在于 CAS 用什么实现、并发下安不安全。
- HMS/JDBC 把原子性落在关系数据库;Hadoop file-based 落在文件系统 rename,在 S3 上不安全;Glue 用乐观锁;Nessie 用类 git 的服务端 CAS 并支持多表事务。
- Iceberg REST Catalog 规范用一份 OpenAPI
契约把「客户端」和「catalog 后端」解耦,提交语义是
requirements(前提断言)+updates(变更),并支持凭证派发。 - Apache Polaris(Snowflake 开源、2024 入 ASF 孵化)是 Iceberg REST catalog 的中立实现 + RBAC 治理;Unity Catalog(Databricks 2024-06 开源)是更宽的「数据 + AI 统一目录」,同样实现 Iceberg REST API,兼容 HMS,治理对象含卷/函数/模型。二者从 Iceberg 引擎看可互换,差异在治理广度与生态叙事。
- 多引擎共享的前提,是所有引擎认同唯一权威指针并对 CAS 语义理解一致——这正是 REST 规范的价值。
下一章把视角转向时间维度:有了 catalog 提供的快照指针,如何做时间旅行、回滚、以及不靠位置而靠 field ID 的 schema 演进。
返回 系列目录 | 上一篇:三者对照与互通 | 下一篇:时间旅行、Schema 与分区演进
参考资料
- Apache Iceberg, REST Catalog OpenAPI
Spec(
apache/iceberg,open-api/rest-catalog-open-api.yaml)— A 级。 - Apache Iceberg, Spec(table spec V2)与
Catalog/TableOperations接口定义 — A 级。 - Apache Iceberg 文档, Hive, JDBC, Hadoop, AWS(Glue) catalog 章节 — A 级。
- Apache Polaris 官网与
apache/polaris仓库、Apache 孵化器状态页(2024-08-09 进入孵化)— A 级。 - Unity Catalog OSS,
unitycatalog/unitycatalog仓库与 Databricks 开源公告(2024-06,Apache 2.0,LF AI & Data)— A/B 级(公告为 B 级,仓库/规范为 A 级)。 - Project Nessie 官方文档(branch/tag、多表提交语义)— A 级。
- Apache Gravitino(孵化中)官方文档 — A 级。
- 本机实验:PyIceberg 0.11.1 + SQLite SQL catalog(环境见第七节)— A 级(实测)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据湖与开放表格式】提交协议与并发控制
没有数据库进程,Iceberg 怎么在对象存储上做原子提交与并发控制?拆解提交=catalog 对元数据指针做 compare-and-swap,乐观并发如何基于当前 snapshot 生成新 snapshot、冲突按操作类型与隔离级别重试,不同 catalog 的原子性来源(DB 行锁/CAS、REST 后端、对象存储条件写、文件系统 rename),以及 REST Catalog 的 requirements+updates 提交语义。基于 pyiceberg 0.11.1 实测并发冲突与重试。
【数据湖与开放表格式】Lakehouse 全景:从 Hive 表到开放表格式
Hive 目录式分区表把『表』等同于『一组目录加 metastore 里的分区行』,于是没有原子提交、planning 要 LIST 目录、schema 与分区演进常要重写。本文用这三个硬伤切入,讲清 lakehouse 把表拆成『不可变数据文件 + 可变元数据指针 + catalog』三层后各自解决了什么,并给出全系列的分层地图。
【数据湖与开放表格式】Parquet 文件格式深拆
拆 Parquet 的物理结构:file → row group → column chunk → page,footer 里的 FileMetaData(Thrift)与 PAR1 magic。讲清 PLAIN/RLE-bitpacking/字典/DELTA_BINARY_PACKED/BYTE_STREAM_SPLIT 各自压谁,Dremel 的 repetition/definition level 如何表达嵌套,column index/offset index 与 split-block bloom filter 怎样让谓词在读盘前裁掉 page。基于本机 pyarrow 24.0.0 真实 dump footer 与编码。
【数据湖与开放表格式】ORC 文件格式与 Parquet 对照
ORC 用 stripe 而非 row group、用三级统计(file/stripe/row-group index)而非独立 page index、用 PRESENT/DATA 等 stream 而非 page 组织一列。本文按 ORC 规范拆其文件尾(postscript + footer)、stripe 内部结构与 RLEv2 整数编码,并用本机 pyarrow 24.0.0 把同一份 30 万行数据写成 ORC 与 Parquet,对比真实体积与物理布局,最后给出什么场景仍用 ORC。