理解 Parquet 和 Iceberg,本质上就是理解大数据计算引擎如何在对象存储之上,通过精妙的物理数据布局和逻辑元数据索引来榨取性能、实现类似关系型数据库(ACID)的功能。
整个数据湖架构可以分成三层:存储底座(对象存储)→ 物理文件格式(Parquet)→ 逻辑表格式(Iceberg)。
Parquet:榨干磁盘与网络 I/O 的物理文件格式
Parquet 是一种列式存储格式。在对象存储的语境下,它解决的核心问题是减少 HTTP GET 请求的数据量。
如果数据是 CSV 或 JSON(行存),查 select age from table 需要扫描整个文件。Parquet 将同一列的数据连续存储,查 age 列只需读取那一列的数据块。又因为同一列的数据类型相同,Parquet 可以上 RLE(游程编码)、字典编码等高效编码,配合 Snappy/Zstd 压缩,压缩比轻松到 10:1 以上。
对对象存储最友好的设计是 Footer:
Parquet 的元数据放在文件尾部,记录了各个列块(Column Chunk)在文件中的 Byte Offset,以及该块内数据的 Min/Max 值。计算引擎(如 Spark)先发起一个 Range GET 读文件末尾几 KB 拿到 Footer,解析出需要的列所在的 Offset 和长度,再发起并行的 Range GET 精准拉取数据块——完美避开下载整个大对象。
Iceberg:对象存储上的”分布式数据库索引”
如果只有 S3 + Parquet,会卡在一个地方:对象存储的 ListObjects API 太慢。传统的 Hive 架构依赖目录(Prefix)划分分区,查询时需要疯狂 List 目录来找文件,在对象存储上是灾难。
Iceberg 的核心思想是 Track Files, Not Directories——放弃依赖底层目录树结构,用一棵严格的元数据树来记录每次 Commit 产生了哪些文件。
元数据层级:
| 层级 | 格式 | 内容 |
|---|---|---|
| Metadata File | .json | Schema、分区配置、当前 Snapshot 指针 |
| Manifest List | .avro | 当前快照包含哪些 Manifest 文件 |
| Manifest File | .avro | 灵魂所在——底层 Parquet 文件的 S3 URI + 每列 Min/Max 统计 |
查 where id = 500 时,Iceberg 读这棵元数据树,利用 Min/Max 直接过滤掉不可能包含 500 的 Parquet 文件(Data Skipping)。原本需要 1000 次 ListObjects 的操作,变成了几次精准的 GetObject 读元数据,然后拿到目标 Parquet 文件的确切 URL。
ACID 与时间旅行也源于此——每次写入生成全新的元数据文件(MVCC),读写互不干扰,随时可以把指针切回旧的 Metadata JSON,实现”时间旅行”查询。
三个核心特性是怎么在对象存储上实现的
1. 事务(ACID)
两个 Spark 任务同时往一个 Prefix 写数据,或者写到一半崩溃,对象存储天然没有跨对象的分布式事务。Iceberg 的解法是 MVCC + Catalog 原子替换:
写完一堆 Parquet 和新的元数据 JSON(比如 v2.json)后,Iceberg 向 Catalog(Hive Metastore、JDBC 或对象存储的条件写锁)发起一个原子操作——把表版本指针从 v1.json 切到 v2.json。正在跑的查询继续读 v1.json(快照隔离),指针一切换,新查询立刻看到 v2.json。写入崩溃了直接删未提交文件,原表毫发无伤。
2. 细粒度 Update/Delete
对象存储不支持修改对象内部某几行。用户 DELETE FROM table WHERE id = 10,传统做法是整分区重写。Iceberg 给了两种策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| Copy-on-Write | 读 Parquet → 内存删行 → 写新 Parquet,元数据指向新文件 | 读多写少 |
| Merge-on-Read | 不碰原文件,追加一个 Delete File(记录”文件 A 第 100 行被删”),查询时 Join 过滤 | 高频写 |
Merge-on-Read 把”随机写/大文件重写”变成了顺序追加小文件,对对象存储极其友好。
3. Schema 演进
传统 Hive 架构中,列名一改(user_id → uid),旧 Parquet 文件因内部还写着 user_id,直接读出 Null。Iceberg 给每个列分配了唯一的、自增的 ID,与列名和位置完全无关。
ALTER TABLE RENAME COLUMN user_id TO uid 只改最新的元数据 JSON,记录”ID 1 现在叫 uid”,底层 Parquet 文件不需要任何重写。下游引擎读旧 Parquet 时,Iceberg 根据 ID 1 自动把文件里的 user_id 映射为 uid。增删列、改列顺序,全部是毫秒级元数据操作。
元数据持久化层:高性能 SSD 集群对吗?
Iceberg 的元数据文件(.json 和 .avro)确实持久化在对象存储上。但最顶层的”根指针”不能完全依赖普通对象存储——原子 CAS(Compare and Swap)需要条件乐观锁,传统 S3 只支持 PUT 覆盖。所以 Iceberg Catalog 通常用关系型数据库(行锁做原子替换)或支持 If-Match 条件的现代对象存储来承载。
搞高性能 SSD 集群存元数据,不仅合理,而且是当前云厂商和大厂存储团队的核心优化方向。Iceberg 架构带来了严重的 Metadata Amplification——一个复杂 SQL 进来,引擎需要先发起好几次 HTTP 请求读 JSON、读 Manifest List、读 Manifest 块,全解析完才能开始读数据。这些元数据文件都是几 KB 到几 MB 的小文件,对象存储如果用 HDD 底座,TTFB 和 QPS 根本撑不住。
业内三种主流方案:
| 方案 | 做法 | 代表 |
|---|---|---|
| 分层存储池 | 网关根据 Prefix/后缀自动路由到 NVMe SSD 池 vs HDD EC 池 | 自研对象存储内核优化 |
| 极速型产品 | 全 SSD 单区部署,个位数毫秒延迟 | AWS S3 Express One Zone |
| 独立缓存层 | 存算之间架 Alluxio/JuiceFS 做 SSD 缓存,异步刷回 | 存算分离架构标配 |
行存 vs 列存:从存储架构师角度看
抛开”查询变快/变慢”的表象,行存(NSM)和列存(DSM)本质上是数据在物理介质上的内存对齐方式、信息熵,以及由此引发的 I/O 放大与 CPU 指令流问题。
行式存储(NSM):面向元组的生命周期管理
行存把同一行的所有属性在物理字节空间上连续存放。存储引擎中,磁盘被划分为固定大小的 Page(如 16KB),采用 Slotted Page 结构:Page Header → Line Pointers(槽位数组,存每个 Tuple 的偏移量)→ Tuples(含隐藏列如 TxID、Rollback Pointer 和用户列)。
优势:
- 写放大极低:一次 INSERT,整行数据在内存组装成连续 Byte 数组,Append 到 Page 空闲区 + 改一次 Line Pointer,一次原子内存写。WAL 也友好。
- O(1) 元组重组:B+Tree 点查定位到页后,CPU 顺着指针一次性把整行读入 L1 Cache,不用跨页拼凑字段。
- 并发控制自然:行级锁和 MVCC 事务状态直接打在 Tuple Header 上。
劣势:
- CPU 缓存污染灾难:
SELECT SUM(salary)时,CPU Cache Line(64 Bytes)的预取会把相邻的name、address等无用字段一并拉进 L1/L2,挤掉真正需要的下一个salary。 - I/O 读放大极高:为读 4 字节的
salary,必须把整行(可能数 KB)从磁盘读出,有效 I/O 利用率不到 1%。
列式存储(DSM):面向分析计算的极致榨取
列存把所有行中相同属性的数据物理连续存放。在 Parquet 的 Column Chunk 或内存结构中,列存本质上退化成了同构类型的强类型数组(Dense Arrays)。
优势:
- 基于信息熵的极致压缩:同列同类型 + 业务高局部性 → RLE(
[男,男,男,女,女]→[男×3, 女×2])、字典编码(长字符串 → Integer ID)、Delta 编码(时间戳只存差值)。行存压缩比 2-3:1,列存轻松 10:1 以上。压缩把 I/O 瓶颈转移给了 CPU 解压开销——牺牲 CPU 换 Disk/Network I/O 是绝对划算的。 - 向量化执行 + SIMD:连续数组意味着计算引擎不再
next()一行行调函数(消灭海量虚函数调用),而是一次塞一个 Batch(如 1024 个值)给计算函数。现代 CPU 的 AVX-512 指令集可以一条指令完成 16 个 INT 的加法。行存因为数据中间夹杂其他列,根本用不了 SIMD。
劣势:
- 昂贵的元组重组:
SELECT *时,100 个列分布在 100 个不同物理位置,引擎必须发起 100 次独立 I/O,再按 Row ID 拼接——这是列存的”阿喀琉斯之踵”。 - Mutation 灾难:单行 INSERT 要把一行切碎写入几十个列文件,引发可怕的随机写和写放大。UPDATE 一个压缩编码后的值,可能触发整个块的字节级重组。
解法就是所有列存引擎(ClickHouse、Iceberg、传统数仓)只支持批量追加,或引入 LSM-Tree 结构(Delta Lake / Iceberg 的 Merge-on-Read)用小文件追加替代原地修改。
PAX:合久必分,分久必合
纯粹的 NSM 或 DSM 在现代复杂业务中都无法包打天下。Parquet、ORC 以及现代 HTAP 数据库实际采用 PAX(Partition Attributes Across)——混合行列存储:
- 水平切分:每 100 万行一个 Row Group,单个 Row Group 几十 MB,可以完整放进内存。
- 垂直切分:每个 Row Group 内部严格列式存储。
这保持了列存的高压缩比和 SIMD 优势,又极大缓解了元组重组代价——SELECT * 时引擎只需将一个 Row Group 读入内存,所有列都在这几十 MB 连续空间内,重组变成纯内存拷贝。
OLTP 用行存,OLAP 用列存——不是选择,是必然
核心业务系统(OLTP)和数据分析系统(OLAP)选不同存储模型,根源在于 I/O 访问模式、数据局部性、计算密集度 上不可调和的矛盾。
OLTP:面向实体的生命周期管理
业务系统处理数据的最小单元是”一个完整的对象”——一笔订单、一个用户。特点是高并发、多点查、频繁增删改、强 ACID。
行存对 OLTP 是唯一解:
- 极低写放大:下订单写几十个字段,行存把整行作为连续字节一次性 Append 到 Page + WAL,一次磁盘 I/O。列存要拆散写几十个列文件,一次逻辑 Insert 触发几十次随机 I/O,高并发下是灾难。
- 实体级数据局部性:
SELECT * FROM orders WHERE order_id = 123——加载一个 16KB 页面,CPU 顺 Cache Line 一次性拿到所有细节。 - 并发控制的物理基础:行级锁和 TxID 直接写在一行数据头部,判断可见性极高效。
OLAP:面向宏观指标的聚合与降维
分析师不关心某个用户叫什么,关心的是”过去一年华东区 18-25 岁用户平均客单价”。特点是低并发、海量扫描、极少更新、宽表中只查少数列。
列存对 OLAP 是降维打击:
- 无情消除无用 I/O:100 列的表,
SELECT SUM(amount) WHERE region = '华东'只用了 2 列。行存必须扫整表(I/O 浪费 98%),列存只读amount和region两个列文件。 - 极致压缩:
region列连续几万行都是”华东”→ RLE 压缩到极致。压缩即减少 I/O。 - 榨干 CPU:解压后是纯 Int64 数组,SIMD 一条指令并发算几十个数值。
在物理世界,数据在磁盘上的排列方式只能有一种。所以传统架构师会建两套系统:MySQL(行存)撑业务,CDC 把数据同步到 ClickHouse / Iceberg(列存)做分析。
2026 年 OLAP 生态
站在 2026 年,ClickHouse 依然是顶流,但不再是所有 OLAP 场景的默认唯一解。生态从”百家争鸣的独立数据库”演变成了”围绕数据湖的计算引擎大乱斗”。
ClickHouse:单表暴力扫描和实时日志的王者。 没有没落,在日志分析(Observability)、时序指标、宽表极速查询上依然统治力极强。全面拥抱了存算分离,底层深度绑定对象存储。边界也彻底被认清——依然不擅长复杂跨表多维 JOIN,运维门槛不低。更多被收敛在”实时数仓”和”高频点查”的特定流水线里。
DuckDB:单机与边缘计算的绝对黑马。 随着单台服务器内存动辄上 TB,80% 的分析场景根本不需要分布式集群。DuckDB 就像分析领域的 SQLite——数据分析师的 Python 脚本、BI 工具的本地缓存、浏览器 WASM 端、数据清洗流水线(dbt)都在内置它。它可以直接通过 HTTP Range GET 极速拉 S3 上的 Parquet 文件做本地计算,这种”无服务”查询模式让大量并发压力直接打到对象存储网关上。
StarRocks / Apache Doris:统领湖仓一体与复杂 JOIN。 分布式 MPP 领域,中国的开源力量在 2026 年已占全球半壁江山。它们完美填补了 ClickHouse 的短板——极其强悍的多表复杂 JOIN 能力,以及极低延迟的实时微批写入。现在企业不再把所有数据都导进 OLAP 引擎,StarRocks/Doris 更多作为”计算网关”存在,内部只存最热的实时数据,历史数据直接外表关联 S3 上的 Iceberg 查。
模块化:没人再从头手写执行引擎了。 Meta 开源的 Velox C++ 向量化执行引擎统一了底层计算逻辑。大家都在用标准的 Arrow 内存格式和 Velox 拼装自己的 OLAP 系统,竞争的焦点变成了”谁的优化器(CBO)写得好”和”谁跟底层对象存储结合得更紧密”。
2026 年 OLAP 生态最大的共识:Storage is commodity, Compute is ephemeral, Format is king(存储是基础设施,计算是弹性的,格式才是王道)。所有 OLAP 引擎都失去了垄断数据的特权,底层数据统一沉淀在 S3 + Iceberg + Parquet 上。对象存储系统已成为企业真正意义上的 Single Source of Truth。
几点心得
- Parquet 的设计哲学是”硬件感知”——Footer 放末尾让 Range GET 精准拉取,列存布局让压缩和 SIMD 火力全开。好的文件格式是算出来的,不是拍出来的。
- Iceberg 解决的是”元数据问题”——对象存储能无限存文件,但找文件很慢。Iceberg 用一棵精心设计的元数据树把 ListObjects 问题变成了 O(log n) 的元数据查询,这才是它作为表格式的真正价值。
- 行存和列存没有优劣,只有场景匹配。OLTP 要的是”一次 I/O 拿整行”,OLAP 要的是”海量数据中只碰有用的列”。理解了 I/O 模式就理解了一切。
- 对象存储是整个数据生态的底座。所有 OLAP 引擎最终都在围着 S3 转——S3 Select 把算力下推到存储节点、Athena 做存算分离查询、DuckDB 直接拉文件本地算。对对象存储开发来说,理解上游引擎的行为,比优化底层 I/O 路径本身更重要。
如果有什么不对,欢迎指正。