HDFS 数据湖存储底座(与 Iceberg 的关系)¶
作者:JiangChong | 撰写时间:2026年09月
适用场景:私有化/本地化部署环境,希望在已有 Hadoop 集群之上构建湖仓一体(数据湖)架构;需要理解 HDFS 与 S3/OSS 等对象存储作为数据湖存储底座各自的适用边界;需要把 HDFS、Iceberg、Vertica 三者关系串起来的工程师。
关联文章(HDFS 系列):HDFS 架构原理 · HDFS 读写流程与副本放置策略 · HDFS 高可用(QJM、ZKFC、Federation) · 小文件问题与治理 · 纠删码 vs 三副本存储策略 · HDFS 数据湖存储底座(与 Iceberg 的关系)(本文)· Vertica Eon communal storage 运维实战
阅读前先认识几个词¶
| 术语 | 一句话解释 |
|---|---|
| 数据湖(Data Lake) | 把各来源原始数据集中堆放的大池子:便宜、灵活,但本身没有事务和「表」的概念 |
| 数据仓库(Data Warehouse) | 结构化、强事务、高性能的传统数仓:好用好查,但模型僵化、变更和存储都贵 |
| 湖仓一体(Lakehouse) | 三者合体:便宜存储(HDFS/对象存储)+ 表格式(Iceberg 等)+ 多引擎读写(Spark/Flink/Trino/Vertica),做出数仓级的 ACID 与版本能力 |
| 表格式(Table Format) | Iceberg/Hudi/Delta Lake 这类元数据层:给一堆文件加上「表」的事务、快照、Schema 语义 |
| Catalog | 表的「登记处」:只维护「表名 → 当前元数据文件位置」这条指针(常见实现:Hive Metastore、REST Catalog) |
理解全文脉络:湖仓一体 = 存储层(HDFS/对象存储)+ 表格式(Iceberg)+ 计算引擎(Spark/Flink/Vertica),三者解耦。本文回答三个问题:为什么私有化部署的湖仓底座常选 HDFS 而不是对象存储(第 1 节);Iceberg 表在 HDFS 上长什么样、它的 ACID/时间旅行能力从哪来(第 2 节);HDFS 的哪些特性恰好支撑湖仓工作负载,以及 Vertica 在这套架构里站在什么位置(第 3、4 节)。核心一句话:Iceberg 把「表」从「存储」中解耦出来,于是 HDFS 和对象存储都能当底座,选哪个取决于你的部署形态。 术语卡壳随时回查上面的表格,文末附新手 FAQ。
1. 数据湖的存储层选型:HDFS 还是对象存储¶
一个湖仓一体架构通常分三层:存储层(文件到底放在哪)、表格式层(Iceberg/Hudi/Delta Lake 这样的"表"语义层,见 湖仓一体架构设计)、计算引擎层(Spark/Flink/Trino/Vertica)。存储层是地基:决定成本模型、性能上限和运维负担。私有化场景下,这个选择题通常就是 HDFS 对对象存储。先看一张总图,记住本文讨论的位置——本文聚焦下面两层(存储层、表格式层),计算引擎层只在第 4 节出场:
flowchart TB
ENG["计算引擎层:Spark / Flink / Trino / Vertica"]
FMT["表格式层:Iceberg / Hudi / Delta Lake"]
STO["存储层:HDFS / S3 / OSS"]
ENG --> FMT --> STO
HDFS 与对象存储(S3、OSS 等)在几个关键维度上的差异如下。注意:S3 并不只指云上服务——MinIO、Ceph 等开源方案同样提供 S3 兼容接口,私有化环境也可以自建对象存储:
| 维度 | HDFS(自建) | 自建 S3(MinIO / Ceph) | S3 / OSS 等对象存储(云上托管) |
|---|---|---|---|
| 成本模型 | 一次性硬件投入 + 运维人力;三副本约 3 倍存储开销,开启纠删码(EC)可降到约 1.5 倍 | 一次性硬件投入 + 运维人力;存储开销取决于配置(副本或 EC 均可) | 按量付费、单价低,容量无限扩展,无运维成本 |
| 性能 | 本地磁盘顺序读写,单文件吞吐高;计算节点与数据同机时可零网络读取 | 读仍走网络(计算存储分离),同机房部署时带宽可控、无公网抖动;list 慢随文件数增长 | 每次读取都有网络往返,受带宽与 IOPS 约束;list 操作延迟高且随文件数增长 |
| 运维复杂度 | 高:NameNode / DataNode / JournalNode / ZooKeeper 多组件,扩容要搬数据 | 高:自行部署与调优(Ceph 复杂度尤高)、容错自己兜底 | 托管服务,近乎零运维 |
| 生态兼容 | Hadoop 生态原生:Hive/Spark/HBase/Spark 等用 HDFS API 开箱即用 | 与云上相同:S3A 连接器做兼容层,多数场景可用但有差异(如 rename、list) | 需 S3A/OSS 连接器做兼容层,多数场景可用但有差异(如 rename、list) |
| 数据本地性 | 计算与存储同节点部署,调度器感知 block 位置(如 Spark 的 node-local 任务) | 无本地性概念,计算与存储物理分离(同机房只是低延迟,不是本地性) | 无本地性概念,计算与存储物理分离 |
| 原子性与一致性 | rename 是原子操作,写入强一致 | S3 API 无原子 rename(需 copy+delete 模拟);一致性取决于实现,主流自建方案为强一致 | 没有原子 rename(需要 copy+delete 模拟),目录级 list 最终一致 |
| 小文件敏感度 | 敏感:每个文件/block 都要占 NameNode 内存(约 150 字节/条,具体以官方文档为准) | 同云上:文件数放大 list 请求与元数据开销,但无按请求计费 | 同样敏感:文件数直接放大 list 请求和请求计费成本 |
自建 S3 的定位一句话:语义是对象存储、运维是自建——介于云上托管与 HDFS 之间的第三选项。是否选它,看两点:
- 一是存量——已有 MinIO/Ceph 基础设施(或强依赖 S3 生态)且数据不能出域时,顺带复用最划算;
- 二是路线——如果计算层要走 Kubernetes(Spark/Flink/Trino 容器化),S3 生态是云原生事实标准(MinIO 有官方 Operator、CSI 挂载成熟、各引擎的 S3 连接器开箱即用),而 HDFS 假设节点拓扑长期固定、数据绑本地盘,跑在 K8s 上并不顺滑。反过来,从零新建且计算层是物理机/虚拟机集群时,HDFS 的运维经验通常更现成。
1.1 HDFS 作为底座的优劣势¶
优势,也是私有化场景选它的理由:
- 本地部署、数据不出域:合规要求(金融、运营商、政务)下数据必须留在自有机房,HDFS 是现成的分布式文件系统,不需要采购额外的对象存储产品。
- 大文件顺序吞吐:默认 128MB 的 block(
dfs.blocksize)按块顺序读写,非常契合湖仓里"扫描成百上千个 Parquet 文件"的分析负载。 - 位置感知(data locality):Spark 调度任务时会优先把 executor 放到数据所在节点,分析查询少走网络。
- 与 Hadoop 生态零距离:Hive 元数据体系、Spark/Flink 的 Hadoop FileSystem 接口、HBase 的 HDFS 存储,全是同一套 API,HDFS 上跑 Iceberg 不需要任何新基础设施。
劣势:
- 运维重:NameNode/DataNode/JournalNode/ZooKeeper 四个组件都要维护,版本升级、扩容、磁盘故障处理都是常态化工作。
- NameNode 瓶颈:所有元数据操作(open、list、rename)都过 NameNode,它既是性能瓶颈也是可用性单点(需要 HA 缓解,见第 3 节)。
- 小文件敏感:每个文件/block 都要在 NameNode 内存里记账,千万级开始明显吃内存(经验估算每 100 万文件约占 1–2GB 堆)、上亿级足以耗尽常规配置的堆内存并拖垮吞吐(详见 小文件问题与治理);对象存储对文件数相对"无感"(代价是 list 慢)。
- 弹性差:容量扩缩需要加/减物理节点,不能像对象存储那样按需付费。
1.2 为什么云上主流是对象存储、私有化主流仍是 HDFS¶
这个分化不是技术优劣,而是部署形态决定的。
- 云上: 计算和存储分离是默认架构,按量付费 + 弹性扩缩 + 托管免运维,对象存储天然契合;而且湖仓的写入方(Spark/Flink)和读取方(Trino/Vertica)都在云上,网络内网可达,没有"跨机房拉数据"的问题。
- 私有化: 多数公司已经有一套跑着 Hive/Spark 的 Hadoop 集群,HDFS 是事实标准;另外本地搭建对象存储(如 MinIO、Ceph)也要运维、也要考虑容错,而 HDFS 的运维经验是现成的。
本节要点:选型本质四问——数据能不能出域(合规)、有没有现成 Hadoop 集群(成本)、计算与存储是否同机房(性能)、计算层走不走 Kubernetes(路线)。前三问都指向私有化且无 K8s 需求时,HDFS 是最顺的底座;有 K8s 需求则优先考虑 S3 生态(云上托管或自建 MinIO/Ceph)。另外 Iceberg 把表和存储解耦,未来迁对象存储不必一步到位。
2. 湖仓表格式与存储解耦:Iceberg 在 HDFS 上的组织方式¶
2.1 核心思想:把「表」从「存储」中解耦¶
Hive 表格式的时代,"表"就是目录:/warehouse/db/table/dt=2026-07-01/ 下有多少文件,表就是什么。表结构和数据文件、文件系统强绑定,于是有了分区扫描慢、无事务、改分区要重写整张表等一堆问题(详见 Apache Iceberg 表格式深度解析)。
拿一张按天分区的销售表举例,Hive 世界里的「表」就是这样一个目录:
/warehouse/db/sales/ # 「表」的全部真相 = 这堆文件
├── dt=2026-07-01/part-00000.parquet
├── dt=2026-07-02/part-00000.parquet
└── dt=2026-07-03/part-00000.parquet
建表的 DDL 长这样:
-- Hive SQL:表定义(列 + 分区)与目录映射一起存进 Metastore
CREATE TABLE sales (
order_id BIGINT,
amount DECIMAL(10,2),
user_id STRING
)
PARTITIONED BY (dt DATE)
STORED AS PARQUET
LOCATION '/warehouse/db/sales';
要查某天的数据,引擎得先把所有分区目录列一遍再挑;往里面塞新文件后,还要 msck repair table 让 Metastore「认识」它们——表的内容完全由文件系统的目录说了算。
Iceberg 的思路完全不同:表不再是文件系统里的目录结构,而是一个由元数据文件链描述的抽象实体。 数据文件放在哪(HDFS、S3 都行)不重要,重要的是 metadata/ 目录下的元数据文件,它们记录了表的完整状态。还是那张销售表,Iceberg 世界里长这样:
/warehouse/db/sales/
├── metadata/ # 「表」的真相在这里:元数据文件链
│ └── v1.metadata.json # Schema、分区规范、快照列表
└── data/ # 数据文件退化为普通文件堆
└── dt=2026-07-01/00000-0.parquet
建表的 DDL 长这样:
-- Spark SQL 创建 Iceberg 表:表定义落到 Catalog 指针 + metadata.json
CREATE TABLE sales (
order_id BIGINT,
amount DECIMAL(10,2),
user_id STRING,
dt DATE
)
USING iceberg
PARTITIONED BY (dt)
LOCATION '/warehouse/db/sales';
两段 DDL 长得几乎一样——这正是表格式层的意义:语法不变,差别全在「定义存到哪」。Hive 的 DDL 只把列与分区的目录映射存进 Metastore,表里有什么文件仍由目录说了算;Iceberg 的 DDL 会生成 v1.metadata.json,之后的 Schema、分区、快照都由元数据文件链记录。引擎先查 Catalog 拿到当前 metadata.json,按其中的 Manifest 索引直接定位数据文件,不需要 list 目录;新数据由写入端以一次事务提交(新元数据文件 + 切换指针),不存在「Metastore 认不认识文件」的问题。这就是"存储解耦"——换存储底座,表不需要重写,Catalog 里指一下新位置即可。
这里先解释一个贯穿全文的词:Catalog 是「表的登记处」——它不存数据,只维护「表名 → 当前 metadata.json 位置」这条指针(常见实现有 Hive Metastore、REST Catalog、AWS Glue 等)。引擎找表时先查 Catalog 拿到指针,再按指针去读元数据文件。第 2.3 节的原子提交,本质就是「写新元数据文件 + 切换 Catalog 指针」这两步。
2.2 一张 Iceberg 表在 HDFS 上的目录结构¶
Iceberg 表在 HDFS 上的组织方式与在对象存储上完全一致(这正是"解耦"的体现)——一个 warehouse 根目录下,每张表是一个目录,内部拆成 metadata/(元数据)和 data/(数据)两部分:
hdfs://namenode:8020/warehouse/db/table/
├── metadata/ # 表的「大脑」:全部元数据
│ ├── v1.metadata.json # 元数据文件版本 1(表刚创建时)
│ ├── v2.metadata.json # 每次提交生成新版本,旧版本不可变、永久保留
│ ├── snap-537929274446779160-1.avro # Manifest List:某个快照的完整文件清单
│ └── 00000-0-3b1f2a6f.m0.avro # Manifest:若干数据文件 + 列级统计信息
└── data/ # 实际数据,Parquet / ORC / Avro
└── dt=2026-07-01/
└── 00000-0.parquet
先解释这个结构再往下讲。data/ 目录就是纯数据文件(列式格式,见 Parquet / ORC 列式文件格式深度解析),任何能读 Parquet 的引擎都能直接读它们——Iceberg 对数据文件本身零侵入。metadata/ 目录才是表格式的灵魂,它分三层:
- Metadata 文件(
vN.metadata.json):表的一次完整"快照状态",记录着 Schema、分区规范、所有历史 snapshot 的列表、以及当前 snapshot 的指针。每次成功提交(INSERT/UPDATE/DELETE/MERGE)都会生成一个编号递增的新元数据文件,旧文件绝不原地修改。 - Manifest List(
snap-<id>.avro):一个 snapshot 对应的完整文件清单,是二级索引——记录该快照包含哪些 Manifest、每个 Manifest 的分区范围。查询时先读它,就能在不打开每个 Manifest 的情况下剪掉不相关的分区。 - Manifest(
.m<序号>.avro):列出若干数据文件,以及每个文件的行数、null 数、min/max、sum 等列级统计。它本身也是 Parquet 或 Avro 格式,可以列裁剪、谓词下推。
同一条 SELECT * FROM sales WHERE dt = '2026-07-01',Hive 与 Iceberg 的定位路径从查询开始分叉,对比如下:
flowchart TB
Q["SELECT * FROM sales<br/>WHERE dt = '2026-07-01'"]
Q --> H1["Hive:① 查 Metastore<br/>表名 → 目录位置"]
H1 --> H2["② list 所有分区目录<br/>挑出命中的分区"]
H2 --> H3["③ 逐个打开 Parquet footer<br/>获取统计信息"]
H3 --> H4["④ 读命中的数据文件"]
Q --> I1["Iceberg:① 查 Catalog<br/>表名 → 当前 metadata.json"]
I1 --> I2["② 读 Manifest List<br/>按分区范围剪枝"]
I2 --> I3["③ 读 Manifest<br/>用 min/max 统计过滤"]
I3 --> I4["④ 只读命中的数据文件"]
两边一对比,差别一目了然:Hive 的第②步 list 分区目录、第③步开 footer,开销都随文件数线性放大;Iceberg 前三步读的全是元数据索引(Manifest 里预存了分区范围与 min/max),只有第④步才碰真正的数据文件。这就是 Iceberg 能在海量文件里快速定位的秘密,也是第 4.1 节 Vertica 读 Iceberg 外部表的同一套路径。
2.3 这些元数据文件如何带来 ACID、时间旅行与 Schema 演进¶
理解了目录结构,三件事就都顺理成章了:
2.3.1 ACID 与快照隔离¶
先拆开 ACID 四个字母:
- 原子性——提交要么全生效要么不生效
- 一致性——数据始终满足 Schema 约束
- 隔离性——并发读写互不干扰
- 持久性——已提交的数据不丢
Iceberg 的快照机制直接兑现「原子性 + 隔离性」,一致性靠写入时的 Schema 校验,持久性交给底层文件系统。
写入一个 Iceberg 表 = 写一组新数据文件 + 写新 Manifest + 写新 Manifest List + 生成新的 metadata.json,然后把 Catalog 指针切到新版本。因为数据文件从来不被原地修改,正在读旧快照的查询完全不受影响,读到的是一致的旧状态——这就是可重复读(Repeatable Read)隔离级别的来源。
更关键的是"切换指针"这个动作本身:在 HDFS 上,Iceberg 通过 HadoopFileIO 把新元数据文件 rename 到最终位置完成提交,而 HDFS 的 rename 是原子操作,所以提交要么生效要么不生效,绝无中间态。
反观对象存储没有原子 rename(见 湖仓一体架构设计),Iceberg 只能依赖 put 的原子性加提交前的版本校验(乐观并发,冲突方失败重试)来达成同样效果——这就是 HDFS 作为底座给 Iceberg 的一个隐性便利。多个 writer 并发提交时,后提交者会带着过期版本号失败并重试,保证不会互相覆盖。
2.3.2 时间旅行¶
每次提交都生成一个新 snapshot 并记录时间戳,而所有历史 snapshot 都保留在元数据文件链里。于是可以回溯任意历史时刻的表状态:
-- 回到某个历史时间点的表状态
SELECT * FROM sales FOR SYSTEM_TIME AS OF '2026-07-01 00:00:00';
-- 或按 snapshot 版本号读取
SELECT * FROM sales FOR SYSTEM_VERSION AS OF 537929274446779160;
上面这段 SQL 的语义是:按给定时间/版本找到对应的 metadata.json,按其中的 Manifest List 定位当时的数据文件。因为旧文件没被删,读历史状态就不需要额外拷贝。这在数据审计、误删恢复场景价值极大。
2.3.3 Schema 演进¶
加列、删列、重命名列、类型提升(如 INT → BIGINT)都只需要写一个新版 metadata.json,数据文件一个字节都不用动。原理是列映射走列 ID 而不是列名:加列时旧文件的该列视为 NULL,删列时旧文件跳过该列,重命名列只是 ID 对应的新名字。引擎读 Parquet 时按元数据里的 Schema 版本自动完成映射。
注意类型变更也不是随便改:Iceberg 只允许「类型提升」这一种方向,且限定在安全路径上(INT→BIGINT、FLOAT→DOUBLE、DECIMAL 精度放大等)——新旧类型是值域包含关系,旧数据保证无损。兼容性检查发生在提交 Schema 变更那一刻:按提升规则做类型级校验,非法变更(如 BIGINT→INT 缩小、STRING→INT 跨类)直接报错拒绝,所以不需要、也不会去扫描已有数据文件;要缩小或跨类转换,只能另想办法(如新建列迁移数据)。
本节要点:Iceberg 的 ACID、时间旅行、Schema 演进,全部来自「元数据文件不可变 + 指针原子切换」这一个机制,与底层是 HDFS 还是对象存储无关。这正是它开放中立、能在私有化 HDFS 集群上无缝落地,且未来迁对象存储不必重写表的原因。
3. HDFS 特性如何支撑湖仓¶
如果说第 2 节是"表格式"视角,这一节换回"存储"视角:HDFS 的哪些原生特性,恰好是湖仓工作负载需要的。
3.1 大文件 block 存储与列式文件的契合¶
湖仓的数据文件几乎都是 Parquet/ORC 这类列式大文件(压缩后单个文件通常在 256MB-1GB,见 Vertica Iceberg 集成实践 对小文件治理的建议)。HDFS 按 128MB 的 block 切分文件,一个大 Parquet 文件 = 若干个连续 block,元数据开销极小:一个 512MB 的文件在 NameNode 上只有 4 条 block 记录;反过来,如果同一份数据是 512 个 1MB 小文件——流式写入(Flink Checkpoint 间隔短)或 Spark 分区数过多会真实产出这个量级——NameNode 就要记 512 条块记录加 512 个文件 inode,查询时还要逐个打开 512 个文件,打开与调度开销放大上百倍(见最佳实践第 3 条与 小文件问题与治理)。
一句话:HDFS 的 block 存储是为"少而大"的文件设计的(block 像标准集装箱,见 HDFS 架构原理 第 6 节),湖仓的列式大文件恰好就是这个形态——几百 MB 的文件只是几只集装箱,元数据开销几乎为零,两者天然契合。 反过来,如果你发现湖仓表里全是小文件,性能问题通常一半出在 HDFS 元数据,一半出在 Iceberg Manifest 膨胀(后面最佳实践会展开)。
3.2 纠删码(EC):冷数据的成本解药¶
三副本策略是 HDFS 最朴素的容错方式:每份数据存 3 份,存储开销 300%。对湖仓这种"热数据少、冷数据多"的负载,全量三副本是巨大的成本浪费。HDFS 的纠删码(Erasure Coding)用算法(如 Reed-Solomon)把数据切成 N 块数据 + M 块校验块,任意 N 块即可还原:默认的 RS-6-3(6 数据块 + 3 校验块)存储开销只有 150%,比三副本省一半空间,且容忍 3 块同时故障(三副本只容忍 2 块)。更极端的 RS-10-4 能压到 140%。原理和取舍详见 纠删码 vs 三副本存储策略。
代价也很明确:写入要做编码计算(吃 CPU)、读取故障块时要从多个块重构数据(慢)。所以 EC 的适用边界是访问频率低、只读为主的数据——湖仓里按分区生命周期降冷的历史分区(3 年以上的审计数据)就是典型对象。
结论:热分区三副本保性能,冷分区开 EC 降成本,通常能把湖仓存储成本从 3 倍压到 1.5 倍左右。
3.3 HA:保障元数据服务连续性¶
湖仓的读写路径是这样的:引擎(Spark/Flink)→ Iceberg Catalog → 读 metadata.json 与 Manifest → 读数据文件。前两步全部要经过 HDFS 的 NameNode——它是湖仓的"元数据总闸门"。NameNode 一挂,整个湖仓不可用,而不只是某张表不可用。 这正是 HDFS 高可用(HA)对湖仓的意义所在。
HA 的机制分两半:QJM(Quorum Journal Manager) 解决"日志不丢"——edits 日志同时写入 2N+1 个 JournalNode(通常 3 或 5 个),过半成功才算提交,Active 和 Standby 随时保持日志一致;ZKFC(ZK Failover Controller) 解决"主从切换"——通过 ZooKeeper 分布式锁确定谁是 Active,Active 心跳丢失后自动触发故障切换。切换通常在秒级完成,对湖仓来说意味着"元数据服务连续性"而不是"元数据服务停机"。打个比方:QJM 是「双人同记一本账、过半生效」,ZKFC 是「哨兵——值班者失联立刻换班」。机制细节详见 HDFS 高可用(QJM、ZKFC、Federation)。
本节要点:HDFS 的三个原生特性恰好对上湖仓的三类需求——大 block 对上列式大文件、EC 对上冷数据成本、HA 对上元数据连续性。选 HDFS 当湖仓底座,等于三项能力开箱即用,其中 HA 是必选项:它保障的不是单张表,而是整个湖的元数据服务。
4. 与 Vertica 的关系¶
Vertica 在 HDFS 湖仓架构里有两层关系:一层是作为分析引擎直接读 Iceberg 湖仓表,另一层是作为数据库本身把公共存储(communal storage)放在 HDFS 上(Eon 存算分离模式)。两条路径并不互斥,私有化环境常常两者并存。
4.1 路径一:Vertica 作为引擎直接查询 Iceberg 湖仓表¶
Vertica 自 12.0.4(2023 年 7 月发布)起原生支持查询外部 Iceberg 表,允许直接以标准 SQL 分析对象存储上的 Parquet 数据文件,无需加载到 Vertica 本地存储(详见 Vertica Iceberg 集成实践)。按 Vertica 官方文档(26.1.x),注册语法是 STORED BY ICEBERG LOCATION:
-- S3 上的 Iceberg 表:给基础位置,Vertica 自动读取最新 metadata 文件
CREATE EXTERNAL TABLE sales_iceberg
STORED BY ICEBERG LOCATION 's3://my-bucket/iceberg/sales/';
LOCATION 支持五类:文件系统基础位置(自动取最新 metadata)、具体的 .metadata.json 文件(可钉住某个历史版本)、S3 + Glue 目录(配 GLUE_DB/GLUE_TABLE)、Hive Metastore(thrift:// URI + HMS_DB/HMS_TABLE)、REST 端点(配 REST_AUTH)。
这段 SQL 的含义是:Vertica 不搬运数据,只在本地注册一张"视图式"的外部表,查询时按第 2 节讲的元数据路径定位文件——先读 metadata/ 下最新的 .metadata.json 拿到当前快照,再读 Manifest List、按分区与 min/max 统计剪枝,最后只读取命中的 Parquet 数据文件。特性与边界(来自官方文档与 Vertica Iceberg 集成实践):
- 只读:外部表不支持 INSERT/UPDATE/DELETE/MERGE,写入必须在 Spark/Flink 侧完成。
- 仅 Parquet:数据文件必须是 Parquet 格式;Iceberg 的 delete files(MOR 删除向量)不支持。
- 元数据:支持 Iceberg v1/v2 元数据格式;Iceberg 列默认值(column defaults)不支持。
- 无本地投影、无物化视图、不收集 DBDesigner 统计:查询计划依赖 Iceberg Manifest 里的现有统计。
- 快照一致性:打开表时读的是当时的快照,之后 Iceberg 表再有新提交,需重建外部表(重新解析 metadata)才能感知;给
LOCATION指定具体.metadata.json则可读取该历史版本。 - 小文件敏感:小于 64MB 的文件会导致 Manifest 条目过多,建议
rewrite_data_files合并到 256MB-1GB。
注意一个边界:26.1 官方文档的 LOCATION 类型中没有 HDFS/ADLS 直接路径的示例——私有化 HDFS 上的湖仓数据,官方路径是经 Hive Metastore(thrift:// URI,Hadoop 生态现成的 Catalog)或 REST Catalog 注册,这也正好用上第 2.1 节讲的 Catalog。另外官方要求所有 Iceberg 文件(数据 + 元数据)必须对所有数据库节点可达,元数据内嵌路径与库内可访问路径不一致时可用 IcebergPathMapping 参数做前缀映射;同机房的网络与吞吐评估见 Vertica 读取 HDFS 文件的原理分析。
4.2 路径二:Eon 存算分离模式的公共存储直接落在 HDFS 上¶
Eon 模式把 Vertica 的存储架构改成"共享公共存储(communal storage)+ 各节点本地缓存(depot)":数据文件集中存在公共存储上,节点只缓存热数据,从而支持弹性扩缩容和节点快速替换。公共存储的选型在私有化场景下同样可以是 HDFS——官方明确支持"公有云存储(AWS/GCP/Azure/Ali)+ minio、HDFS 等私有云存储"(见 怎样将企业模式的 Vertica 数据库迁移到 EON),生产上也已有 Eon on HDFS 的成熟实践(备份可用 VBR 直接指向 HDFS,见 使用 vbr 备份 Vertica Eon on HDFS 数据库)。
这样一来,HDFS 集群同时承载两类数据:Iceberg 湖仓表(多引擎共享、开放式)和 Eon 公共存储(Vertica 原生表)。两者可以共存于同一套 HDFS 上,但要分目录、分开容量规划与备份策略。运维细节见 Vertica Eon communal storage 运维实战。
4.3 三条路径怎么选¶
私有化场景下,同一个 Vertica 集群可能同时面临三种数据形态:
| 形态 | 数据放哪 | 典型场景 |
|---|---|---|
| 企业模式本地表 | 各节点本地磁盘(投影) | 高频 OLTP 型查询,微秒级点查,实时更新 |
| Eon 模式表 | HDFS/对象存储公共存储 + depot 本地缓存 | 需要弹性扩缩容、节点快速替换、共享存储 |
| Iceberg 外部表 | 湖仓(HDFS 或对象存储上的 Iceberg 表) | 多引擎共享同一份数据、低频分析、数据更新由 Spark/Flink 负责 |
本节要点:三条路径按访问频率分流——高频低延迟查询进本地表(或 Eon depot 覆盖的热数据),低频多引擎共享分析留在 Iceberg 湖仓,Eon 是「要弹性又要 Vertica 原生能力」的中间选项。三者不是替代关系,可以共存于一套 HDFS 底座之上。
5. 最佳实践清单¶
-
私有化湖仓底座优先选 HDFS,并保持 Iceberg warehouse 目录单一可管理 为什么:数据不出域 + 已有 Hadoop 运维经验是私有化环境的最大杠杆;warehouse 根目录固定(如
/warehouse),便于统一容量规划、备份和权限治理。 -
写入端控制文件大小,目标 256MB-1GB,至少不低于 128MB(一个 block) 为什么:HDFS 元数据、Iceberg Manifest 条目数、查询时的打开开销都随文件数线性增长;低于一个 block 的文件等于白白浪费 block 的流式读取优势。
-
定期执行
rewrite_data_files合并小文件,配合 Flink Checkpoint 间隔(10-30 分钟以上) 为什么:流式写入天然产生小文件,不合并会让 HDFS inode 和 Iceberg Manifest 同时膨胀,Vertica 等引擎打开表、扫元数据都变慢。 -
定期
expire_snapshots清理历史快照,保留 3-7 天 为什么:每个写入都留一个快照,metadata/会无限增长;保留期覆盖审计需求即可,超期快照只占空间不产生价值(数据文件本身的备份恢复靠 HDFS 回收站/异地备份兜底,见第 10 条)。 -
冷数据分区启用 EC(RS-6-3),热数据保持三副本 为什么:存储成本从 3 倍降到 1.5 倍;EC 的读重构代价对低频冷数据可接受,对高频热数据则不可接受。
-
NameNode 必须部署 HA(3 个 JournalNode + ZKFC),并监控其内存 为什么:湖仓全部元数据读取都要过 NameNode,它挂 = 全湖不可用;且小文件治理得不好时,NameNode 内存会先于磁盘打满。
-
分区列选低基数列(如
dt、region),让查询谓词尽量落在分区列上 为什么:分区剪枝直接决定一次查询要读多少 HDFS 数据——剪枝有效时只读命中的少数 Parquet 文件,剪枝失效则退化为全表扫描、全湖读 block;把复杂表达式(如SUBSTR(col,1,3)='abc')用于谓词会推不下去,等于亲手关掉剪枝。 -
Vertica 侧按访问频率分流:高频查询建本地表/Eon 表,低频多引擎共享查询走 Iceberg 外部表 为什么:本地表点查微秒级、外部表百毫秒级(还要走网络读 Parquet),把高频查询留在本地表是性价比最高的优化;外部表只读、无投影的特性决定了它不适合热路径。
-
HDFS 扩容(加 DataNode)后及时跑
hdfs balancer均衡数据,低峰执行 为什么:新节点刚加入时几乎没有数据,热点仍压在老节点上;balancer 按容量重新分布 block(EC 文件同样参与),会占用迁移带宽,安排低峰并预留时间。 -
备份与容灾单独规划:Eon on HDFS 用 VBR 指到 HDFS/异地存储,Iceberg 元数据靠快照保留 + HDFS 回收站兜底 为什么:两类数据形态的恢复手段不同——Eon 数据库走 VBR 备份集恢复,Iceberg 表靠
expire_snapshots保留期的快照做时间旅行恢复;混在一起规划容易两头落空。
附录:新手常见疑问(FAQ)¶
Q1:数据湖和数据仓库到底差在哪? 数据湖是「先堆放、后定义」:原始数据便宜地集中存放,Schema 读时再定,但没有事务保障;数据仓库是「先定义、后入仓」:模型严格、事务齐全、查询快,但变更贵、容量贵。湖仓一体就是用 Iceberg 这类表格式,在便宜的湖存储上做出数仓级的 ACID 与版本能力——本文第 1 节的选择题就是在这个背景下展开的。
Q2:表格式(Iceberg)到底是什么?为什么以前的 HDFS 没有它? 表格式是加在文件系统之上的一层元数据协议:记录「表有哪些文件、每个文件的统计信息、每次变更的版本」。以前 Hive 用「目录即表」省掉了这层,代价是分区扫描慢、无事务、改分区要重写整张表。Iceberg 把「表」正式定义为元数据文件链(见第 2.2 节),于是 HDFS 和对象存储都能当底座。
Q3:Catalog 是必须的吗?它挂了表还能读吗? Catalog 只维护「表名 → 当前 metadata.json」这条指针,是引擎找表的入口,所以必须有;但数据文件和元数据文件都在存储层,Catalog 故障重建注册即可,数据不丢——这正是解耦的好处。私有化环境最常见的 Catalog 实现是 Hive Metastore。
Q4:已有 Hive 表,值得迁到 Iceberg 吗? 看需求:如果只是离线 ETL 加批量查询,Hive 表也能用;如果需要 ACID、时间旅行、Schema/分区演化、多引擎并发读写,迁移的收益才大于成本(迁移需要以 Iceberg 格式重写数据文件)。迁移实操见 Apache Iceberg 表格式深度解析。
Q5:Eon communal storage 和 Iceberg 湖仓表都放在 HDFS 上,会互相干扰吗? 不会,但必须分目录、分开容量规划与备份策略:Eon 公共存储由 Vertica 自行管理(备份走 VBR),Iceberg 表由 Spark/Flink 写入(靠快照保留 + HDFS 回收站兜底),混在一个目录里会让权限和备份两头落空(见最佳实践第 10 条与 Vertica Eon communal storage 运维实战)。
Q6:Iceberg 外部表为什么是只读的? 这是 Vertica 集成的设计定位:Vertica 扮演分析读引擎,写入由 Spark/Flink 负责,避免多个引擎同时改同一份元数据引发冲突。查询结果需要落到 Vertica 本地时,写到本地表即可(见第 4.1 节)。
参考¶
- Apache Iceberg 表格式深度解析
- Vertica Iceberg 集成实践
- 湖仓一体架构设计
- Parquet / ORC 列式文件格式深度解析
- 怎样将企业模式的 Vertica 数据库迁移到 EON
- 使用 vbr 备份 Vertica Eon on HDFS 数据库
- Apache Iceberg Specification
- Apache HDFS 官方文档